Merge pull request #26350 from jjd-uk/estuary_media_align
[xbmc.git] / xbmc / favourites / FavouritesService.cpp
blob8002c9853e4eb282195556d037ac30b652b90df1
1 /*
2 * Copyright (C) 2005-2018 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
7 */
9 #include "FavouritesService.h"
11 #include "FileItem.h"
12 #include "GUIPassword.h"
13 #include "ServiceBroker.h"
14 #include "Util.h"
15 #include "favourites/FavouritesURL.h"
16 #include "input/WindowTranslator.h"
17 #include "music/MusicFileItemClassify.h"
18 #include "profiles/ProfileManager.h"
19 #include "settings/SettingsComponent.h"
20 #include "utils/ContentUtils.h"
21 #include "utils/FileUtils.h"
22 #include "utils/URIUtils.h"
23 #include "utils/XBMCTinyXML2.h"
24 #include "utils/log.h"
25 #include "video/VideoFileItemClassify.h"
27 #include <mutex>
29 using namespace KODI;
31 namespace
33 bool IsMediasourceOfFavItemUnlocked(const std::shared_ptr<CFileItem>& item)
35 if (!item)
37 CLog::Log(LOGERROR, "{}: No item passed (nullptr).", __func__);
38 return true;
41 if (!item->IsFavourite())
43 CLog::Log(LOGERROR, "{}: Wrong item passed (not a favourite).", __func__);
44 return true;
47 const auto settingsComponent = CServiceBroker::GetSettingsComponent();
48 if (!settingsComponent)
50 CLog::Log(LOGERROR, "{}: returned nullptr.", __func__);
51 return true;
54 const auto profileManager = settingsComponent->GetProfileManager();
55 if (!profileManager)
57 CLog::Log(LOGERROR, "{}: returned nullptr.", __func__);
58 return true;
61 const CFavouritesURL url(item->GetPath());
62 if (!url.IsValid())
64 CLog::Log(LOGERROR, "{}: Invalid exec string (syntax error).", __func__);
65 return true;
68 const CFavouritesURL::Action action = url.GetAction();
70 if (action != CFavouritesURL::Action::PLAY_MEDIA &&
71 action != CFavouritesURL::Action::SHOW_PICTURE)
72 return true;
74 const CFileItem itemToCheck(url.GetTarget(), url.IsDir());
76 if (action == CFavouritesURL::Action::PLAY_MEDIA)
78 if (VIDEO::IsVideo(itemToCheck))
80 if (!profileManager->GetCurrentProfile().videoLocked())
81 return g_passwordManager.IsMediaFileUnlocked("video", itemToCheck.GetPath());
83 return false;
85 else if (MUSIC::IsAudio(itemToCheck))
87 if (!profileManager->GetCurrentProfile().musicLocked())
88 return g_passwordManager.IsMediaFileUnlocked("music", itemToCheck.GetPath());
90 return false;
93 else if (action == CFavouritesURL::Action::SHOW_PICTURE && itemToCheck.IsPicture())
95 if (!profileManager->GetCurrentProfile().picturesLocked())
96 return g_passwordManager.IsMediaFileUnlocked("pictures", itemToCheck.GetPath());
98 return false;
101 return true;
104 bool LoadFromFile(const std::string& strPath, CFileItemList& items)
106 CXBMCTinyXML2 doc;
107 if (!doc.LoadFile(strPath))
109 CLog::Log(LOGERROR, "Unable to load {} (line {})", strPath, doc.ErrorLineNum());
110 return false;
112 auto* root = doc.RootElement();
113 if (!root || strcmp(root->Value(), "favourites"))
115 CLog::Log(LOGERROR, "Favourites.xml doesn't contain the <favourites> root element");
116 return false;
119 auto* favourite = root->FirstChildElement("favourite");
120 while (favourite)
122 // format:
123 // <favourite name="Cool Video" thumb="foo.jpg">PlayMedia(c:\videos\cool_video.avi)</favourite>
124 // <favourite name="My Album" thumb="bar.tbn">ActivateWindow(MyMusic,c:\music\my album)</favourite>
125 // <favourite name="Apple Movie Trailers" thumb="path_to_thumb.png">RunScript(special://xbmc/scripts/apple movie trailers/default.py)</favourite>
126 const char *name = favourite->Attribute("name");
127 const char *thumb = favourite->Attribute("thumb");
128 if (name && favourite->FirstChild())
130 const std::string favURL(
131 CFavouritesURL(CExecString(favourite->FirstChild()->Value())).GetURL());
132 if (!items.Contains(favURL))
134 const CFileItemPtr item(std::make_shared<CFileItem>(name));
135 item->SetPath(favURL);
136 if (thumb)
137 item->SetArt("thumb", thumb);
138 items.Add(item);
141 favourite = favourite->NextSiblingElement("favourite");
143 return true;
145 } // unnamed namespace
147 CFavouritesService::CFavouritesService(std::string userDataFolder) : m_favourites("favourites://")
149 ReInit(std::move(userDataFolder));
152 void CFavouritesService::ReInit(std::string userDataFolder)
154 std::unique_lock<CCriticalSection> lock(m_criticalSection);
156 m_userDataFolder = std::move(userDataFolder);
157 m_favourites.Clear();
158 m_targets.clear();
159 m_favourites.SetContent("favourites");
161 std::string favourites = "special://xbmc/system/favourites.xml";
162 if (CFileUtils::Exists(favourites))
163 LoadFromFile(favourites, m_favourites);
164 else
165 CLog::Log(LOGDEBUG, "CFavourites::Load - no system favourites found, skipping");
167 favourites = URIUtils::AddFileToFolder(m_userDataFolder, "favourites.xml");
168 if (CFileUtils::Exists(favourites))
169 LoadFromFile(favourites, m_favourites);
170 else
171 CLog::Log(LOGDEBUG, "CFavourites::Load - no userdata favourites found, skipping");
174 void CFavouritesService::CleanupTargetsCache(const CFileItem& item)
176 // Cleanup cache. Resume info etc. of cached target items might need refresh.
177 std::unique_lock<CCriticalSection> lock(m_criticalSection);
179 const std::string dynPath{item.GetDynPath()};
180 std::erase_if(m_targets,
181 [&dynPath](const auto& entry)
183 auto const& [key, value] = entry;
184 return (value->GetDynPath() == dynPath);
188 void CFavouritesService::OnPlaybackStopped(const CFileItem& item)
190 CleanupTargetsCache(item);
193 void CFavouritesService::OnPlaybackEnded(const CFileItem& item)
195 CleanupTargetsCache(item);
198 bool CFavouritesService::Persist()
200 CXBMCTinyXML2 doc;
201 auto* element = doc.NewElement("favourites");
202 auto* rootNode = doc.InsertEndChild(element);
203 if (!rootNode)
204 return false;
206 for (const auto& item : m_favourites)
208 auto* favNode = doc.NewElement("favourite");
209 favNode->SetAttribute("name", item->GetLabel().c_str());
210 if (item->HasArt("thumb"))
211 favNode->SetAttribute("thumb", item->GetArt("thumb").c_str());
213 auto* execute = doc.NewText(CFavouritesURL(item->GetPath()).GetExecString().c_str());
214 favNode->InsertEndChild(execute);
215 rootNode->InsertEndChild(favNode);
218 auto path = URIUtils::AddFileToFolder(m_userDataFolder, "favourites.xml");
219 return doc.SaveFile(path);
222 bool CFavouritesService::Save(const CFileItemList& items)
225 std::unique_lock<CCriticalSection> lock(m_criticalSection);
226 m_favourites.Clear();
227 m_targets.clear();
228 m_favourites.Copy(items);
229 Persist();
231 OnUpdated();
232 return true;
235 void CFavouritesService::OnUpdated()
237 m_events.Publish(FavouritesUpdated{});
240 bool CFavouritesService::AddOrRemove(const CFileItem& item, int contextWindow)
243 std::unique_lock<CCriticalSection> lock(m_criticalSection);
245 const std::shared_ptr<CFileItem> match{GetFavourite(item, contextWindow)};
246 if (match)
248 // remove the item
249 const auto it = m_targets.find(match->GetPath());
250 if (it != m_targets.end())
251 m_targets.erase(it);
253 m_favourites.Remove(match.get());
255 else
257 // create our new favourite item
258 const auto favourite{std::make_shared<CFileItem>(item.GetLabel())};
259 if (item.GetLabel().empty())
260 favourite->SetLabel(CUtil::GetTitleFromPath(item.GetPath(), item.m_bIsFolder));
261 favourite->SetArt("thumb", ContentUtils::GetPreferredArtImage(item));
262 const std::string favUrl{CFavouritesURL(item, contextWindow).GetURL()};
263 favourite->SetPath(favUrl);
264 m_favourites.Add(favourite);
266 Persist();
268 OnUpdated();
269 return true;
272 std::shared_ptr<CFileItem> CFavouritesService::GetFavourite(const CFileItem& item,
273 int contextWindow) const
275 std::unique_lock<CCriticalSection> lock(m_criticalSection);
277 const CFavouritesURL favURL{item, contextWindow};
278 const bool isVideoDb{URIUtils::IsVideoDb(favURL.GetTarget())};
279 const bool isMusicDb{URIUtils::IsMusicDb(favURL.GetTarget())};
281 for (const auto& favItem : m_favourites)
283 const CFavouritesURL favItemURL{*favItem, contextWindow};
285 // Compare the whole target URLs
286 if (favItemURL.GetTarget() == item.GetPath())
287 return favItem;
289 // Compare the target URLs ignoring optional parameters
290 if (favItemURL.GetAction() == favURL.GetAction() &&
291 (favItemURL.GetAction() != CFavouritesURL::Action::ACTIVATE_WINDOW ||
292 favItemURL.GetWindowID() == favURL.GetWindowID()))
294 if (favItemURL.GetTarget() == favURL.GetTarget())
295 return favItem;
297 // Check videodb and musicdb paths. Might be different strings pointing to same resource!
298 // Example: "musicdb://recentlyaddedalbums/4711/" and "musicdb://recentlyplayedalbums/4711/",
299 // both pointing to same album with db id 4711.
300 if ((isVideoDb && URIUtils::IsVideoDb(favItemURL.GetTarget())) ||
301 (isMusicDb && URIUtils::IsMusicDb(favItemURL.GetTarget())))
303 const std::shared_ptr<CFileItem> targetItem{ResolveFavourite(*favItem)};
304 if (targetItem && targetItem->IsSamePath(&item))
305 return favItem;
309 return {};
312 bool CFavouritesService::IsFavourited(const CFileItem& item, int contextWindow) const
314 return (GetFavourite(item, contextWindow) != nullptr);
317 std::shared_ptr<CFileItem> CFavouritesService::ResolveFavourite(const CFileItem& item) const
319 if (item.IsFavourite())
321 std::unique_lock<CCriticalSection> lock(m_criticalSection);
323 const auto it = m_targets.find(item.GetPath());
324 if (it != m_targets.end())
325 return (*it).second;
327 const CFavouritesURL favURL{item.GetPath()};
328 if (favURL.IsValid())
330 auto targetItem{std::make_shared<CFileItem>(favURL.GetTarget(), favURL.IsDir())};
331 targetItem->LoadDetails();
332 if (favURL.GetWindowID() != -1)
334 const std::string window{CWindowTranslator::TranslateWindow(favURL.GetWindowID())};
335 targetItem->SetProperty("targetwindow", CVariant{window});
337 m_targets.insert({item.GetPath(), targetItem});
338 return targetItem;
341 return {};
344 int CFavouritesService::Size() const
346 std::unique_lock<CCriticalSection> lock(m_criticalSection);
347 return m_favourites.Size();
350 void CFavouritesService::GetAll(CFileItemList& items) const
352 std::unique_lock<CCriticalSection> lock(m_criticalSection);
353 items.Clear();
354 if (g_passwordManager.IsMasterLockUnlocked(false)) // don't prompt
356 items.Copy(m_favourites, true); // copy items
358 else
360 for (const auto& fav : m_favourites)
362 if (IsMediasourceOfFavItemUnlocked(fav))
363 items.Add(fav);
367 int index = 0;
368 for (const auto& item : items)
370 const CFavouritesURL favURL(item->GetPath());
371 item->SetProperty("favourite.action", favURL.GetActionLabel());
372 item->SetProperty("favourite.provider", favURL.GetProviderLabel());
373 item->SetProperty("favourite.index", index++);
377 void CFavouritesService::RefreshFavourites()
379 m_events.Publish(FavouritesUpdated{});