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