1 /*
2  *  Copyright (C) 2014-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 "VideoLibraryRefreshingJob.h"
10 
11 #include "ServiceBroker.h"
12 #include "TextureDatabase.h"
13 #include "addons/Scraper.h"
14 #include "dialogs/GUIDialogSelect.h"
15 #include "dialogs/GUIDialogYesNo.h"
16 #include "filesystem/PluginDirectory.h"
17 #include "guilib/GUIComponent.h"
18 #include "guilib/GUIKeyboardFactory.h"
19 #include "guilib/GUIWindowManager.h"
20 #include "guilib/LocalizeStrings.h"
21 #include "media/MediaType.h"
22 #include "messaging/helpers/DialogOKHelper.h"
23 #include "utils/StringUtils.h"
24 #include "utils/URIUtils.h"
25 #include "utils/log.h"
26 #include "video/VideoDatabase.h"
27 #include "video/VideoInfoDownloader.h"
28 #include "video/VideoInfoScanner.h"
29 #include "video/tags/IVideoInfoTagLoader.h"
30 #include "video/tags/VideoInfoTagLoaderFactory.h"
31 #include "video/tags/VideoTagLoaderPlugin.h"
32 
33 #include <utility>
34 
35 using namespace KODI::MESSAGING;
36 using namespace VIDEO;
37 
CVideoLibraryRefreshingJob(CFileItemPtr item,bool forceRefresh,bool refreshAll,bool ignoreNfo,const std::string & searchTitle)38 CVideoLibraryRefreshingJob::CVideoLibraryRefreshingJob(CFileItemPtr item,
39                                                        bool forceRefresh,
40                                                        bool refreshAll,
41                                                        bool ignoreNfo /* = false */,
42                                                        const std::string& searchTitle /* = "" */)
43   : CVideoLibraryProgressJob(nullptr),
44     m_item(std::move(item)),
45     m_forceRefresh(forceRefresh),
46     m_refreshAll(refreshAll),
47     m_ignoreNfo(ignoreNfo),
48     m_searchTitle(searchTitle)
49 { }
50 
51 CVideoLibraryRefreshingJob::~CVideoLibraryRefreshingJob() = default;
52 
operator ==(const CJob * job) const53 bool CVideoLibraryRefreshingJob::operator==(const CJob* job) const
54 {
55   if (strcmp(job->GetType(), GetType()) != 0)
56     return false;
57 
58   const CVideoLibraryRefreshingJob* refreshingJob = dynamic_cast<const CVideoLibraryRefreshingJob*>(job);
59   if (refreshingJob == nullptr)
60     return false;
61 
62   return m_item->GetPath() == refreshingJob->m_item->GetPath();
63 }
64 
Work(CVideoDatabase & db)65 bool CVideoLibraryRefreshingJob::Work(CVideoDatabase &db)
66 {
67   if (m_item == nullptr)
68     return false;
69 
70   // determine the scraper for the item's path
71   VIDEO::SScanSettings scanSettings;
72   ADDON::ScraperPtr scraper = db.GetScraperForPath(m_item->GetPath(), scanSettings);
73   if (scraper == nullptr)
74     return false;
75 
76   if (URIUtils::IsPlugin(m_item->GetPath()) && !XFILE::CPluginDirectory::IsMediaLibraryScanningAllowed(ADDON::TranslateContent(scraper->Content()), m_item->GetPath()))
77   {
78     CLog::Log(LOGINFO,
79               "CVideoLibraryRefreshingJob: Plugin '%s' does not support media library scanning and "
80               "refreshing",
81               CURL::GetRedacted(m_item->GetPath()).c_str());
82     return false;
83   }
84 
85   // copy the scraper in case we need it again
86   ADDON::ScraperPtr originalScraper(scraper);
87 
88   // get the item's correct title
89   std::string itemTitle = m_searchTitle;
90   if (itemTitle.empty())
91     itemTitle = m_item->GetMovieName(scanSettings.parent_name);
92 
93   CScraperUrl scraperUrl;
94   bool needsRefresh = m_forceRefresh;
95   bool hasDetails = false;
96   bool ignoreNfo = m_ignoreNfo;
97 
98   // run this in a loop in case we need to refresh again
99   bool failure = false;
100   do
101   {
102     std::unique_ptr<CVideoInfoTag> pluginTag;
103     std::unique_ptr<CGUIListItem::ArtMap> pluginArt;
104 
105     if (!ignoreNfo)
106     {
107       std::unique_ptr<IVideoInfoTagLoader> loader;
108       loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(*m_item, scraper,
109                                                             scanSettings.parent_name_root, m_forceRefresh));
110       // check if there's an NFO for the item
111       CInfoScanner::INFO_TYPE nfoResult = CInfoScanner::NO_NFO;
112       if (loader)
113       {
114         std::unique_ptr<CVideoInfoTag> tag(new CVideoInfoTag());
115         nfoResult = loader->Load(*tag, false);
116         if (nfoResult == CInfoScanner::FULL_NFO && m_item->IsPlugin() && scraper->ID() == "metadata.local")
117         {
118           // get video info and art from plugin source with metadata.local scraper
119           if (scraper->Content() == CONTENT_TVSHOWS && !m_item->m_bIsFolder && tag->m_iIdShow < 0)
120             // preserve show_id for episode
121             tag->m_iIdShow = m_item->GetVideoInfoTag()->m_iIdShow;
122           pluginTag = std::move(tag);
123           CVideoTagLoaderPlugin* nfo = dynamic_cast<CVideoTagLoaderPlugin*>(loader.get());
124           if (nfo && nfo->GetArt())
125             pluginArt = std::move(nfo->GetArt());
126         }
127         else if (nfoResult == CInfoScanner::URL_NFO)
128           scraperUrl = loader->ScraperUrl();
129       }
130 
131       // if there's no NFO remember it in case we have to refresh again
132       if (nfoResult == CInfoScanner::ERROR_NFO)
133         ignoreNfo = true;
134       else if (nfoResult != CInfoScanner::NO_NFO)
135         hasDetails = true;
136 
137       // if we are performing a forced refresh ask the user to choose between using a valid NFO and a valid scraper
138       if (needsRefresh && IsModal() && !scraper->IsNoop()
139           && nfoResult != CInfoScanner::ERROR_NFO)
140       {
141         int heading = 20159;
142         if (scraper->Content() == CONTENT_MOVIES)
143           heading = 13346;
144         else if (scraper->Content() == CONTENT_TVSHOWS)
145           heading = m_item->m_bIsFolder ? 20351 : 20352;
146         else if (scraper->Content() == CONTENT_MUSICVIDEOS)
147           heading = 20393;
148         if (CGUIDialogYesNo::ShowAndGetInput(heading, 20446))
149         {
150           hasDetails = false;
151           ignoreNfo = true;
152           scraperUrl.Clear();
153           scraper = originalScraper;
154         }
155       }
156     }
157 
158     // no need to re-fetch the episode guide for episodes
159     if (scraper->Content() == CONTENT_TVSHOWS && !m_item->m_bIsFolder)
160       hasDetails = true;
161 
162     // if we don't have an url or need to refresh anyway do the web search
163     if (!hasDetails && (needsRefresh || !scraperUrl.HasUrls()))
164     {
165       SetTitle(StringUtils::Format(g_localizeStrings.Get(197).c_str(), scraper->Name().c_str()));
166       SetText(itemTitle);
167       SetProgress(0);
168 
169       // clear any cached data from the scraper
170       scraper->ClearCache();
171 
172       // create the info downloader for the scraper
173       CVideoInfoDownloader infoDownloader(scraper);
174 
175       // try to find a matching item
176       MOVIELIST itemResultList;
177       int result = infoDownloader.FindMovie(itemTitle, -1, itemResultList, GetProgressDialog());
178 
179       // close the progress dialog
180       MarkFinished();
181 
182       if (result > 0)
183       {
184         // there are multiple matches for the item
185         if (!itemResultList.empty())
186         {
187           // choose the first match
188           if (!IsModal())
189             scraperUrl = itemResultList.at(0);
190           else
191           {
192             // ask the user what to do
193             CGUIDialogSelect* selectDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
194             selectDialog->Reset();
195             selectDialog->SetHeading(scraper->Content() == CONTENT_TVSHOWS ? 20356 : 196);
196             for (const auto& itemResult : itemResultList)
197               selectDialog->Add(itemResult.GetTitle());
198             selectDialog->EnableButton(true, 413); // "Manual"
199             selectDialog->Open();
200 
201             // check if the user has chosen one of the results
202             int selectedItem = selectDialog->GetSelectedItem();
203             if (selectedItem >= 0)
204               scraperUrl = itemResultList.at(selectedItem);
205             // the user hasn't chosen one of the results and but has chosen to manually enter a title to use
206             else if (selectDialog->IsButtonPressed())
207             {
208               // ask the user to input a title to use
209               if (!CGUIKeyboardFactory::ShowAndGetInput(itemTitle, g_localizeStrings.Get(scraper->Content() == CONTENT_TVSHOWS ? 20357 : 16009), false))
210                 return false;
211 
212               // go through the whole process again
213               needsRefresh = true;
214               continue;
215             }
216             // nothing else we can do
217             else
218               return false;
219           }
220 
221           CLog::Log(LOGDEBUG, "CVideoLibraryRefreshingJob: user selected item '%s' with URL '%s'",
222                     scraperUrl.GetTitle().c_str(), scraperUrl.GetFirstThumbUrl());
223         }
224       }
225       else if (result < 0 || !VIDEO::CVideoInfoScanner::DownloadFailed(GetProgressDialog()))
226       {
227         failure = true;
228         break;
229       }
230     }
231 
232     // if the URL is still empty, check whether or not we're allowed
233     // to prompt and ask the user to input a new search title
234     if (!hasDetails && !scraperUrl.HasUrls())
235     {
236       if (IsModal())
237       {
238         // ask the user to input a title to use
239         if (!CGUIKeyboardFactory::ShowAndGetInput(itemTitle, g_localizeStrings.Get(scraper->Content() == CONTENT_TVSHOWS ? 20357 : 16009), false))
240           return false;
241 
242         // go through the whole process again
243         needsRefresh = true;
244         continue;
245       }
246 
247       // nothing else we can do
248       failure = true;
249       break;
250     }
251 
252     // before we start downloading all the necessary information cleanup any existing artwork and hashes
253     CTextureDatabase textureDb;
254     if (textureDb.Open())
255     {
256       for (const auto& artwork : m_item->GetArt())
257         textureDb.InvalidateCachedTexture(artwork.second);
258 
259       textureDb.Close();
260     }
261     m_item->ClearArt();
262 
263     // put together the list of items to refresh
264     std::string path = m_item->GetPath();
265     CFileItemList items;
266     if (m_item->HasVideoInfoTag() && m_item->GetVideoInfoTag()->m_iDbId > 0)
267     {
268       // for a tvshow we need to handle all paths of it
269       std::vector<std::string> tvshowPaths;
270       if (CMediaTypes::IsMediaType(m_item->GetVideoInfoTag()->m_type, MediaTypeTvShow) && m_refreshAll &&
271           db.GetPathsLinkedToTvShow(m_item->GetVideoInfoTag()->m_iDbId, tvshowPaths))
272       {
273         for (const auto& tvshowPath : tvshowPaths)
274         {
275           CFileItemPtr tvshowItem(new CFileItem(*m_item->GetVideoInfoTag()));
276           tvshowItem->SetPath(tvshowPath);
277           items.Add(tvshowItem);
278         }
279       }
280       // otherwise just add a copy of the item
281       else
282         items.Add(CFileItemPtr(new CFileItem(*m_item->GetVideoInfoTag())));
283 
284       // update the path to the real path (instead of a videodb:// one)
285       path = m_item->GetVideoInfoTag()->m_strPath;
286     }
287     else
288       items.Add(CFileItemPtr(new CFileItem(*m_item)));
289 
290     // set the proper path of the list of items to lookup
291     items.SetPath(m_item->m_bIsFolder ? URIUtils::GetParentPath(path) : URIUtils::GetDirectory(path));
292 
293     int headingLabel = 198;
294     if (scraper->Content() == CONTENT_TVSHOWS)
295     {
296       if (m_item->m_bIsFolder)
297         headingLabel = 20353;
298       else
299         headingLabel = 20361;
300     }
301     else if (scraper->Content() == CONTENT_MUSICVIDEOS)
302       headingLabel = 20394;
303 
304     // prepare the progress dialog for downloading all the necessary information
305     SetTitle(g_localizeStrings.Get(headingLabel));
306     SetText(scraperUrl.GetTitle());
307     SetProgress(0);
308 
309     // remove any existing data for the item we're going to refresh
310     if (m_item->GetVideoInfoTag()->m_iDbId > 0)
311     {
312       int dbId = m_item->GetVideoInfoTag()->m_iDbId;
313       if (scraper->Content() == CONTENT_MOVIES)
314         db.DeleteMovie(dbId);
315       else if (scraper->Content() == CONTENT_MUSICVIDEOS)
316         db.DeleteMusicVideo(dbId);
317       else if (scraper->Content() == CONTENT_TVSHOWS)
318       {
319         if (!m_item->m_bIsFolder)
320           db.DeleteEpisode(dbId);
321         else if (m_refreshAll)
322           db.DeleteTvShow(dbId);
323         else
324           db.DeleteDetailsForTvShow(dbId);
325       }
326     }
327 
328     if (pluginTag || pluginArt)
329       // set video info and art from plugin source with metadata.local scraper to items
330       for (auto &i: items)
331       {
332         if (pluginTag)
333           *i->GetVideoInfoTag() = *pluginTag;
334         if (pluginArt)
335           i->SetArt(*pluginArt);
336       }
337 
338     // finally download the information for the item
339     CVideoInfoScanner scanner;
340     if (!scanner.RetrieveVideoInfo(items, scanSettings.parent_name,
341                                    scraper->Content(), !ignoreNfo,
342                                    scraperUrl.HasUrls() ? &scraperUrl : nullptr,
343                                    m_refreshAll, GetProgressDialog()))
344     {
345       // something went wrong
346       MarkFinished();
347 
348       // check if the user cancelled
349       if (!IsCancelled() && IsModal())
350         HELPERS::ShowOKDialogText(CVariant{195}, CVariant{itemTitle});
351 
352       return false;
353     }
354 
355     // retrieve the updated information from the database
356     if (scraper->Content() == CONTENT_MOVIES)
357       db.GetMovieInfo(m_item->GetPath(), *m_item->GetVideoInfoTag());
358     else if (scraper->Content() == CONTENT_MUSICVIDEOS)
359       db.GetMusicVideoInfo(m_item->GetPath(), *m_item->GetVideoInfoTag());
360     else if (scraper->Content() == CONTENT_TVSHOWS)
361     {
362       // update tvshow info to get updated episode numbers
363       if (m_item->m_bIsFolder)
364         db.GetTvShowInfo(m_item->GetPath(), *m_item->GetVideoInfoTag());
365       else
366         db.GetEpisodeInfo(m_item->GetPath(), *m_item->GetVideoInfoTag());
367     }
368 
369     // we're finally done
370     MarkFinished();
371     break;
372   } while (needsRefresh);
373 
374   if (failure && IsModal())
375     HELPERS::ShowOKDialogText(CVariant{195}, CVariant{itemTitle});
376 
377   return true;
378 }
379