/* * Copyright (C) 2005-2013 Team XBMC * http://xbmc.org * * This Program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This Program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with XBMC; see the file COPYING. If not, see * . * */ #include "TextureCache.h" #include "TextureCacheJob.h" #include "filesystem/File.h" #include "profiles/ProfilesManager.h" #include "threads/SingleLock.h" #include "utils/Crc32.h" #include "settings/AdvancedSettings.h" #include "utils/log.h" #include "utils/URIUtils.h" #include "utils/StringUtils.h" #include "URL.h" #include "utils/StringUtils.h" using namespace XFILE; CTextureCache &CTextureCache::Get() { static CTextureCache s_cache; return s_cache; } CTextureCache::CTextureCache() : CJobQueue(false, 1, CJob::PRIORITY_LOW_PAUSABLE) { } CTextureCache::~CTextureCache() { } void CTextureCache::Initialize() { CSingleLock lock(m_databaseSection); if (!m_database.IsOpen()) m_database.Open(); } void CTextureCache::Deinitialize() { CancelJobs(); CSingleLock lock(m_databaseSection); m_database.Close(); } bool CTextureCache::IsCachedImage(const CStdString &url) const { if (url != "-" && !CURL::IsFullPath(url)) return true; if (URIUtils::IsInPath(url, "special://skin/") || URIUtils::IsInPath(url, "special://temp/") || URIUtils::IsInPath(url, "androidapp://") || URIUtils::IsInPath(url, CProfilesManager::Get().GetThumbnailsFolder())) return true; return false; } bool CTextureCache::HasCachedImage(const CStdString &url) { CTextureDetails details; CStdString cachedImage(GetCachedImage(url, details)); return (!cachedImage.empty() && cachedImage != url); } CStdString CTextureCache::GetCachedImage(const CStdString &image, CTextureDetails &details, bool trackUsage) { CStdString url = CTextureUtils::UnwrapImageURL(image); if (IsCachedImage(url)) return url; // lookup the item in the database if (GetCachedTexture(url, details)) { if (trackUsage) IncrementUseCount(details); return GetCachedPath(details.file); } return ""; } bool CTextureCache::CanCacheImageURL(const CURL &url) { return (url.GetUserName().empty() || url.GetUserName() == "music"); } CStdString CTextureCache::CheckCachedImage(const CStdString &url, bool returnDDS, bool &needsRecaching) { CTextureDetails details; CStdString path(GetCachedImage(url, details, true)); needsRecaching = !details.hash.empty(); if (!path.empty()) { if (!needsRecaching && returnDDS && !URIUtils::IsInPath(url, "special://skin/")) // TODO: should skin images be .dds'd (currently they're not necessarily writeable) { // check for dds version CStdString ddsPath = URIUtils::ReplaceExtension(path, ".dds"); if (CFile::Exists(ddsPath)) return ddsPath; if (g_advancedSettings.m_useDDSFanart) AddJob(new CTextureDDSJob(path)); } return path; } return ""; } void CTextureCache::BackgroundCacheImage(const CStdString &url) { CTextureDetails details; CStdString path(GetCachedImage(url, details)); if (!path.empty() && details.hash.empty()) return; // image is already cached and doesn't need to be checked further // needs (re)caching AddJob(new CTextureCacheJob(CTextureUtils::UnwrapImageURL(url), details.hash)); } bool CTextureCache::CacheImage(const CStdString &image, CTextureDetails &details) { CStdString path = GetCachedImage(image, details); if (path.empty()) // not cached path = CacheImage(image, NULL, &details); return !path.empty(); } CStdString CTextureCache::CacheImage(const CStdString &image, CBaseTexture **texture, CTextureDetails *details) { CStdString url = CTextureUtils::UnwrapImageURL(image); CSingleLock lock(m_processingSection); if (m_processinglist.find(url) == m_processinglist.end()) { m_processinglist.insert(url); lock.Leave(); // cache the texture directly CTextureCacheJob job(url); bool success = job.CacheTexture(texture); OnCachingComplete(success, &job); if (success && details) *details = job.m_details; return success ? GetCachedPath(job.m_details.file) : ""; } lock.Leave(); // wait for currently processing job to end. while (true) { m_completeEvent.WaitMSec(1000); { CSingleLock lock(m_processingSection); if (m_processinglist.find(url) == m_processinglist.end()) break; } } CTextureDetails tempDetails; if (!details) details = &tempDetails; return GetCachedImage(url, *details, true); } void CTextureCache::ClearCachedImage(const CStdString &url, bool deleteSource /*= false */) { // TODO: This can be removed when the texture cache covers everything. CStdString path = deleteSource ? url : ""; CStdString cachedFile; if (ClearCachedTexture(url, cachedFile)) path = GetCachedPath(cachedFile); if (CFile::Exists(path)) CFile::Delete(path); path = URIUtils::ReplaceExtension(path, ".dds"); if (CFile::Exists(path)) CFile::Delete(path); } bool CTextureCache::ClearCachedImage(int id) { CStdString cachedFile; if (ClearCachedTexture(id, cachedFile)) { cachedFile = GetCachedPath(cachedFile); if (CFile::Exists(cachedFile)) CFile::Delete(cachedFile); cachedFile = URIUtils::ReplaceExtension(cachedFile, ".dds"); if (CFile::Exists(cachedFile)) CFile::Delete(cachedFile); return true; } return false; } bool CTextureCache::GetCachedTexture(const CStdString &url, CTextureDetails &details) { CSingleLock lock(m_databaseSection); return m_database.GetCachedTexture(url, details); } bool CTextureCache::AddCachedTexture(const CStdString &url, const CTextureDetails &details) { CSingleLock lock(m_databaseSection); return m_database.AddCachedTexture(url, details); } void CTextureCache::IncrementUseCount(const CTextureDetails &details) { static const size_t count_before_update = 100; CSingleLock lock(m_useCountSection); m_useCounts.reserve(count_before_update); m_useCounts.push_back(details); if (m_useCounts.size() >= count_before_update) { AddJob(new CTextureUseCountJob(m_useCounts)); m_useCounts.clear(); } } bool CTextureCache::SetCachedTextureValid(const CStdString &url, bool updateable) { CSingleLock lock(m_databaseSection); return m_database.SetCachedTextureValid(url, updateable); } bool CTextureCache::ClearCachedTexture(const CStdString &url, CStdString &cachedURL) { CSingleLock lock(m_databaseSection); return m_database.ClearCachedTexture(url, cachedURL); } bool CTextureCache::ClearCachedTexture(int id, CStdString &cachedURL) { CSingleLock lock(m_databaseSection); return m_database.ClearCachedTexture(id, cachedURL); } CStdString CTextureCache::GetCacheFile(const CStdString &url) { Crc32 crc; crc.ComputeFromLowerCase(url); CStdString hex = StringUtils::Format("%08x", (unsigned int)crc); CStdString hash = StringUtils::Format("%c/%s", hex[0], hex.c_str()); return hash; } CStdString CTextureCache::GetCachedPath(const CStdString &file) { return URIUtils::AddFileToFolder(CProfilesManager::Get().GetThumbnailsFolder(), file); } void CTextureCache::OnCachingComplete(bool success, CTextureCacheJob *job) { if (success) { if (job->m_oldHash == job->m_details.hash) SetCachedTextureValid(job->m_url, job->m_details.updateable); else AddCachedTexture(job->m_url, job->m_details); } { // remove from our processing list CSingleLock lock(m_processingSection); std::set::iterator i = m_processinglist.find(job->m_url); if (i != m_processinglist.end()) m_processinglist.erase(i); } m_completeEvent.Set(); // TODO: call back to the UI indicating that it can update it's image... if (success && g_advancedSettings.m_useDDSFanart && !job->m_details.file.empty()) AddJob(new CTextureDDSJob(GetCachedPath(job->m_details.file))); } void CTextureCache::OnJobComplete(unsigned int jobID, bool success, CJob *job) { if (strcmp(job->GetType(), kJobTypeCacheImage) == 0) OnCachingComplete(success, (CTextureCacheJob *)job); return CJobQueue::OnJobComplete(jobID, success, job); } void CTextureCache::OnJobProgress(unsigned int jobID, unsigned int progress, unsigned int total, const CJob *job) { if (strcmp(job->GetType(), kJobTypeCacheImage) == 0 && !progress) { // check our processing list { CSingleLock lock(m_processingSection); const CTextureCacheJob *cacheJob = (CTextureCacheJob *)job; std::set::iterator i = m_processinglist.find(cacheJob->m_url); if (i == m_processinglist.end()) { m_processinglist.insert(cacheJob->m_url); return; } } CancelJob(job); } else CJobQueue::OnJobProgress(jobID, progress, total, job); } bool CTextureCache::Export(const CStdString &image, const CStdString &destination, bool overwrite) { CTextureDetails details; CStdString cachedImage(GetCachedImage(image, details)); if (!cachedImage.empty()) { CStdString dest = destination + CStdString(URIUtils::GetExtension(cachedImage)); if (overwrite || !CFile::Exists(dest)) { if (CFile::Copy(cachedImage, dest)) return true; CLog::Log(LOGERROR, "%s failed exporting '%s' to '%s'", __FUNCTION__, cachedImage.c_str(), dest.c_str()); } } return false; } bool CTextureCache::Export(const CStdString &image, const CStdString &destination) { CTextureDetails details; CStdString cachedImage(GetCachedImage(image, details)); if (!cachedImage.empty()) { if (CFile::Copy(cachedImage, destination)) return true; CLog::Log(LOGERROR, "%s failed exporting '%s' to '%s'", __FUNCTION__, cachedImage.c_str(), destination.c_str()); } return false; }