1 /*
2  *  Copyright (C) 2005-2018 Team Kodi
3  *  This file is part of Kodi - https://kodi.tv
4  *
5  *  SPDX-License-Identifier: GPL-2.0-or-later
6  *  See LICENSES/README.md for more information.
7  */
8 
9 #include "TextureCache.h"
10 
11 #include "ServiceBroker.h"
12 #include "TextureCacheJob.h"
13 #include "URL.h"
14 #include "filesystem/File.h"
15 #include "guilib/Texture.h"
16 #include "profiles/ProfileManager.h"
17 #include "settings/AdvancedSettings.h"
18 #include "settings/SettingsComponent.h"
19 #include "threads/SingleLock.h"
20 #include "utils/Crc32.h"
21 #include "utils/StringUtils.h"
22 #include "utils/URIUtils.h"
23 #include "utils/log.h"
24 
25 using namespace XFILE;
26 
GetInstance()27 CTextureCache &CTextureCache::GetInstance()
28 {
29   static CTextureCache s_cache;
30   return s_cache;
31 }
32 
CTextureCache()33 CTextureCache::CTextureCache() : CJobQueue(false, 1, CJob::PRIORITY_LOW_PAUSABLE)
34 {
35 }
36 
37 CTextureCache::~CTextureCache() = default;
38 
Initialize()39 void CTextureCache::Initialize()
40 {
41   CSingleLock lock(m_databaseSection);
42   if (!m_database.IsOpen())
43     m_database.Open();
44 }
45 
Deinitialize()46 void CTextureCache::Deinitialize()
47 {
48   CancelJobs();
49   CSingleLock lock(m_databaseSection);
50   m_database.Close();
51 }
52 
IsCachedImage(const std::string & url) const53 bool CTextureCache::IsCachedImage(const std::string &url) const
54 {
55   if (url.empty())
56     return false;
57 
58   if (!CURL::IsFullPath(url))
59     return true;
60 
61   const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
62 
63   return URIUtils::PathHasParent(url, "special://skin", true) ||
64       URIUtils::PathHasParent(url, "special://temp", true) ||
65       URIUtils::PathHasParent(url, "resource://", true) ||
66       URIUtils::PathHasParent(url, "androidapp://", true)   ||
67       URIUtils::PathHasParent(url, profileManager->GetThumbnailsFolder(), true);
68 }
69 
HasCachedImage(const std::string & url)70 bool CTextureCache::HasCachedImage(const std::string &url)
71 {
72   CTextureDetails details;
73   std::string cachedImage(GetCachedImage(url, details));
74   return (!cachedImage.empty() && cachedImage != url);
75 }
76 
GetCachedImage(const std::string & image,CTextureDetails & details,bool trackUsage)77 std::string CTextureCache::GetCachedImage(const std::string &image, CTextureDetails &details, bool trackUsage)
78 {
79   std::string url = CTextureUtils::UnwrapImageURL(image);
80   if (url.empty())
81     return "";
82   if (IsCachedImage(url))
83     return url;
84 
85   // lookup the item in the database
86   if (GetCachedTexture(url, details))
87   {
88     if (trackUsage)
89       IncrementUseCount(details);
90     return GetCachedPath(details.file);
91   }
92   return "";
93 }
94 
CanCacheImageURL(const CURL & url)95 bool CTextureCache::CanCacheImageURL(const CURL &url)
96 {
97   return url.GetUserName().empty() || url.GetUserName() == "music" ||
98           StringUtils::StartsWith(url.GetUserName(), "video_");
99 }
100 
CheckCachedImage(const std::string & url,bool & needsRecaching)101 std::string CTextureCache::CheckCachedImage(const std::string &url, bool &needsRecaching)
102 {
103   CTextureDetails details;
104   std::string path(GetCachedImage(url, details, true));
105   needsRecaching = !details.hash.empty();
106   if (!path.empty())
107     return path;
108   return "";
109 }
110 
BackgroundCacheImage(const std::string & url)111 void CTextureCache::BackgroundCacheImage(const std::string &url)
112 {
113   if (url.empty())
114     return;
115 
116   CTextureDetails details;
117   std::string path(GetCachedImage(url, details));
118   if (!path.empty() && details.hash.empty())
119     return; // image is already cached and doesn't need to be checked further
120 
121   path = CTextureUtils::UnwrapImageURL(url);
122   if (path.empty())
123     return;
124 
125   // needs (re)caching
126   AddJob(new CTextureCacheJob(path, details.hash));
127 }
128 
CacheImage(const std::string & image,CTexture ** texture,CTextureDetails * details)129 std::string CTextureCache::CacheImage(const std::string& image,
130                                       CTexture** texture /* = NULL */,
131                                       CTextureDetails* details /* = NULL */)
132 {
133   std::string url = CTextureUtils::UnwrapImageURL(image);
134   if (url.empty())
135     return "";
136 
137   CSingleLock lock(m_processingSection);
138   if (m_processinglist.find(url) == m_processinglist.end())
139   {
140     m_processinglist.insert(url);
141     lock.Leave();
142     // cache the texture directly
143     CTextureCacheJob job(url);
144     bool success = job.CacheTexture(texture);
145     OnCachingComplete(success, &job);
146     if (success && details)
147       *details = job.m_details;
148     return success ? GetCachedPath(job.m_details.file) : "";
149   }
150   lock.Leave();
151 
152   // wait for currently processing job to end.
153   while (true)
154   {
155     m_completeEvent.WaitMSec(1000);
156     {
157       CSingleLock lock(m_processingSection);
158       if (m_processinglist.find(url) == m_processinglist.end())
159         break;
160     }
161   }
162   CTextureDetails tempDetails;
163   if (!details)
164     details = &tempDetails;
165 
166   std::string cachedpath = GetCachedImage(url, *details, true);
167   if (!cachedpath.empty())
168   {
169     if (texture)
170       *texture = CTexture::LoadFromFile(cachedpath, 0, 0);
171   }
172   else
173   {
174     CLog::Log(LOGDEBUG, "CTextureCache::%s - Return NULL texture because cache is not ready",
175               __FUNCTION__);
176   }
177 
178   return cachedpath;
179 }
180 
CacheImage(const std::string & image,CTextureDetails & details)181 bool CTextureCache::CacheImage(const std::string &image, CTextureDetails &details)
182 {
183   std::string path = GetCachedImage(image, details);
184   if (path.empty()) // not cached
185     path = CacheImage(image, NULL, &details);
186 
187   return !path.empty();
188 }
189 
ClearCachedImage(const std::string & url,bool deleteSource)190 void CTextureCache::ClearCachedImage(const std::string &url, bool deleteSource /*= false */)
191 {
192   //! @todo This can be removed when the texture cache covers everything.
193   std::string path = deleteSource ? url : "";
194   std::string cachedFile;
195   if (ClearCachedTexture(url, cachedFile))
196     path = GetCachedPath(cachedFile);
197   if (CFile::Exists(path))
198     CFile::Delete(path);
199   path = URIUtils::ReplaceExtension(path, ".dds");
200   if (CFile::Exists(path))
201     CFile::Delete(path);
202 }
203 
ClearCachedImage(int id)204 bool CTextureCache::ClearCachedImage(int id)
205 {
206   std::string cachedFile;
207   if (ClearCachedTexture(id, cachedFile))
208   {
209     cachedFile = GetCachedPath(cachedFile);
210     if (CFile::Exists(cachedFile))
211       CFile::Delete(cachedFile);
212     cachedFile = URIUtils::ReplaceExtension(cachedFile, ".dds");
213     if (CFile::Exists(cachedFile))
214       CFile::Delete(cachedFile);
215     return true;
216   }
217   return false;
218 }
219 
GetCachedTexture(const std::string & url,CTextureDetails & details)220 bool CTextureCache::GetCachedTexture(const std::string &url, CTextureDetails &details)
221 {
222   CSingleLock lock(m_databaseSection);
223   return m_database.GetCachedTexture(url, details);
224 }
225 
AddCachedTexture(const std::string & url,const CTextureDetails & details)226 bool CTextureCache::AddCachedTexture(const std::string &url, const CTextureDetails &details)
227 {
228   CSingleLock lock(m_databaseSection);
229   return m_database.AddCachedTexture(url, details);
230 }
231 
IncrementUseCount(const CTextureDetails & details)232 void CTextureCache::IncrementUseCount(const CTextureDetails &details)
233 {
234   static const size_t count_before_update = 100;
235   CSingleLock lock(m_useCountSection);
236   m_useCounts.reserve(count_before_update);
237   m_useCounts.push_back(details);
238   if (m_useCounts.size() >= count_before_update)
239   {
240     AddJob(new CTextureUseCountJob(m_useCounts));
241     m_useCounts.clear();
242   }
243 }
244 
SetCachedTextureValid(const std::string & url,bool updateable)245 bool CTextureCache::SetCachedTextureValid(const std::string &url, bool updateable)
246 {
247   CSingleLock lock(m_databaseSection);
248   return m_database.SetCachedTextureValid(url, updateable);
249 }
250 
ClearCachedTexture(const std::string & url,std::string & cachedURL)251 bool CTextureCache::ClearCachedTexture(const std::string &url, std::string &cachedURL)
252 {
253   CSingleLock lock(m_databaseSection);
254   return m_database.ClearCachedTexture(url, cachedURL);
255 }
256 
ClearCachedTexture(int id,std::string & cachedURL)257 bool CTextureCache::ClearCachedTexture(int id, std::string &cachedURL)
258 {
259   CSingleLock lock(m_databaseSection);
260   return m_database.ClearCachedTexture(id, cachedURL);
261 }
262 
GetCacheFile(const std::string & url)263 std::string CTextureCache::GetCacheFile(const std::string &url)
264 {
265   auto crc = Crc32::ComputeFromLowerCase(url);
266   std::string hex = StringUtils::Format("%08x", crc);
267   std::string hash = StringUtils::Format("%c/%s", hex[0], hex.c_str());
268   return hash;
269 }
270 
GetCachedPath(const std::string & file)271 std::string CTextureCache::GetCachedPath(const std::string &file)
272 {
273   const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
274 
275   return URIUtils::AddFileToFolder(profileManager->GetThumbnailsFolder(), file);
276 }
277 
OnCachingComplete(bool success,CTextureCacheJob * job)278 void CTextureCache::OnCachingComplete(bool success, CTextureCacheJob *job)
279 {
280   if (success)
281   {
282     if (job->m_oldHash == job->m_details.hash)
283       SetCachedTextureValid(job->m_url, job->m_details.updateable);
284     else
285       AddCachedTexture(job->m_url, job->m_details);
286   }
287 
288   { // remove from our processing list
289     CSingleLock lock(m_processingSection);
290     std::set<std::string>::iterator i = m_processinglist.find(job->m_url);
291     if (i != m_processinglist.end())
292       m_processinglist.erase(i);
293   }
294 
295   m_completeEvent.Set();
296 }
297 
OnJobComplete(unsigned int jobID,bool success,CJob * job)298 void CTextureCache::OnJobComplete(unsigned int jobID, bool success, CJob *job)
299 {
300   if (strcmp(job->GetType(), kJobTypeCacheImage) == 0)
301     OnCachingComplete(success, static_cast<CTextureCacheJob*>(job));
302   return CJobQueue::OnJobComplete(jobID, success, job);
303 }
304 
OnJobProgress(unsigned int jobID,unsigned int progress,unsigned int total,const CJob * job)305 void CTextureCache::OnJobProgress(unsigned int jobID, unsigned int progress, unsigned int total, const CJob *job)
306 {
307   if (strcmp(job->GetType(), kJobTypeCacheImage) == 0 && !progress)
308   { // check our processing list
309     {
310       CSingleLock lock(m_processingSection);
311       const CTextureCacheJob *cacheJob = static_cast<const CTextureCacheJob*>(job);
312       std::set<std::string>::iterator i = m_processinglist.find(cacheJob->m_url);
313       if (i == m_processinglist.end())
314       {
315         m_processinglist.insert(cacheJob->m_url);
316         return;
317       }
318     }
319     CancelJob(job);
320   }
321   else
322     CJobQueue::OnJobProgress(jobID, progress, total, job);
323 }
324 
Export(const std::string & image,const std::string & destination,bool overwrite)325 bool CTextureCache::Export(const std::string &image, const std::string &destination, bool overwrite)
326 {
327   CTextureDetails details;
328   std::string cachedImage(GetCachedImage(image, details));
329   if (!cachedImage.empty())
330   {
331     std::string dest = destination + URIUtils::GetExtension(cachedImage);
332     if (overwrite || !CFile::Exists(dest))
333     {
334       if (CFile::Copy(cachedImage, dest))
335         return true;
336       CLog::Log(LOGERROR, "%s failed exporting '%s' to '%s'", __FUNCTION__, cachedImage.c_str(), dest.c_str());
337     }
338   }
339   return false;
340 }
341 
Export(const std::string & image,const std::string & destination)342 bool CTextureCache::Export(const std::string &image, const std::string &destination)
343 {
344   CTextureDetails details;
345   std::string cachedImage(GetCachedImage(image, details));
346   if (!cachedImage.empty())
347   {
348     if (CFile::Copy(cachedImage, destination))
349       return true;
350     CLog::Log(LOGERROR, "%s failed exporting '%s' to '%s'", __FUNCTION__, cachedImage.c_str(), destination.c_str());
351   }
352   return false;
353 }
354