Merge pull request #5039 from CEikermann/patch-1
[vuplus_xbmc] / xbmc / TextureDatabase.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 "TextureDatabase.h"
22 #include "utils/log.h"
23 #include "XBDateTime.h"
24 #include "dbwrappers/dataset.h"
25 #include "URL.h"
26 #include "utils/StringUtils.h"
27 #include "utils/Variant.h"
28
29 enum TextureField
30 {
31   TF_None = 0,
32   TF_Id,
33   TF_Url,
34   TF_CachedUrl,
35   TF_LastHashCheck,
36   TF_ImageHash,
37   TF_Width,
38   TF_Height,
39   TF_UseCount,
40   TF_LastUsed,
41   TF_Max
42 };
43
44 typedef struct
45 {
46   char string[14];
47   TextureField field;
48   CDatabaseQueryRule::FIELD_TYPE type;
49   bool browseable;
50   int localizedString;
51 } translateField;
52
53 static const translateField fields[] = {
54   { "none",          TF_None,          CDatabaseQueryRule::TEXT_FIELD    },
55   { "textureid",     TF_Id,            CDatabaseQueryRule::NUMERIC_FIELD },
56   { "url",           TF_Url,           CDatabaseQueryRule::TEXT_FIELD    },
57   { "cachedurl",     TF_CachedUrl,     CDatabaseQueryRule::TEXT_FIELD    },
58   { "lasthashcheck", TF_LastHashCheck, CDatabaseQueryRule::TEXT_FIELD    },
59   { "imagehash",     TF_ImageHash,     CDatabaseQueryRule::TEXT_FIELD    },
60   { "width",         TF_Width,         CDatabaseQueryRule::NUMERIC_FIELD },
61   { "height",        TF_Height,        CDatabaseQueryRule::NUMERIC_FIELD },
62   { "usecount",      TF_UseCount,      CDatabaseQueryRule::NUMERIC_FIELD },
63   { "lastused",      TF_LastUsed,      CDatabaseQueryRule::TEXT_FIELD    }
64 };
65
66 static const size_t NUM_FIELDS = sizeof(fields) / sizeof(translateField);
67
68 int CTextureRule::TranslateField(const char *field) const
69 {
70   for (unsigned int i = 0; i < NUM_FIELDS; i++)
71     if (StringUtils::EqualsNoCase(field, fields[i].string)) return fields[i].field;
72   return FieldNone;
73 }
74
75 CStdString CTextureRule::TranslateField(int field) const
76 {
77   for (unsigned int i = 0; i < NUM_FIELDS; i++)
78     if (field == fields[i].field) return fields[i].string;
79   return "none";
80 }
81
82 CStdString CTextureRule::GetField(int field, const CStdString &type) const
83 {
84   if (field == TF_Id) return "texture.id";
85   else if (field == TF_Url) return "texture.url";
86   else if (field == TF_CachedUrl) return "texture.cachedurl";
87   else if (field == TF_LastHashCheck) return "texture.lasthashcheck";
88   else if (field == TF_ImageHash) return "texture.imagehash";
89   else if (field == TF_Width) return "sizes.width";
90   else if (field == TF_Height) return "sizes.height";
91   else if (field == TF_UseCount) return "sizes.usecount";
92   else if (field == TF_LastUsed) return "sizes.lastusetime";
93   return "";
94 }
95
96 CDatabaseQueryRule::FIELD_TYPE CTextureRule::GetFieldType(int field) const
97 {
98   for (unsigned int i = 0; i < NUM_FIELDS; i++)
99     if (field == fields[i].field) return fields[i].type;
100   return TEXT_FIELD;
101 }
102
103 CStdString CTextureRule::FormatParameter(const CStdString &operatorString, const CStdString &param, const CDatabase &db, const CStdString &strType) const
104 {
105   CStdString parameter(param);
106   if (m_field == TF_Url)
107     parameter = CTextureUtils::UnwrapImageURL(param);
108   return CDatabaseQueryRule::FormatParameter(operatorString, parameter, db, strType);
109 }
110
111 void CTextureRule::GetAvailableFields(std::vector<std::string> &fieldList)
112 {
113   // start at 1 to skip TF_None
114   for (unsigned int i = 1; i < NUM_FIELDS; i++)
115     fieldList.push_back(fields[i].string);
116 }
117
118 CStdString CTextureUtils::GetWrappedImageURL(const CStdString &image, const CStdString &type, const CStdString &options)
119 {
120   if (StringUtils::StartsWith(image, "image://"))
121     return image; // already wrapped
122
123   CURL url;
124   url.SetProtocol("image");
125   url.SetUserName(type);
126   url.SetHostName(image);
127   if (!options.empty())
128   {
129     url.SetFileName("transform");
130     url.SetOptions("?" + options);
131   }
132   return url.Get();
133 }
134
135 CStdString CTextureUtils::GetWrappedThumbURL(const CStdString &image)
136 {
137   return GetWrappedImageURL(image, "", "size=thumb");
138 }
139
140 CStdString CTextureUtils::UnwrapImageURL(const CStdString &image)
141 {
142   if (StringUtils::StartsWith(image, "image://"))
143   {
144     CURL url(image);
145     if (url.GetUserName().empty() && url.GetOptions().empty())
146       return url.GetHostName();
147   }
148   return image;
149 }
150
151 CTextureDatabase::CTextureDatabase()
152 {
153 }
154
155 CTextureDatabase::~CTextureDatabase()
156 {
157 }
158
159 bool CTextureDatabase::Open()
160 {
161   return CDatabase::Open();
162 }
163
164 void CTextureDatabase::CreateTables()
165 {
166   CLog::Log(LOGINFO, "create texture table");
167   m_pDS->exec("CREATE TABLE texture (id integer primary key, url text, cachedurl text, imagehash text, lasthashcheck text)");
168
169   CLog::Log(LOGINFO, "create sizes table, index,  and trigger");
170   m_pDS->exec("CREATE TABLE sizes (idtexture integer, size integer, width integer, height integer, usecount integer, lastusetime text)");
171
172   CLog::Log(LOGINFO, "create path table");
173   m_pDS->exec("CREATE TABLE path (id integer primary key, url text, type text, texture text)\n");
174 }
175
176 void CTextureDatabase::CreateAnalytics()
177 {
178   CLog::Log(LOGINFO, "%s creating indices", __FUNCTION__);
179   m_pDS->exec("CREATE INDEX idxTexture ON texture(url)");
180   m_pDS->exec("CREATE INDEX idxSize ON sizes(idtexture, size)");
181   m_pDS->exec("CREATE INDEX idxSize2 ON sizes(idtexture, width, height)");
182   // TODO: Should the path index be a covering index? (we need only retrieve texture)
183   m_pDS->exec("CREATE INDEX idxPath ON path(url, type)");
184
185   CLog::Log(LOGINFO, "%s creating triggers", __FUNCTION__);
186   m_pDS->exec("CREATE TRIGGER textureDelete AFTER delete ON texture FOR EACH ROW BEGIN delete from sizes where sizes.idtexture=old.id; END");
187 }
188
189 void CTextureDatabase::UpdateTables(int version)
190 {
191   if (version < 7)
192   { // update all old thumb://foo urls to image://foo?size=thumb
193     m_pDS->query("select id,texture from path where texture like 'thumb://%'");
194     while (!m_pDS->eof())
195     {
196       unsigned int id = m_pDS->fv(0).get_asInt();
197       CURL url(m_pDS->fv(1).get_asString());
198       m_pDS2->exec(PrepareSQL("update path set texture='image://%s?size=thumb' where id=%u", url.GetHostName().c_str(), id));
199       m_pDS->next();
200     }
201     m_pDS->query("select id, url from texture where url like 'thumb://%'");
202     while (!m_pDS->eof())
203     {
204       unsigned int id = m_pDS->fv(0).get_asInt();
205       CURL url(m_pDS->fv(1).get_asString());
206       m_pDS2->exec(PrepareSQL("update texture set url='image://%s?size=thumb', urlhash=0 where id=%u", url.GetHostName().c_str(), id));
207       m_pDS->next();
208     }
209     m_pDS->close();
210   }
211   if (version < 8)
212   { // get rid of old cached thumbs as they were previously set to the cached thumb name instead of the source thumb
213     m_pDS->exec("delete from path");
214   }
215   if (version < 9)
216   { // get rid of the old path table and add the type column
217     m_pDS->exec("DROP TABLE IF EXISTS path");
218     m_pDS->exec("CREATE TABLE path (id integer primary key, urlhash integer, url text, type text, texture text)\n");
219   }
220   if (version < 10)
221   { // get rid of urlhash in both tables...
222     m_pDS->exec("DROP TABLE IF EXISTS path");
223     m_pDS->exec("CREATE TABLE path (id integer primary key, url text, type text, texture text)\n");
224
225     m_pDS->exec("CREATE TEMPORARY TABLE texture_backup(id,url,cachedurl,usecount,lastusetime,imagehash,lasthashcheck)");
226     m_pDS->exec("INSERT INTO texture_backup SELECT id,url,cachedurl,usecount,lastusetime,imagehash,lasthashcheck FROM texture");
227     m_pDS->exec("DROP TABLE texture");
228     m_pDS->exec("CREATE TABLE texture (id integer primary key, url text, cachedurl text, usecount integer, lastusetime text, imagehash text, lasthashcheck text)");
229     m_pDS->exec("INSERT INTO texture SELECT * FROM texture_backup");
230     m_pDS->exec("DROP TABLE texture_backup");
231   }
232   if (version < 11)
233   { // get rid of cached URLs that don't have the correct extension
234     m_pDS->exec("DELETE FROM texture WHERE SUBSTR(cachedUrl,-4,4) NOT IN ('.jpg', '.png')");
235   }
236   if (version < 12)
237   { // create new sizes table and move usecount info to it.
238     m_pDS->exec("DROP TABLE IF EXISTS texture");
239     m_pDS->exec("CREATE TABLE texture (id integer primary key, url text, cachedurl text, imagehash text, lasthashcheck text)");
240     m_pDS->exec("CREATE TABLE sizes (idtexture integer, size integer, width integer, height integer, usecount integer, lastusetime text)");
241   }
242 }
243
244 bool CTextureDatabase::IncrementUseCount(const CTextureDetails &details)
245 {
246   CStdString sql = PrepareSQL("UPDATE sizes SET usecount=usecount+1, lastusetime=CURRENT_TIMESTAMP WHERE idtexture=%u AND width=%u AND height=%u", details.id, details.width, details.height);
247   return ExecuteQuery(sql);
248 }
249
250 bool CTextureDatabase::GetCachedTexture(const CStdString &url, CTextureDetails &details)
251 {
252   try
253   {
254     if (NULL == m_pDB.get()) return false;
255     if (NULL == m_pDS.get()) return false;
256
257     CStdString sql = PrepareSQL("SELECT id, cachedurl, lasthashcheck, imagehash, width, height FROM texture JOIN sizes ON (texture.id=sizes.idtexture AND sizes.size=1) WHERE url='%s'", url.c_str());
258     m_pDS->query(sql.c_str());
259     if (!m_pDS->eof())
260     { // have some information
261       details.id = m_pDS->fv(0).get_asInt();
262       details.file  = m_pDS->fv(1).get_asString();
263       CDateTime lastCheck;
264       lastCheck.SetFromDBDateTime(m_pDS->fv(2).get_asString());
265       if (lastCheck.IsValid() && lastCheck + CDateTimeSpan(1,0,0,0) < CDateTime::GetCurrentDateTime())
266         details.hash = m_pDS->fv(3).get_asString();
267       details.width = m_pDS->fv(4).get_asInt();
268       details.height = m_pDS->fv(5).get_asInt();
269       m_pDS->close();
270       return true;
271     }
272     m_pDS->close();
273   }
274   catch (...)
275   {
276     CLog::Log(LOGERROR, "%s, failed on url '%s'", __FUNCTION__, url.c_str());
277   }
278   return false;
279 }
280
281 bool CTextureDatabase::GetTextures(CVariant &items, const Filter &filter)
282 {
283   try
284   {
285     if (NULL == m_pDB.get()) return false;
286     if (NULL == m_pDS.get()) return false;
287
288     CStdString sql = "SELECT %s FROM texture JOIN sizes ON (texture.id=sizes.idtexture AND sizes.size=1)";
289     CStdString sqlFilter;
290     if (!CDatabase::BuildSQL("", filter, sqlFilter))
291       return false;
292
293     sql = PrepareSQL(sql, !filter.fields.empty() ? filter.fields.c_str() : "*") + sqlFilter;
294     if (!m_pDS->query(sql.c_str()))
295       return false;
296
297     while (!m_pDS->eof())
298     {
299       CVariant texture;
300       texture["textureid"] = m_pDS->fv(0).get_asInt();
301       texture["url"] = m_pDS->fv(1).get_asString();
302       texture["cachedurl"] = m_pDS->fv(2).get_asString();
303       texture["imagehash"] = m_pDS->fv(3).get_asString();
304       texture["lasthashcheck"] = m_pDS->fv(4).get_asString();
305       CVariant size(CVariant::VariantTypeObject);
306       // 5 is sizes.idtexture
307       size["size"]  = m_pDS->fv(6).get_asInt();
308       size["width"] = m_pDS->fv(7).get_asInt();
309       size["height"] = m_pDS->fv(8).get_asInt();
310       size["usecount"] = m_pDS->fv(9).get_asInt();
311       size["lastused"] = m_pDS->fv(10).get_asString();
312       texture["sizes"] = CVariant(CVariant::VariantTypeArray);
313       texture["sizes"].push_back(size);
314       items.push_back(texture);
315       m_pDS->next();
316     }
317     m_pDS->close();
318     return true;
319   }
320   catch (...)
321   {
322     CLog::Log(LOGERROR, "%s, failed", __FUNCTION__);
323   }
324   return false;
325 }
326
327 bool CTextureDatabase::SetCachedTextureValid(const CStdString &url, bool updateable)
328 {
329   CStdString date = updateable ? CDateTime::GetCurrentDateTime().GetAsDBDateTime() : "";
330   CStdString sql = PrepareSQL("UPDATE texture SET lasthashcheck='%s' WHERE url='%s'", date.c_str(), url.c_str());
331   return ExecuteQuery(sql);
332 }
333
334 bool CTextureDatabase::AddCachedTexture(const CStdString &url, const CTextureDetails &details)
335 {
336   try
337   {
338     if (NULL == m_pDB.get()) return false;
339     if (NULL == m_pDS.get()) return false;
340
341     CStdString sql = PrepareSQL("DELETE FROM texture WHERE url='%s'", url.c_str());
342     m_pDS->exec(sql.c_str());
343
344     CStdString date = details.updateable ? CDateTime::GetCurrentDateTime().GetAsDBDateTime() : "";
345     sql = PrepareSQL("INSERT INTO texture (id, url, cachedurl, imagehash, lasthashcheck) VALUES(NULL, '%s', '%s', '%s', '%s')", url.c_str(), details.file.c_str(), details.hash.c_str(), date.c_str());
346     m_pDS->exec(sql.c_str());
347     int textureID = (int)m_pDS->lastinsertid();
348
349     // set the size information
350     sql = PrepareSQL("INSERT INTO sizes (idtexture, size, usecount, lastusetime, width, height) VALUES(%u, 1, 1, CURRENT_TIMESTAMP, %u, %u)", textureID, details.width, details.height);
351     m_pDS->exec(sql.c_str());
352   }
353   catch (...)
354   {
355     CLog::Log(LOGERROR, "%s failed on url '%s'", __FUNCTION__, url.c_str());
356   }
357   return true;
358 }
359
360 bool CTextureDatabase::ClearCachedTexture(const CStdString &url, CStdString &cacheFile)
361 {
362   std::string id = GetSingleValue(PrepareSQL("select id from texture where url='%s'", url.c_str()));
363   return !id.empty() ? ClearCachedTexture(strtol(id.c_str(), NULL, 10), cacheFile) : false;
364 }
365
366 bool CTextureDatabase::ClearCachedTexture(int id, CStdString &cacheFile)
367 {
368   try
369   {
370     if (NULL == m_pDB.get()) return false;
371     if (NULL == m_pDS.get()) return false;
372
373     CStdString sql = PrepareSQL("select cachedurl from texture where id=%u", id);
374     m_pDS->query(sql.c_str());
375
376     if (!m_pDS->eof())
377     { // have some information
378       cacheFile = m_pDS->fv(0).get_asString();
379       m_pDS->close();
380       // remove it
381       sql = PrepareSQL("delete from texture where id=%u", id);
382       m_pDS->exec(sql.c_str());
383       return true;
384     }
385     m_pDS->close();
386   }
387   catch (...)
388   {
389     CLog::Log(LOGERROR, "%s, failed on texture id %u", __FUNCTION__, id);
390   }
391   return false;
392 }
393
394 bool CTextureDatabase::InvalidateCachedTexture(const CStdString &url)
395 {
396   CStdString date = (CDateTime::GetCurrentDateTime() - CDateTimeSpan(2, 0, 0, 0)).GetAsDBDateTime();
397   CStdString sql = PrepareSQL("UPDATE texture SET lasthashcheck='%s' WHERE url='%s'", date.c_str(), url.c_str());
398   return ExecuteQuery(sql);
399 }
400
401 CStdString CTextureDatabase::GetTextureForPath(const CStdString &url, const CStdString &type)
402 {
403   try
404   {
405     if (NULL == m_pDB.get()) return "";
406     if (NULL == m_pDS.get()) return "";
407
408     if (url.empty())
409       return "";
410
411     CStdString sql = PrepareSQL("select texture from path where url='%s' and type='%s'", url.c_str(), type.c_str());
412     m_pDS->query(sql.c_str());
413
414     if (!m_pDS->eof())
415     { // have some information
416       CStdString texture = m_pDS->fv(0).get_asString();
417       m_pDS->close();
418       return texture;
419     }
420     m_pDS->close();
421   }
422   catch (...)
423   {
424     CLog::Log(LOGERROR, "%s, failed on url '%s'", __FUNCTION__, url.c_str());
425   }
426   return "";
427 }
428
429 void CTextureDatabase::SetTextureForPath(const CStdString &url, const CStdString &type, const CStdString &texture)
430 {
431   try
432   {
433     if (NULL == m_pDB.get()) return;
434     if (NULL == m_pDS.get()) return;
435
436     if (url.empty())
437       return;
438
439     CStdString sql = PrepareSQL("select id from path where url='%s' and type='%s'", url.c_str(), type.c_str());
440     m_pDS->query(sql.c_str());
441     if (!m_pDS->eof())
442     { // update
443       int pathID = m_pDS->fv(0).get_asInt();
444       m_pDS->close();
445       sql = PrepareSQL("update path set texture='%s' where id=%u", texture.c_str(), pathID);
446       m_pDS->exec(sql.c_str());
447     }
448     else
449     { // add the texture
450       m_pDS->close();
451       sql = PrepareSQL("insert into path (id, url, type, texture) values(NULL, '%s', '%s', '%s')", url.c_str(), type.c_str(), texture.c_str());
452       m_pDS->exec(sql.c_str());
453     }
454   }
455   catch (...)
456   {
457     CLog::Log(LOGERROR, "%s failed on url '%s'", __FUNCTION__, url.c_str());
458   }
459   return;
460 }
461
462 void CTextureDatabase::ClearTextureForPath(const CStdString &url, const CStdString &type)
463 {
464   try
465   {
466     if (NULL == m_pDB.get()) return;
467     if (NULL == m_pDS.get()) return;
468
469     CStdString sql = PrepareSQL("DELETE FROM path WHERE url='%s' and type='%s'", url.c_str(), type.c_str());
470     m_pDS->exec(sql.c_str());
471   }
472   catch (...)
473   {
474     CLog::Log(LOGERROR, "%s failed on url '%s'", __FUNCTION__, url.c_str());
475   }
476   return;
477 }
478
479 CDatabaseQueryRule *CTextureDatabase::CreateRule() const
480 {
481   return new CTextureRule();
482 }
483
484 CDatabaseQueryRuleCombination *CTextureDatabase::CreateCombination() const
485 {
486   return new CDatabaseQueryRuleCombination();
487 }