1 /*
2  *  Copyright (C) 2012-2018 Team Kodi
3  *  This file is part of Kodi - https://kodi.tv
4  *
5  *  SPDX-License-Identifier: GPL-2.0-or-later
6  *  See LICENSES/README.md for more information.
7  */
8 
9 #include "TextureCacheJob.h"
10 #include "ServiceBroker.h"
11 #include "TextureCache.h"
12 #include "guilib/Texture.h"
13 #include "settings/AdvancedSettings.h"
14 #include "settings/SettingsComponent.h"
15 #include "utils/log.h"
16 #include "filesystem/File.h"
17 #include "pictures/Picture.h"
18 #include "utils/URIUtils.h"
19 #include "utils/StringUtils.h"
20 #include "video/VideoThumbLoader.h"
21 #include "URL.h"
22 #include "FileItem.h"
23 #include "music/MusicThumbLoader.h"
24 #include "music/tags/MusicInfoTag.h"
25 
26 #include <inttypes.h>
27 
CTextureCacheJob(const std::string & url,const std::string & oldHash)28 CTextureCacheJob::CTextureCacheJob(const std::string &url, const std::string &oldHash):
29   m_url(url),
30   m_oldHash(oldHash),
31   m_cachePath(CTextureCache::GetCacheFile(m_url))
32 {
33 }
34 
35 CTextureCacheJob::~CTextureCacheJob() = default;
36 
operator ==(const CJob * job) const37 bool CTextureCacheJob::operator==(const CJob* job) const
38 {
39   if (strcmp(job->GetType(),GetType()) == 0)
40   {
41     const CTextureCacheJob* cacheJob = dynamic_cast<const CTextureCacheJob*>(job);
42     if (cacheJob && cacheJob->m_cachePath == m_cachePath)
43       return true;
44   }
45   return false;
46 }
47 
DoWork()48 bool CTextureCacheJob::DoWork()
49 {
50   if (ShouldCancel(0, 0))
51     return false;
52   if (ShouldCancel(1, 0)) // HACK: second check is because we cancel the job in the first callback, but we don't detect it
53     return false;         //       until the second
54 
55   // check whether we need cache the job anyway
56   bool needsRecaching = false;
57   std::string path(CTextureCache::GetInstance().CheckCachedImage(m_url, needsRecaching));
58   if (!path.empty() && !needsRecaching)
59     return false;
60   return CacheTexture();
61 }
62 
CacheTexture(CTexture ** out_texture)63 bool CTextureCacheJob::CacheTexture(CTexture** out_texture)
64 {
65   // unwrap the URL as required
66   std::string additional_info;
67   unsigned int width, height;
68   CPictureScalingAlgorithm::Algorithm scalingAlgorithm;
69   std::string image = DecodeImageURL(m_url, width, height, scalingAlgorithm, additional_info);
70 
71   m_details.updateable = additional_info != "music" && UpdateableURL(image);
72 
73   // generate the hash
74   m_details.hash = GetImageHash(image);
75   if (m_details.hash.empty())
76     return false;
77   else if (m_details.hash == m_oldHash)
78     return true;
79 
80   CTexture* texture = LoadImage(image, width, height, additional_info, true);
81   if (texture)
82   {
83     if (texture->HasAlpha())
84       m_details.file = m_cachePath + ".png";
85     else
86       m_details.file = m_cachePath + ".jpg";
87 
88     CLog::Log(LOGDEBUG, "%s image '%s' to '%s':", m_oldHash.empty() ? "Caching" : "Recaching", CURL::GetRedacted(image).c_str(), m_details.file.c_str());
89 
90     if (CPicture::CacheTexture(texture, width, height, CTextureCache::GetCachedPath(m_details.file), scalingAlgorithm))
91     {
92       m_details.width = width;
93       m_details.height = height;
94       if (out_texture) // caller wants the texture
95         *out_texture = texture;
96       else
97         delete texture;
98       return true;
99     }
100   }
101   delete texture;
102   return false;
103 }
104 
ResizeTexture(const std::string & url,uint8_t * & result,size_t & result_size)105 bool CTextureCacheJob::ResizeTexture(const std::string &url, uint8_t* &result, size_t &result_size)
106 {
107   result = NULL;
108   result_size = 0;
109 
110   if (url.empty())
111     return false;
112 
113   // unwrap the URL as required
114   std::string additional_info;
115   unsigned int width, height;
116   CPictureScalingAlgorithm::Algorithm scalingAlgorithm;
117   std::string image = DecodeImageURL(url, width, height, scalingAlgorithm, additional_info);
118   if (image.empty())
119     return false;
120 
121   CTexture* texture = LoadImage(image, width, height, additional_info, true);
122   if (texture == NULL)
123     return false;
124 
125   bool success = CPicture::ResizeTexture(image, texture, width, height, result, result_size, scalingAlgorithm);
126   delete texture;
127 
128   return success;
129 }
130 
DecodeImageURL(const std::string & url,unsigned int & width,unsigned int & height,CPictureScalingAlgorithm::Algorithm & scalingAlgorithm,std::string & additional_info)131 std::string CTextureCacheJob::DecodeImageURL(const std::string &url, unsigned int &width, unsigned int &height, CPictureScalingAlgorithm::Algorithm& scalingAlgorithm, std::string &additional_info)
132 {
133   // unwrap the URL as required
134   std::string image(url);
135   additional_info.clear();
136   width = height = 0;
137   scalingAlgorithm = CPictureScalingAlgorithm::NoAlgorithm;
138   if (StringUtils::StartsWith(url, "image://"))
139   {
140     // format is image://[type@]<url_encoded_path>?options
141     CURL thumbURL(url);
142 
143     if (!CTextureCache::CanCacheImageURL(thumbURL))
144       return "";
145     if (thumbURL.GetUserName() == "music")
146       additional_info = "music";
147     if (StringUtils::StartsWith(thumbURL.GetUserName(), "video_"))
148       additional_info = thumbURL.GetUserName();
149 
150     image = thumbURL.GetHostName();
151 
152     if (thumbURL.HasOption("flipped"))
153       additional_info = "flipped";
154 
155     if (thumbURL.GetOption("size") == "thumb")
156       width = height = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_imageRes;
157     else
158     {
159       if (thumbURL.HasOption("width") && StringUtils::IsInteger(thumbURL.GetOption("width")))
160         width = strtol(thumbURL.GetOption("width").c_str(), NULL, 0);
161       if (thumbURL.HasOption("height") && StringUtils::IsInteger(thumbURL.GetOption("height")))
162         height = strtol(thumbURL.GetOption("height").c_str(), NULL, 0);
163     }
164 
165     if (thumbURL.HasOption("scaling_algorithm"))
166       scalingAlgorithm = CPictureScalingAlgorithm::FromString(thumbURL.GetOption("scaling_algorithm"));
167   }
168   return image;
169 }
170 
LoadImage(const std::string & image,unsigned int width,unsigned int height,const std::string & additional_info,bool requirePixels)171 CTexture* CTextureCacheJob::LoadImage(const std::string& image,
172                                       unsigned int width,
173                                       unsigned int height,
174                                       const std::string& additional_info,
175                                       bool requirePixels)
176 {
177   if (additional_info == "music")
178   { // special case for embedded music images
179     EmbeddedArt art;
180     if (CMusicThumbLoader::GetEmbeddedThumb(image, art))
181       return CTexture::LoadFromFileInMemory(art.m_data.data(), art.m_size, art.m_mime, width,
182                                             height);
183   }
184 
185   if (StringUtils::StartsWith(additional_info, "video_"))
186   {
187     EmbeddedArt art;
188     if (CVideoThumbLoader::GetEmbeddedThumb(image, additional_info.substr(6), art))
189       return CTexture::LoadFromFileInMemory(art.m_data.data(), art.m_size, art.m_mime, width,
190                                             height);
191   }
192 
193   // Validate file URL to see if it is an image
194   CFileItem file(image, false);
195   file.FillInMimeType();
196   if (!(file.IsPicture() && !(file.IsZIP() || file.IsRAR() || file.IsCBR() || file.IsCBZ() ))
197       && !StringUtils::StartsWithNoCase(file.GetMimeType(), "image/") && !StringUtils::EqualsNoCase(file.GetMimeType(), "application/octet-stream")) // ignore non-pictures
198     return NULL;
199 
200   CTexture* texture =
201       CTexture::LoadFromFile(image, width, height, requirePixels, file.GetMimeType());
202   if (!texture)
203     return NULL;
204 
205   // EXIF bits are interpreted as: <flipXY><flipY*flipX><flipX>
206   // where to undo the operation we apply them in reverse order <flipX>*<flipY*flipX>*<flipXY>
207   // When flipped we have an additional <flipX> on the left, which is equivalent to toggling the last bit
208   if (additional_info == "flipped")
209     texture->SetOrientation(texture->GetOrientation() ^ 1);
210 
211   return texture;
212 }
213 
UpdateableURL(const std::string & url) const214 bool CTextureCacheJob::UpdateableURL(const std::string &url) const
215 {
216   // we don't constantly check online images
217   return !(StringUtils::StartsWith(url, "http://") || StringUtils::StartsWith(url, "https://"));
218 }
219 
GetImageHash(const std::string & url)220 std::string CTextureCacheJob::GetImageHash(const std::string &url)
221 {
222   // silently ignore - we cannot state these
223   if (URIUtils::IsProtocol(url,"addons") || URIUtils::IsProtocol(url,"plugin"))
224     return "";
225 
226   struct __stat64 st;
227   if (XFILE::CFile::Stat(url, &st) == 0)
228   {
229     int64_t time = st.st_mtime;
230     if (!time)
231       time = st.st_ctime;
232     if (time || st.st_size)
233       return StringUtils::Format("d%" PRId64"s%" PRId64, time, st.st_size);
234 
235     // the image exists but we couldn't determine the mtime/ctime and/or size
236     // so set an obviously bad hash
237     return "BADHASH";
238   }
239   CLog::Log(LOGDEBUG, "%s - unable to stat url %s", __FUNCTION__, CURL::GetRedacted(url).c_str());
240   return "";
241 }
242 
CTextureUseCountJob(const std::vector<CTextureDetails> & textures)243 CTextureUseCountJob::CTextureUseCountJob(const std::vector<CTextureDetails> &textures) : m_textures(textures)
244 {
245 }
246 
operator ==(const CJob * job) const247 bool CTextureUseCountJob::operator==(const CJob* job) const
248 {
249   if (strcmp(job->GetType(),GetType()) == 0)
250   {
251     const CTextureUseCountJob* useJob = dynamic_cast<const CTextureUseCountJob*>(job);
252     if (useJob && useJob->m_textures == m_textures)
253       return true;
254   }
255   return false;
256 }
257 
DoWork()258 bool CTextureUseCountJob::DoWork()
259 {
260   CTextureDatabase db;
261   if (db.Open())
262   {
263     db.BeginTransaction();
264     for (std::vector<CTextureDetails>::const_iterator i = m_textures.begin(); i != m_textures.end(); ++i)
265       db.IncrementUseCount(*i);
266     db.CommitTransaction();
267   }
268   return true;
269 }
270