[database] adds CreateAnalytics() for creation of indicies, triggers and views
[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 bool CTextureDatabase::UpdateOldVersion(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->dropIndex("path", "idxPath");
218     m_pDS->exec("DROP TABLE path");
219     m_pDS->exec("CREATE TABLE path (id integer primary key, urlhash integer, url text, type text, texture text)\n");
220     m_pDS->exec("CREATE INDEX idxPath ON path(urlhash, type)");
221   }
222   if (version < 10)
223   { // get rid of urlhash in both tables...
224     m_pDS->dropIndex("path", "idxPath");
225     m_pDS->exec("DROP TABLE path");
226     m_pDS->exec("CREATE TABLE path (id integer primary key, url text, type text, texture text)\n");
227     m_pDS->exec("CREATE INDEX idxPath ON path(url, type)");
228
229     m_pDS->dropIndex("texture", "idxTexture");
230     m_pDS->exec("CREATE TEMPORARY TABLE texture_backup(id,url,cachedurl,usecount,lastusetime,imagehash,lasthashcheck)");
231     m_pDS->exec("INSERT INTO texture_backup SELECT id,url,cachedurl,usecount,lastusetime,imagehash,lasthashcheck FROM texture");
232     m_pDS->exec("DROP TABLE texture");
233     m_pDS->exec("CREATE TABLE texture (id integer primary key, url text, cachedurl text, usecount integer, lastusetime text, imagehash text, lasthashcheck text)");
234     m_pDS->exec("CREATE INDEX idxTexture ON texture(url)");
235     m_pDS->exec("INSERT INTO texture SELECT * FROM texture_backup");
236     m_pDS->exec("DROP TABLE texture_backup");
237   }
238   if (version < 11)
239   { // get rid of cached URLs that don't have the correct extension
240     m_pDS->exec("DELETE FROM texture WHERE SUBSTR(cachedUrl,-4,4) NOT IN ('.jpg', '.png')");
241   }
242   if (version < 12)
243   { // create new sizes table and move usecount info to it.
244     m_pDS->exec("DROP TABLE texture");
245     m_pDS->exec("CREATE TABLE texture (id integer primary key, url text, cachedurl text, imagehash text, lasthashcheck text)");
246     m_pDS->exec("CREATE INDEX idxTexture ON texture(url)");
247     m_pDS->exec("CREATE TABLE sizes (idtexture integer, size integer, width integer, height integer, usecount integer, lastusetime text)");
248     m_pDS->exec("CREATE INDEX idxSize ON sizes(idtexture, size)");
249     m_pDS->exec("CREATE TRIGGER textureDelete AFTER delete ON texture FOR EACH ROW BEGIN delete from sizes where sizes.idtexture=old.id; END");
250   }
251   if (version < 13)
252   { // index for updateusecount
253     m_pDS->exec("CREATE INDEX idxSize2 ON sizes(idtexture, width, height)");
254   }
255   return true;
256 }
257
258 bool CTextureDatabase::IncrementUseCount(const CTextureDetails &details)
259 {
260   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);
261   return ExecuteQuery(sql);
262 }
263
264 bool CTextureDatabase::GetCachedTexture(const CStdString &url, CTextureDetails &details)
265 {
266   try
267   {
268     if (NULL == m_pDB.get()) return false;
269     if (NULL == m_pDS.get()) return false;
270
271     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());
272     m_pDS->query(sql.c_str());
273     if (!m_pDS->eof())
274     { // have some information
275       details.id = m_pDS->fv(0).get_asInt();
276       details.file  = m_pDS->fv(1).get_asString();
277       CDateTime lastCheck;
278       lastCheck.SetFromDBDateTime(m_pDS->fv(2).get_asString());
279       if (lastCheck.IsValid() && lastCheck + CDateTimeSpan(1,0,0,0) < CDateTime::GetCurrentDateTime())
280         details.hash = m_pDS->fv(3).get_asString();
281       details.width = m_pDS->fv(4).get_asInt();
282       details.height = m_pDS->fv(5).get_asInt();
283       m_pDS->close();
284       return true;
285     }
286     m_pDS->close();
287   }
288   catch (...)
289   {
290     CLog::Log(LOGERROR, "%s, failed on url '%s'", __FUNCTION__, url.c_str());
291   }
292   return false;
293 }
294
295 bool CTextureDatabase::GetTextures(CVariant &items, const Filter &filter)
296 {
297   try
298   {
299     if (NULL == m_pDB.get()) return false;
300     if (NULL == m_pDS.get()) return false;
301
302     CStdString sql = "SELECT %s FROM texture JOIN sizes ON (texture.id=sizes.idtexture AND sizes.size=1)";
303     CStdString sqlFilter;
304     if (!CDatabase::BuildSQL("", filter, sqlFilter))
305       return false;
306
307     sql = PrepareSQL(sql, !filter.fields.empty() ? filter.fields.c_str() : "*") + sqlFilter;
308     if (!m_pDS->query(sql.c_str()))
309       return false;
310
311     while (!m_pDS->eof())
312     {
313       CVariant texture;
314       texture["textureid"] = m_pDS->fv(0).get_asInt();
315       texture["url"] = m_pDS->fv(1).get_asString();
316       texture["cachedurl"] = m_pDS->fv(2).get_asString();
317       texture["imagehash"] = m_pDS->fv(3).get_asString();
318       texture["lasthashcheck"] = m_pDS->fv(4).get_asString();
319       CVariant size(CVariant::VariantTypeObject);
320       // 5 is sizes.idtexture
321       size["size"]  = m_pDS->fv(6).get_asInt();
322       size["width"] = m_pDS->fv(7).get_asInt();
323       size["height"] = m_pDS->fv(8).get_asInt();
324       size["usecount"] = m_pDS->fv(9).get_asInt();
325       size["lastused"] = m_pDS->fv(10).get_asString();
326       texture["sizes"] = CVariant(CVariant::VariantTypeArray);
327       texture["sizes"].push_back(size);
328       items.push_back(texture);
329       m_pDS->next();
330     }
331     m_pDS->close();
332     return true;
333   }
334   catch (...)
335   {
336     CLog::Log(LOGERROR, "%s, failed", __FUNCTION__);
337   }
338   return false;
339 }
340
341 bool CTextureDatabase::SetCachedTextureValid(const CStdString &url, bool updateable)
342 {
343   CStdString date = updateable ? CDateTime::GetCurrentDateTime().GetAsDBDateTime() : "";
344   CStdString sql = PrepareSQL("UPDATE texture SET lasthashcheck='%s' WHERE url='%s'", date.c_str(), url.c_str());
345   return ExecuteQuery(sql);
346 }
347
348 bool CTextureDatabase::AddCachedTexture(const CStdString &url, const CTextureDetails &details)
349 {
350   try
351   {
352     if (NULL == m_pDB.get()) return false;
353     if (NULL == m_pDS.get()) return false;
354
355     CStdString sql = PrepareSQL("DELETE FROM texture WHERE url='%s'", url.c_str());
356     m_pDS->exec(sql.c_str());
357
358     CStdString date = details.updateable ? CDateTime::GetCurrentDateTime().GetAsDBDateTime() : "";
359     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());
360     m_pDS->exec(sql.c_str());
361     int textureID = (int)m_pDS->lastinsertid();
362
363     // set the size information
364     sql = PrepareSQL("INSERT INTO sizes (idtexture, size, usecount, lastusetime, width, height) VALUES(%u, 1, 1, CURRENT_TIMESTAMP, %u, %u)", textureID, details.width, details.height);
365     m_pDS->exec(sql.c_str());
366   }
367   catch (...)
368   {
369     CLog::Log(LOGERROR, "%s failed on url '%s'", __FUNCTION__, url.c_str());
370   }
371   return true;
372 }
373
374 bool CTextureDatabase::ClearCachedTexture(const CStdString &url, CStdString &cacheFile)
375 {
376   std::string id = GetSingleValue(PrepareSQL("select id from texture where url='%s'", url.c_str()));
377   return !id.empty() ? ClearCachedTexture(strtol(id.c_str(), NULL, 10), cacheFile) : false;
378 }
379
380 bool CTextureDatabase::ClearCachedTexture(int id, CStdString &cacheFile)
381 {
382   try
383   {
384     if (NULL == m_pDB.get()) return false;
385     if (NULL == m_pDS.get()) return false;
386
387     CStdString sql = PrepareSQL("select cachedurl from texture where id=%u", id);
388     m_pDS->query(sql.c_str());
389
390     if (!m_pDS->eof())
391     { // have some information
392       cacheFile = m_pDS->fv(0).get_asString();
393       m_pDS->close();
394       // remove it
395       sql = PrepareSQL("delete from texture where id=%u", id);
396       m_pDS->exec(sql.c_str());
397       return true;
398     }
399     m_pDS->close();
400   }
401   catch (...)
402   {
403     CLog::Log(LOGERROR, "%s, failed on texture id %u", __FUNCTION__, id);
404   }
405   return false;
406 }
407
408 bool CTextureDatabase::InvalidateCachedTexture(const CStdString &url)
409 {
410   CStdString date = (CDateTime::GetCurrentDateTime() - CDateTimeSpan(2, 0, 0, 0)).GetAsDBDateTime();
411   CStdString sql = PrepareSQL("UPDATE texture SET lasthashcheck='%s' WHERE url='%s'", date.c_str(), url.c_str());
412   return ExecuteQuery(sql);
413 }
414
415 CStdString CTextureDatabase::GetTextureForPath(const CStdString &url, const CStdString &type)
416 {
417   try
418   {
419     if (NULL == m_pDB.get()) return "";
420     if (NULL == m_pDS.get()) return "";
421
422     if (url.empty())
423       return "";
424
425     CStdString sql = PrepareSQL("select texture from path where url='%s' and type='%s'", url.c_str(), type.c_str());
426     m_pDS->query(sql.c_str());
427
428     if (!m_pDS->eof())
429     { // have some information
430       CStdString texture = m_pDS->fv(0).get_asString();
431       m_pDS->close();
432       return texture;
433     }
434     m_pDS->close();
435   }
436   catch (...)
437   {
438     CLog::Log(LOGERROR, "%s, failed on url '%s'", __FUNCTION__, url.c_str());
439   }
440   return "";
441 }
442
443 void CTextureDatabase::SetTextureForPath(const CStdString &url, const CStdString &type, const CStdString &texture)
444 {
445   try
446   {
447     if (NULL == m_pDB.get()) return;
448     if (NULL == m_pDS.get()) return;
449
450     if (url.empty())
451       return;
452
453     CStdString sql = PrepareSQL("select id from path where url='%s' and type='%s'", url.c_str(), type.c_str());
454     m_pDS->query(sql.c_str());
455     if (!m_pDS->eof())
456     { // update
457       int pathID = m_pDS->fv(0).get_asInt();
458       m_pDS->close();
459       sql = PrepareSQL("update path set texture='%s' where id=%u", texture.c_str(), pathID);
460       m_pDS->exec(sql.c_str());
461     }
462     else
463     { // add the texture
464       m_pDS->close();
465       sql = PrepareSQL("insert into path (id, url, type, texture) values(NULL, '%s', '%s', '%s')", url.c_str(), type.c_str(), texture.c_str());
466       m_pDS->exec(sql.c_str());
467     }
468   }
469   catch (...)
470   {
471     CLog::Log(LOGERROR, "%s failed on url '%s'", __FUNCTION__, url.c_str());
472   }
473   return;
474 }
475
476 void CTextureDatabase::ClearTextureForPath(const CStdString &url, const CStdString &type)
477 {
478   try
479   {
480     if (NULL == m_pDB.get()) return;
481     if (NULL == m_pDS.get()) return;
482
483     CStdString sql = PrepareSQL("DELETE FROM path WHERE url='%s' and type='%s'", url.c_str(), type.c_str());
484     m_pDS->exec(sql.c_str());
485   }
486   catch (...)
487   {
488     CLog::Log(LOGERROR, "%s failed on url '%s'", __FUNCTION__, url.c_str());
489   }
490   return;
491 }
492
493 CDatabaseQueryRule *CTextureDatabase::CreateRule() const
494 {
495   return new CTextureRule();
496 }
497
498 CDatabaseQueryRuleCombination *CTextureDatabase::CreateCombination() const
499 {
500   return new CDatabaseQueryRuleCombination();
501 }