2 * Copyright (C) 2005-2018 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
9 #include "TextureCache.h"
11 #include "ServiceBroker.h"
12 #include "TextureCacheJob.h"
14 #include "commons/ilog.h"
15 #include "dialogs/GUIDialogProgress.h"
16 #include "filesystem/File.h"
17 #include "filesystem/IFileTypes.h"
18 #include "guilib/GUIComponent.h"
19 #include "guilib/GUIWindowManager.h"
20 #include "guilib/Texture.h"
21 #include "imagefiles/ImageCacheCleaner.h"
22 #include "imagefiles/ImageFileURL.h"
23 #include "profiles/ProfileManager.h"
24 #include "settings/SettingsComponent.h"
25 #include "utils/Crc32.h"
26 #include "utils/Job.h"
27 #include "utils/JobManager.h"
28 #include "utils/StringUtils.h"
29 #include "utils/URIUtils.h"
30 #include "utils/log.h"
38 using namespace XFILE
;
39 using namespace std::chrono_literals
;
41 CTextureCache::CTextureCache()
42 : CJobQueue(false, 1, CJob::PRIORITY_LOW_PAUSABLE
), m_cleanTimer
{[this]() { CleanTimer(); }}
46 CTextureCache::~CTextureCache() = default;
48 void CTextureCache::Initialize()
50 m_cleanTimer
.Start(60s
);
51 std::unique_lock
<CCriticalSection
> lock(m_databaseSection
);
52 if (!m_database
.IsOpen())
56 void CTextureCache::Deinitialize()
60 std::unique_lock
<CCriticalSection
> lock(m_databaseSection
);
64 bool CTextureCache::IsCachedImage(const std::string
&url
) const
69 if (!CURL::IsFullPath(url
))
72 const std::shared_ptr
<CProfileManager
> profileManager
= CServiceBroker::GetSettingsComponent()->GetProfileManager();
74 return URIUtils::PathHasParent(url
, "special://skin", true) ||
75 URIUtils::PathHasParent(url
, "special://temp", true) ||
76 URIUtils::PathHasParent(url
, "resource://", true) ||
77 URIUtils::PathHasParent(url
, "androidapp://", true) ||
78 URIUtils::PathHasParent(url
, profileManager
->GetThumbnailsFolder(), true);
81 bool CTextureCache::HasCachedImage(const std::string
&url
)
83 CTextureDetails details
;
84 std::string
cachedImage(GetCachedImage(url
, details
));
85 return (!cachedImage
.empty() && cachedImage
!= url
);
88 std::string
CTextureCache::GetCachedImage(const std::string
&image
, CTextureDetails
&details
, bool trackUsage
)
90 std::string url
= IMAGE_FILES::ToCacheKey(image
);
93 if (IsCachedImage(url
))
96 // lookup the item in the database
97 if (GetCachedTexture(url
, details
))
99 if (details
.file
.empty())
103 IncrementUseCount(details
);
104 return GetCachedPath(details
.file
);
109 std::string
CTextureCache::CheckCachedImage(const std::string
&url
, bool &needsRecaching
)
111 CTextureDetails details
;
112 std::string
path(GetCachedImage(url
, details
, true));
113 needsRecaching
= !details
.hash
.empty();
119 void CTextureCache::BackgroundCacheImage(const std::string
&url
)
124 CTextureDetails details
;
125 std::string
path(GetCachedImage(url
, details
));
126 if (!path
.empty() && details
.hash
.empty())
127 return; // image is already cached and doesn't need to be checked further
129 path
= IMAGE_FILES::ToCacheKey(url
);
134 AddJob(new CTextureCacheJob(path
, details
.hash
));
137 bool CTextureCache::StartCacheImage(const std::string
& image
)
139 std::unique_lock
<CCriticalSection
> lock(m_processingSection
);
140 std::set
<std::string
>::iterator i
= m_processinglist
.find(image
);
141 if (i
== m_processinglist
.end())
143 m_processinglist
.insert(image
);
149 std::string
CTextureCache::CacheImage(const std::string
& image
,
150 std::unique_ptr
<CTexture
>* texture
/*= nullptr*/,
151 CTextureDetails
* details
/*= nullptr*/)
153 std::string url
= IMAGE_FILES::ToCacheKey(image
);
157 std::unique_lock
<CCriticalSection
> lock(m_processingSection
);
158 if (m_processinglist
.find(url
) == m_processinglist
.end())
160 m_processinglist
.insert(url
);
162 // cache the texture directly
163 CTextureCacheJob
job(url
);
164 bool success
= job
.CacheTexture(texture
);
165 OnCachingComplete(success
, &job
);
166 if (success
&& details
)
167 *details
= job
.m_details
;
168 return success
? GetCachedPath(job
.m_details
.file
) : "";
172 // wait for currently processing job to end.
175 m_completeEvent
.Wait(1000ms
);
177 std::unique_lock
<CCriticalSection
> lock(m_processingSection
);
178 if (m_processinglist
.find(url
) == m_processinglist
.end())
182 CTextureDetails tempDetails
;
184 details
= &tempDetails
;
186 std::string cachedpath
= GetCachedImage(url
, *details
, true);
187 if (!cachedpath
.empty())
190 *texture
= CTexture::LoadFromFile(cachedpath
, 0, 0);
194 CLog::Log(LOGDEBUG
, "CTextureCache::{} - Return NULL texture because cache is not ready",
201 bool CTextureCache::CacheImage(const std::string
&image
, CTextureDetails
&details
)
203 std::string path
= GetCachedImage(image
, details
);
204 if (path
.empty()) // not cached
205 path
= CacheImage(image
, NULL
, &details
);
207 return !path
.empty();
210 void CTextureCache::ClearCachedImage(const std::string
& image
, bool deleteSource
/*= false */)
212 //! @todo This can be removed when the texture cache covers everything.
213 const std::string url
= IMAGE_FILES::ToCacheKey(image
);
214 std::string path
= deleteSource
? url
: "";
215 std::string cachedFile
;
216 if (ClearCachedTexture(url
, cachedFile
))
217 path
= GetCachedPath(cachedFile
);
218 if (CFile::Exists(path
))
220 path
= URIUtils::ReplaceExtension(path
, ".dds");
221 if (CFile::Exists(path
))
225 bool CTextureCache::ClearCachedImage(int id
)
227 std::string cachedFile
;
228 if (ClearCachedTexture(id
, cachedFile
))
230 cachedFile
= GetCachedPath(cachedFile
);
231 if (CFile::Exists(cachedFile
))
232 CFile::Delete(cachedFile
);
233 cachedFile
= URIUtils::ReplaceExtension(cachedFile
, ".dds");
234 if (CFile::Exists(cachedFile
))
235 CFile::Delete(cachedFile
);
241 bool CTextureCache::GetCachedTexture(const std::string
&url
, CTextureDetails
&details
)
243 std::unique_lock
<CCriticalSection
> lock(m_databaseSection
);
244 return m_database
.GetCachedTexture(url
, details
);
247 bool CTextureCache::AddCachedTexture(const std::string
&url
, const CTextureDetails
&details
)
249 std::unique_lock
<CCriticalSection
> lock(m_databaseSection
);
250 return m_database
.AddCachedTexture(url
, details
);
253 void CTextureCache::IncrementUseCount(const CTextureDetails
&details
)
255 static const size_t count_before_update
= 100;
256 std::unique_lock
<CCriticalSection
> lock(m_useCountSection
);
257 m_useCounts
.reserve(count_before_update
);
258 m_useCounts
.push_back(details
);
259 if (m_useCounts
.size() >= count_before_update
)
261 AddJob(new CTextureUseCountJob(m_useCounts
));
266 bool CTextureCache::SetCachedTextureValid(const std::string
&url
, bool updateable
)
268 std::unique_lock
<CCriticalSection
> lock(m_databaseSection
);
269 return m_database
.SetCachedTextureValid(url
, updateable
);
272 bool CTextureCache::ClearCachedTexture(const std::string
&url
, std::string
&cachedURL
)
274 std::unique_lock
<CCriticalSection
> lock(m_databaseSection
);
275 return m_database
.ClearCachedTexture(url
, cachedURL
);
278 bool CTextureCache::ClearCachedTexture(int id
, std::string
&cachedURL
)
280 std::unique_lock
<CCriticalSection
> lock(m_databaseSection
);
281 return m_database
.ClearCachedTexture(id
, cachedURL
);
284 std::string
CTextureCache::GetCacheFile(const std::string
&url
)
286 auto crc
= Crc32::ComputeFromLowerCase(url
);
287 std::string hex
= StringUtils::Format("{:08x}", crc
);
288 std::string hash
= StringUtils::Format("{}/{}", hex
[0], hex
.c_str());
292 std::string
CTextureCache::GetCachedPath(const std::string
&file
)
294 const std::shared_ptr
<CProfileManager
> profileManager
= CServiceBroker::GetSettingsComponent()->GetProfileManager();
296 return URIUtils::AddFileToFolder(profileManager
->GetThumbnailsFolder(), file
);
299 void CTextureCache::OnCachingComplete(bool success
, CTextureCacheJob
*job
)
303 if (job
->m_details
.hashRevalidated
)
304 SetCachedTextureValid(job
->m_url
, job
->m_details
.updateable
);
306 AddCachedTexture(job
->m_url
, job
->m_details
);
309 { // remove from our processing list
310 std::unique_lock
<CCriticalSection
> lock(m_processingSection
);
311 std::set
<std::string
>::iterator i
= m_processinglist
.find(job
->m_url
);
312 if (i
!= m_processinglist
.end())
313 m_processinglist
.erase(i
);
316 m_completeEvent
.Set();
319 void CTextureCache::OnJobComplete(unsigned int jobID
, bool success
, CJob
*job
)
321 if (strcmp(job
->GetType(), kJobTypeCacheImage
) == 0)
322 OnCachingComplete(success
, static_cast<CTextureCacheJob
*>(job
));
323 return CJobQueue::OnJobComplete(jobID
, success
, job
);
326 bool CTextureCache::Export(const std::string
&image
, const std::string
&destination
, bool overwrite
)
328 CTextureDetails details
;
329 std::string
cachedImage(GetCachedImage(image
, details
));
330 if (!cachedImage
.empty())
332 std::string dest
= destination
+ URIUtils::GetExtension(cachedImage
);
333 if (overwrite
|| !CFile::Exists(dest
))
335 if (CFile::Copy(cachedImage
, dest
))
337 CLog::Log(LOGERROR
, "{} failed exporting '{}' to '{}'", __FUNCTION__
, cachedImage
, dest
);
343 bool CTextureCache::Export(const std::string
&image
, const std::string
&destination
)
345 CTextureDetails details
;
346 std::string
cachedImage(GetCachedImage(image
, details
));
347 if (!cachedImage
.empty())
349 if (CFile::Copy(cachedImage
, destination
))
351 CLog::Log(LOGERROR
, "{} failed exporting '{}' to '{}'", __FUNCTION__
, cachedImage
, destination
);
356 bool CTextureCache::CleanAllUnusedImages()
358 if (m_cleaningInProgress
.test_and_set())
361 auto progress
= CServiceBroker::GetGUI()->GetWindowManager().GetWindow
<CGUIDialogProgress
>(
362 WINDOW_DIALOG_PROGRESS
);
365 progress
->SetHeading(CVariant
{14281}); //"Clean image cache"
366 progress
->SetText(CVariant
{313}); //"Cleaning database"
367 progress
->SetPercentage(0);
369 progress
->ShowProgressBar(true);
372 bool failure
= false;
373 CServiceBroker::GetJobManager()->Submit([this, progress
, &failure
]()
374 { failure
= CleanAllUnusedImagesJob(progress
); });
376 // Wait for clean to complete or be canceled, but render every 10ms so that the
377 // pointer movements work on dialog even when clean is reporting progress infrequently
381 m_cleaningInProgress
.clear();
385 bool CTextureCache::CleanAllUnusedImagesJob(CGUIDialogProgress
* progress
)
387 auto cleaner
= IMAGE_FILES::CImageCacheCleaner::Create();
395 const unsigned int cleanAmount
= 1000000;
396 const auto result
= cleaner
->ScanOldestCache(cleanAmount
);
397 if (progress
&& progress
->IsCanceled())
403 const auto total
= result
.imagesToClean
.size();
404 unsigned int current
= 0;
405 for (const auto& image
: result
.imagesToClean
)
407 ClearCachedImage(image
);
410 if (progress
->IsCanceled())
415 int percentage
= static_cast<unsigned long long>(current
) * 100 / total
;
416 if (progress
->GetPercentage() != percentage
)
418 progress
->SetPercentage(percentage
);
419 progress
->Progress();
430 void CTextureCache::CleanTimer()
432 CServiceBroker::GetJobManager()->Submit(
435 auto next
= m_cleaningInProgress
.test_and_set() ? std::chrono::hours(1) : ScanOldestCache();
436 m_cleaningInProgress
.clear();
437 m_cleanTimer
.Start(next
);
439 CJob::PRIORITY_LOW_PAUSABLE
);
442 std::chrono::milliseconds
CTextureCache::ScanOldestCache()
444 auto cleaner
= IMAGE_FILES::CImageCacheCleaner::Create();
446 return std::chrono::hours(1);
448 const unsigned int cleanAmount
= 1000;
449 const auto result
= cleaner
->ScanOldestCache(cleanAmount
);
450 for (const auto& image
: result
.imagesToClean
)
452 ClearCachedImage(image
);
455 // update in the next 6 - 48 hours depending on number of items processed
456 const auto minTime
= 6;
457 const auto maxTime
= 24;
458 const auto zeroItemTime
= 48;
459 const unsigned int next
=
460 result
.processedCount
== 0
462 : minTime
+ (maxTime
- minTime
) *
463 (1 - (static_cast<float>(result
.processedCount
) / cleanAmount
));
464 CLog::LogF(LOGDEBUG
, "scheduling the next image cache cleaning in {} hours", next
);
465 return std::chrono::hours(next
);