Merge pull request #2973 from LS80/master
[vuplus_xbmc] / xbmc / TextureCache.cpp
1 /*
2  *      Copyright (C) 2005-2013 Team XBMC
3  *      http://xbmc.org
4  *
5  *  This Program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2, or (at your option)
8  *  any later version.
9  *
10  *  This Program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with XBMC; see the file COPYING.  If not, see
17  *  <http://www.gnu.org/licenses/>.
18  *
19  */
20
21 #include "TextureCache.h"
22 #include "TextureCacheJob.h"
23 #include "filesystem/File.h"
24 #include "profiles/ProfilesManager.h"
25 #include "threads/SingleLock.h"
26 #include "utils/Crc32.h"
27 #include "settings/AdvancedSettings.h"
28 #include "utils/log.h"
29 #include "utils/URIUtils.h"
30 #include "URL.h"
31 #include "utils/StringUtils.h"
32
33 using namespace XFILE;
34
35 CTextureCache &CTextureCache::Get()
36 {
37   static CTextureCache s_cache;
38   return s_cache;
39 }
40
41 CTextureCache::CTextureCache() : CJobQueue(false, 1, CJob::PRIORITY_LOW_PAUSABLE)
42 {
43 }
44
45 CTextureCache::~CTextureCache()
46 {
47 }
48
49 void CTextureCache::Initialize()
50 {
51   CSingleLock lock(m_databaseSection);
52   if (!m_database.IsOpen())
53     m_database.Open();
54 }
55
56 void CTextureCache::Deinitialize()
57 {
58   CancelJobs();
59   CSingleLock lock(m_databaseSection);
60   m_database.Close();
61 }
62
63 bool CTextureCache::IsCachedImage(const CStdString &url) const
64 {
65   if (url != "-" && !CURL::IsFullPath(url))
66     return true;
67   if (URIUtils::IsInPath(url, "special://skin/") ||
68       URIUtils::IsInPath(url, "androidapp://")   ||
69       URIUtils::IsInPath(url, CProfilesManager::Get().GetThumbnailsFolder()))
70     return true;
71   return false;
72 }
73
74 bool CTextureCache::HasCachedImage(const CStdString &url)
75 {
76   CTextureDetails details;
77   CStdString cachedImage(GetCachedImage(url, details));
78   return (!cachedImage.IsEmpty() && cachedImage != url);
79 }
80
81 CStdString CTextureCache::GetCachedImage(const CStdString &image, CTextureDetails &details, bool trackUsage)
82 {
83   CStdString url = UnwrapImageURL(image);
84
85   if (IsCachedImage(url))
86     return url;
87
88   // lookup the item in the database
89   if (GetCachedTexture(url, details))
90   {
91     if (trackUsage)
92       IncrementUseCount(details);
93     return GetCachedPath(details.file);
94   }
95   return "";
96 }
97
98 CStdString CTextureCache::GetWrappedImageURL(const CStdString &image, const CStdString &type, const CStdString &options)
99 {
100   if (StringUtils::StartsWith(image, "image://"))
101     return image; // already wrapped
102
103   CURL url;
104   url.SetProtocol("image");
105   url.SetUserName(type);
106   url.SetHostName(image);
107   if (!options.IsEmpty())
108   {
109     url.SetFileName("transform");
110     url.SetOptions("?" + options);
111   }
112   return url.Get();
113 }
114
115 CStdString CTextureCache::GetWrappedThumbURL(const CStdString &image)
116 {
117   return GetWrappedImageURL(image, "", "size=thumb");
118 }
119
120 CStdString CTextureCache::UnwrapImageURL(const CStdString &image)
121 {
122   if (StringUtils::StartsWith(image, "image://"))
123   {
124     CURL url(image);
125     if (url.GetUserName().IsEmpty() && url.GetOptions().IsEmpty())
126       return url.GetHostName();
127   }
128   return image;
129 }
130
131 bool CTextureCache::CanCacheImageURL(const CURL &url)
132 {
133   return (url.GetUserName().empty() || url.GetUserName() == "music");
134 }
135
136 CStdString CTextureCache::CheckCachedImage(const CStdString &url, bool returnDDS, bool &needsRecaching)
137 {
138   CTextureDetails details;
139   CStdString path(GetCachedImage(url, details, true));
140   needsRecaching = !details.hash.empty();
141   if (!path.IsEmpty())
142   {
143     if (!needsRecaching && returnDDS && !URIUtils::IsInPath(url, "special://skin/")) // TODO: should skin images be .dds'd (currently they're not necessarily writeable)
144     { // check for dds version
145       CStdString ddsPath = URIUtils::ReplaceExtension(path, ".dds");
146       if (CFile::Exists(ddsPath))
147         return ddsPath;
148       if (g_advancedSettings.m_useDDSFanart)
149         AddJob(new CTextureDDSJob(path));
150     }
151     return path;
152   }
153   return "";
154 }
155
156 void CTextureCache::BackgroundCacheImage(const CStdString &url)
157 {
158   CTextureDetails details;
159   CStdString path(GetCachedImage(url, details));
160   if (!path.IsEmpty() && details.hash.empty())
161     return; // image is already cached and doesn't need to be checked further
162
163   // needs (re)caching
164   AddJob(new CTextureCacheJob(UnwrapImageURL(url), details.hash));
165 }
166
167 bool CTextureCache::CacheImage(const CStdString &image, CTextureDetails &details)
168 {
169   CStdString path = GetCachedImage(image, details);
170   if (path.empty()) // not cached
171     path = CacheImage(image, NULL, &details);
172
173   return !path.empty();
174 }
175
176 CStdString CTextureCache::CacheImage(const CStdString &image, CBaseTexture **texture, CTextureDetails *details)
177 {
178   CStdString url = UnwrapImageURL(image);
179   CSingleLock lock(m_processingSection);
180   if (m_processing.find(url) == m_processing.end())
181   {
182     m_processing.insert(url);
183     lock.Leave();
184     // cache the texture directly
185     CTextureCacheJob job(url);
186     bool success = job.CacheTexture(texture);
187     OnCachingComplete(success, &job);
188     if (success && details)
189       *details = job.m_details;
190     return success ? GetCachedPath(job.m_details.file) : "";
191   }
192   lock.Leave();
193
194   // wait for currently processing job to end.
195   while (true)
196   {
197     m_completeEvent.WaitMSec(1000);
198     {
199       CSingleLock lock(m_processingSection);
200       if (m_processing.find(url) == m_processing.end())
201         break;
202     }
203   }
204   CTextureDetails tempDetails;
205   if (!details)
206     details = &tempDetails;
207   return GetCachedImage(url, *details, true);
208 }
209
210 void CTextureCache::ClearCachedImage(const CStdString &url, bool deleteSource /*= false */)
211 {
212   // TODO: This can be removed when the texture cache covers everything.
213   CStdString path = deleteSource ? url : "";
214   CStdString cachedFile;
215   if (ClearCachedTexture(url, cachedFile))
216     path = GetCachedPath(cachedFile);
217   if (CFile::Exists(path))
218     CFile::Delete(path);
219   path = URIUtils::ReplaceExtension(path, ".dds");
220   if (CFile::Exists(path))
221     CFile::Delete(path);
222 }
223
224 bool CTextureCache::GetCachedTexture(const CStdString &url, CTextureDetails &details)
225 {
226   CSingleLock lock(m_databaseSection);
227   return m_database.GetCachedTexture(url, details);
228 }
229
230 bool CTextureCache::AddCachedTexture(const CStdString &url, const CTextureDetails &details)
231 {
232   CSingleLock lock(m_databaseSection);
233   return m_database.AddCachedTexture(url, details);
234 }
235
236 void CTextureCache::IncrementUseCount(const CTextureDetails &details)
237 {
238   static const size_t count_before_update = 100;
239   CSingleLock lock(m_useCountSection);
240   m_useCounts.reserve(count_before_update);
241   m_useCounts.push_back(details);
242   if (m_useCounts.size() >= count_before_update)
243   {
244     AddJob(new CTextureUseCountJob(m_useCounts));
245     m_useCounts.clear();
246   }
247 }
248
249 bool CTextureCache::SetCachedTextureValid(const CStdString &url, bool updateable)
250 {
251   CSingleLock lock(m_databaseSection);
252   return m_database.SetCachedTextureValid(url, updateable);
253 }
254
255 bool CTextureCache::ClearCachedTexture(const CStdString &url, CStdString &cachedURL)
256 {
257   CSingleLock lock(m_databaseSection);
258   return m_database.ClearCachedTexture(url, cachedURL);
259 }
260
261 CStdString CTextureCache::GetCacheFile(const CStdString &url)
262 {
263   Crc32 crc;
264   crc.ComputeFromLowerCase(url);
265   CStdString hex;
266   hex.Format("%08x", (unsigned int)crc);
267   CStdString hash;
268   hash.Format("%c/%s", hex[0], hex.c_str());
269   return hash;
270 }
271
272 CStdString CTextureCache::GetCachedPath(const CStdString &file)
273 {
274   return URIUtils::AddFileToFolder(CProfilesManager::Get().GetThumbnailsFolder(), file);
275 }
276
277 void CTextureCache::OnCachingComplete(bool success, CTextureCacheJob *job)
278 {
279   if (success)
280   {
281     if (job->m_oldHash == job->m_details.hash)
282       SetCachedTextureValid(job->m_url, job->m_details.updateable);
283     else
284       AddCachedTexture(job->m_url, job->m_details);
285   }
286
287   { // remove from our processing list
288     CSingleLock lock(m_processingSection);
289     std::set<CStdString>::iterator i = m_processing.find(job->m_url);
290     if (i != m_processing.end())
291       m_processing.erase(i);
292   }
293
294   m_completeEvent.Set();
295
296   // TODO: call back to the UI indicating that it can update it's image...
297   if (success && g_advancedSettings.m_useDDSFanart && !job->m_details.file.empty())
298     AddJob(new CTextureDDSJob(GetCachedPath(job->m_details.file)));
299 }
300
301 void CTextureCache::OnJobComplete(unsigned int jobID, bool success, CJob *job)
302 {
303   if (strcmp(job->GetType(), kJobTypeCacheImage) == 0)
304     OnCachingComplete(success, (CTextureCacheJob *)job);
305   return CJobQueue::OnJobComplete(jobID, success, job);
306 }
307
308 void CTextureCache::OnJobProgress(unsigned int jobID, unsigned int progress, unsigned int total, const CJob *job)
309 {
310   if (strcmp(job->GetType(), kJobTypeCacheImage) == 0 && !progress)
311   { // check our processing list
312     {
313       CSingleLock lock(m_processingSection);
314       const CTextureCacheJob *cacheJob = (CTextureCacheJob *)job;
315       std::set<CStdString>::iterator i = m_processing.find(cacheJob->m_url);
316       if (i == m_processing.end())
317       {
318         m_processing.insert(cacheJob->m_url);
319         return;
320       }
321     }
322     CancelJob(job);
323   }
324   else
325     CJobQueue::OnJobProgress(jobID, progress, total, job);
326 }
327
328 bool CTextureCache::Export(const CStdString &image, const CStdString &destination, bool overwrite)
329 {
330   CTextureDetails details;
331   CStdString cachedImage(GetCachedImage(image, details));
332   if (!cachedImage.IsEmpty())
333   {
334     CStdString dest = destination + URIUtils::GetExtension(cachedImage);
335     if (overwrite || !CFile::Exists(dest))
336     {
337       if (CFile::Cache(cachedImage, dest))
338         return true;
339       CLog::Log(LOGERROR, "%s failed exporting '%s' to '%s'", __FUNCTION__, cachedImage.c_str(), dest.c_str());
340     }
341   }
342   return false;
343 }
344
345 bool CTextureCache::Export(const CStdString &image, const CStdString &destination)
346 {
347   CTextureDetails details;
348   CStdString cachedImage(GetCachedImage(image, details));
349   if (!cachedImage.IsEmpty())
350   {
351     if (CFile::Cache(cachedImage, destination))
352       return true;
353     CLog::Log(LOGERROR, "%s failed exporting '%s' to '%s'", __FUNCTION__, cachedImage.c_str(), destination.c_str());
354   }
355   return false;
356 }