1 /*
2 * Copyright (C) 2005-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 "GUIDialogMediaSource.h"
10 #include "ServiceBroker.h"
11 #include "guilib/GUIKeyboardFactory.h"
12 #include "GUIDialogFileBrowser.h"
13 #include "video/windows/GUIWindowVideoBase.h"
14 #include "music/windows/GUIWindowMusicBase.h"
15 #include "guilib/GUIComponent.h"
16 #include "guilib/GUIWindowManager.h"
17 #include "input/Key.h"
18 #include "Util.h"
19 #include "utils/URIUtils.h"
20 #include "utils/StringUtils.h"
21 #include "utils/Variant.h"
22 #include "filesystem/Directory.h"
23 #include "filesystem/PVRDirectory.h"
24 #include "GUIDialogYesNo.h"
25 #include "FileItem.h"
26 #include "settings/MediaSourceSettings.h"
27 #include "settings/Settings.h"
28 #include "settings/SettingsComponent.h"
29 #include "guilib/LocalizeStrings.h"
30 #include "PasswordManager.h"
31 #include "URL.h"
32 #include "pvr/recordings/PVRRecordingsPath.h"
33
34 #if defined(TARGET_ANDROID)
35 #include "platform/android/activity/XBMCApp.h"
36 #include "filesystem/File.h"
37 #endif
38
39 #ifdef TARGET_WINDOWS_STORE
40 #include "platform/win10/filesystem/WinLibraryDirectory.h"
41 #endif
42
43 using namespace XFILE;
44
45 #define CONTROL_HEADING 2
46 #define CONTROL_PATH 10
47 #define CONTROL_PATH_BROWSE 11
48 #define CONTROL_NAME 12
49 #define CONTROL_PATH_ADD 13
50 #define CONTROL_PATH_REMOVE 14
51 #define CONTROL_OK 18
52 #define CONTROL_CANCEL 19
53 #define CONTROL_CONTENT 20
54
CGUIDialogMediaSource(void)55 CGUIDialogMediaSource::CGUIDialogMediaSource(void)
56 : CGUIDialog(WINDOW_DIALOG_MEDIA_SOURCE, "DialogMediaSource.xml")
57 {
58 m_paths = new CFileItemList;
59 m_loadType = KEEP_IN_MEMORY;
60 }
61
~CGUIDialogMediaSource()62 CGUIDialogMediaSource::~CGUIDialogMediaSource()
63 {
64 delete m_paths;
65 }
66
OnBack(int actionID)67 bool CGUIDialogMediaSource::OnBack(int actionID)
68 {
69 m_confirmed = false;
70 return CGUIDialog::OnBack(actionID);
71 }
72
OnMessage(CGUIMessage & message)73 bool CGUIDialogMediaSource::OnMessage(CGUIMessage& message)
74 {
75 switch (message.GetMessage())
76 {
77 case GUI_MSG_CLICKED:
78 {
79 int iControl = message.GetSenderId();
80 int iAction = message.GetParam1();
81 if (iControl == CONTROL_PATH && (iAction == ACTION_SELECT_ITEM || iAction == ACTION_MOUSE_LEFT_CLICK))
82 OnPath(GetSelectedItem());
83 else if (iControl == CONTROL_PATH_BROWSE)
84 OnPathBrowse(GetSelectedItem());
85 else if (iControl == CONTROL_PATH_ADD)
86 OnPathAdd();
87 else if (iControl == CONTROL_PATH_REMOVE)
88 OnPathRemove(GetSelectedItem());
89 else if (iControl == CONTROL_NAME)
90 {
91 OnEditChanged(iControl, m_name);
92 UpdateButtons();
93 }
94 else if (iControl == CONTROL_OK)
95 OnOK();
96 else if (iControl == CONTROL_CANCEL)
97 OnCancel();
98 else
99 break;
100 return true;
101 }
102 break;
103 case GUI_MSG_WINDOW_INIT:
104 {
105 UpdateButtons();
106 }
107 break;
108 case GUI_MSG_SETFOCUS:
109 if (message.GetControlId() == CONTROL_PATH_BROWSE ||
110 message.GetControlId() == CONTROL_PATH_ADD ||
111 message.GetControlId() == CONTROL_PATH_REMOVE)
112 {
113 HighlightItem(GetSelectedItem());
114 }
115 else
116 HighlightItem(-1);
117 break;
118 }
119 return CGUIDialog::OnMessage(message);
120 }
121
122 // \brief Show CGUIDialogMediaSource dialog and prompt for a new media source.
123 // \return True if the media source is added, false otherwise.
ShowAndAddMediaSource(const std::string & type)124 bool CGUIDialogMediaSource::ShowAndAddMediaSource(const std::string &type)
125 {
126 CGUIDialogMediaSource *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogMediaSource>(WINDOW_DIALOG_MEDIA_SOURCE);
127 if (!dialog) return false;
128 dialog->Initialize();
129 dialog->SetShare(CMediaSource());
130 dialog->SetTypeOfMedia(type);
131 dialog->Open();
132 bool confirmed(dialog->IsConfirmed());
133 if (confirmed)
134 {
135 // Add this media source
136 // Get unique source name
137 std::string strName = dialog->GetUniqueMediaSourceName();
138
139 CMediaSource share;
140 share.FromNameAndPaths(type, strName, dialog->GetPaths());
141 if (dialog->m_paths->Size() > 0)
142 share.m_strThumbnailImage = dialog->m_paths->Get(0)->GetArt("thumb");
143 CMediaSourceSettings::GetInstance().AddShare(type, share);
144 OnMediaSourceChanged(type, "", share);
145 }
146 dialog->m_paths->Clear();
147 return confirmed;
148 }
149
ShowAndEditMediaSource(const std::string & type,const std::string & share)150 bool CGUIDialogMediaSource::ShowAndEditMediaSource(const std::string &type, const std::string&share)
151 {
152 VECSOURCES* pShares = CMediaSourceSettings::GetInstance().GetSources(type);
153 if (pShares)
154 {
155 for (unsigned int i = 0;i<pShares->size();++i)
156 {
157 if (StringUtils::EqualsNoCase((*pShares)[i].strName, share))
158 return ShowAndEditMediaSource(type, (*pShares)[i]);
159 }
160 }
161 return false;
162 }
163
ShowAndEditMediaSource(const std::string & type,const CMediaSource & share)164 bool CGUIDialogMediaSource::ShowAndEditMediaSource(const std::string &type, const CMediaSource &share)
165 {
166 std::string strOldName = share.strName;
167 CGUIDialogMediaSource *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogMediaSource>(WINDOW_DIALOG_MEDIA_SOURCE);
168 if (!dialog) return false;
169 dialog->Initialize();
170 dialog->SetShare(share);
171 dialog->SetTypeOfMedia(type, true);
172 dialog->Open();
173 bool confirmed(dialog->IsConfirmed());
174 if (confirmed)
175 {
176 // Update media source
177 // Get unique new source name when changed
178 std::string strName(dialog->m_name);
179 if (!StringUtils::EqualsNoCase(dialog->m_name, strOldName))
180 strName = dialog->GetUniqueMediaSourceName();
181
182 CMediaSource newShare;
183 newShare.FromNameAndPaths(type, strName, dialog->GetPaths());
184 CMediaSourceSettings::GetInstance().UpdateShare(type, strOldName, newShare);
185
186 OnMediaSourceChanged(type, strOldName, newShare);
187 }
188 dialog->m_paths->Clear();
189 return confirmed;
190 }
191
GetUniqueMediaSourceName()192 std::string CGUIDialogMediaSource::GetUniqueMediaSourceName()
193 {
194 // Get unique source name for this media type
195 unsigned int i, j = 2;
196 bool bConfirmed = false;
197 VECSOURCES* pShares = CMediaSourceSettings::GetInstance().GetSources(m_type);
198 std::string strName = m_name;
199 while (!bConfirmed)
200 {
201 for (i = 0; i<pShares->size(); ++i)
202 {
203 if (StringUtils::EqualsNoCase((*pShares)[i].strName, strName))
204 break;
205 }
206 if (i < pShares->size())
207 // found a match - try next
208 strName = StringUtils::Format("%s (%i)", m_name.c_str(), j++);
209 else
210 bConfirmed = true;
211 }
212 return strName;
213 }
214
OnMediaSourceChanged(const std::string & type,const std::string & oldName,const CMediaSource & share)215 void CGUIDialogMediaSource::OnMediaSourceChanged(const std::string& type, const std::string& oldName, const CMediaSource& share)
216 {
217 // Processing once media source added/edited - library scraping and scanning
218 if (!StringUtils::StartsWithNoCase(share.strPath, "rss://") &&
219 !StringUtils::StartsWithNoCase(share.strPath, "rsss://") &&
220 !StringUtils::StartsWithNoCase(share.strPath, "upnp://"))
221 {
222 if (type == "video" && !URIUtils::IsLiveTV(share.strPath))
223 // Assign content to a path, refresh scraper information optionally start a scan
224 CGUIWindowVideoBase::OnAssignContent(share.strPath);
225 else if (type == "music")
226 CGUIWindowMusicBase::OnAssignContent(oldName, share);
227 }
228 }
229
OnPathBrowse(int item)230 void CGUIDialogMediaSource::OnPathBrowse(int item)
231 {
232 if (item < 0 || item >= m_paths->Size()) return;
233 // Browse is called. Open the filebrowser dialog.
234 // Ignore current path is best at this stage??
235 std::string path = m_paths->Get(item)->GetPath();
236 bool allowNetworkShares(m_type != "programs");
237 VECSOURCES extraShares;
238
239 if (m_name != CUtil::GetTitleFromPath(path))
240 m_bNameChanged = true;
241 path.clear();
242
243 if (m_type == "music")
244 {
245 CMediaSource share1;
246 #if defined(TARGET_ANDROID)
247 // add the default android music directory
248 std::string path;
249 if (CXBMCApp::GetExternalStorage(path, "music") && !path.empty() && CDirectory::Exists(path))
250 {
251 share1.strPath = path;
252 share1.strName = g_localizeStrings.Get(20240);
253 share1.m_ignore = true;
254 extraShares.push_back(share1);
255 }
256 #endif
257
258 #if defined(TARGET_WINDOWS_STORE)
259 // add the default UWP music directory
260 std::string path;
261 if (XFILE::CWinLibraryDirectory::GetStoragePath(m_type, path) && !path.empty() && CDirectory::Exists(path))
262 {
263 share1.strPath = path;
264 share1.strName = g_localizeStrings.Get(20245);
265 share1.m_ignore = true;
266 extraShares.push_back(share1);
267 }
268 #endif
269
270 // add the music playlist location
271 share1.strPath = "special://musicplaylists/";
272 share1.strName = g_localizeStrings.Get(20011);
273 share1.m_ignore = true;
274 extraShares.push_back(share1);
275
276 // add the recordings dir as needed
277 if (CPVRDirectory::HasRadioRecordings())
278 {
279 share1.strPath = PVR::CPVRRecordingsPath::PATH_ACTIVE_RADIO_RECORDINGS;
280 share1.strName = g_localizeStrings.Get(19017); // Recordings
281 extraShares.push_back(share1);
282 }
283 if (CPVRDirectory::HasDeletedRadioRecordings())
284 {
285 share1.strPath = PVR::CPVRRecordingsPath::PATH_DELETED_RADIO_RECORDINGS;
286 share1.strName = g_localizeStrings.Get(19184); // Deleted recordings
287 extraShares.push_back(share1);
288 }
289
290 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_AUDIOCDS_RECORDINGPATH) != "")
291 {
292 share1.strPath = "special://recordings/";
293 share1.strName = g_localizeStrings.Get(21883);
294 extraShares.push_back(share1);
295 }
296 }
297 else if (m_type == "video")
298 {
299 CMediaSource share1;
300 #if defined(TARGET_ANDROID)
301 // add the default android video directory
302 std::string path;
303 if (CXBMCApp::GetExternalStorage(path, "videos") && !path.empty() && CFile::Exists(path))
304 {
305 share1.strPath = path;
306 share1.strName = g_localizeStrings.Get(20241);
307 share1.m_ignore = true;
308 extraShares.push_back(share1);
309 }
310 #endif
311 #if defined(TARGET_WINDOWS_STORE)
312 // add the default UWP music directory
313 std::string path;
314 if (XFILE::CWinLibraryDirectory::GetStoragePath(m_type, path) && !path.empty() && CDirectory::Exists(path))
315 {
316 share1.strPath = path;
317 share1.strName = g_localizeStrings.Get(20246);
318 share1.m_ignore = true;
319 extraShares.push_back(share1);
320 }
321 #endif
322
323 // add the video playlist location
324 share1.m_ignore = true;
325 share1.strPath = "special://videoplaylists/";
326 share1.strName = g_localizeStrings.Get(20012);
327 extraShares.push_back(share1);
328
329 // add the recordings dir as needed
330 if (CPVRDirectory::HasTVRecordings())
331 {
332 share1.strPath = PVR::CPVRRecordingsPath::PATH_ACTIVE_TV_RECORDINGS;
333 share1.strName = g_localizeStrings.Get(19017); // Recordings
334 extraShares.push_back(share1);
335 }
336 if (CPVRDirectory::HasDeletedTVRecordings())
337 {
338 share1.strPath = PVR::CPVRRecordingsPath::PATH_DELETED_TV_RECORDINGS;
339 share1.strName = g_localizeStrings.Get(19184); // Deleted recordings
340 extraShares.push_back(share1);
341 }
342 }
343 else if (m_type == "pictures")
344 {
345 CMediaSource share1;
346 #if defined(TARGET_ANDROID)
347 // add the default android music directory
348 std::string path;
349 if (CXBMCApp::GetExternalStorage(path, "pictures") && !path.empty() && CFile::Exists(path))
350 {
351 share1.strPath = path;
352 share1.strName = g_localizeStrings.Get(20242);
353 share1.m_ignore = true;
354 extraShares.push_back(share1);
355 }
356
357 path.clear();
358 if (CXBMCApp::GetExternalStorage(path, "photos") && !path.empty() && CFile::Exists(path))
359 {
360 share1.strPath = path;
361 share1.strName = g_localizeStrings.Get(20243);
362 share1.m_ignore = true;
363 extraShares.push_back(share1);
364 }
365 #endif
366 #if defined(TARGET_WINDOWS_STORE)
367 // add the default UWP music directory
368 std::string path;
369 if (XFILE::CWinLibraryDirectory::GetStoragePath(m_type, path) && !path.empty() && CDirectory::Exists(path))
370 {
371 share1.strPath = path;
372 share1.strName = g_localizeStrings.Get(20247);
373 share1.m_ignore = true;
374 extraShares.push_back(share1);
375 }
376 path.clear();
377 if (XFILE::CWinLibraryDirectory::GetStoragePath("photos", path) && !path.empty() && CDirectory::Exists(path))
378 {
379 share1.strPath = path;
380 share1.strName = g_localizeStrings.Get(20248);
381 share1.m_ignore = true;
382 extraShares.push_back(share1);
383 }
384 #endif
385
386 share1.m_ignore = true;
387 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_DEBUG_SCREENSHOTPATH) != "")
388 {
389 share1.strPath = "special://screenshots/";
390 share1.strName = g_localizeStrings.Get(20008);
391 extraShares.push_back(share1);
392 }
393 }
394 else if (m_type == "games")
395 {
396 // nothing to add
397 }
398 else if (m_type == "programs")
399 {
400 // nothing to add
401 }
402 if (CGUIDialogFileBrowser::ShowAndGetSource(path, allowNetworkShares, extraShares.size() == 0 ? NULL : &extraShares))
403 {
404 if (item < m_paths->Size()) // if the skin does funky things, m_paths may have been cleared
405 m_paths->Get(item)->SetPath(path);
406 if (!m_bNameChanged || m_name.empty())
407 {
408 CURL url(path);
409 m_name = url.GetWithoutUserDetails();
410 URIUtils::RemoveSlashAtEnd(m_name);
411 m_name = CUtil::GetTitleFromPath(m_name);
412 }
413 UpdateButtons();
414 }
415 }
416
OnPath(int item)417 void CGUIDialogMediaSource::OnPath(int item)
418 {
419 if (item < 0 || item >= m_paths->Size()) return;
420
421 std::string path(m_paths->Get(item)->GetPath());
422 if (m_name != CUtil::GetTitleFromPath(path))
423 m_bNameChanged = true;
424
425 CGUIKeyboardFactory::ShowAndGetInput(path, CVariant{ g_localizeStrings.Get(1021) }, false);
426 m_paths->Get(item)->SetPath(path);
427
428 if (!m_bNameChanged || m_name.empty())
429 {
430 CURL url(m_paths->Get(item)->GetPath());
431 m_name = url.GetWithoutUserDetails();
432 URIUtils::RemoveSlashAtEnd(m_name);
433 m_name = CUtil::GetTitleFromPath(m_name);
434 }
435 UpdateButtons();
436 }
437
OnOK()438 void CGUIDialogMediaSource::OnOK()
439 {
440 // Verify the paths by doing a GetDirectory.
441 CFileItemList items;
442
443 // Create temp media source to encode path urls as multipath
444 // Name of actual source may need to be made unique when saved in sources
445 CMediaSource share;
446 share.FromNameAndPaths(m_type, m_name, GetPaths());
447
448 if (StringUtils::StartsWithNoCase(share.strPath, "plugin://") ||
449 CDirectory::GetDirectory(share.strPath, items, "", DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_ALLOW_PROMPT) ||
450 CGUIDialogYesNo::ShowAndGetInput(CVariant{ 1001 }, CVariant{ 1025 }))
451 {
452 m_confirmed = true;
453 Close();
454 }
455 }
456
OnCancel()457 void CGUIDialogMediaSource::OnCancel()
458 {
459 m_confirmed = false;
460 Close();
461 }
462
UpdateButtons()463 void CGUIDialogMediaSource::UpdateButtons()
464 {
465 if (!m_paths->Size()) // sanity
466 return;
467
468 CONTROL_ENABLE_ON_CONDITION(CONTROL_OK, !m_paths->Get(0)->GetPath().empty() && !m_name.empty());
469 CONTROL_ENABLE_ON_CONDITION(CONTROL_PATH_ADD, !m_paths->Get(0)->GetPath().empty());
470 CONTROL_ENABLE_ON_CONDITION(CONTROL_PATH_REMOVE, m_paths->Size() > 1);
471 // name
472 SET_CONTROL_LABEL2(CONTROL_NAME, m_name);
473 SendMessage(GUI_MSG_SET_TYPE, CONTROL_NAME, 0, 1022);
474
475 int currentItem = GetSelectedItem();
476 SendMessage(GUI_MSG_LABEL_RESET, CONTROL_PATH);
477 for (int i = 0; i < m_paths->Size(); i++)
478 {
479 CFileItemPtr item = m_paths->Get(i);
480 std::string path;
481 CURL url(item->GetPath());
482 path = url.GetWithoutUserDetails();
483 if (path.empty()) path = "<" + g_localizeStrings.Get(231) + ">"; // <None>
484 item->SetLabel(path);
485 }
486 CGUIMessage msg(GUI_MSG_LABEL_BIND, GetID(), CONTROL_PATH, 0, 0, m_paths);
487 OnMessage(msg);
488 SendMessage(GUI_MSG_ITEM_SELECT, CONTROL_PATH, currentItem);
489
490 SET_CONTROL_HIDDEN(CONTROL_CONTENT);
491 }
492
SetShare(const CMediaSource & share)493 void CGUIDialogMediaSource::SetShare(const CMediaSource &share)
494 {
495 m_paths->Clear();
496 for (unsigned int i = 0; i < share.vecPaths.size(); i++)
497 {
498 CFileItemPtr item(new CFileItem(share.vecPaths[i], true));
499 m_paths->Add(item);
500 }
501 if (0 == share.vecPaths.size())
502 {
503 CFileItemPtr item(new CFileItem("", true));
504 m_paths->Add(item);
505 }
506 m_name = share.strName;
507 UpdateButtons();
508 }
509
SetTypeOfMedia(const std::string & type,bool editNotAdd)510 void CGUIDialogMediaSource::SetTypeOfMedia(const std::string &type, bool editNotAdd)
511 {
512 m_type = type;
513 std::string heading;
514 if (editNotAdd)
515 {
516 if (type == "video")
517 heading = g_localizeStrings.Get(10053);
518 else if (type == "music")
519 heading = g_localizeStrings.Get(10054);
520 else if (type == "pictures")
521 heading = g_localizeStrings.Get(10055);
522 else if (type == "games")
523 heading = g_localizeStrings.Get(35252); // "Edit game source"
524 else if (type == "programs")
525 heading = g_localizeStrings.Get(10056);
526 else
527 heading = g_localizeStrings.Get(10057);
528 }
529 else
530 {
531 if (type == "video")
532 heading = g_localizeStrings.Get(10048);
533 else if (type == "music")
534 heading = g_localizeStrings.Get(10049);
535 else if (type == "pictures")
536 heading = g_localizeStrings.Get(13006);
537 else if (type == "games")
538 heading = g_localizeStrings.Get(35251); // "Add game source"
539 else if (type == "programs")
540 heading = g_localizeStrings.Get(10051);
541 else
542 heading = g_localizeStrings.Get(10052);
543 }
544 SET_CONTROL_LABEL(CONTROL_HEADING, heading);
545 }
546
GetSelectedItem()547 int CGUIDialogMediaSource::GetSelectedItem()
548 {
549 CGUIMessage message(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_PATH);
550 OnMessage(message);
551 int value = message.GetParam1();
552 if (value < 0 || value >= m_paths->Size()) return 0;
553 return value;
554 }
555
HighlightItem(int item)556 void CGUIDialogMediaSource::HighlightItem(int item)
557 {
558 for (int i = 0; i < m_paths->Size(); i++)
559 m_paths->Get(i)->Select(false);
560 if (item >= 0 && item < m_paths->Size())
561 m_paths->Get(item)->Select(true);
562 CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(), CONTROL_PATH, item);
563 OnMessage(msg);
564 }
565
OnPathRemove(int item)566 void CGUIDialogMediaSource::OnPathRemove(int item)
567 {
568 m_paths->Remove(item);
569 UpdateButtons();
570 if (item >= m_paths->Size())
571 HighlightItem(m_paths->Size() - 1);
572 else
573 HighlightItem(item);
574 if (m_paths->Size() <= 1)
575 {
576 SET_CONTROL_FOCUS(CONTROL_PATH_ADD, 0);
577 }
578 }
579
OnPathAdd()580 void CGUIDialogMediaSource::OnPathAdd()
581 {
582 // add a new item and select it as well
583 CFileItemPtr item(new CFileItem("", true));
584 m_paths->Add(item);
585 UpdateButtons();
586 HighlightItem(m_paths->Size() - 1);
587 }
588
GetPaths() const589 std::vector<std::string> CGUIDialogMediaSource::GetPaths() const
590 {
591 std::vector<std::string> paths;
592 for (int i = 0; i < m_paths->Size(); i++)
593 {
594 if (!m_paths->Get(i)->GetPath().empty())
595 { // strip off the user and password for supported paths (anything that the password manager can auth)
596 // and add the user/pass to the password manager - note, we haven't confirmed that it works
597 // at this point, but if it doesn't, the user will get prompted anyway in implementation.
598 CURL url(m_paths->Get(i)->GetPath());
599 if (CPasswordManager::GetInstance().IsURLSupported(url) && !url.GetUserName().empty())
600 {
601 CPasswordManager::GetInstance().SaveAuthenticatedURL(url);
602 url.SetPassword("");
603 url.SetUserName("");
604 url.SetDomain("");
605 }
606 paths.push_back(url.Get());
607 }
608 }
609 return paths;
610 }
611
OnDeinitWindow(int nextWindowID)612 void CGUIDialogMediaSource::OnDeinitWindow(int nextWindowID)
613 {
614 CGUIDialog::OnDeinitWindow(nextWindowID);
615
616 // clear paths container
617 CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), CONTROL_PATH, 0);
618 OnMessage(msg);
619 }
620