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