1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
|
/*
* Copyright (C) 2005-2010 Team XBMC
* http://www.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, write to
* the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
* http://www.gnu.org/copyleft/gpl.html
*
*/
#include "TextureCache.h"
#include "filesystem/File.h"
#include "threads/SingleLock.h"
#include "utils/Crc32.h"
#include "settings/Settings.h"
#include "settings/AdvancedSettings.h"
#include "utils/log.h"
#include "guilib/Texture.h"
#include "guilib/DDSImage.h"
#include "pictures/Picture.h"
#include "guilib/TextureManager.h"
#include "utils/URIUtils.h"
using namespace XFILE;
CTextureCache::CCacheJob::CCacheJob(const CStdString &url, const CStdString &oldHash)
{
m_url = url;
m_oldHash = oldHash;
m_original = CTextureCache::GetCacheFile(m_url);
}
bool CTextureCache::CCacheJob::operator==(const CJob* job) const
{
if (strcmp(job->GetType(),GetType()) == 0)
{
const CCacheJob* cacheJob = dynamic_cast<const CCacheJob*>(job);
if (cacheJob && cacheJob->m_original == m_original)
return true;
}
return false;
}
bool CTextureCache::CCacheJob::DoWork()
{
m_hash = CacheImage(m_url, m_original, m_oldHash);
return !m_hash.IsEmpty();
}
CStdString CTextureCache::CCacheJob::CacheImage(const CStdString &url, const CStdString &original, const CStdString &oldHash)
{
// unwrap the URL as required
CStdString image(url);
bool fullSize = true;
if (0 == strncmp(url.c_str(), "thumb://", 8))
{
fullSize = false;
image = CURL(url).GetHostName();
CURL::Decode(image);
}
// generate the hash
CStdString hash = CTextureCache::Get().GetImageHash(image);
if (hash.IsEmpty() || hash == oldHash)
return hash;
if (!oldHash.IsEmpty())
CLog::Log(LOGDEBUG, "Re-caching image '%s' as '%s' %s size", image.c_str(), original.c_str(), fullSize ? "full" : "thumb");
else
CLog::Log(LOGDEBUG, "Caching image '%s' as '%s' %s size", image.c_str(), original.c_str(), fullSize ? "full" : "thumb");
CStdString originalURL = CTextureCache::GetCachedPath(original);
if (fullSize && CPicture::CacheFanart(image, originalURL))
return hash;
if (!fullSize && CPicture::CacheThumb(image, originalURL))
return hash;
return "";
}
CTextureCache::CDDSJob::CDDSJob(const CStdString &original)
{
m_original = original;
}
bool CTextureCache::CDDSJob::operator==(const CJob* job) const
{
if (strcmp(job->GetType(),GetType()) == 0)
{
const CDDSJob* ddsJob = dynamic_cast<const CDDSJob*>(job);
if (ddsJob && ddsJob->m_original == m_original)
return true;
}
return false;
}
bool CTextureCache::CDDSJob::DoWork()
{
CTexture texture;
if (URIUtils::GetExtension(m_original).Equals(".dds"))
return false;
if (texture.LoadFromFile(m_original))
{ // convert to DDS
CDDSImage dds;
CLog::Log(LOGDEBUG, "Creating DDS version of: %s", m_original.c_str());
return dds.Create(URIUtils::ReplaceExtension(m_original, ".dds"), texture.GetWidth(), texture.GetHeight(), texture.GetPitch(), texture.GetPixels(), 40);
}
return false;
}
CTextureCache &CTextureCache::Get()
{
static CTextureCache s_cache;
return s_cache;
}
CTextureCache::CTextureCache()
{
}
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, g_settings.GetThumbnailsFolder()))
return true;
return false;
}
CStdString CTextureCache::GetCachedImage(const CStdString &url)
{
if (IsCachedImage(url))
return url;
// lookup the item in the database
CStdString cacheFile;
if (GetCachedTexture(url, cacheFile))
return GetCachedPath(cacheFile);
return "";
}
CStdString CTextureCache::GetWrappedThumbURL(const CStdString &image)
{
CStdString url(image);
CURL::Encode(url);
return URIUtils::AddFileToFolder("thumb://" + url, URIUtils::GetFileName(image));
}
CStdString CTextureCache::CheckAndCacheImage(const CStdString &url, bool returnDDS)
{
CStdString path(GetCachedImage(url));
if (!path.IsEmpty())
{
if (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 CDDSJob(path));
}
return path;
}
// Uncached image - best we can do for now is cache it so that the texture manager
// can load it.
// TODO: In the future we need a cache job to callback when the image is loaded
// thus automatically updating the images. We'd also need fallback code inside
// the CGUITexture class to display something from this point on.
// Have this caching stuff be a lifo stack, and bump things up the stack when we should (check whether
// CJobQueue does this...) That way we can have a bunch of "cache thumb kthxpls" run from a background
// thread, and all we do here is cache from the size we've already been given. If we haven't been
// given a size, we must assume that the user wants fullsize. We could, in fact, add the sizing of it
// into the URL scheme using options... i.e. http://my.thumb/file|width=blah|height=foo
// that gives us sizing goodness, plus pre-caching goodness where we know shit should be cached
// all with the fandangled jobmanager....
// We almost need a better interface on the loading side of things - i.e. we need the textures to
// request a particular image and to get some (immediate?) recognition as to whether it's cached
// so that a fallback image can be specified. Perhaps the texture updating routines (i.e.
// UpdateInfo and/or SetFileName) might handle this? The procedure would be to hit the db
// to see if this image is available or not. If it isn't, then we simply load the fallback or
// "loading..." image, but keep testing to see if the image has been cached or not. Hmm,
// that's inefficient as well - at the very least a string compare or 12 per frame as it tests
// a list of caching jobs, or (more inefficiently) a db query every frame.
// The "best" method is the callback technique - this can be done quite easily though if the texture
// is the one that makes the request I think? At least when one texture is involved - we pass in our
// pointer to the callback list. In fact, we could generalize this somewhat with the texture
// manager handling those pointers - after all, it already handles reference counting, so why not
// count using the callback pointers instead? We then wouldn't have to call AllocResources() all
// the time. When the texture is loaded it's moved from the queued to the allocated list, and at
// that point we could have an interim list (loaded) that we then run the callbacks on once a frame.
// There'd be a "LOADING" enum for allocation of the image and the texture could then show a fallback?
// Main problem with this is CGUITexture doesn't have any concept of a fallback: CGUIImage does instead.
// The main fallback mechanism we use is LISTITEM_ICON vs LISTITEM_THUMB - with the former we actually
// use the thumb if it's available, and drop back to the icon if it's not. In either case, having
// a "loading" fallback would be useful even if it wasn't the icon. I guess this could be a property
// of CGUITexture similar to how background="true" is? The loading texture would be displayed if
// and only if there is an image being loaded. Need to talk to Jezz_X about this - eg if you have
// a current image and a new one is loading we currently hold on to the current one and render
// the current one faded out - we'd need to change this so that the fading only happened once it
// was ready to render.
CStdString originalFile = GetCacheFile(url);
CStdString hash = CCacheJob::CacheImage(url, originalFile);
if (!hash.IsEmpty())
{
AddCachedTexture(url, originalFile, hash);
if (g_advancedSettings.m_useDDSFanart)
AddJob(new CDDSJob(GetCachedPath(originalFile)));
return GetCachedPath(originalFile);
}
return "";
}
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::GetCachedTexture(const CStdString &url, CStdString &cachedURL)
{
CSingleLock lock(m_databaseSection);
CStdString imageHash;
if (m_database.GetCachedTexture(url, cachedURL, imageHash))
{
if (!imageHash.IsEmpty()) // check for an updated image
AddJob(new CCacheJob(url, imageHash));
return true;
}
return false;
}
bool CTextureCache::AddCachedTexture(const CStdString &url, const CStdString &cachedURL, const CStdString &hash)
{
CSingleLock lock(m_databaseSection);
return m_database.AddCachedTexture(url, cachedURL, hash);
}
bool CTextureCache::ClearCachedTexture(const CStdString &url, CStdString &cachedURL)
{
CSingleLock lock(m_databaseSection);
return m_database.ClearCachedTexture(url, cachedURL);
}
CStdString CTextureCache::GetImageHash(const CStdString &url) const
{
struct __stat64 st;
if (CFile::Stat(url, &st) == 0)
{
int64_t time = st.st_mtime;
if (!time)
time = st.st_ctime;
if (time || st.st_size)
{
CStdString hash;
hash.Format("d%"PRId64"s%"PRId64, time, st.st_size);
return hash;
}
}
CLog::Log(LOGDEBUG, "%s - unable to stat url %s", __FUNCTION__, url.c_str());
return "";
}
CStdString CTextureCache::GetCacheFile(const CStdString &url)
{
Crc32 crc;
crc.ComputeFromLowerCase(url);
CStdString hex;
hex.Format("%08x", (unsigned int)crc);
CStdString hash;
hash.Format("%c/%s%s", hex[0], hex.c_str(), URIUtils::GetExtension(url).c_str());
return hash;
}
CStdString CTextureCache::GetCachedPath(const CStdString &file)
{
return URIUtils::AddFileToFolder(g_settings.GetThumbnailsFolder(), file);
}
void CTextureCache::OnJobComplete(unsigned int jobID, bool success, CJob *job)
{
if (strcmp(job->GetType(), "cacheimage") == 0 && success)
{
CCacheJob *cacheJob = (CCacheJob *)job;
AddCachedTexture(cacheJob->m_url, cacheJob->m_original, cacheJob->m_hash);
// TODO: call back to the UI indicating that it can update it's image...
if (g_advancedSettings.m_useDDSFanart)
AddJob(new CDDSJob(GetCachedPath(cacheJob->m_original)));
}
return CJobQueue::OnJobComplete(jobID, success, job);
}
CStdString CTextureCache::GetUniqueImage(const CStdString &url, const CStdString &extension)
{
Crc32 crc;
crc.ComputeFromLowerCase(url);
CStdString hex;
hex.Format("%08x", (unsigned int)crc);
CStdString hash;
hash.Format("generated/%c/%s%s", hex[0], hex.c_str(), extension.c_str());
return GetCachedPath(hash);
}
|