diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 18:07:22 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 18:07:22 +0000 |
commit | c04dcc2e7d834218ef2d4194331e383402495ae1 (patch) | |
tree | 7333e38d10d75386e60f336b80c2443c1166031d /xbmc/filesystem | |
parent | Initial commit. (diff) | |
download | kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.tar.xz kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.zip |
Adding upstream version 2:20.4+dfsg.upstream/2%20.4+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
249 files changed, 29881 insertions, 0 deletions
diff --git a/xbmc/filesystem/AddonsDirectory.cpp b/xbmc/filesystem/AddonsDirectory.cpp new file mode 100644 index 0000000..1439ae0 --- /dev/null +++ b/xbmc/filesystem/AddonsDirectory.cpp @@ -0,0 +1,953 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "AddonsDirectory.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "addons/AddonDatabase.h" +#include "addons/AddonInstaller.h" +#include "addons/AddonManager.h" +#include "addons/AddonRepos.h" +#include "addons/AddonSystemSettings.h" +#include "addons/PluginSource.h" +#include "addons/RepositoryUpdater.h" +#include "addons/addoninfo/AddonInfo.h" +#include "addons/addoninfo/AddonType.h" +#include "games/GameUtils.h" +#include "games/addons/GameClient.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/TextureManager.h" +#include "interfaces/generic/ScriptInvocationManager.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +#include <algorithm> +#include <array> +#include <functional> +#include <set> + +using namespace KODI; +using namespace ADDON; +using namespace KODI::MESSAGING; + +namespace XFILE +{ + +CAddonsDirectory::CAddonsDirectory(void) = default; + +CAddonsDirectory::~CAddonsDirectory(void) = default; + +const auto CATEGORY_INFO_PROVIDERS = "category.infoproviders"; +const auto CATEGORY_LOOK_AND_FEEL = "category.lookandfeel"; +const auto CATEGORY_GAME_ADDONS = "category.gameaddons"; +const auto CATEGORY_EMULATORS = "category.emulators"; +const auto CATEGORY_STANDALONE_GAMES = "category.standalonegames"; +const auto CATEGORY_GAME_PROVIDERS = "category.gameproviders"; +const auto CATEGORY_GAME_RESOURCES = "category.gameresources"; +const auto CATEGORY_GAME_SUPPORT_ADDONS = "category.gamesupport"; + +const std::set<AddonType> infoProviderTypes = { + AddonType::SCRAPER_ALBUMS, AddonType::SCRAPER_ARTISTS, AddonType::SCRAPER_MOVIES, + AddonType::SCRAPER_MUSICVIDEOS, AddonType::SCRAPER_TVSHOWS, +}; + +const std::set<AddonType> lookAndFeelTypes = { + AddonType::SKIN, + AddonType::SCREENSAVER, + AddonType::RESOURCE_IMAGES, + AddonType::RESOURCE_LANGUAGE, + AddonType::RESOURCE_UISOUNDS, + AddonType::RESOURCE_FONT, + AddonType::VISUALIZATION, +}; + +const std::set<AddonType> gameTypes = { + AddonType::GAME_CONTROLLER, + AddonType::GAMEDLL, + AddonType::GAME, + AddonType::RESOURCE_GAMES, +}; + +static bool IsInfoProviderType(AddonType type) +{ + return infoProviderTypes.find(type) != infoProviderTypes.end(); +} + +static bool IsInfoProviderTypeAddon(const AddonPtr& addon) +{ + return IsInfoProviderType(addon->Type()); +} + +static bool IsLookAndFeelType(AddonType type) +{ + return lookAndFeelTypes.find(type) != lookAndFeelTypes.end(); +} + +static bool IsLookAndFeelTypeAddon(const AddonPtr& addon) +{ + return IsLookAndFeelType(addon->Type()); +} + +static bool IsGameType(AddonType type) +{ + return gameTypes.find(type) != gameTypes.end(); +} + +static bool IsStandaloneGame(const AddonPtr& addon) +{ + return GAME::CGameUtils::IsStandaloneGame(addon); +} + +static bool IsEmulator(const AddonPtr& addon) +{ + return addon->Type() == AddonType::GAMEDLL && + std::static_pointer_cast<GAME::CGameClient>(addon)->SupportsPath(); +} + +static bool IsGameProvider(const AddonPtr& addon) +{ + return addon->Type() == AddonType::PLUGIN && addon->HasType(AddonType::GAME); +} + +static bool IsGameResource(const AddonPtr& addon) +{ + return addon->Type() == AddonType::RESOURCE_GAMES; +} + +static bool IsGameSupportAddon(const AddonPtr& addon) +{ + return addon->Type() == AddonType::GAMEDLL && + !std::static_pointer_cast<GAME::CGameClient>(addon)->SupportsPath() && + !std::static_pointer_cast<GAME::CGameClient>(addon)->SupportsStandalone(); +} + +static bool IsGameAddon(const AddonPtr& addon) +{ + return IsGameType(addon->Type()) || + IsStandaloneGame(addon) || + IsGameProvider(addon) || + IsGameResource(addon) || + IsGameSupportAddon(addon); +} + +static bool IsUserInstalled(const AddonPtr& addon) +{ + return !CAddonType::IsDependencyType(addon->MainType()); +} + +// Creates categories from addon types, if we have any addons with that type. +static void GenerateTypeListing(const CURL& path, + const std::set<AddonType>& types, + const VECADDONS& addons, + CFileItemList& items) +{ + for (const auto& type : types) + { + for (const auto& addon : addons) + { + if (addon->HasType(type)) + { + CFileItemPtr item(new CFileItem(CAddonInfo::TranslateType(type, true))); + CURL itemPath = path; + itemPath.SetFileName(CAddonInfo::TranslateType(type, false)); + item->SetPath(itemPath.Get()); + item->m_bIsFolder = true; + std::string thumb = CAddonInfo::TranslateIconType(type); + if (!thumb.empty() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture(thumb)) + item->SetArt("thumb", thumb); + items.Add(item); + break; + } + } + } +} + +// Creates categories for game add-ons, if we have any game add-ons +static void GenerateGameListing(const CURL& path, const VECADDONS& addons, CFileItemList& items) +{ + // Game controllers + for (const auto& addon : addons) + { + if (addon->Type() == AddonType::GAME_CONTROLLER) + { + CFileItemPtr item(new CFileItem(CAddonInfo::TranslateType(AddonType::GAME_CONTROLLER, true))); + CURL itemPath = path; + itemPath.SetFileName(CAddonInfo::TranslateType(AddonType::GAME_CONTROLLER, false)); + item->SetPath(itemPath.Get()); + item->m_bIsFolder = true; + std::string thumb = CAddonInfo::TranslateIconType(AddonType::GAME_CONTROLLER); + if (!thumb.empty() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture(thumb)) + item->SetArt("thumb", thumb); + items.Add(item); + break; + } + } + // Emulators + for (const auto& addon : addons) + { + if (IsEmulator(addon)) + { + CFileItemPtr item(new CFileItem(g_localizeStrings.Get(35207))); // Emulators + CURL itemPath = path; + itemPath.SetFileName(CATEGORY_EMULATORS); + item->SetPath(itemPath.Get()); + item->m_bIsFolder = true; + std::string thumb = CAddonInfo::TranslateIconType(AddonType::GAMEDLL); + if (!thumb.empty() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture(thumb)) + item->SetArt("thumb", thumb); + items.Add(item); + break; + } + } + // Standalone games + for (const auto& addon : addons) + { + if (IsStandaloneGame(addon)) + { + CFileItemPtr item(new CFileItem(g_localizeStrings.Get(35208))); // Standalone games + CURL itemPath = path; + itemPath.SetFileName(CATEGORY_STANDALONE_GAMES); + item->SetPath(itemPath.Get()); + item->m_bIsFolder = true; + std::string thumb = CAddonInfo::TranslateIconType(AddonType::GAMEDLL); + if (!thumb.empty() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture(thumb)) + item->SetArt("thumb", thumb); + items.Add(item); + break; + } + } + // Game providers + for (const auto& addon : addons) + { + if (IsGameProvider(addon)) + { + CFileItemPtr item(new CFileItem(g_localizeStrings.Get(35220))); // Game providers + CURL itemPath = path; + itemPath.SetFileName(CATEGORY_GAME_PROVIDERS); + item->SetPath(itemPath.Get()); + item->m_bIsFolder = true; + std::string thumb = CAddonInfo::TranslateIconType(AddonType::GAMEDLL); + if (!thumb.empty() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture(thumb)) + item->SetArt("thumb", thumb); + items.Add(item); + break; + } + } + // Game resources + for (const auto& addon : addons) + { + if (IsGameResource(addon)) + { + CFileItemPtr item(new CFileItem(g_localizeStrings.Get(35209))); // Game resources + CURL itemPath = path; + itemPath.SetFileName(CATEGORY_GAME_RESOURCES); + item->SetPath(itemPath.Get()); + item->m_bIsFolder = true; + std::string thumb = CAddonInfo::TranslateIconType(AddonType::GAMEDLL); + if (!thumb.empty() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture(thumb)) + item->SetArt("thumb", thumb); + items.Add(item); + break; + } + } + // Game support add-ons + for (const auto& addon : addons) + { + if (IsGameSupportAddon(addon)) + { + CFileItemPtr item(new CFileItem(g_localizeStrings.Get(35216))); // Support add-ons + CURL itemPath = path; + itemPath.SetFileName(CATEGORY_GAME_SUPPORT_ADDONS); + item->SetPath(itemPath.Get()); + item->m_bIsFolder = true; + std::string thumb = CAddonInfo::TranslateIconType(AddonType::GAMEDLL); + if (!thumb.empty() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture(thumb)) + item->SetArt("thumb", thumb); + items.Add(item); + break; + } + } +} + +//Creates the top-level category list +static void GenerateMainCategoryListing(const CURL& path, const VECADDONS& addons, + CFileItemList& items) +{ + if (std::any_of(addons.begin(), addons.end(), IsInfoProviderTypeAddon)) + { + CFileItemPtr item(new CFileItem(g_localizeStrings.Get(24993))); + item->SetPath(URIUtils::AddFileToFolder(path.Get(), CATEGORY_INFO_PROVIDERS)); + item->m_bIsFolder = true; + const std::string thumb = "DefaultAddonInfoProvider.png"; + if (CServiceBroker::GetGUI()->GetTextureManager().HasTexture(thumb)) + item->SetArt("thumb", thumb); + items.Add(item); + } + if (std::any_of(addons.begin(), addons.end(), IsLookAndFeelTypeAddon)) + { + CFileItemPtr item(new CFileItem(g_localizeStrings.Get(24997))); + item->SetPath(URIUtils::AddFileToFolder(path.Get(), CATEGORY_LOOK_AND_FEEL)); + item->m_bIsFolder = true; + const std::string thumb = "DefaultAddonLookAndFeel.png"; + if (CServiceBroker::GetGUI()->GetTextureManager().HasTexture(thumb)) + item->SetArt("thumb", thumb); + items.Add(item); + } + if (std::any_of(addons.begin(), addons.end(), IsGameAddon)) + { + CFileItemPtr item(new CFileItem(CAddonInfo::TranslateType(AddonType::GAME, true))); + item->SetPath(URIUtils::AddFileToFolder(path.Get(), CATEGORY_GAME_ADDONS)); + item->m_bIsFolder = true; + const std::string thumb = CAddonInfo::TranslateIconType(AddonType::GAME); + if (CServiceBroker::GetGUI()->GetTextureManager().HasTexture(thumb)) + item->SetArt("thumb", thumb); + items.Add(item); + } + + std::set<AddonType> uncategorized; + for (unsigned int i = static_cast<unsigned int>(AddonType::UNKNOWN) + 1; + i < static_cast<unsigned int>(AddonType::MAX_TYPES) - 1; ++i) + { + const AddonType type = static_cast<AddonType>(i); + /* + * Check and prevent insert for this cases: + * - By a provider, look and feel, dependency and game becomes given to + * subdirectory to control the types + * - By ADDON_SCRIPT and ADDON_PLUGIN, them contains one of the possible + * subtypes (audio, video, app or/and game) and not needed to show + * together in a Script or Plugin list + */ + if (!IsInfoProviderType(type) && !IsLookAndFeelType(type) && + !CAddonType::IsDependencyType(type) && !IsGameType(type) && type != AddonType::SCRIPT && + type != AddonType::PLUGIN) + uncategorized.insert(type); + } + GenerateTypeListing(path, uncategorized, addons, items); +} + +//Creates sub-categories or addon list for a category +static void GenerateCategoryListing(const CURL& path, VECADDONS& addons, + CFileItemList& items) +{ + const std::string& category = path.GetFileName(); + if (category == CATEGORY_INFO_PROVIDERS) + { + items.SetProperty("addoncategory", g_localizeStrings.Get(24993)); + items.SetLabel(g_localizeStrings.Get(24993)); + GenerateTypeListing(path, infoProviderTypes, addons, items); + } + else if (category == CATEGORY_LOOK_AND_FEEL) + { + items.SetProperty("addoncategory", g_localizeStrings.Get(24997)); + items.SetLabel(g_localizeStrings.Get(24997)); + GenerateTypeListing(path, lookAndFeelTypes, addons, items); + } + else if (category == CATEGORY_GAME_ADDONS) + { + items.SetProperty("addoncategory", CAddonInfo::TranslateType(AddonType::GAME, true)); + items.SetLabel(CAddonInfo::TranslateType(AddonType::GAME, true)); + GenerateGameListing(path, addons, items); + } + else if (category == CATEGORY_EMULATORS) + { + items.SetProperty("addoncategory", g_localizeStrings.Get(35207)); // Emulators + addons.erase(std::remove_if(addons.begin(), addons.end(), + [](const AddonPtr& addon){ return !IsEmulator(addon); }), addons.end()); + CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(35207)); // Emulators + } + else if (category == CATEGORY_STANDALONE_GAMES) + { + items.SetProperty("addoncategory", g_localizeStrings.Get(35208)); // Standalone games + addons.erase(std::remove_if(addons.begin(), addons.end(), + [](const AddonPtr& addon){ return !IsStandaloneGame(addon); }), addons.end()); + CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(35208)); // Standalone games + } + else if (category == CATEGORY_GAME_PROVIDERS) + { + items.SetProperty("addoncategory", g_localizeStrings.Get(35220)); // Game providers + addons.erase(std::remove_if(addons.begin(), addons.end(), + [](const AddonPtr& addon){ return !IsGameProvider(addon); }), addons.end()); + CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(35220)); // Game providers + } + else if (category == CATEGORY_GAME_RESOURCES) + { + items.SetProperty("addoncategory", g_localizeStrings.Get(35209)); // Game resources + addons.erase(std::remove_if(addons.begin(), addons.end(), + [](const AddonPtr& addon){ return !IsGameResource(addon); }), addons.end()); + CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(35209)); // Game resources + } + else if (category == CATEGORY_GAME_SUPPORT_ADDONS) + { + items.SetProperty("addoncategory", g_localizeStrings.Get(35216)); // Support add-ons + addons.erase(std::remove_if(addons.begin(), addons.end(), + [](const AddonPtr& addon) { return !IsGameSupportAddon(addon); }), addons.end()); + CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(35216)); // Support add-ons + } + else + { // fallback to addon type + AddonType type = CAddonInfo::TranslateType(category); + items.SetProperty("addoncategory", CAddonInfo::TranslateType(type, true)); + addons.erase(std::remove_if(addons.begin(), addons.end(), + [type](const AddonPtr& addon) { return !addon->HasType(type); }), + addons.end()); + CAddonsDirectory::GenerateAddonListing(path, addons, items, CAddonInfo::TranslateType(type, true)); + } +} + +bool CAddonsDirectory::GetSearchResults(const CURL& path, CFileItemList &items) +{ + std::string search(path.GetFileName()); + if (search.empty() && !GetKeyboardInput(16017, search)) + return false; + + CAddonDatabase database; + database.Open(); + + VECADDONS addons; + database.Search(search, addons); + CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(283)); + CURL searchPath(path); + searchPath.SetFileName(search); + items.SetPath(searchPath.Get()); + return true; +} + +static void UserInstalledAddons(const CURL& path, CFileItemList &items) +{ + items.ClearItems(); + items.SetLabel(g_localizeStrings.Get(24998)); + + VECADDONS addons; + CServiceBroker::GetAddonMgr().GetInstalledAddons(addons); + addons.erase(std::remove_if(addons.begin(), addons.end(), + [](const AddonPtr& addon) { return !IsUserInstalled(addon); }), addons.end()); + + if (addons.empty()) + return; + + const std::string& category = path.GetFileName(); + if (category.empty()) + { + GenerateMainCategoryListing(path, addons, items); + + //"All" node + CFileItemPtr item(new CFileItem()); + item->m_bIsFolder = true; + CURL itemPath = path; + itemPath.SetFileName("all"); + item->SetPath(itemPath.Get()); + item->SetLabel(g_localizeStrings.Get(593)); + item->SetSpecialSort(SortSpecialOnTop); + items.Add(item); + } + else if (category == "all") + CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(24998)); + else + GenerateCategoryListing(path, addons, items); +} + +static void DependencyAddons(const CURL& path, CFileItemList &items) +{ + VECADDONS all; + CServiceBroker::GetAddonMgr().GetInstalledAddons(all); + + VECADDONS deps; + std::copy_if(all.begin(), all.end(), std::back_inserter(deps), + [&](const AddonPtr& _){ return !IsUserInstalled(_); }); + + CAddonsDirectory::GenerateAddonListing(path, deps, items, g_localizeStrings.Get(24996)); + + //Set orphaned status + std::set<std::string> orphaned; + for (const auto& addon : deps) + { + if (CServiceBroker::GetAddonMgr().IsOrphaned(addon, all)) + orphaned.insert(addon->ID()); + } + + for (int i = 0; i < items.Size(); ++i) + { + if (orphaned.find(items[i]->GetProperty("Addon.ID").asString()) != orphaned.end()) + { + items[i]->SetProperty("Addon.Status", g_localizeStrings.Get(24995)); + items[i]->SetProperty("Addon.Orphaned", true); + } + } +} + +static void OutdatedAddons(const CURL& path, CFileItemList &items) +{ + VECADDONS addons = CServiceBroker::GetAddonMgr().GetAvailableUpdates(); + CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(24043)); + + if (!items.IsEmpty()) + { + if (CAddonSystemSettings::GetInstance().GetAddonAutoUpdateMode() == AUTO_UPDATES_ON) + { + const CFileItemPtr itemUpdateAllowed( + std::make_shared<CFileItem>("addons://update_allowed/", false)); + itemUpdateAllowed->SetLabel(g_localizeStrings.Get(24137)); + itemUpdateAllowed->SetSpecialSort(SortSpecialOnTop); + items.Add(itemUpdateAllowed); + } + + const CFileItemPtr itemUpdateAll(std::make_shared<CFileItem>("addons://update_all/", false)); + itemUpdateAll->SetLabel(g_localizeStrings.Get(24122)); + itemUpdateAll->SetSpecialSort(SortSpecialOnTop); + items.Add(itemUpdateAll); + } +} + +static void RunningAddons(const CURL& path, CFileItemList &items) +{ + VECADDONS addons; + CServiceBroker::GetAddonMgr().GetAddons(addons, AddonType::SERVICE); + + addons.erase(std::remove_if(addons.begin(), addons.end(), + [](const AddonPtr& addon){ return !CScriptInvocationManager::GetInstance().IsRunning(addon->LibPath()); }), addons.end()); + CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(24994)); +} + +static bool Browse(const CURL& path, CFileItemList &items) +{ + const std::string& repoId = path.GetHostName(); + + VECADDONS addons; + items.SetPath(path.Get()); + if (repoId == "all") + { + CAddonRepos addonRepos; + if (!addonRepos.IsValid()) + return false; + + // get all latest addon versions by repo + addonRepos.GetLatestAddonVersionsFromAllRepos(addons); + + items.SetProperty("reponame", g_localizeStrings.Get(24087)); + items.SetLabel(g_localizeStrings.Get(24087)); + } + else + { + AddonPtr repoAddon; + if (!CServiceBroker::GetAddonMgr().GetAddon(repoId, repoAddon, AddonType::REPOSITORY, + OnlyEnabled::CHOICE_YES)) + { + return false; + } + + CAddonRepos addonRepos(repoAddon); + if (!addonRepos.IsValid()) + return false; + + // get all addons from the single repository + addonRepos.GetLatestAddonVersions(addons); + + items.SetProperty("reponame", repoAddon->Name()); + items.SetLabel(repoAddon->Name()); + } + + const std::string& category = path.GetFileName(); + if (category.empty()) + GenerateMainCategoryListing(path, addons, items); + else + GenerateCategoryListing(path, addons, items); + return true; +} + +static bool GetRecentlyUpdatedAddons(VECADDONS& addons) +{ + if (!CServiceBroker::GetAddonMgr().GetInstalledAddons(addons)) + return false; + + auto limit = CDateTime::GetCurrentDateTime() - CDateTimeSpan(14, 0, 0, 0); + auto isOld = [limit](const AddonPtr& addon){ return addon->LastUpdated() < limit; }; + addons.erase(std::remove_if(addons.begin(), addons.end(), isOld), addons.end()); + return true; +} + +static bool HasRecentlyUpdatedAddons() +{ + VECADDONS addons; + return GetRecentlyUpdatedAddons(addons) && !addons.empty(); +} + +static bool Repos(const CURL& path, CFileItemList &items) +{ + items.SetLabel(g_localizeStrings.Get(24033)); + + VECADDONS addons; + CServiceBroker::GetAddonMgr().GetAddons(addons, AddonType::REPOSITORY); + if (addons.empty()) + return true; + else if (addons.size() == 1) + return Browse(CURL("addons://" + addons[0]->ID()), items); + CFileItemPtr item(new CFileItem("addons://all/", true)); + item->SetLabel(g_localizeStrings.Get(24087)); + item->SetSpecialSort(SortSpecialOnTop); + items.Add(item); + for (const auto& repo : addons) + { + CFileItemPtr item = CAddonsDirectory::FileItemFromAddon(repo, "addons://" + repo->ID(), true); + items.Add(item); + } + items.SetContent("addons"); + return true; +} + +static void RootDirectory(CFileItemList& items) +{ + items.SetLabel(g_localizeStrings.Get(10040)); + { + CFileItemPtr item(new CFileItem("addons://user/", true)); + item->SetLabel(g_localizeStrings.Get(24998)); + item->SetArt("icon", "DefaultAddonsInstalled.png"); + items.Add(item); + } + if (CServiceBroker::GetAddonMgr().HasAvailableUpdates()) + { + CFileItemPtr item(new CFileItem("addons://outdated/", true)); + item->SetLabel(g_localizeStrings.Get(24043)); + item->SetArt("icon", "DefaultAddonsUpdates.png"); + items.Add(item); + } + if (CAddonInstaller::GetInstance().IsDownloading()) + { + CFileItemPtr item(new CFileItem("addons://downloading/", true)); + item->SetLabel(g_localizeStrings.Get(24067)); + item->SetArt("icon", "DefaultNetwork.png"); + items.Add(item); + } + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_ADDONS_AUTOUPDATES) == ADDON::AUTO_UPDATES_ON + && HasRecentlyUpdatedAddons()) + { + CFileItemPtr item(new CFileItem("addons://recently_updated/", true)); + item->SetLabel(g_localizeStrings.Get(24004)); + item->SetArt("icon", "DefaultAddonsRecentlyUpdated.png"); + items.Add(item); + } + if (CServiceBroker::GetAddonMgr().HasAddons(AddonType::REPOSITORY)) + { + CFileItemPtr item(new CFileItem("addons://repos/", true)); + item->SetLabel(g_localizeStrings.Get(24033)); + item->SetArt("icon", "DefaultAddonsRepo.png"); + items.Add(item); + } + { + CFileItemPtr item(new CFileItem("addons://install/", false)); + item->SetLabel(g_localizeStrings.Get(24041)); + item->SetArt("icon", "DefaultAddonsZip.png"); + items.Add(item); + } + { + CFileItemPtr item(new CFileItem("addons://search/", true)); + item->SetLabel(g_localizeStrings.Get(137)); + item->SetArt("icon", "DefaultAddonsSearch.png"); + items.Add(item); + } +} + +bool CAddonsDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + std::string tmp(url.Get()); + URIUtils::RemoveSlashAtEnd(tmp); + CURL path(tmp); + const std::string& endpoint = path.GetHostName(); + items.ClearItems(); + items.ClearProperties(); + items.SetCacheToDisc(CFileItemList::CACHE_NEVER); + items.SetPath(path.Get()); + + if (endpoint.empty()) + { + RootDirectory(items); + return true; + } + else if (endpoint == "user") + { + UserInstalledAddons(path, items); + return true; + } + else if (endpoint == "dependencies") + { + DependencyAddons(path, items); + return true; + } + // PVR hardcodes this view so keep for compatibility + else if (endpoint == "disabled") + { + VECADDONS addons; + AddonType type; + + if (path.GetFileName() == "kodi.pvrclient") + type = AddonType::PVRDLL; + else if (path.GetFileName() == "kodi.vfs") + type = AddonType::VFS; + else + type = AddonType::UNKNOWN; + + if (type != AddonType::UNKNOWN && + CServiceBroker::GetAddonMgr().GetInstalledAddons(addons, type)) + { + CAddonsDirectory::GenerateAddonListing(path, addons, items, CAddonInfo::TranslateType(type, true)); + return true; + } + return false; + } + else if (endpoint == "outdated") + { + OutdatedAddons(path, items); + return true; + } + else if (endpoint == "running") + { + RunningAddons(path, items); + return true; + } + else if (endpoint == "repos") + { + return Repos(path, items); + } + else if (endpoint == "sources") + { + return GetScriptsAndPlugins(path.GetFileName(), items); + } + else if (endpoint == "search") + { + return GetSearchResults(path, items); + } + else if (endpoint == "recently_updated") + { + VECADDONS addons; + if (!GetRecentlyUpdatedAddons(addons)) + return false; + + CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(24004)); + return true; + } + else if (endpoint == "downloading") + { + VECADDONS addons; + CAddonInstaller::GetInstance().GetInstallList(addons); + CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(24067)); + return true; + } + else if (endpoint == "more") + { + const std::string& type = path.GetFileName(); + if (type == "video" || type == "audio" || type == "image" || type == "executable") + return Browse(CURL("addons://all/xbmc.addon." + type), items); + else if (type == "game") + return Browse(CURL("addons://all/category.gameaddons"), items); + return false; + } + else + { + return Browse(path, items); + } +} + +bool CAddonsDirectory::IsRepoDirectory(const CURL& url) +{ + if (url.GetHostName().empty() || !url.IsProtocol("addons")) + return false; + + AddonPtr tmp; + return url.GetHostName() == "repos" || url.GetHostName() == "all" || + url.GetHostName() == "search" || + CServiceBroker::GetAddonMgr().GetAddon(url.GetHostName(), tmp, AddonType::REPOSITORY, + OnlyEnabled::CHOICE_YES); +} + +void CAddonsDirectory::GenerateAddonListing(const CURL& path, + const VECADDONS& addons, + CFileItemList& items, + const std::string& label) +{ + std::map<std::string, AddonWithUpdate> addonsWithUpdate = + CServiceBroker::GetAddonMgr().GetAddonsWithAvailableUpdate(); + + items.ClearItems(); + items.SetContent("addons"); + items.SetLabel(label); + for (const auto& addon : addons) + { + CURL itemPath = path; + itemPath.SetFileName(addon->ID()); + CFileItemPtr pItem = FileItemFromAddon(addon, itemPath.Get(), false); + + bool installed = CServiceBroker::GetAddonMgr().IsAddonInstalled(addon->ID(), addon->Origin(), + addon->Version()); + bool disabled = CServiceBroker::GetAddonMgr().IsAddonDisabled(addon->ID()); + + std::function<bool(bool)> CheckOutdatedOrUpdate = [&](bool checkOutdated) -> bool { + auto mapEntry = addonsWithUpdate.find(addon->ID()); + if (mapEntry != addonsWithUpdate.end()) + { + const std::shared_ptr<IAddon>& checkedObject = + checkOutdated ? mapEntry->second.m_installed : mapEntry->second.m_update; + + return (checkedObject->Origin() == addon->Origin() && + checkedObject->Version() == addon->Version()); + } + return false; + }; + + bool isUpdate = CheckOutdatedOrUpdate(false); // check if it's an available update + bool hasUpdate = CheckOutdatedOrUpdate(true); // check if it's an outdated addon + + std::string validUpdateVersion; + std::string validUpdateOrigin; + if (hasUpdate) + { + auto mapEntry = addonsWithUpdate.find(addon->ID()); + validUpdateVersion = mapEntry->second.m_update->Version().asString(); + validUpdateOrigin = mapEntry->second.m_update->Origin(); + } + + bool fromOfficialRepo = CAddonRepos::IsFromOfficialRepo(addon, CheckAddonPath::CHOICE_NO); + + pItem->SetProperty("Addon.IsInstalled", installed); + pItem->SetProperty("Addon.IsEnabled", installed && !disabled); + pItem->SetProperty("Addon.HasUpdate", hasUpdate); + pItem->SetProperty("Addon.IsUpdate", isUpdate); + pItem->SetProperty("Addon.ValidUpdateVersion", validUpdateVersion); + pItem->SetProperty("Addon.ValidUpdateOrigin", validUpdateOrigin); + pItem->SetProperty("Addon.IsFromOfficialRepo", fromOfficialRepo); + pItem->SetProperty("Addon.IsBinary", addon->IsBinary()); + + if (installed) + pItem->SetProperty("Addon.Status", g_localizeStrings.Get(305)); + if (disabled) + pItem->SetProperty("Addon.Status", g_localizeStrings.Get(24023)); + if (hasUpdate) + pItem->SetProperty("Addon.Status", g_localizeStrings.Get(24068)); + else if (addon->LifecycleState() == AddonLifecycleState::BROKEN) + pItem->SetProperty("Addon.Status", g_localizeStrings.Get(24098)); + else if (addon->LifecycleState() == AddonLifecycleState::DEPRECATED) + pItem->SetProperty("Addon.Status", g_localizeStrings.Get(24170)); + + items.Add(pItem); + } +} + +CFileItemPtr CAddonsDirectory::FileItemFromAddon(const AddonPtr &addon, + const std::string& path, bool folder) +{ + if (!addon) + return CFileItemPtr(); + + CFileItemPtr item(new CFileItem(addon)); + item->m_bIsFolder = folder; + item->SetPath(path); + + std::string strLabel(addon->Name()); + if (CURL(path).GetHostName() == "search") + strLabel = StringUtils::Format("{} - {}", CAddonInfo::TranslateType(addon->Type(), true), + addon->Name()); + item->SetLabel(strLabel); + item->SetArt(addon->Art()); + item->SetArt("thumb", addon->Icon()); + item->SetArt("icon", "DefaultAddon.png"); + + //! @todo fix hacks that depends on these + item->SetProperty("Addon.ID", addon->ID()); + item->SetProperty("Addon.Name", addon->Name()); + item->SetCanQueue(false); + const auto it = addon->ExtraInfo().find("language"); + if (it != addon->ExtraInfo().end()) + item->SetProperty("Addon.Language", it->second); + + return item; +} + +bool CAddonsDirectory::GetScriptsAndPlugins(const std::string &content, VECADDONS &addons) +{ + CPluginSource::Content type = CPluginSource::Translate(content); + if (type == CPluginSource::UNKNOWN) + return false; + + VECADDONS tempAddons; + CServiceBroker::GetAddonMgr().GetAddons(tempAddons, AddonType::PLUGIN); + for (unsigned i=0; i<tempAddons.size(); i++) + { + const auto plugin = std::dynamic_pointer_cast<CPluginSource>(tempAddons[i]); + if (plugin && plugin->Provides(type)) + addons.push_back(tempAddons[i]); + } + tempAddons.clear(); + CServiceBroker::GetAddonMgr().GetAddons(tempAddons, AddonType::SCRIPT); + for (unsigned i=0; i<tempAddons.size(); i++) + { + const auto plugin = std::dynamic_pointer_cast<CPluginSource>(tempAddons[i]); + if (plugin && plugin->Provides(type)) + addons.push_back(tempAddons[i]); + } + tempAddons.clear(); + + if (type == CPluginSource::GAME) + { + CServiceBroker::GetAddonMgr().GetAddons(tempAddons, AddonType::GAMEDLL); + for (auto& addon : tempAddons) + { + if (IsStandaloneGame(addon)) + addons.push_back(addon); + } + } + + return true; +} + +bool CAddonsDirectory::GetScriptsAndPlugins(const std::string &content, CFileItemList &items) +{ + VECADDONS addons; + if (!GetScriptsAndPlugins(content, addons)) + return false; + + for (AddonPtr& addon : addons) + { + const bool bIsFolder = (addon->Type() == AddonType::PLUGIN); + + std::string path; + if (addon->HasType(AddonType::PLUGIN)) + { + path = "plugin://" + addon->ID(); + const auto plugin = std::dynamic_pointer_cast<CPluginSource>(addon); + if (plugin && plugin->ProvidesSeveral()) + { + CURL url(path); + std::string opt = StringUtils::Format("?content_type={}", content); + url.SetOptions(opt); + path = url.Get(); + } + } + else if (addon->HasType(AddonType::SCRIPT)) + { + path = "script://" + addon->ID(); + } + else if (addon->HasType(AddonType::GAMEDLL)) + { + // Kodi fails to launch games with empty path from home screen + path = "game://" + addon->ID(); + } + + items.Add(FileItemFromAddon(addon, path, bIsFolder)); + } + + items.SetContent("addons"); + items.SetLabel(g_localizeStrings.Get(24001)); // Add-ons + + return true; +} + +} + diff --git a/xbmc/filesystem/AddonsDirectory.h b/xbmc/filesystem/AddonsDirectory.h new file mode 100644 index 0000000..0ff0c11 --- /dev/null +++ b/xbmc/filesystem/AddonsDirectory.h @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" + +#include <memory> +#include <vector> + +class CFileItem; +class CFileItemList; +class CURL; +typedef std::shared_ptr<CFileItem> CFileItemPtr; + +namespace ADDON +{ +class IAddon; +using VECADDONS = std::vector<std::shared_ptr<IAddon>>; +} // namespace ADDON + +namespace XFILE +{ + + /*! + \ingroup windows + \brief Get access to shares and it's directories. + */ + class CAddonsDirectory : public IDirectory + { + public: + CAddonsDirectory(void); + ~CAddonsDirectory(void) override; + bool GetDirectory(const CURL& url, CFileItemList &items) override; + bool Create(const CURL& url) override { return true; } + bool Exists(const CURL& url) override { return true; } + bool AllowAll() const override { return true; } + + /*! \brief Fetch script and plugin addons of a given content type + \param content the content type to fetch + \param addons the list of addons to fill with scripts and plugin content + \return true if content is valid, false if it's invalid. + */ + static bool GetScriptsAndPlugins(const std::string &content, ADDON::VECADDONS &addons); + + /*! \brief Fetch scripts and plugins of a given content type + \param content the content type to fetch + \param items the list to fill with scripts and content + \return true if more than one item is found, false otherwise. + */ + static bool GetScriptsAndPlugins(const std::string &content, CFileItemList &items); + + static void GenerateAddonListing(const CURL& path, + const ADDON::VECADDONS& addons, + CFileItemList& items, + const std::string& label); + static CFileItemPtr FileItemFromAddon(const std::shared_ptr<ADDON::IAddon>& addon, + const std::string& path, + bool folder = false); + + /*! \brief Returns true if `path` is a path or subpath of the repository directory, otherwise false */ + static bool IsRepoDirectory(const CURL& path); + + private: + bool GetSearchResults(const CURL& path, CFileItemList &items); + }; +} diff --git a/xbmc/filesystem/AudioBookFileDirectory.cpp b/xbmc/filesystem/AudioBookFileDirectory.cpp new file mode 100644 index 0000000..04d1c14 --- /dev/null +++ b/xbmc/filesystem/AudioBookFileDirectory.cpp @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2014 Arne Morten Kvarving + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "AudioBookFileDirectory.h" + +#include "FileItem.h" +#include "TextureDatabase.h" +#include "URL.h" +#include "Util.h" +#include "filesystem/File.h" +#include "guilib/LocalizeStrings.h" +#include "music/tags/MusicInfoTag.h" +#include "utils/StringUtils.h" + +using namespace XFILE; + +static int cfile_file_read(void *h, uint8_t* buf, int size) +{ + CFile* pFile = static_cast<CFile*>(h); + return pFile->Read(buf, size); +} + +static int64_t cfile_file_seek(void *h, int64_t pos, int whence) +{ + CFile* pFile = static_cast<CFile*>(h); + if(whence == AVSEEK_SIZE) + return pFile->GetLength(); + else + return pFile->Seek(pos, whence & ~AVSEEK_FORCE); +} + +CAudioBookFileDirectory::~CAudioBookFileDirectory(void) +{ + if (m_fctx) + avformat_close_input(&m_fctx); + if (m_ioctx) + { + av_free(m_ioctx->buffer); + av_free(m_ioctx); + } +} + +bool CAudioBookFileDirectory::GetDirectory(const CURL& url, + CFileItemList &items) +{ + if (!m_fctx && !ContainsFiles(url)) + return true; + + std::string title; + std::string author; + std::string album; + + AVDictionaryEntry* tag=nullptr; + while ((tag = av_dict_get(m_fctx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) + { + if (StringUtils::CompareNoCase(tag->key, "title") == 0) + title = tag->value; + else if (StringUtils::CompareNoCase(tag->key, "album") == 0) + album = tag->value; + else if (StringUtils::CompareNoCase(tag->key, "artist") == 0) + author = tag->value; + } + + std::string thumb; + if (m_fctx->nb_chapters > 1) + thumb = CTextureUtils::GetWrappedImageURL(url.Get(), "music"); + + for (size_t i=0;i<m_fctx->nb_chapters;++i) + { + tag=nullptr; + std::string chaptitle = StringUtils::Format(g_localizeStrings.Get(25010), i + 1); + std::string chapauthor; + std::string chapalbum; + while ((tag=av_dict_get(m_fctx->chapters[i]->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) + { + if (StringUtils::CompareNoCase(tag->key, "title") == 0) + chaptitle = tag->value; + else if (StringUtils::CompareNoCase(tag->key, "artist") == 0) + chapauthor = tag->value; + else if (StringUtils::CompareNoCase(tag->key, "album") == 0) + chapalbum = tag->value; + } + CFileItemPtr item(new CFileItem(url.Get(),false)); + item->GetMusicInfoTag()->SetTrackNumber(i+1); + item->GetMusicInfoTag()->SetLoaded(true); + item->GetMusicInfoTag()->SetTitle(chaptitle); + if (album.empty()) + item->GetMusicInfoTag()->SetAlbum(title); + else if (chapalbum.empty()) + item->GetMusicInfoTag()->SetAlbum(album); + else + item->GetMusicInfoTag()->SetAlbum(chapalbum); + if (chapauthor.empty()) + item->GetMusicInfoTag()->SetArtist(author); + else + item->GetMusicInfoTag()->SetArtist(chapauthor); + + item->SetLabel(StringUtils::Format("{0:02}. {1} - {2}", i + 1, + item->GetMusicInfoTag()->GetAlbum(), + item->GetMusicInfoTag()->GetTitle())); + item->SetStartOffset(CUtil::ConvertSecsToMilliSecs(m_fctx->chapters[i]->start * + av_q2d(m_fctx->chapters[i]->time_base))); + item->SetEndOffset(m_fctx->chapters[i]->end * av_q2d(m_fctx->chapters[i]->time_base)); + int compare = m_fctx->duration / (AV_TIME_BASE); + if (item->GetEndOffset() < 0 || item->GetEndOffset() > compare) + { + if (i < m_fctx->nb_chapters-1) + item->SetEndOffset(m_fctx->chapters[i + 1]->start * + av_q2d(m_fctx->chapters[i + 1]->time_base)); + else + item->SetEndOffset(compare); + } + item->SetEndOffset(CUtil::ConvertSecsToMilliSecs(item->GetEndOffset())); + item->GetMusicInfoTag()->SetDuration( + CUtil::ConvertMilliSecsToSecsInt(item->GetEndOffset() - item->GetStartOffset())); + item->SetProperty("item_start", item->GetStartOffset()); + if (!thumb.empty()) + item->SetArt("thumb", thumb); + items.Add(item); + } + + return true; +} + +bool CAudioBookFileDirectory::Exists(const CURL& url) +{ + return CFile::Exists(url) && ContainsFiles(url); +} + +bool CAudioBookFileDirectory::ContainsFiles(const CURL& url) +{ + CFile file; + if (!file.Open(url)) + return false; + + uint8_t* buffer = (uint8_t*)av_malloc(32768); + m_ioctx = avio_alloc_context(buffer, 32768, 0, &file, cfile_file_read, + nullptr, cfile_file_seek); + + m_fctx = avformat_alloc_context(); + m_fctx->pb = m_ioctx; + + if (file.IoControl(IOCTRL_SEEK_POSSIBLE, nullptr) == 0) + m_ioctx->seekable = 0; + + m_ioctx->max_packet_size = 32768; + + AVInputFormat* iformat=nullptr; + av_probe_input_buffer(m_ioctx, &iformat, url.Get().c_str(), nullptr, 0, 0); + + bool contains = false; + if (avformat_open_input(&m_fctx, url.Get().c_str(), iformat, nullptr) < 0) + { + if (m_fctx) + avformat_close_input(&m_fctx); + av_free(m_ioctx->buffer); + av_free(m_ioctx); + return false; + } + + contains = m_fctx->nb_chapters > 1; + + return contains; +} diff --git a/xbmc/filesystem/AudioBookFileDirectory.h b/xbmc/filesystem/AudioBookFileDirectory.h new file mode 100644 index 0000000..ccc01cd --- /dev/null +++ b/xbmc/filesystem/AudioBookFileDirectory.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2014 Arne Morten Kvarving + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IFileDirectory.h" +extern "C" { +#include <libavformat/avformat.h> +} + +namespace XFILE +{ + class CAudioBookFileDirectory : public IFileDirectory + { + public: + ~CAudioBookFileDirectory(void) override; + bool GetDirectory(const CURL& url, CFileItemList &items) override; + bool Exists(const CURL& url) override; + bool ContainsFiles(const CURL& url) override; + bool IsAllowed(const CURL& url) const override { return true; } + + protected: + AVIOContext* m_ioctx = nullptr; + AVFormatContext* m_fctx = nullptr; + }; +} diff --git a/xbmc/filesystem/BlurayCallback.cpp b/xbmc/filesystem/BlurayCallback.cpp new file mode 100644 index 0000000..fbf9e7c --- /dev/null +++ b/xbmc/filesystem/BlurayCallback.cpp @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "BlurayCallback.h" + +#include "FileItem.h" +#include "filesystem/Directory.h" +#include "filesystem/File.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +using namespace XFILE; + +struct SDirState +{ + CFileItemList list; + int curr = 0; +}; + +void CBlurayCallback::bluray_logger(const char* msg) +{ + CLog::Log(LOGDEBUG, "CBlurayCallback::Logger - {}", msg); +} + +void CBlurayCallback::dir_close(BD_DIR_H *dir) +{ + if (dir) + { + CLog::Log(LOGDEBUG, "CBlurayCallback - Closed dir ({})", fmt::ptr(dir)); + delete static_cast<SDirState*>(dir->internal); + delete dir; + } +} + +BD_DIR_H* CBlurayCallback::dir_open(void *handle, const char* rel_path) +{ + std::string strRelPath(rel_path); + std::string* strBasePath = reinterpret_cast<std::string*>(handle); + if (!strBasePath) + { + CLog::Log(LOGDEBUG, "CBlurayCallback - Error opening dir, null handle!"); + return nullptr; + } + + std::string strDirname = URIUtils::AddFileToFolder(*strBasePath, strRelPath); + if (URIUtils::HasSlashAtEnd(strDirname)) + URIUtils::RemoveSlashAtEnd(strDirname); + + CLog::Log(LOGDEBUG, "CBlurayCallback - Opening dir {}", CURL::GetRedacted(strDirname)); + + SDirState *st = new SDirState(); + if (!CDirectory::GetDirectory(strDirname, st->list, "", DIR_FLAG_DEFAULTS)) + { + if (!CFile::Exists(strDirname)) + CLog::Log(LOGDEBUG, "CBlurayCallback - Error opening dir! ({})", + CURL::GetRedacted(strDirname)); + delete st; + return nullptr; + } + + BD_DIR_H *dir = new BD_DIR_H; + dir->close = dir_close; + dir->read = dir_read; + dir->internal = (void*)st; + + return dir; +} + +int CBlurayCallback::dir_read(BD_DIR_H *dir, BD_DIRENT *entry) +{ + SDirState* state = static_cast<SDirState*>(dir->internal); + + if (state->curr >= state->list.Size()) + return 1; + + strncpy(entry->d_name, state->list[state->curr]->GetLabel().c_str(), sizeof(entry->d_name)); + entry->d_name[sizeof(entry->d_name) - 1] = 0; + state->curr++; + + return 0; +} + +void CBlurayCallback::file_close(BD_FILE_H *file) +{ + if (file) + { + delete static_cast<CFile*>(file->internal); + delete file; + } +} + +int CBlurayCallback::file_eof(BD_FILE_H *file) +{ + if (static_cast<CFile*>(file->internal)->GetPosition() == static_cast<CFile*>(file->internal)->GetLength()) + return 1; + else + return 0; +} + +BD_FILE_H * CBlurayCallback::file_open(void *handle, const char *rel_path) +{ + std::string strRelPath(rel_path); + std::string* strBasePath = reinterpret_cast<std::string*>(handle); + if (!strBasePath) + { + CLog::Log(LOGDEBUG, "CBlurayCallback - Error opening dir, null handle!"); + return nullptr; + } + + std::string strFilename = URIUtils::AddFileToFolder(*strBasePath, strRelPath); + + BD_FILE_H *file = new BD_FILE_H; + + file->close = file_close; + file->seek = file_seek; + file->read = file_read; + file->write = file_write; + file->tell = file_tell; + file->eof = file_eof; + + CFile* fp = new CFile(); + if (fp->Open(strFilename)) + { + file->internal = (void*)fp; + return file; + } + + CLog::Log(LOGDEBUG, "CBlurayCallback - Error opening file! ({})", CURL::GetRedacted(strFilename)); + + delete fp; + delete file; + + return nullptr; +} + +int64_t CBlurayCallback::file_seek(BD_FILE_H *file, int64_t offset, int32_t origin) +{ + return static_cast<CFile*>(file->internal)->Seek(offset, origin); +} + +int64_t CBlurayCallback::file_tell(BD_FILE_H *file) +{ + return static_cast<CFile*>(file->internal)->GetPosition(); +} + +int64_t CBlurayCallback::file_read(BD_FILE_H *file, uint8_t *buf, int64_t size) +{ + return static_cast<int64_t>(static_cast<CFile*>(file->internal)->Read(buf, static_cast<size_t>(size))); +} + +int64_t CBlurayCallback::file_write(BD_FILE_H *file, const uint8_t *buf, int64_t size) +{ + return static_cast<int64_t>(static_cast<CFile*>(file->internal)->Write(buf, static_cast<size_t>(size))); +} diff --git a/xbmc/filesystem/BlurayCallback.h b/xbmc/filesystem/BlurayCallback.h new file mode 100644 index 0000000..181b056 --- /dev/null +++ b/xbmc/filesystem/BlurayCallback.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include <libbluray/filesystem.h> + +class CBlurayCallback +{ +public: + static void bluray_logger(const char* msg); + static void dir_close(BD_DIR_H* dir); + static BD_DIR_H* dir_open(void* handle, const char* rel_path); + static int dir_read(BD_DIR_H* dir, BD_DIRENT* entry); + static void file_close(BD_FILE_H* file); + static int file_eof(BD_FILE_H* file); + static BD_FILE_H* file_open(void* handle, const char* rel_path); + static int64_t file_read(BD_FILE_H* file, uint8_t* buf, int64_t size); + static int64_t file_seek(BD_FILE_H* file, int64_t offset, int32_t origin); + static int64_t file_tell(BD_FILE_H* file); + static int64_t file_write(BD_FILE_H* file, const uint8_t* buf, int64_t size); + +private: + CBlurayCallback() = default; + ~CBlurayCallback() = default; +}; diff --git a/xbmc/filesystem/BlurayDirectory.cpp b/xbmc/filesystem/BlurayDirectory.cpp new file mode 100644 index 0000000..dbfb41b --- /dev/null +++ b/xbmc/filesystem/BlurayDirectory.cpp @@ -0,0 +1,344 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ +#include "BlurayDirectory.h" + +#include "File.h" +#include "FileItem.h" +#include "LangInfo.h" +#include "URL.h" +#include "filesystem/BlurayCallback.h" +#include "filesystem/Directory.h" +#include "guilib/LocalizeStrings.h" +#include "utils/LangCodeExpander.h" +#include "utils/RegExp.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" +#include "video/VideoInfoTag.h" + +#include <array> +#include <cassert> +#include <climits> +#include <stdlib.h> +#include <string> + +#include <libbluray/bluray-version.h> +#include <libbluray/bluray.h> +#include <libbluray/filesystem.h> +#include <libbluray/log_control.h> + +namespace XFILE +{ + +#define MAIN_TITLE_LENGTH_PERCENT 70 /** Minimum length of main titles, based on longest title */ + +CBlurayDirectory::~CBlurayDirectory() +{ + Dispose(); +} + +void CBlurayDirectory::Dispose() +{ + if(m_bd) + { + bd_close(m_bd); + m_bd = nullptr; + } +} + +std::string CBlurayDirectory::GetBlurayTitle() +{ + return GetDiscInfoString(DiscInfo::TITLE); +} + +std::string CBlurayDirectory::GetBlurayID() +{ + return GetDiscInfoString(DiscInfo::ID); +} + +std::string CBlurayDirectory::GetDiscInfoString(DiscInfo info) +{ + switch (info) + { + case XFILE::CBlurayDirectory::DiscInfo::TITLE: + { + if (!m_blurayInitialized) + return ""; + const BLURAY_DISC_INFO* disc_info = bd_get_disc_info(m_bd); + if (!disc_info || !disc_info->bluray_detected) + return ""; + + std::string title = ""; + +#if (BLURAY_VERSION > BLURAY_VERSION_CODE(1,0,0)) + title = disc_info->disc_name ? disc_info->disc_name : ""; +#endif + + return title; + } + case XFILE::CBlurayDirectory::DiscInfo::ID: + { + if (!m_blurayInitialized) + return ""; + + const BLURAY_DISC_INFO* disc_info = bd_get_disc_info(m_bd); + if (!disc_info || !disc_info->bluray_detected) + return ""; + + std::string id = ""; + +#if (BLURAY_VERSION > BLURAY_VERSION_CODE(1,0,0)) + id = disc_info->udf_volume_id ? disc_info->udf_volume_id : ""; + + if (id.empty()) + { + id = HexToString(disc_info->disc_id, 20); + } +#endif + + return id; + } + default: + break; + } + + return ""; +} + +std::shared_ptr<CFileItem> CBlurayDirectory::GetTitle(const BLURAY_TITLE_INFO* title, + const std::string& label) +{ + std::string buf; + std::string chap; + CFileItemPtr item(new CFileItem("", false)); + CURL path(m_url); + buf = StringUtils::Format("BDMV/PLAYLIST/{:05}.mpls", title->playlist); + path.SetFileName(buf); + item->SetPath(path.Get()); + int duration = (int)(title->duration / 90000); + item->GetVideoInfoTag()->SetDuration(duration); + item->GetVideoInfoTag()->m_iTrack = title->playlist; + buf = StringUtils::Format(label, title->playlist); + item->m_strTitle = buf; + item->SetLabel(buf); + chap = StringUtils::Format(g_localizeStrings.Get(25007), title->chapter_count, + StringUtils::SecondsToTimeString(duration)); + item->SetLabel2(chap); + item->m_dwSize = 0; + item->SetArt("icon", "DefaultVideo.png"); + for(unsigned int i = 0; i < title->clip_count; ++i) + item->m_dwSize += title->clips[i].pkt_count * 192; + + return item; +} + +void CBlurayDirectory::GetTitles(bool main, CFileItemList &items) +{ + std::vector<BLURAY_TITLE_INFO*> titleList; + uint64_t minDuration = 0; + + // Searching for a user provided list of playlists. + if (main) + titleList = GetUserPlaylists(); + + if (!main || titleList.empty()) + { + uint32_t numTitles = bd_get_titles(m_bd, TITLES_RELEVANT, 0); + + for (uint32_t i = 0; i < numTitles; i++) + { + BLURAY_TITLE_INFO* t = bd_get_title_info(m_bd, i, 0); + + if (!t) + { + CLog::Log(LOGDEBUG, "CBlurayDirectory - unable to get title {}", i); + continue; + } + + if (main && t->duration > minDuration) + minDuration = t->duration; + + titleList.emplace_back(t); + } + } + + minDuration = minDuration * MAIN_TITLE_LENGTH_PERCENT / 100; + + for (auto& title : titleList) + { + if (title->duration < minDuration) + continue; + + items.Add(GetTitle(title, main ? g_localizeStrings.Get(25004) /* Main Title */ : g_localizeStrings.Get(25005) /* Title */)); + bd_free_title_info(title); + } +} + +void CBlurayDirectory::GetRoot(CFileItemList &items) +{ + GetTitles(true, items); + + CURL path(m_url); + CFileItemPtr item; + + path.SetFileName(URIUtils::AddFileToFolder(m_url.GetFileName(), "titles")); + item.reset(new CFileItem()); + item->SetPath(path.Get()); + item->m_bIsFolder = true; + item->SetLabel(g_localizeStrings.Get(25002) /* All titles */); + item->SetArt("icon", "DefaultVideoPlaylists.png"); + items.Add(item); + + const BLURAY_DISC_INFO* disc_info = bd_get_disc_info(m_bd); + if (disc_info && disc_info->no_menu_support) + { + CLog::Log(LOGDEBUG, "CBlurayDirectory::GetRoot - no menu support, skipping menu entry"); + return; + } + + path.SetFileName("menu"); + item.reset(new CFileItem()); + item->SetPath(path.Get()); + item->m_bIsFolder = false; + item->SetLabel(g_localizeStrings.Get(25003) /* Menus */); + item->SetArt("icon", "DefaultProgram.png"); + items.Add(item); +} + +bool CBlurayDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + Dispose(); + m_url = url; + std::string root = m_url.GetHostName(); + std::string file = m_url.GetFileName(); + URIUtils::RemoveSlashAtEnd(file); + URIUtils::RemoveSlashAtEnd(root); + + if (!InitializeBluray(root)) + return false; + + if(file == "root") + GetRoot(items); + else if(file == "root/titles") + GetTitles(false, items); + else + { + CURL url2 = GetUnderlyingCURL(url); + CDirectory::CHints hints; + hints.flags = m_flags; + if (!CDirectory::GetDirectory(url2, items, hints)) + return false; + } + + items.AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS("%L", "%D", "%L", "")); // FileName, Duration | Foldername, empty + items.AddSortMethod(SortBySize, 553, LABEL_MASKS("%L", "%I", "%L", "%I")); // FileName, Size | Foldername, Size + + return true; +} + +CURL CBlurayDirectory::GetUnderlyingCURL(const CURL& url) +{ + assert(url.IsProtocol("bluray")); + std::string host = url.GetHostName(); + const std::string& filename = url.GetFileName(); + return CURL(host.append(filename)); +} + +bool CBlurayDirectory::InitializeBluray(const std::string &root) +{ + bd_set_debug_handler(CBlurayCallback::bluray_logger); + bd_set_debug_mask(DBG_CRIT | DBG_BLURAY | DBG_NAV); + + m_bd = bd_init(); + + if (!m_bd) + { + CLog::Log(LOGERROR, "CBlurayDirectory::InitializeBluray - failed to initialize libbluray"); + return false; + } + + std::string langCode; + g_LangCodeExpander.ConvertToISO6392T(g_langInfo.GetDVDMenuLanguage(), langCode); + bd_set_player_setting_str(m_bd, BLURAY_PLAYER_SETTING_MENU_LANG, langCode.c_str()); + + if (!bd_open_files(m_bd, const_cast<std::string*>(&root), CBlurayCallback::dir_open, CBlurayCallback::file_open)) + { + CLog::Log(LOGERROR, "CBlurayDirectory::InitializeBluray - failed to open {}", + CURL::GetRedacted(root)); + return false; + } + m_blurayInitialized = true; + + return true; +} + +std::string CBlurayDirectory::HexToString(const uint8_t *buf, int count) +{ + std::array<char, 42> tmp; + + for (int i = 0; i < count; i++) + { + sprintf(tmp.data() + (i * 2), "%02x", buf[i]); + } + + return std::string(std::begin(tmp), std::end(tmp)); +} + +std::vector<BLURAY_TITLE_INFO*> CBlurayDirectory::GetUserPlaylists() +{ + std::string root = m_url.GetHostName(); + std::string discInfPath = URIUtils::AddFileToFolder(root, "disc.inf"); + std::vector<BLURAY_TITLE_INFO*> userTitles; + CFile file; + char buffer[1025]; + + if (file.Open(discInfPath)) + { + CLog::Log(LOGDEBUG, "CBlurayDirectory::GetTitles - disc.inf found"); + + CRegExp pl(true); + if (!pl.RegComp("(\\d+)")) + { + file.Close(); + return userTitles; + } + + uint8_t maxLines = 100; + while ((maxLines > 0) && file.ReadString(buffer, 1024)) + { + maxLines--; + if (StringUtils::StartsWithNoCase(buffer, "playlists")) + { + int pos = 0; + while ((pos = pl.RegFind(buffer, static_cast<unsigned int>(pos))) >= 0) + { + std::string playlist = pl.GetMatch(0); + uint32_t len = static_cast<uint32_t>(playlist.length()); + + if (len <= 5) + { + unsigned long int plNum = strtoul(playlist.c_str(), nullptr, 10); + + BLURAY_TITLE_INFO* t = bd_get_playlist_info(m_bd, static_cast<uint32_t>(plNum), 0); + if (t) + userTitles.emplace_back(t); + } + + if (static_cast<int64_t>(pos) + static_cast<int64_t>(len) > INT_MAX) + break; + else + pos += len; + } + } + } + file.Close(); + } + return userTitles; +} + +} /* namespace XFILE */ diff --git a/xbmc/filesystem/BlurayDirectory.h b/xbmc/filesystem/BlurayDirectory.h new file mode 100644 index 0000000..f4b2be6 --- /dev/null +++ b/xbmc/filesystem/BlurayDirectory.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" +#include "URL.h" + +#include <memory> + +class CFileItem; +class CFileItemList; + +typedef struct bluray BLURAY; +typedef struct bd_title_info BLURAY_TITLE_INFO; + +namespace XFILE +{ + +class CBlurayDirectory : public IDirectory +{ +public: + CBlurayDirectory() = default; + ~CBlurayDirectory() override; + bool GetDirectory(const CURL& url, CFileItemList &items) override; + + bool InitializeBluray(const std::string &root); + std::string GetBlurayTitle(); + std::string GetBlurayID(); + +private: + enum class DiscInfo + { + TITLE, + ID + }; + + void Dispose(); + std::string GetDiscInfoString(DiscInfo info); + void GetRoot (CFileItemList &items); + void GetTitles(bool main, CFileItemList &items); + std::vector<BLURAY_TITLE_INFO*> GetUserPlaylists(); + std::shared_ptr<CFileItem> GetTitle(const BLURAY_TITLE_INFO* title, const std::string& label); + CURL GetUnderlyingCURL(const CURL& url); + std::string HexToString(const uint8_t * buf, int count); + CURL m_url; + BLURAY* m_bd = nullptr; + bool m_blurayInitialized = false; +}; + +} diff --git a/xbmc/filesystem/BlurayFile.cpp b/xbmc/filesystem/BlurayFile.cpp new file mode 100644 index 0000000..34d2732 --- /dev/null +++ b/xbmc/filesystem/BlurayFile.cpp @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "BlurayFile.h" + +#include "URL.h" + +#include <assert.h> + +namespace XFILE +{ + + CBlurayFile::CBlurayFile(void) + : COverrideFile(false) + { } + + CBlurayFile::~CBlurayFile(void) = default; + + std::string CBlurayFile::TranslatePath(const CURL& url) + { + assert(url.IsProtocol("bluray")); + + std::string host = url.GetHostName(); + const std::string& filename = url.GetFileName(); + if (host.empty() || filename.empty()) + return ""; + + return host.append(filename); + } +} /* namespace XFILE */ diff --git a/xbmc/filesystem/BlurayFile.h b/xbmc/filesystem/BlurayFile.h new file mode 100644 index 0000000..1d43936 --- /dev/null +++ b/xbmc/filesystem/BlurayFile.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "filesystem/OverrideFile.h" + +namespace XFILE +{ + + class CBlurayFile : public COverrideFile + { + public: + CBlurayFile(); + ~CBlurayFile() override; + + protected: + std::string TranslatePath(const CURL& url) override; + }; +} diff --git a/xbmc/filesystem/CDDADirectory.cpp b/xbmc/filesystem/CDDADirectory.cpp new file mode 100644 index 0000000..9b6beaa --- /dev/null +++ b/xbmc/filesystem/CDDADirectory.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "CDDADirectory.h" + +#include "File.h" +#include "FileItem.h" +#include "ServiceBroker.h" +#include "music/MusicDatabase.h" +#include "storage/MediaManager.h" +#include "utils/StringUtils.h" + +using namespace XFILE; +using namespace MEDIA_DETECT; + +CCDDADirectory::CCDDADirectory(void) = default; + +CCDDADirectory::~CCDDADirectory(void) = default; + + +bool CCDDADirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + // Reads the tracks from an audio cd + std::string strPath = url.Get(); + + if (!CServiceBroker::GetMediaManager().IsDiscInDrive(strPath)) + return false; + + // Get information for the inserted disc + CCdInfo* pCdInfo = CServiceBroker::GetMediaManager().GetCdInfo(strPath); + if (pCdInfo == NULL) + return false; + + // Preload CDDB info + CMusicDatabase musicdatabase; + musicdatabase.LookupCDDBInfo(); + + // If the disc has no tracks, we are finished here. + int nTracks = pCdInfo->GetTrackCount(); + if (nTracks <= 0) + return false; + + // Generate fileitems + for (int i = 1;i <= nTracks;++i) + { + // Skip Datatracks for display, + // but needed to query cddb + if (!pCdInfo->IsAudio(i)) + continue; + + // Format standard cdda item label + std::string strLabel = StringUtils::Format("Track {:02}", i); + + CFileItemPtr pItem(new CFileItem(strLabel)); + pItem->m_bIsFolder = false; + std::string path = StringUtils::Format("cdda://local/{:02}.cdda", i); + pItem->SetPath(path); + + struct __stat64 s64; + if (CFile::Stat(pItem->GetPath(), &s64) == 0) + pItem->m_dwSize = s64.st_size; + + items.Add(pItem); + } + return true; +} diff --git a/xbmc/filesystem/CDDADirectory.h b/xbmc/filesystem/CDDADirectory.h new file mode 100644 index 0000000..12e5d42 --- /dev/null +++ b/xbmc/filesystem/CDDADirectory.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" + +namespace XFILE +{ + +class CCDDADirectory : + public IDirectory +{ +public: + CCDDADirectory(void); + ~CCDDADirectory(void) override; + bool GetDirectory(const CURL& url, CFileItemList &items) override; +}; +} diff --git a/xbmc/filesystem/CDDAFile.cpp b/xbmc/filesystem/CDDAFile.cpp new file mode 100644 index 0000000..cd98b08 --- /dev/null +++ b/xbmc/filesystem/CDDAFile.cpp @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "CDDAFile.h" + +#include "ServiceBroker.h" +#include "URL.h" +#include "storage/MediaManager.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +#include <algorithm> + +#include <sys/stat.h> + +using namespace MEDIA_DETECT; +using namespace XFILE; + +CFileCDDA::CFileCDDA(void) +{ + m_pCdIo = NULL; + m_cdio = CLibcdio::GetInstance(); + m_iSectorCount = 52; +} + +CFileCDDA::~CFileCDDA(void) +{ + Close(); +} + +bool CFileCDDA::Open(const CURL& url) +{ + std::string strURL = url.GetWithoutFilename(); + + if (!CServiceBroker::GetMediaManager().IsDiscInDrive(strURL) || !IsValidFile(url)) + return false; + + // Open the dvd drive +#ifdef TARGET_POSIX + m_pCdIo = m_cdio->cdio_open(CServiceBroker::GetMediaManager().TranslateDevicePath(strURL).c_str(), + DRIVER_UNKNOWN); +#elif defined(TARGET_WINDOWS) + m_pCdIo = m_cdio->cdio_open_win32( + CServiceBroker::GetMediaManager().TranslateDevicePath(strURL, true).c_str()); +#endif + if (!m_pCdIo) + { + CLog::Log(LOGERROR, "file cdda: Opening the dvd drive failed"); + return false; + } + + int iTrack = GetTrackNum(url); + + m_lsnStart = m_cdio->cdio_get_track_lsn(m_pCdIo, iTrack); + m_lsnEnd = m_cdio->cdio_get_track_last_lsn(m_pCdIo, iTrack); + m_lsnCurrent = m_lsnStart; + + if (m_lsnStart == CDIO_INVALID_LSN || m_lsnEnd == CDIO_INVALID_LSN) + { + m_cdio->cdio_destroy(m_pCdIo); + m_pCdIo = NULL; + return false; + } + + return true; +} + +bool CFileCDDA::Exists(const CURL& url) +{ + if (!IsValidFile(url)) + return false; + + int iTrack = GetTrackNum(url); + + if (!Open(url)) + return false; + + int iLastTrack = m_cdio->cdio_get_last_track_num(m_pCdIo); + if (iLastTrack == CDIO_INVALID_TRACK) + return false; + + return (iTrack > 0 && iTrack <= iLastTrack); +} + +int CFileCDDA::Stat(const CURL& url, struct __stat64* buffer) +{ + if (Open(url) && buffer) + { + *buffer = {}; + buffer->st_size = GetLength(); + buffer->st_mode = _S_IFREG; + Close(); + return 0; + } + errno = ENOENT; + return -1; +} + +ssize_t CFileCDDA::Read(void* lpBuf, size_t uiBufSize) +{ + if (!m_pCdIo || !CServiceBroker::GetMediaManager().IsDiscInDrive()) + return -1; + + if (uiBufSize > SSIZE_MAX) + uiBufSize = SSIZE_MAX; + + // limit number of sectors that fits in buffer by m_iSectorCount + int iSectorCount = std::min((int)uiBufSize / CDIO_CD_FRAMESIZE_RAW, m_iSectorCount); + + if (iSectorCount <= 0) + return -1; + + // Are there enough sectors left to read + if (m_lsnCurrent + iSectorCount > m_lsnEnd) + iSectorCount = m_lsnEnd - m_lsnCurrent; + + // The loop tries to solve read error problem by lowering number of sectors to read (iSectorCount). + // When problem is solved the proper number of sectors is stored in m_iSectorCount + int big_iSectorCount = iSectorCount; + while (iSectorCount > 0) + { + int iret = m_cdio->cdio_read_audio_sectors(m_pCdIo, lpBuf, m_lsnCurrent, iSectorCount); + + if (iret == DRIVER_OP_SUCCESS) + { + // If lower iSectorCount solved the problem limit it's value + if (iSectorCount < big_iSectorCount) + { + m_iSectorCount = iSectorCount; + } + break; + } + + // iSectorCount is low so it cannot solve read problem + if (iSectorCount <= 10) + { + CLog::Log(LOGERROR, + "file cdda: Reading {} sectors of audio data starting at lsn {} failed with error " + "code {}", + iSectorCount, m_lsnCurrent, iret); + return -1; + } + + iSectorCount = 10; + } + m_lsnCurrent += iSectorCount; + + return iSectorCount*CDIO_CD_FRAMESIZE_RAW; +} + +int64_t CFileCDDA::Seek(int64_t iFilePosition, int iWhence /*=SEEK_SET*/) +{ + if (!m_pCdIo) + return -1; + + lsn_t lsnPosition = (int)iFilePosition / CDIO_CD_FRAMESIZE_RAW; + + switch (iWhence) + { + case SEEK_SET: + // cur = pos + m_lsnCurrent = m_lsnStart + lsnPosition; + break; + case SEEK_CUR: + // cur += pos + m_lsnCurrent += lsnPosition; + break; + case SEEK_END: + // end += pos + m_lsnCurrent = m_lsnEnd + lsnPosition; + break; + default: + return -1; + } + + return ((int64_t)(m_lsnCurrent -m_lsnStart)*CDIO_CD_FRAMESIZE_RAW); +} + +void CFileCDDA::Close() +{ + if (m_pCdIo) + { + m_cdio->cdio_destroy(m_pCdIo); + m_pCdIo = NULL; + } +} + +int64_t CFileCDDA::GetPosition() +{ + if (!m_pCdIo) + return 0; + + return ((int64_t)(m_lsnCurrent -m_lsnStart)*CDIO_CD_FRAMESIZE_RAW); +} + +int64_t CFileCDDA::GetLength() +{ + if (!m_pCdIo) + return 0; + + return ((int64_t)(m_lsnEnd -m_lsnStart)*CDIO_CD_FRAMESIZE_RAW); +} + +bool CFileCDDA::IsValidFile(const CURL& url) +{ + // Only .cdda files are supported + return URIUtils::HasExtension(url.Get(), ".cdda"); +} + +int CFileCDDA::GetTrackNum(const CURL& url) +{ + std::string strFileName = url.Get(); + + // get track number from "cdda://local/01.cdda" + return atoi(strFileName.substr(13, strFileName.size() - 13 - 5).c_str()); +} + +#define SECTOR_COUNT 52 // max. sectors that can be read at once +int CFileCDDA::GetChunkSize() +{ + return SECTOR_COUNT*CDIO_CD_FRAMESIZE_RAW; +} diff --git a/xbmc/filesystem/CDDAFile.h b/xbmc/filesystem/CDDAFile.h new file mode 100644 index 0000000..654e0f9 --- /dev/null +++ b/xbmc/filesystem/CDDAFile.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IFile.h" +#include "storage/cdioSupport.h" + +namespace XFILE +{ +class CFileCDDA : public IFile +{ +public: + CFileCDDA(void); + ~CFileCDDA(void) override; + bool Open(const CURL& url) override; + bool Exists(const CURL& url) override; + int Stat(const CURL& url, struct __stat64* buffer) override; + + ssize_t Read(void* lpBuf, size_t uiBufSize) override; + int64_t Seek(int64_t iFilePosition, int iWhence = SEEK_SET) override; + void Close() override; + int64_t GetPosition() override; + int64_t GetLength() override; + int GetChunkSize() override; + +protected: + bool IsValidFile(const CURL& url); + int GetTrackNum(const CURL& url); + +protected: + CdIo_t* m_pCdIo; + lsn_t m_lsnStart = CDIO_INVALID_LSN; // Start of m_iTrack in logical sector number + lsn_t m_lsnCurrent = CDIO_INVALID_LSN; // Position inside the track in logical sector number + lsn_t m_lsnEnd = CDIO_INVALID_LSN; // End of m_iTrack in logical sector number + int m_iSectorCount; // max number of sectors to read at once + std::shared_ptr<MEDIA_DETECT::CLibcdio> m_cdio; +}; +} diff --git a/xbmc/filesystem/CMakeLists.txt b/xbmc/filesystem/CMakeLists.txt new file mode 100644 index 0000000..8bbaa44 --- /dev/null +++ b/xbmc/filesystem/CMakeLists.txt @@ -0,0 +1,177 @@ +set(SOURCES AddonsDirectory.cpp + AudioBookFileDirectory.cpp + CacheStrategy.cpp + CircularCache.cpp + CurlFile.cpp + DAVCommon.cpp + DAVDirectory.cpp + DAVFile.cpp + DirectoryCache.cpp + Directory.cpp + DirectoryFactory.cpp + DirectoryHistory.cpp + DllLibCurl.cpp + EventsDirectory.cpp + FavouritesDirectory.cpp + FileCache.cpp + File.cpp + FileDirectoryFactory.cpp + FileFactory.cpp + FTPDirectory.cpp + FTPParse.cpp + HTTPDirectory.cpp + IDirectory.cpp + IFile.cpp + ImageFile.cpp + LibraryDirectory.cpp + MultiPathDirectory.cpp + MultiPathFile.cpp + MusicDatabaseDirectory.cpp + MusicDatabaseFile.cpp + MusicFileDirectory.cpp + MusicSearchDirectory.cpp + OverrideDirectory.cpp + OverrideFile.cpp + PipeFile.cpp + PipesManager.cpp + PlaylistDirectory.cpp + PlaylistFileDirectory.cpp + PluginDirectory.cpp + PluginFile.cpp + PVRDirectory.cpp + ResourceDirectory.cpp + ResourceFile.cpp + RSSDirectory.cpp + ShoutcastFile.cpp + SmartPlaylistDirectory.cpp + SourcesDirectory.cpp + SpecialProtocol.cpp + SpecialProtocolDirectory.cpp + SpecialProtocolFile.cpp + StackDirectory.cpp + VideoDatabaseDirectory.cpp + VideoDatabaseFile.cpp + VirtualDirectory.cpp + XbtDirectory.cpp + XbtFile.cpp + XbtManager.cpp + ZeroconfDirectory.cpp + ZipDirectory.cpp + ZipFile.cpp + ZipManager.cpp) + +set(HEADERS AddonsDirectory.h + CacheStrategy.h + CircularCache.h + CurlFile.h + DAVCommon.h + DAVDirectory.h + DAVFile.h + Directorization.h + Directory.h + DirectoryCache.h + DirectoryFactory.h + DirectoryHistory.h + DllLibCurl.h + EventsDirectory.h + FTPDirectory.h + FTPParse.h + FavouritesDirectory.h + File.h + FileCache.h + FileDirectoryFactory.h + FileFactory.h + HTTPDirectory.h + IDirectory.h + IFile.h + IFileDirectory.h + IFileTypes.h + ImageFile.h + LibraryDirectory.h + MultiPathDirectory.h + MultiPathFile.h + MusicDatabaseDirectory.h + MusicDatabaseFile.h + MusicFileDirectory.h + MusicSearchDirectory.h + OverrideDirectory.h + OverrideFile.h + PVRDirectory.h + PipeFile.h + PipesManager.h + PlaylistDirectory.h + PlaylistFileDirectory.h + PluginDirectory.h + PluginFile.h + RSSDirectory.h + ResourceDirectory.h + ResourceFile.h + ShoutcastFile.h + SmartPlaylistDirectory.h + SourcesDirectory.h + SpecialProtocol.h + SpecialProtocolDirectory.h + SpecialProtocolFile.h + StackDirectory.h + VideoDatabaseDirectory.h + VideoDatabaseFile.h + VirtualDirectory.h + XbtDirectory.h + XbtFile.h + XbtManager.h + ZeroconfDirectory.h + ZipDirectory.h + ZipFile.h + ZipManager.h) + +if(ISO9660PP_FOUND) + list(APPEND SOURCES ISO9660Directory.cpp + ISO9660File.cpp) + list(APPEND HEADERS ISO9660Directory.h + ISO9660File.h) +endif() + +if(UDFREAD_FOUND) + list(APPEND SOURCES UDFBlockInput.cpp + UDFDirectory.cpp + UDFFile.cpp) + list(APPEND HEADERS UDFBlockInput.h + UDFDirectory.h + UDFFile.h) +endif() + +if(BLURAY_FOUND) + list(APPEND SOURCES BlurayCallback.cpp + BlurayDirectory.cpp + BlurayFile.cpp) + list(APPEND HEADERS BlurayCallback.h + BlurayDirectory.h + BlurayFile.h) +endif() + +if(ENABLE_OPTICAL) + list(APPEND SOURCES CDDADirectory.cpp + CDDAFile.cpp) + list(APPEND HEADERS CDDADirectory.h + CDDAFile.h) +endif() + +if(NFS_FOUND) + list(APPEND SOURCES NFSDirectory.cpp + NFSFile.cpp) + list(APPEND HEADERS NFSDirectory.h + NFSFile.h) +endif() + +if(ENABLE_UPNP) + list(APPEND SOURCES NptXbmcFile.cpp + UPnPDirectory.cpp + UPnPFile.cpp) + list(APPEND HEADERS UPnPDirectory.h + UPnPFile.h) +endif() + +core_add_library(filesystem) +if(ENABLE_STATIC_LIBS AND ENABLE_UPNP) + target_link_libraries(filesystem PRIVATE upnp) +endif() diff --git a/xbmc/filesystem/CacheStrategy.cpp b/xbmc/filesystem/CacheStrategy.cpp new file mode 100644 index 0000000..7aaeec9 --- /dev/null +++ b/xbmc/filesystem/CacheStrategy.cpp @@ -0,0 +1,447 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "threads/SystemClock.h" +#include "CacheStrategy.h" +#include "IFile.h" +#ifdef TARGET_POSIX +#include "PlatformDefs.h" +#include "platform/posix/ConvUtils.h" +#endif +#include "Util.h" +#include "utils/log.h" +#include "SpecialProtocol.h" +#include "URL.h" +#if defined(TARGET_POSIX) +#include "platform/posix/filesystem/PosixFile.h" +#define CacheLocalFile CPosixFile +#elif defined(TARGET_WINDOWS) +#include "platform/win32/filesystem/Win32File.h" +#define CacheLocalFile CWin32File +#endif // TARGET_WINDOWS + +#include <cassert> +#include <algorithm> + +using namespace XFILE; + +using namespace std::chrono_literals; + +CCacheStrategy::~CCacheStrategy() = default; + +void CCacheStrategy::EndOfInput() { + m_bEndOfInput = true; +} + +bool CCacheStrategy::IsEndOfInput() +{ + return m_bEndOfInput; +} + +void CCacheStrategy::ClearEndOfInput() +{ + m_bEndOfInput = false; +} + +CSimpleFileCache::CSimpleFileCache() + : m_cacheFileRead(new CacheLocalFile()) + , m_cacheFileWrite(new CacheLocalFile()) + , m_hDataAvailEvent(NULL) +{ +} + +CSimpleFileCache::~CSimpleFileCache() +{ + Close(); + delete m_cacheFileRead; + delete m_cacheFileWrite; +} + +int CSimpleFileCache::Open() +{ + Close(); + + m_hDataAvailEvent = new CEvent; + + m_filename = CSpecialProtocol::TranslatePath( + CUtil::GetNextFilename("special://temp/filecache{:03}.cache", 999)); + if (m_filename.empty()) + { + CLog::Log(LOGERROR, "CSimpleFileCache::{} - Unable to generate a new filename", __FUNCTION__); + Close(); + return CACHE_RC_ERROR; + } + + CURL fileURL(m_filename); + + if (!m_cacheFileWrite->OpenForWrite(fileURL, false)) + { + CLog::Log(LOGERROR, "CSimpleFileCache::{} - Failed to create file \"{}\" for writing", + __FUNCTION__, m_filename); + Close(); + return CACHE_RC_ERROR; + } + + if (!m_cacheFileRead->Open(fileURL)) + { + CLog::Log(LOGERROR, "CSimpleFileCache::{} - Failed to open file \"{}\" for reading", + __FUNCTION__, m_filename); + Close(); + return CACHE_RC_ERROR; + } + + return CACHE_RC_OK; +} + +void CSimpleFileCache::Close() +{ + if (m_hDataAvailEvent) + delete m_hDataAvailEvent; + + m_hDataAvailEvent = NULL; + + m_cacheFileWrite->Close(); + m_cacheFileRead->Close(); + + if (!m_filename.empty() && !m_cacheFileRead->Delete(CURL(m_filename))) + CLog::Log(LOGWARNING, "SimpleFileCache::{} - Failed to delete cache file \"{}\"", __FUNCTION__, + m_filename); + + m_filename.clear(); +} + +size_t CSimpleFileCache::GetMaxWriteSize(const size_t& iRequestSize) +{ + return iRequestSize; // Can always write since it's on disk +} + +int CSimpleFileCache::WriteToCache(const char *pBuffer, size_t iSize) +{ + size_t written = 0; + while (iSize > 0) + { + const ssize_t lastWritten = + m_cacheFileWrite->Write(pBuffer, std::min(iSize, static_cast<size_t>(SSIZE_MAX))); + if (lastWritten <= 0) + { + CLog::Log(LOGERROR, "SimpleFileCache::{} - <{}> Failed to write to cache", __FUNCTION__, + m_filename); + return CACHE_RC_ERROR; + } + m_nWritePosition += lastWritten; + iSize -= lastWritten; + written += lastWritten; + } + + // when reader waits for data it will wait on the event. + m_hDataAvailEvent->Set(); + + return written; +} + +int64_t CSimpleFileCache::GetAvailableRead() +{ + return m_nWritePosition - m_nReadPosition; +} + +int CSimpleFileCache::ReadFromCache(char *pBuffer, size_t iMaxSize) +{ + int64_t iAvailable = GetAvailableRead(); + if ( iAvailable <= 0 ) + return m_bEndOfInput ? 0 : CACHE_RC_WOULD_BLOCK; + + size_t toRead = std::min(iMaxSize, static_cast<size_t>(iAvailable)); + + size_t readBytes = 0; + while (toRead > 0) + { + const ssize_t lastRead = + m_cacheFileRead->Read(pBuffer, std::min(toRead, static_cast<size_t>(SSIZE_MAX))); + + if (lastRead == 0) + break; + if (lastRead < 0) + { + CLog::Log(LOGERROR, "CSimpleFileCache::{} - <{}> Failed to read from cache", __FUNCTION__, + m_filename); + return CACHE_RC_ERROR; + } + m_nReadPosition += lastRead; + toRead -= lastRead; + readBytes += lastRead; + } + + if (readBytes > 0) + m_space.Set(); + + return readBytes; +} + +int64_t CSimpleFileCache::WaitForData(uint32_t iMinAvail, std::chrono::milliseconds timeout) +{ + if (timeout == 0ms || IsEndOfInput()) + return GetAvailableRead(); + + XbmcThreads::EndTime<> endTime{timeout}; + while (!IsEndOfInput()) + { + int64_t iAvail = GetAvailableRead(); + if (iAvail >= iMinAvail) + return iAvail; + + if (!m_hDataAvailEvent->Wait(endTime.GetTimeLeft())) + return CACHE_RC_TIMEOUT; + } + return GetAvailableRead(); +} + +int64_t CSimpleFileCache::Seek(int64_t iFilePosition) +{ + int64_t iTarget = iFilePosition - m_nStartPosition; + + if (iTarget < 0) + { + CLog::Log(LOGDEBUG, "CSimpleFileCache::{} - <{}> Request seek to {} before start of cache", + __FUNCTION__, iFilePosition, m_filename); + return CACHE_RC_ERROR; + } + + int64_t nDiff = iTarget - m_nWritePosition; + if (nDiff > 500000) + { + CLog::Log(LOGDEBUG, + "CSimpleFileCache::{} - <{}> Requested position {} is beyond cached data ({})", + __FUNCTION__, m_filename, iFilePosition, m_nWritePosition); + return CACHE_RC_ERROR; + } + + if (nDiff > 0 && + WaitForData(static_cast<uint32_t>(iTarget - m_nReadPosition), 5s) == CACHE_RC_TIMEOUT) + { + CLog::Log(LOGDEBUG, "CSimpleFileCache::{} - <{}> Wait for position {} failed. Ended up at {}", + __FUNCTION__, m_filename, iFilePosition, m_nWritePosition); + return CACHE_RC_ERROR; + } + + m_nReadPosition = m_cacheFileRead->Seek(iTarget, SEEK_SET); + if (m_nReadPosition != iTarget) + { + CLog::Log(LOGERROR, "CSimpleFileCache::{} - <{}> Can't seek cache file for position {}", + __FUNCTION__, iFilePosition, m_filename); + return CACHE_RC_ERROR; + } + + m_space.Set(); + + return iFilePosition; +} + +bool CSimpleFileCache::Reset(int64_t iSourcePosition) +{ + if (IsCachedPosition(iSourcePosition)) + { + m_nReadPosition = m_cacheFileRead->Seek(iSourcePosition - m_nStartPosition, SEEK_SET); + return false; + } + + m_nStartPosition = iSourcePosition; + m_nWritePosition = m_cacheFileWrite->Seek(0, SEEK_SET); + m_nReadPosition = m_cacheFileRead->Seek(0, SEEK_SET); + return true; +} + +void CSimpleFileCache::EndOfInput() +{ + CCacheStrategy::EndOfInput(); + m_hDataAvailEvent->Set(); +} + +int64_t CSimpleFileCache::CachedDataEndPosIfSeekTo(int64_t iFilePosition) +{ + if (iFilePosition >= m_nStartPosition && iFilePosition <= m_nStartPosition + m_nWritePosition) + return m_nStartPosition + m_nWritePosition; + return iFilePosition; +} + +int64_t CSimpleFileCache::CachedDataStartPos() +{ + return m_nStartPosition; +} + +int64_t CSimpleFileCache::CachedDataEndPos() +{ + return m_nStartPosition + m_nWritePosition; +} + +bool CSimpleFileCache::IsCachedPosition(int64_t iFilePosition) +{ + return iFilePosition >= m_nStartPosition && iFilePosition <= m_nStartPosition + m_nWritePosition; +} + +CCacheStrategy *CSimpleFileCache::CreateNew() +{ + return new CSimpleFileCache(); +} + + +CDoubleCache::CDoubleCache(CCacheStrategy *impl) +{ + assert(NULL != impl); + m_pCache = impl; + m_pCacheOld = NULL; +} + +CDoubleCache::~CDoubleCache() +{ + delete m_pCache; + delete m_pCacheOld; +} + +int CDoubleCache::Open() +{ + return m_pCache->Open(); +} + +void CDoubleCache::Close() +{ + m_pCache->Close(); + if (m_pCacheOld) + { + delete m_pCacheOld; + m_pCacheOld = NULL; + } +} + +size_t CDoubleCache::GetMaxWriteSize(const size_t& iRequestSize) +{ + return m_pCache->GetMaxWriteSize(iRequestSize); // NOTE: Check the active cache only +} + +int CDoubleCache::WriteToCache(const char *pBuffer, size_t iSize) +{ + return m_pCache->WriteToCache(pBuffer, iSize); +} + +int CDoubleCache::ReadFromCache(char *pBuffer, size_t iMaxSize) +{ + return m_pCache->ReadFromCache(pBuffer, iMaxSize); +} + +int64_t CDoubleCache::WaitForData(uint32_t iMinAvail, std::chrono::milliseconds timeout) +{ + return m_pCache->WaitForData(iMinAvail, timeout); +} + +int64_t CDoubleCache::Seek(int64_t iFilePosition) +{ + /* Check whether position is NOT in our current cache but IS in our old cache. + * This is faster/more efficient than having to possibly wait for data in the + * Seek() call below + */ + if (!m_pCache->IsCachedPosition(iFilePosition) && + m_pCacheOld && m_pCacheOld->IsCachedPosition(iFilePosition)) + { + // Return error to trigger a seek event which will swap the caches: + return CACHE_RC_ERROR; + } + + return m_pCache->Seek(iFilePosition); // Normal seek +} + +bool CDoubleCache::Reset(int64_t iSourcePosition) +{ + /* Check if we should (not) swap the caches. Note that when both caches have the + * requested position, we prefer the cache that has the most forward data + */ + if (m_pCache->IsCachedPosition(iSourcePosition) && + (!m_pCacheOld || !m_pCacheOld->IsCachedPosition(iSourcePosition) || + m_pCache->CachedDataEndPos() >= m_pCacheOld->CachedDataEndPos())) + { + // No swap: Just use current cache + return m_pCache->Reset(iSourcePosition); + } + + // Need to swap caches + CCacheStrategy* pCacheTmp; + if (!m_pCacheOld) + { + pCacheTmp = m_pCache->CreateNew(); + if (pCacheTmp->Open() != CACHE_RC_OK) + { + delete pCacheTmp; + return m_pCache->Reset(iSourcePosition); + } + } + else + { + pCacheTmp = m_pCacheOld; + } + + // Perform actual swap: + m_pCacheOld = m_pCache; + m_pCache = pCacheTmp; + + // If new active cache still doesn't have this position, log it + if (!m_pCache->IsCachedPosition(iSourcePosition)) + { + CLog::Log(LOGDEBUG, "CDoubleCache::{} - ({}) Cache miss for {} with new={}-{} and old={}-{}", + __FUNCTION__, fmt::ptr(this), iSourcePosition, m_pCache->CachedDataStartPos(), + m_pCache->CachedDataEndPos(), m_pCacheOld->CachedDataStartPos(), + m_pCacheOld->CachedDataEndPos()); + } + + return m_pCache->Reset(iSourcePosition); +} + +void CDoubleCache::EndOfInput() +{ + m_pCache->EndOfInput(); +} + +bool CDoubleCache::IsEndOfInput() +{ + return m_pCache->IsEndOfInput(); +} + +void CDoubleCache::ClearEndOfInput() +{ + m_pCache->ClearEndOfInput(); +} + +int64_t CDoubleCache::CachedDataStartPos() +{ + return m_pCache->CachedDataStartPos(); +} + +int64_t CDoubleCache::CachedDataEndPos() +{ + return m_pCache->CachedDataEndPos(); +} + +int64_t CDoubleCache::CachedDataEndPosIfSeekTo(int64_t iFilePosition) +{ + /* Return the position on source we would end up after a cache-seek(/reset) + * Note that we select the cache that has the most forward data already cached + * for this position + */ + int64_t ret = m_pCache->CachedDataEndPosIfSeekTo(iFilePosition); + if (m_pCacheOld) + return std::max(ret, m_pCacheOld->CachedDataEndPosIfSeekTo(iFilePosition)); + return ret; +} + +bool CDoubleCache::IsCachedPosition(int64_t iFilePosition) +{ + return m_pCache->IsCachedPosition(iFilePosition) || (m_pCacheOld && m_pCacheOld->IsCachedPosition(iFilePosition)); +} + +CCacheStrategy *CDoubleCache::CreateNew() +{ + return new CDoubleCache(m_pCache->CreateNew()); +} + diff --git a/xbmc/filesystem/CacheStrategy.h b/xbmc/filesystem/CacheStrategy.h new file mode 100644 index 0000000..76c66bb --- /dev/null +++ b/xbmc/filesystem/CacheStrategy.h @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/Event.h" + +#include <stdint.h> +#include <string> + +namespace XFILE { + +#define CACHE_RC_OK 0 +#define CACHE_RC_ERROR -1 +#define CACHE_RC_WOULD_BLOCK -2 +#define CACHE_RC_TIMEOUT -3 + +class IFile; // forward declaration + +class CCacheStrategy{ +public: + virtual ~CCacheStrategy(); + + virtual int Open() = 0; + virtual void Close() = 0; + + virtual size_t GetMaxWriteSize(const size_t& iRequestSize) = 0; + virtual int WriteToCache(const char *pBuffer, size_t iSize) = 0; + virtual int ReadFromCache(char *pBuffer, size_t iMaxSize) = 0; + virtual int64_t WaitForData(uint32_t iMinAvail, std::chrono::milliseconds timeout) = 0; + + virtual int64_t Seek(int64_t iFilePosition) = 0; + + /*! + \brief Reset cache position + \param iSourcePosition position to reset to + \return Whether a full reset was performed, or not (e.g. only cache swap) + \sa CCacheStrategy + */ + virtual bool Reset(int64_t iSourcePosition) = 0; + + virtual void EndOfInput(); // mark the end of the input stream so that Read will know when to return EOF + virtual bool IsEndOfInput(); + virtual void ClearEndOfInput(); + + virtual int64_t CachedDataEndPosIfSeekTo(int64_t iFilePosition) = 0; + virtual int64_t CachedDataStartPos() = 0; + virtual int64_t CachedDataEndPos() = 0; + virtual bool IsCachedPosition(int64_t iFilePosition) = 0; + + virtual CCacheStrategy *CreateNew() = 0; + + CEvent m_space; +protected: + bool m_bEndOfInput = false; +}; + +/** +*/ +class CSimpleFileCache : public CCacheStrategy { +public: + CSimpleFileCache(); + ~CSimpleFileCache() override; + + int Open() override; + void Close() override; + + size_t GetMaxWriteSize(const size_t& iRequestSize) override; + int WriteToCache(const char *pBuffer, size_t iSize) override; + int ReadFromCache(char *pBuffer, size_t iMaxSize) override; + int64_t WaitForData(uint32_t iMinAvail, std::chrono::milliseconds timeout) override; + + int64_t Seek(int64_t iFilePosition) override; + bool Reset(int64_t iSourcePosition) override; + void EndOfInput() override; + + int64_t CachedDataEndPosIfSeekTo(int64_t iFilePosition) override; + int64_t CachedDataStartPos() override; + int64_t CachedDataEndPos() override; + bool IsCachedPosition(int64_t iFilePosition) override; + + CCacheStrategy *CreateNew() override; + + int64_t GetAvailableRead(); + +protected: + std::string m_filename; + IFile* m_cacheFileRead; + IFile* m_cacheFileWrite; + CEvent* m_hDataAvailEvent; + volatile int64_t m_nStartPosition = 0; + volatile int64_t m_nWritePosition = 0; + volatile int64_t m_nReadPosition = 0; +}; + +class CDoubleCache : public CCacheStrategy{ +public: + explicit CDoubleCache(CCacheStrategy *impl); + ~CDoubleCache() override; + + int Open() override; + void Close() override; + + size_t GetMaxWriteSize(const size_t& iRequestSize) override; + int WriteToCache(const char *pBuffer, size_t iSize) override; + int ReadFromCache(char *pBuffer, size_t iMaxSize) override; + int64_t WaitForData(uint32_t iMinAvail, std::chrono::milliseconds timeout) override; + + int64_t Seek(int64_t iFilePosition) override; + bool Reset(int64_t iSourcePosition) override; + void EndOfInput() override; + bool IsEndOfInput() override; + void ClearEndOfInput() override; + + int64_t CachedDataEndPosIfSeekTo(int64_t iFilePosition) override; + int64_t CachedDataStartPos() override; + int64_t CachedDataEndPos() override; + bool IsCachedPosition(int64_t iFilePosition) override; + + CCacheStrategy *CreateNew() override; + +protected: + CCacheStrategy *m_pCache; + CCacheStrategy *m_pCacheOld; +}; + +} + diff --git a/xbmc/filesystem/CircularCache.cpp b/xbmc/filesystem/CircularCache.cpp new file mode 100644 index 0000000..443736a --- /dev/null +++ b/xbmc/filesystem/CircularCache.cpp @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "CircularCache.h" + +#include "threads/SystemClock.h" +#include "utils/log.h" + +#include <algorithm> +#include <mutex> +#include <string.h> + +using namespace XFILE; +using namespace std::chrono_literals; + +CCircularCache::CCircularCache(size_t front, size_t back) + : CCacheStrategy() + , m_beg(0) + , m_end(0) + , m_cur(0) + , m_buf(NULL) + , m_size(front + back) + , m_size_back(back) +#ifdef TARGET_WINDOWS + , m_handle(NULL) +#endif +{ +} + +CCircularCache::~CCircularCache() +{ + Close(); +} + +int CCircularCache::Open() +{ +#ifdef TARGET_WINDOWS + m_handle = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, m_size, NULL); + if(m_handle == NULL) + return CACHE_RC_ERROR; + m_buf = (uint8_t*)MapViewOfFile(m_handle, FILE_MAP_ALL_ACCESS, 0, 0, 0); +#else + m_buf = new uint8_t[m_size]; +#endif + if (m_buf == NULL) + return CACHE_RC_ERROR; + m_beg = 0; + m_end = 0; + m_cur = 0; + return CACHE_RC_OK; +} + +void CCircularCache::Close() +{ +#ifdef TARGET_WINDOWS + if (m_buf != NULL) + UnmapViewOfFile(m_buf); + if (m_handle != NULL) + CloseHandle(m_handle); + m_handle = NULL; +#else + delete[] m_buf; +#endif + m_buf = NULL; +} + +size_t CCircularCache::GetMaxWriteSize(const size_t& iRequestSize) +{ + std::unique_lock<CCriticalSection> lock(m_sync); + + size_t back = (size_t)(m_cur - m_beg); // Backbuffer size + size_t front = (size_t)(m_end - m_cur); // Frontbuffer size + size_t limit = m_size - std::min(back, m_size_back) - front; + + // Never return more than limit and size requested by caller + return std::min(iRequestSize, limit); +} + +/** + * Function will write to m_buf at m_end % m_size location + * it will write at maximum m_size, but it will only write + * as much it can without wrapping around in the buffer + * + * It will always leave m_size_back of the backbuffer intact + * but if the back buffer is less than that, that space is + * usable to write. + * + * If back buffer is filled to an larger extent than + * m_size_back, it will allow it to be overwritten + * until only m_size_back data remains. + * + * The following always apply: + * * m_end <= m_cur <= m_end + * * m_end - m_beg <= m_size + * + * Multiple calls may be needed to fill buffer completely. + */ +int CCircularCache::WriteToCache(const char *buf, size_t len) +{ + std::unique_lock<CCriticalSection> lock(m_sync); + + // where are we in the buffer + size_t pos = m_end % m_size; + size_t back = (size_t)(m_cur - m_beg); + size_t front = (size_t)(m_end - m_cur); + + size_t limit = m_size - std::min(back, m_size_back) - front; + size_t wrap = m_size - pos; + + // limit by max forward size + if(len > limit) + len = limit; + + // limit to wrap point + if(len > wrap) + len = wrap; + + if(len == 0) + return 0; + + if (m_buf == NULL) + return 0; + + // write the data + memcpy(m_buf + pos, buf, len); + m_end += len; + + // drop history that was overwritten + if(m_end - m_beg > (int64_t)m_size) + m_beg = m_end - m_size; + + m_written.Set(); + + return len; +} + +/** + * Reads data from cache. Will only read up till + * the buffer wrap point. So multiple calls + * may be needed to empty the whole cache + */ +int CCircularCache::ReadFromCache(char *buf, size_t len) +{ + std::unique_lock<CCriticalSection> lock(m_sync); + + size_t pos = m_cur % m_size; + size_t front = (size_t)(m_end - m_cur); + size_t avail = std::min(m_size - pos, front); + + if(avail == 0) + { + if(IsEndOfInput()) + return 0; + else + return CACHE_RC_WOULD_BLOCK; + } + + if(len > avail) + len = avail; + + if(len == 0) + return 0; + + if (m_buf == NULL) + return 0; + + memcpy(buf, m_buf + pos, len); + m_cur += len; + + m_space.Set(); + + return len; +} + +/* Wait "millis" milliseconds for "minimum" amount of data to come in. + * Note that caller needs to make sure there's sufficient space in the forward + * buffer for "minimum" bytes else we may block the full timeout time + */ +int64_t CCircularCache::WaitForData(uint32_t minimum, std::chrono::milliseconds timeout) +{ + std::unique_lock<CCriticalSection> lock(m_sync); + int64_t avail = m_end - m_cur; + + if (timeout == 0ms || IsEndOfInput()) + return avail; + + if(minimum > m_size - m_size_back) + minimum = m_size - m_size_back; + + XbmcThreads::EndTime<> endtime{timeout}; + while (!IsEndOfInput() && avail < minimum && !endtime.IsTimePast() ) + { + lock.unlock(); + m_written.Wait(50ms); // may miss the deadline. shouldn't be a problem. + lock.lock(); + avail = m_end - m_cur; + } + + return avail; +} + +int64_t CCircularCache::Seek(int64_t pos) +{ + std::unique_lock<CCriticalSection> lock(m_sync); + + // if seek is a bit over what we have, try to wait a few seconds for the data to be available. + // we try to avoid a (heavy) seek on the source + if (pos >= m_end && pos < m_end + 100000) + { + /* Make everything in the cache (back & forward) back-cache, to make sure + * there's sufficient forward space. Increasing it with only 100000 may not be + * sufficient due to variable filesystem chunksize + */ + m_cur = m_end; + + lock.unlock(); + WaitForData((size_t)(pos - m_cur), 5s); + lock.lock(); + + if (pos < m_beg || pos > m_end) + CLog::Log(LOGDEBUG, + "CCircularCache::{} - ({}) Wait for data failed for pos {}, ended up at {}", + __FUNCTION__, fmt::ptr(this), pos, m_cur); + } + + if (pos >= m_beg && pos <= m_end) + { + m_cur = pos; + return pos; + } + + return CACHE_RC_ERROR; +} + +bool CCircularCache::Reset(int64_t pos) +{ + std::unique_lock<CCriticalSection> lock(m_sync); + if (IsCachedPosition(pos)) + { + m_cur = pos; + return false; + } + m_end = pos; + m_beg = pos; + m_cur = pos; + + return true; +} + +int64_t CCircularCache::CachedDataEndPosIfSeekTo(int64_t iFilePosition) +{ + if (IsCachedPosition(iFilePosition)) + return m_end; + return iFilePosition; +} + +int64_t CCircularCache::CachedDataStartPos() +{ + return m_beg; +} + +int64_t CCircularCache::CachedDataEndPos() +{ + return m_end; +} + +bool CCircularCache::IsCachedPosition(int64_t iFilePosition) +{ + return iFilePosition >= m_beg && iFilePosition <= m_end; +} + +CCacheStrategy *CCircularCache::CreateNew() +{ + return new CCircularCache(m_size - m_size_back, m_size_back); +} + diff --git a/xbmc/filesystem/CircularCache.h b/xbmc/filesystem/CircularCache.h new file mode 100644 index 0000000..21d3e6b --- /dev/null +++ b/xbmc/filesystem/CircularCache.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "CacheStrategy.h" +#include "threads/CriticalSection.h" +#include "threads/Event.h" + +namespace XFILE { + +class CCircularCache : public CCacheStrategy +{ +public: + CCircularCache(size_t front, size_t back); + ~CCircularCache() override; + + int Open() override; + void Close() override; + + size_t GetMaxWriteSize(const size_t& iRequestSize) override; + int WriteToCache(const char *buf, size_t len) override; + int ReadFromCache(char *buf, size_t len) override; + int64_t WaitForData(uint32_t minimum, std::chrono::milliseconds timeout) override; + + int64_t Seek(int64_t pos) override; + bool Reset(int64_t pos) override; + + int64_t CachedDataEndPosIfSeekTo(int64_t iFilePosition) override; + int64_t CachedDataStartPos() override; + int64_t CachedDataEndPos() override; + bool IsCachedPosition(int64_t iFilePosition) override; + + CCacheStrategy *CreateNew() override; +protected: + int64_t m_beg; /**< index in file (not buffer) of beginning of valid data */ + int64_t m_end; /**< index in file (not buffer) of end of valid data */ + int64_t m_cur; /**< current reading index in file */ + uint8_t *m_buf; /**< buffer holding data */ + size_t m_size; /**< size of data buffer used (m_buf) */ + size_t m_size_back; /**< guaranteed size of back buffer (actual size can be smaller, or larger if front buffer doesn't need it) */ + CCriticalSection m_sync; + CEvent m_written; +#ifdef TARGET_WINDOWS + HANDLE m_handle; +#endif +}; + +} // namespace XFILE + diff --git a/xbmc/filesystem/CurlFile.cpp b/xbmc/filesystem/CurlFile.cpp new file mode 100644 index 0000000..11a5272 --- /dev/null +++ b/xbmc/filesystem/CurlFile.cpp @@ -0,0 +1,2141 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "CurlFile.h" + +#include "File.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "filesystem/SpecialProtocol.h" +#include "network/DNSNameCache.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "threads/SystemClock.h" +#include "utils/Base64.h" +#include "utils/XTimeUtils.h" + +#include <algorithm> +#include <cassert> +#include <climits> +#include <vector> + +#ifdef TARGET_POSIX +#include <errno.h> +#include <inttypes.h> +#include "platform/posix/ConvUtils.h" +#endif + +#include "DllLibCurl.h" +#include "ShoutcastFile.h" +#include "utils/CharsetConverter.h" +#include "utils/log.h" +#include "utils/StringUtils.h" + +using namespace XFILE; +using namespace XCURL; + +using namespace std::chrono_literals; + +#define FITS_INT(a) (((a) <= INT_MAX) && ((a) >= INT_MIN)) + +curl_proxytype proxyType2CUrlProxyType[] = { + CURLPROXY_HTTP, CURLPROXY_SOCKS4, CURLPROXY_SOCKS4A, + CURLPROXY_SOCKS5, CURLPROXY_SOCKS5_HOSTNAME, CURLPROXY_HTTPS, +}; + +#define FILLBUFFER_OK 0 +#define FILLBUFFER_NO_DATA 1 +#define FILLBUFFER_FAIL 2 + +// curl calls this routine to debug +extern "C" int debug_callback(CURL_HANDLE *handle, curl_infotype info, char *output, size_t size, void *data) +{ + if (info == CURLINFO_DATA_IN || info == CURLINFO_DATA_OUT) + return 0; + + if (!CServiceBroker::GetLogging().CanLogComponent(LOGCURL)) + return 0; + + std::string strLine; + strLine.append(output, size); + std::vector<std::string> vecLines; + StringUtils::Tokenize(strLine, vecLines, "\r\n"); + std::vector<std::string>::const_iterator it = vecLines.begin(); + + const char *infotype; + switch(info) + { + case CURLINFO_TEXT : infotype = "TEXT: "; break; + case CURLINFO_HEADER_IN : infotype = "HEADER_IN: "; break; + case CURLINFO_HEADER_OUT : infotype = "HEADER_OUT: "; break; + case CURLINFO_SSL_DATA_IN : infotype = "SSL_DATA_IN: "; break; + case CURLINFO_SSL_DATA_OUT : infotype = "SSL_DATA_OUT: "; break; + case CURLINFO_END : infotype = "END: "; break; + default : infotype = ""; break; + } + + while (it != vecLines.end()) + { + CLog::Log(LOGDEBUG, "Curl::Debug - {}{}", infotype, (*it)); + ++it; + } + return 0; +} + +/* curl calls this routine to get more data */ +extern "C" size_t write_callback(char *buffer, + size_t size, + size_t nitems, + void *userp) +{ + if(userp == NULL) return 0; + + CCurlFile::CReadState *state = (CCurlFile::CReadState *)userp; + return state->WriteCallback(buffer, size, nitems); +} + +extern "C" size_t read_callback(char *buffer, + size_t size, + size_t nitems, + void *userp) +{ + if(userp == NULL) return 0; + + CCurlFile::CReadState *state = (CCurlFile::CReadState *)userp; + return state->ReadCallback(buffer, size, nitems); +} + +extern "C" size_t header_callback(void *ptr, size_t size, size_t nmemb, void *stream) +{ + CCurlFile::CReadState *state = (CCurlFile::CReadState *)stream; + return state->HeaderCallback(ptr, size, nmemb); +} + +/* used only by CCurlFile::Stat to bail out of unwanted transfers */ +extern "C" int transfer_abort_callback(void *clientp, + curl_off_t dltotal, + curl_off_t dlnow, + curl_off_t ultotal, + curl_off_t ulnow) +{ + if(dlnow > 0) + return 1; + else + return 0; +} + +/* fix for silly behavior of realloc */ +static inline void* realloc_simple(void *ptr, size_t size) +{ + void *ptr2 = realloc(ptr, size); + if(ptr && !ptr2 && size > 0) + { + free(ptr); + return NULL; + } + else + return ptr2; +} + +static constexpr int CURL_OFF = 0L; +static constexpr int CURL_ON = 1L; + +size_t CCurlFile::CReadState::HeaderCallback(void *ptr, size_t size, size_t nmemb) +{ + std::string inString; + // libcurl doc says that this info is not always \0 terminated + const char* strBuf = (const char*)ptr; + const size_t iSize = size * nmemb; + if (strBuf[iSize - 1] == 0) + inString.assign(strBuf, iSize - 1); // skip last char if it's zero + else + inString.append(strBuf, iSize); + + m_httpheader.Parse(inString); + + return iSize; +} + +size_t CCurlFile::CReadState::ReadCallback(char *buffer, size_t size, size_t nitems) +{ + if (m_fileSize == 0) + return 0; + + if (m_filePos >= m_fileSize) + { + m_isPaused = true; + return CURL_READFUNC_PAUSE; + } + + int64_t retSize = std::min(m_fileSize - m_filePos, int64_t(nitems * size)); + memcpy(buffer, m_readBuffer + m_filePos, retSize); + m_filePos += retSize; + + return retSize; +} + +size_t CCurlFile::CReadState::WriteCallback(char *buffer, size_t size, size_t nitems) +{ + unsigned int amount = size * nitems; + if (m_overflowSize) + { + // we have our overflow buffer - first get rid of as much as we can + unsigned int maxWriteable = std::min(m_buffer.getMaxWriteSize(), m_overflowSize); + if (maxWriteable) + { + if (!m_buffer.WriteData(m_overflowBuffer, maxWriteable)) + { + CLog::Log(LOGERROR, "CCurlFile::CReadState::{} - ({}) Unable to write to buffer", + __FUNCTION__, fmt::ptr(this)); + return 0; + } + + if (maxWriteable < m_overflowSize) + { + // still have some more - copy it down + memmove(m_overflowBuffer, m_overflowBuffer + maxWriteable, m_overflowSize - maxWriteable); + } + m_overflowSize -= maxWriteable; + + // Shrink memory: + m_overflowBuffer = (char*)realloc_simple(m_overflowBuffer, m_overflowSize); + } + } + // ok, now copy the data into our ring buffer + unsigned int maxWriteable = std::min(m_buffer.getMaxWriteSize(), amount); + if (maxWriteable) + { + if (!m_buffer.WriteData(buffer, maxWriteable)) + { + CLog::Log(LOGERROR, + "CCurlFile::CReadState::{} - ({}) Unable to write to buffer with {} bytes", + __FUNCTION__, fmt::ptr(this), maxWriteable); + return 0; + } + else + { + amount -= maxWriteable; + buffer += maxWriteable; + } + } + if (amount) + { + //! @todo Limit max. amount of the overflowbuffer + m_overflowBuffer = (char*)realloc_simple(m_overflowBuffer, amount + m_overflowSize); + if(m_overflowBuffer == NULL) + { + CLog::Log(LOGWARNING, + "CCurlFile::CReadState::{} - ({}) Failed to grow overflow buffer from {} bytes to " + "{} bytes", + __FUNCTION__, fmt::ptr(this), m_overflowSize, amount + m_overflowSize); + return 0; + } + memcpy(m_overflowBuffer + m_overflowSize, buffer, amount); + m_overflowSize += amount; + } + return size * nitems; +} + +CCurlFile::CReadState::CReadState() +{ + m_easyHandle = NULL; + m_multiHandle = NULL; + m_overflowBuffer = NULL; + m_overflowSize = 0; + m_stillRunning = 0; + m_filePos = 0; + m_fileSize = 0; + m_bufferSize = 0; + m_cancelled = false; + m_bFirstLoop = true; + m_sendRange = true; + m_bLastError = false; + m_readBuffer = 0; + m_isPaused = false; + m_bRetry = true; + m_curlHeaderList = NULL; + m_curlAliasList = NULL; +} + +CCurlFile::CReadState::~CReadState() +{ + Disconnect(); + + if(m_easyHandle) + g_curlInterface.easy_release(&m_easyHandle, &m_multiHandle); +} + +bool CCurlFile::CReadState::Seek(int64_t pos) +{ + if(pos == m_filePos) + return true; + + if(FITS_INT(pos - m_filePos) && m_buffer.SkipBytes((int)(pos - m_filePos))) + { + m_filePos = pos; + return true; + } + + if(pos > m_filePos && pos < m_filePos + m_bufferSize) + { + int len = m_buffer.getMaxReadSize(); + m_filePos += len; + m_buffer.SkipBytes(len); + if (FillBuffer(m_bufferSize) != FILLBUFFER_OK) + { + if(!m_buffer.SkipBytes(-len)) + CLog::Log(LOGERROR, + "CCurlFile::CReadState::{} - ({}) Failed to restore position after failed fill", + __FUNCTION__, fmt::ptr(this)); + else + m_filePos -= len; + return false; + } + + if(!FITS_INT(pos - m_filePos) || !m_buffer.SkipBytes((int)(pos - m_filePos))) + { + CLog::Log( + LOGERROR, + "CCurlFile::CReadState::{} - ({}) Failed to skip to position after having filled buffer", + __FUNCTION__, fmt::ptr(this)); + if(!m_buffer.SkipBytes(-len)) + CLog::Log(LOGERROR, + "CCurlFile::CReadState::{} - ({}) Failed to restore position after failed seek", + __FUNCTION__, fmt::ptr(this)); + else + m_filePos -= len; + return false; + } + m_filePos = pos; + return true; + } + return false; +} + +void CCurlFile::CReadState::SetResume(void) +{ + /* + * Explicitly set RANGE header when filepos=0 as some http servers require us to always send the range + * request header. If we don't the server may provide different content causing seeking to fail. + * This only affects HTTP-like items, for FTP it's a null operation. + */ + if (m_sendRange && m_filePos == 0) + g_curlInterface.easy_setopt(m_easyHandle, CURLOPT_RANGE, "0-"); + else + { + g_curlInterface.easy_setopt(m_easyHandle, CURLOPT_RANGE, NULL); + m_sendRange = false; + } + + g_curlInterface.easy_setopt(m_easyHandle, CURLOPT_RESUME_FROM_LARGE, m_filePos); +} + +long CCurlFile::CReadState::Connect(unsigned int size) +{ + if (m_filePos != 0) + CLog::Log(LOGDEBUG, "CurlFile::CReadState::{} - ({}) Resume from position {}", __FUNCTION__, + fmt::ptr(this), m_filePos); + + SetResume(); + g_curlInterface.multi_add_handle(m_multiHandle, m_easyHandle); + + m_bufferSize = size; + m_buffer.Destroy(); + m_buffer.Create(size * 3); + m_httpheader.Clear(); + + // read some data in to try and obtain the length + // maybe there's a better way to get this info?? + m_stillRunning = 1; + + // (Try to) fill buffer + if (FillBuffer(1) != FILLBUFFER_OK) + { + // Check response code + long response; + if (CURLE_OK == g_curlInterface.easy_getinfo(m_easyHandle, CURLINFO_RESPONSE_CODE, &response)) + return response; + else + return -1; + } + + double length; + if (CURLE_OK == g_curlInterface.easy_getinfo(m_easyHandle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &length)) + { + if (length < 0) + length = 0.0; + m_fileSize = m_filePos + (int64_t)length; + } + + long response; + if (CURLE_OK == g_curlInterface.easy_getinfo(m_easyHandle, CURLINFO_RESPONSE_CODE, &response)) + return response; + + return -1; +} + +void CCurlFile::CReadState::Disconnect() +{ + if(m_multiHandle && m_easyHandle) + g_curlInterface.multi_remove_handle(m_multiHandle, m_easyHandle); + + m_buffer.Clear(); + free(m_overflowBuffer); + m_overflowBuffer = NULL; + m_overflowSize = 0; + m_filePos = 0; + m_fileSize = 0; + m_bufferSize = 0; + m_readBuffer = 0; + + /* cleanup */ + if( m_curlHeaderList ) + g_curlInterface.slist_free_all(m_curlHeaderList); + m_curlHeaderList = NULL; + + if( m_curlAliasList ) + g_curlInterface.slist_free_all(m_curlAliasList); + m_curlAliasList = NULL; +} + + +CCurlFile::~CCurlFile() +{ + Close(); + delete m_state; + delete m_oldState; +} + +CCurlFile::CCurlFile() + : m_overflowBuffer(NULL) +{ + m_opened = false; + m_forWrite = false; + m_inError = false; + m_multisession = true; + m_seekable = true; + m_connecttimeout = 0; + m_redirectlimit = 5; + m_lowspeedtime = 0; + m_ftppasvip = false; + m_bufferSize = 32768; + m_postdataset = false; + m_state = new CReadState(); + m_oldState = NULL; + m_skipshout = false; + m_httpresponse = -1; + m_acceptCharset = "UTF-8,*;q=0.8"; /* prefer UTF-8 if available */ + m_allowRetry = true; + m_acceptencoding = "all"; /* Accept all supported encoding by default */ +} + +//Has to be called before Open() +void CCurlFile::SetBufferSize(unsigned int size) +{ + m_bufferSize = size; +} + +void CCurlFile::Close() +{ + if (m_opened && m_forWrite && !m_inError) + Write(NULL, 0); + + m_state->Disconnect(); + delete m_oldState; + m_oldState = NULL; + + m_url.clear(); + m_referer.clear(); + m_cookie.clear(); + + m_opened = false; + m_forWrite = false; + m_inError = false; + + if (m_dnsCacheList) + g_curlInterface.slist_free_all(m_dnsCacheList); + m_dnsCacheList = nullptr; +} + +void CCurlFile::SetCommonOptions(CReadState* state, bool failOnError /* = true */) +{ + CURL_HANDLE* h = state->m_easyHandle; + + g_curlInterface.easy_reset(h); + + g_curlInterface.easy_setopt(h, CURLOPT_DEBUGFUNCTION, debug_callback); + + if( CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel >= LOG_LEVEL_DEBUG ) + g_curlInterface.easy_setopt(h, CURLOPT_VERBOSE, CURL_ON); + else + g_curlInterface.easy_setopt(h, CURLOPT_VERBOSE, CURL_OFF); + + g_curlInterface.easy_setopt(h, CURLOPT_WRITEDATA, state); + g_curlInterface.easy_setopt(h, CURLOPT_WRITEFUNCTION, write_callback); + + g_curlInterface.easy_setopt(h, CURLOPT_READDATA, state); + g_curlInterface.easy_setopt(h, CURLOPT_READFUNCTION, read_callback); + + // use DNS cache + g_curlInterface.easy_setopt(h, CURLOPT_RESOLVE, m_dnsCacheList); + + // make sure headers are separated from the data stream + g_curlInterface.easy_setopt(h, CURLOPT_WRITEHEADER, state); + g_curlInterface.easy_setopt(h, CURLOPT_HEADERFUNCTION, header_callback); + g_curlInterface.easy_setopt(h, CURLOPT_HEADER, CURL_OFF); + + g_curlInterface.easy_setopt(h, CURLOPT_FTP_USE_EPSV, 0); // turn off epsv + + // Allow us to follow redirects + g_curlInterface.easy_setopt(h, CURLOPT_FOLLOWLOCATION, m_redirectlimit != 0); + g_curlInterface.easy_setopt(h, CURLOPT_MAXREDIRS, m_redirectlimit); + + // Enable cookie engine for current handle + g_curlInterface.easy_setopt(h, CURLOPT_COOKIEFILE, ""); + + // Set custom cookie if requested + if (!m_cookie.empty()) + g_curlInterface.easy_setopt(h, CURLOPT_COOKIE, m_cookie.c_str()); + + g_curlInterface.easy_setopt(h, CURLOPT_COOKIELIST, "FLUSH"); + + // When using multiple threads you should set the CURLOPT_NOSIGNAL option to + // TRUE for all handles. Everything will work fine except that timeouts are not + // honored during the DNS lookup - which you can work around by building libcurl + // with c-ares support. c-ares is a library that provides asynchronous name + // resolves. Unfortunately, c-ares does not yet support IPv6. + g_curlInterface.easy_setopt(h, CURLOPT_NOSIGNAL, CURL_ON); + + if (failOnError) + { + // not interested in failed requests + g_curlInterface.easy_setopt(h, CURLOPT_FAILONERROR, 1); + } + + // enable support for icecast / shoutcast streams + if ( NULL == state->m_curlAliasList ) + // m_curlAliasList is used only by this one place, but SetCommonOptions can + // be called multiple times, only append to list if it's empty. + state->m_curlAliasList = g_curlInterface.slist_append(state->m_curlAliasList, "ICY 200 OK"); + g_curlInterface.easy_setopt(h, CURLOPT_HTTP200ALIASES, state->m_curlAliasList); + + if (!m_verifyPeer) + g_curlInterface.easy_setopt(h, CURLOPT_SSL_VERIFYPEER, 0); + + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_URL, m_url.c_str()); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_TRANSFERTEXT, CURL_OFF); + + // setup POST data if it is set (and it may be empty) + if (m_postdataset) + { + g_curlInterface.easy_setopt(h, CURLOPT_POST, 1 ); + g_curlInterface.easy_setopt(h, CURLOPT_POSTFIELDSIZE, m_postdata.length()); + g_curlInterface.easy_setopt(h, CURLOPT_POSTFIELDS, m_postdata.c_str()); + } + + // setup Referer header if needed + if (!m_referer.empty()) + g_curlInterface.easy_setopt(h, CURLOPT_REFERER, m_referer.c_str()); + else + { + g_curlInterface.easy_setopt(h, CURLOPT_REFERER, NULL); + // Do not send referer header on redirects (same behaviour as ffmpeg and browsers) + g_curlInterface.easy_setopt(h, CURLOPT_AUTOREFERER, CURL_OFF); + } + + // setup any requested authentication + if( !m_ftpauth.empty() ) + { + g_curlInterface.easy_setopt(h, CURLOPT_FTP_SSL, CURLFTPSSL_TRY); + if( m_ftpauth == "any" ) + g_curlInterface.easy_setopt(h, CURLOPT_FTPSSLAUTH, CURLFTPAUTH_DEFAULT); + else if( m_ftpauth == "ssl" ) + g_curlInterface.easy_setopt(h, CURLOPT_FTPSSLAUTH, CURLFTPAUTH_SSL); + else if( m_ftpauth == "tls" ) + g_curlInterface.easy_setopt(h, CURLOPT_FTPSSLAUTH, CURLFTPAUTH_TLS); + } + + // setup requested http authentication method + bool bAuthSet = false; + if(!m_httpauth.empty()) + { + bAuthSet = true; + if( m_httpauth == "any" ) + g_curlInterface.easy_setopt(h, CURLOPT_HTTPAUTH, CURLAUTH_ANY); + else if( m_httpauth == "anysafe" ) + g_curlInterface.easy_setopt(h, CURLOPT_HTTPAUTH, CURLAUTH_ANYSAFE); + else if( m_httpauth == "digest" ) + g_curlInterface.easy_setopt(h, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); + else if( m_httpauth == "ntlm" ) + g_curlInterface.easy_setopt(h, CURLOPT_HTTPAUTH, CURLAUTH_NTLM); + else + bAuthSet = false; + } + + // set username and password for current handle + if (!m_username.empty()) + { + g_curlInterface.easy_setopt(h, CURLOPT_USERNAME, m_username.c_str()); + if (!m_password.empty()) + g_curlInterface.easy_setopt(h, CURLOPT_PASSWORD, m_password.c_str()); + + if (!bAuthSet) + g_curlInterface.easy_setopt(h, CURLOPT_HTTPAUTH, CURLAUTH_ANY); + } + + // allow passive mode for ftp + if( m_ftpport.length() > 0 ) + g_curlInterface.easy_setopt(h, CURLOPT_FTPPORT, m_ftpport.c_str()); + else + g_curlInterface.easy_setopt(h, CURLOPT_FTPPORT, NULL); + + // allow curl to not use the ip address in the returned pasv response + if( m_ftppasvip ) + g_curlInterface.easy_setopt(h, CURLOPT_FTP_SKIP_PASV_IP, 0); + else + g_curlInterface.easy_setopt(h, CURLOPT_FTP_SKIP_PASV_IP, 1); + + // setup Accept-Encoding if requested + if (m_acceptencoding.length() > 0) + g_curlInterface.easy_setopt(h, CURLOPT_ACCEPT_ENCODING, m_acceptencoding == "all" ? "" : m_acceptencoding.c_str()); + + if (!m_acceptCharset.empty()) + SetRequestHeader("Accept-Charset", m_acceptCharset); + + if (m_userAgent.length() > 0) + g_curlInterface.easy_setopt(h, CURLOPT_USERAGENT, m_userAgent.c_str()); + else /* set some default agent as shoutcast doesn't return proper stuff otherwise */ + g_curlInterface.easy_setopt(h, CURLOPT_USERAGENT, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_userAgent.c_str()); + + if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlDisableIPV6) + g_curlInterface.easy_setopt(h, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); + + if (!m_proxyhost.empty()) + { + g_curlInterface.easy_setopt(h, CURLOPT_PROXYTYPE, proxyType2CUrlProxyType[m_proxytype]); + + const std::string hostport = m_proxyhost + StringUtils::Format(":{}", m_proxyport); + g_curlInterface.easy_setopt(h, CURLOPT_PROXY, hostport.c_str()); + + std::string userpass; + + if (!m_proxyuser.empty() && !m_proxypassword.empty()) + userpass = CURL::Encode(m_proxyuser) + ":" + CURL::Encode(m_proxypassword); + + if (!userpass.empty()) + g_curlInterface.easy_setopt(h, CURLOPT_PROXYUSERPWD, userpass.c_str()); + } + if (m_customrequest.length() > 0) + g_curlInterface.easy_setopt(h, CURLOPT_CUSTOMREQUEST, m_customrequest.c_str()); + + if (m_connecttimeout == 0) + m_connecttimeout = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlconnecttimeout; + + // set our timeouts, we abort connection after m_timeout, and reads after no data for m_timeout seconds + g_curlInterface.easy_setopt(h, CURLOPT_CONNECTTIMEOUT, m_connecttimeout); + + // We abort in case we transfer less than 1byte/second + g_curlInterface.easy_setopt(h, CURLOPT_LOW_SPEED_LIMIT, 1); + + if (m_lowspeedtime == 0) + m_lowspeedtime = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curllowspeedtime; + + // Set the lowspeed time very low as it seems Curl takes much longer to detect a lowspeed condition + g_curlInterface.easy_setopt(h, CURLOPT_LOW_SPEED_TIME, m_lowspeedtime); + + // enable tcp keepalive + if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlKeepAliveInterval > 0) + { + g_curlInterface.easy_setopt(h, CURLOPT_TCP_KEEPALIVE, 1L); + g_curlInterface.easy_setopt( + h, CURLOPT_TCP_KEEPIDLE, + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlKeepAliveInterval / 2); + g_curlInterface.easy_setopt( + h, CURLOPT_TCP_KEEPINTVL, + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlKeepAliveInterval); + } + + // Setup allowed TLS/SSL ciphers. New versions of cURL may deprecate things that are still in use. + if (!m_cipherlist.empty()) + g_curlInterface.easy_setopt(h, CURLOPT_SSL_CIPHER_LIST, m_cipherlist.c_str()); + + if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlDisableHTTP2) + g_curlInterface.easy_setopt(h, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + else + // enable HTTP2 support. default: CURL_HTTP_VERSION_1_1. Curl >= 7.62.0 defaults to CURL_HTTP_VERSION_2TLS + g_curlInterface.easy_setopt(h, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS); + + // set CA bundle file + std::string caCert = CSpecialProtocol::TranslatePath( + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_caTrustFile); +#ifdef TARGET_WINDOWS_STORE + // UWP Curl - Setting CURLOPT_CAINFO with a valid cacert file path is required for UWP + g_curlInterface.easy_setopt(h, CURLOPT_CAINFO, "system\\certs\\cacert.pem"); +#endif + if (!caCert.empty() && XFILE::CFile::Exists(caCert)) + g_curlInterface.easy_setopt(h, CURLOPT_CAINFO, caCert.c_str()); +} + +void CCurlFile::SetRequestHeaders(CReadState* state) +{ + if(state->m_curlHeaderList) + { + g_curlInterface.slist_free_all(state->m_curlHeaderList); + state->m_curlHeaderList = NULL; + } + + for (const auto& it : m_requestheaders) + { + std::string buffer = it.first + ": " + it.second; + state->m_curlHeaderList = g_curlInterface.slist_append(state->m_curlHeaderList, buffer.c_str()); + } + + // add user defined headers + if (state->m_easyHandle) + g_curlInterface.easy_setopt(state->m_easyHandle, CURLOPT_HTTPHEADER, state->m_curlHeaderList); +} + +void CCurlFile::SetCorrectHeaders(CReadState* state) +{ + CHttpHeader& h = state->m_httpheader; + /* workaround for shoutcast server which doesn't set content type on standard mp3 */ + if( h.GetMimeType().empty() ) + { + if( !h.GetValue("icy-notice1").empty() + || !h.GetValue("icy-name").empty() + || !h.GetValue("icy-br").empty() ) + h.AddParam("Content-Type", "audio/mpeg"); + } + + /* hack for google video */ + if (StringUtils::EqualsNoCase(h.GetMimeType(),"text/html") + && !h.GetValue("Content-Disposition").empty() ) + { + std::string strValue = h.GetValue("Content-Disposition"); + if (strValue.find("filename=") != std::string::npos && + strValue.find(".flv") != std::string::npos) + h.AddParam("Content-Type", "video/flv"); + } +} + +void CCurlFile::ParseAndCorrectUrl(CURL &url2) +{ + std::string strProtocol = url2.GetTranslatedProtocol(); + url2.SetProtocol(strProtocol); + + // lookup host in DNS cache + std::string resolvedHost; + if (CDNSNameCache::GetCached(url2.GetHostName(), resolvedHost)) + { + struct curl_slist* tempCache; + int entryPort = url2.GetPort(); + + if (entryPort == 0) + { + if (strProtocol == "http") + entryPort = 80; + else if (strProtocol == "https") + entryPort = 443; + else if (strProtocol == "ftp") + entryPort = 21; + else if (strProtocol == "ftps") + entryPort = 990; + } + + std::string entryString = + url2.GetHostName() + ":" + std::to_string(entryPort) + ":" + resolvedHost; + tempCache = g_curlInterface.slist_append(m_dnsCacheList, entryString.c_str()); + + if (tempCache) + m_dnsCacheList = tempCache; + } + + if( url2.IsProtocol("ftp") + || url2.IsProtocol("ftps") ) + { + // we was using url options for urls, keep the old code work and warning + if (!url2.GetOptions().empty()) + { + CLog::Log(LOGWARNING, + "CCurlFile::{} - <{}> FTP url option is deprecated, please switch to use protocol " + "option (change " + "'?' to '|')", + __FUNCTION__, url2.GetRedacted()); + url2.SetProtocolOptions(url2.GetOptions().substr(1)); + /* ftp has no options */ + url2.SetOptions(""); + } + + /* this is ugly, depending on where we get */ + /* the link from, it may or may not be */ + /* url encoded. if handed from ftpdirectory */ + /* it won't be so let's handle that case */ + + std::string filename(url2.GetFileName()); + std::vector<std::string> array; + + // if server sent us the filename in non-utf8, we need send back with same encoding. + if (url2.GetProtocolOption("utf8") == "0") + g_charsetConverter.utf8ToStringCharset(filename); + + //! @todo create a tokenizer that doesn't skip empty's + StringUtils::Tokenize(filename, array, "/"); + filename.clear(); + for (std::vector<std::string>::iterator it = array.begin(); it != array.end(); ++it) + { + if(it != array.begin()) + filename += "/"; + + filename += CURL::Encode(*it); + } + + /* make sure we keep slashes */ + if(StringUtils::EndsWith(url2.GetFileName(), "/")) + filename += "/"; + + url2.SetFileName(filename); + + m_ftpauth.clear(); + if (url2.HasProtocolOption("auth")) + { + m_ftpauth = url2.GetProtocolOption("auth"); + StringUtils::ToLower(m_ftpauth); + if(m_ftpauth.empty()) + m_ftpauth = "any"; + } + m_ftpport = ""; + if (url2.HasProtocolOption("active")) + { + m_ftpport = url2.GetProtocolOption("active"); + if(m_ftpport.empty()) + m_ftpport = "-"; + } + if (url2.HasProtocolOption("verifypeer")) + { + if (url2.GetProtocolOption("verifypeer") == "false") + m_verifyPeer = false; + } + m_ftppasvip = url2.HasProtocolOption("pasvip") && url2.GetProtocolOption("pasvip") != "0"; + } + else if(url2.IsProtocol("http") || + url2.IsProtocol("https")) + { + std::shared_ptr<CSettings> s = CServiceBroker::GetSettingsComponent()->GetSettings(); + if (!s) + return; + + if (!url2.IsLocalHost() && + m_proxyhost.empty() && + s->GetBool(CSettings::SETTING_NETWORK_USEHTTPPROXY) && + !s->GetString(CSettings::SETTING_NETWORK_HTTPPROXYSERVER).empty() && + s->GetInt(CSettings::SETTING_NETWORK_HTTPPROXYPORT) > 0) + { + m_proxytype = (ProxyType)s->GetInt(CSettings::SETTING_NETWORK_HTTPPROXYTYPE); + m_proxyhost = s->GetString(CSettings::SETTING_NETWORK_HTTPPROXYSERVER); + m_proxyport = s->GetInt(CSettings::SETTING_NETWORK_HTTPPROXYPORT); + m_proxyuser = s->GetString(CSettings::SETTING_NETWORK_HTTPPROXYUSERNAME); + m_proxypassword = s->GetString(CSettings::SETTING_NETWORK_HTTPPROXYPASSWORD); + CLog::LogFC(LOGDEBUG, LOGCURL, "<{}> Using proxy {}, type {}", url2.GetRedacted(), + m_proxyhost, proxyType2CUrlProxyType[m_proxytype]); + } + + // get username and password + m_username = url2.GetUserName(); + m_password = url2.GetPassWord(); + + // handle any protocol options + std::map<std::string, std::string> options; + url2.GetProtocolOptions(options); + if (!options.empty()) + { + // set xbmc headers + for (const auto& it : options) + { + std::string name = it.first; + StringUtils::ToLower(name); + const std::string& value = it.second; + + if (name == "auth") + { + m_httpauth = value; + StringUtils::ToLower(m_httpauth); + if(m_httpauth.empty()) + m_httpauth = "any"; + } + else if (name == "referer") + SetReferer(value); + else if (name == "user-agent") + SetUserAgent(value); + else if (name == "cookie") + SetCookie(value); + else if (name == "acceptencoding" || name == "encoding") + SetAcceptEncoding(value); + else if (name == "noshout" && value == "true") + m_skipshout = true; + else if (name == "seekable" && value == "0") + m_seekable = false; + else if (name == "accept-charset") + SetAcceptCharset(value); + else if (name == "sslcipherlist") + m_cipherlist = value; + else if (name == "connection-timeout") + m_connecttimeout = strtol(value.c_str(), NULL, 10); + else if (name == "failonerror") + m_failOnError = value == "true"; + else if (name == "redirect-limit") + m_redirectlimit = strtol(value.c_str(), NULL, 10); + else if (name == "postdata") + { + m_postdata = Base64::Decode(value); + m_postdataset = true; + } + else if (name == "active-remote")// needed for DACP! + { + SetRequestHeader(it.first, value); + } + else if (name == "customrequest") + { + SetCustomRequest(value); + } + else if (name == "verifypeer") + { + if (value == "false") + m_verifyPeer = false; + } + else + { + if (name.length() > 0 && name[0] == '!') + { + SetRequestHeader(it.first.substr(1), value); + CLog::LogFC(LOGDEBUG, LOGCURL, "<{}> Adding custom header option '{}: ***********'", + url2.GetRedacted(), it.first.substr(1)); + } + else + { + SetRequestHeader(it.first, value); + if (name == "authorization") + CLog::LogFC(LOGDEBUG, LOGCURL, "<{}> Adding custom header option '{}: ***********'", + url2.GetRedacted(), it.first); + else + CLog::LogFC(LOGDEBUG, LOGCURL, "<{}> Adding custom header option '{}: {}'", + url2.GetRedacted(), it.first, value); + } + } + } + } + } + + // Unset the protocol options to have an url without protocol options + url2.SetProtocolOptions(""); + + if (m_username.length() > 0 && m_password.length() > 0) + m_url = url2.GetWithoutUserDetails(); + else + m_url = url2.Get(); +} + +bool CCurlFile::Post(const std::string& strURL, const std::string& strPostData, std::string& strHTML) +{ + m_postdata = strPostData; + m_postdataset = true; + return Service(strURL, strHTML); +} + +bool CCurlFile::Get(const std::string& strURL, std::string& strHTML) +{ + m_postdata = ""; + m_postdataset = false; + return Service(strURL, strHTML); +} + +bool CCurlFile::Service(const std::string& strURL, std::string& strHTML) +{ + const CURL pathToUrl(strURL); + if (Open(pathToUrl)) + { + if (ReadData(strHTML)) + { + Close(); + return true; + } + } + Close(); + return false; +} + +bool CCurlFile::ReadData(std::string& strHTML) +{ + int size_read = 0; + strHTML = ""; + char buffer[16384]; + while( (size_read = Read(buffer, sizeof(buffer)-1) ) > 0 ) + { + buffer[size_read] = 0; + strHTML.append(buffer, size_read); + } + if (m_state->m_cancelled) + return false; + return true; +} + +bool CCurlFile::Download(const std::string& strURL, const std::string& strFileName, unsigned int* pdwSize) +{ + CLog::Log(LOGINFO, "CCurlFile::{} - {}->{}", __FUNCTION__, CURL::GetRedacted(strURL), + strFileName); + + std::string strData; + if (!Get(strURL, strData)) + return false; + + XFILE::CFile file; + if (!file.OpenForWrite(strFileName, true)) + { + CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Unable to open file for write: {} ({})", __FUNCTION__, + CURL::GetRedacted(strURL), strFileName, GetLastError()); + return false; + } + ssize_t written = 0; + if (!strData.empty()) + written = file.Write(strData.c_str(), strData.size()); + + if (pdwSize != NULL) + *pdwSize = written > 0 ? written : 0; + + return written == static_cast<ssize_t>(strData.size()); +} + +// Detect whether we are "online" or not! Very simple and dirty! +bool CCurlFile::IsInternet() +{ + CURL url("http://www.msftconnecttest.com/connecttest.txt"); + bool found = Exists(url); + if (!found) + { + // fallback + Close(); + url.Parse("http://www.w3.org/"); + found = Exists(url); + } + Close(); + + return found; +} + +void CCurlFile::Cancel() +{ + m_state->m_cancelled = true; + while (m_opened) + KODI::TIME::Sleep(1ms); +} + +void CCurlFile::Reset() +{ + m_state->m_cancelled = false; +} + +void CCurlFile::SetProxy(const std::string &type, const std::string &host, + uint16_t port, const std::string &user, const std::string &password) +{ + m_proxytype = CCurlFile::PROXY_HTTP; + if (type == "http") + m_proxytype = CCurlFile::PROXY_HTTP; + else if (type == "https") + m_proxytype = CCurlFile::PROXY_HTTPS; + else if (type == "socks4") + m_proxytype = CCurlFile::PROXY_SOCKS4; + else if (type == "socks4a") + m_proxytype = CCurlFile::PROXY_SOCKS4A; + else if (type == "socks5") + m_proxytype = CCurlFile::PROXY_SOCKS5; + else if (type == "socks5-remote") + m_proxytype = CCurlFile::PROXY_SOCKS5_REMOTE; + else + CLog::Log(LOGERROR, "CCurFile::{} - <{}> Invalid proxy type \"{}\"", __FUNCTION__, + CURL::GetRedacted(m_url), type); + m_proxyhost = host; + m_proxyport = port; + m_proxyuser = user; + m_proxypassword = password; +} + +bool CCurlFile::Open(const CURL& url) +{ + m_opened = true; + m_seekable = true; + + CURL url2(url); + ParseAndCorrectUrl(url2); + + std::string redactPath = CURL::GetRedacted(m_url); + CLog::Log(LOGDEBUG, "CurlFile::{} - <{}>", __FUNCTION__, redactPath); + + assert(!(!m_state->m_easyHandle ^ !m_state->m_multiHandle)); + if( m_state->m_easyHandle == NULL ) + g_curlInterface.easy_acquire(url2.GetProtocol().c_str(), + url2.GetHostName().c_str(), + &m_state->m_easyHandle, + &m_state->m_multiHandle); + + // setup common curl options + SetCommonOptions(m_state, + m_failOnError && !CServiceBroker::GetLogging().CanLogComponent(LOGCURL)); + SetRequestHeaders(m_state); + m_state->m_sendRange = m_seekable; + m_state->m_bRetry = m_allowRetry; + + m_httpresponse = m_state->Connect(m_bufferSize); + + if (m_httpresponse <= 0 || (m_failOnError && m_httpresponse >= 400)) + { + std::string error; + if (m_httpresponse >= 400 && CServiceBroker::GetLogging().CanLogComponent(LOGCURL)) + { + error.resize(4096); + ReadString(&error[0], 4095); + } + + CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Failed with code {}:\n{}", __FUNCTION__, + redactPath, m_httpresponse, error); + + return false; + } + + SetCorrectHeaders(m_state); + + // since we can't know the stream size up front if we're gzipped/deflated + // flag the stream with an unknown file size rather than the compressed + // file size. + if (!m_state->m_httpheader.GetValue("Content-Encoding").empty() && !StringUtils::EqualsNoCase(m_state->m_httpheader.GetValue("Content-Encoding"), "identity")) + m_state->m_fileSize = 0; + + // check if this stream is a shoutcast stream. sometimes checking the protocol line is not enough so examine other headers as well. + // shoutcast streams should be handled by FileShoutcast. + if ((m_state->m_httpheader.GetProtoLine().substr(0, 3) == "ICY" || !m_state->m_httpheader.GetValue("icy-notice1").empty() + || !m_state->m_httpheader.GetValue("icy-name").empty() + || !m_state->m_httpheader.GetValue("icy-br").empty()) && !m_skipshout) + { + CLog::Log(LOGDEBUG, "CCurlFile::{} - <{}> File is a shoutcast stream. Re-opening", __FUNCTION__, + redactPath); + throw new CRedirectException(new CShoutcastFile); + } + + m_multisession = false; + if(url2.IsProtocol("http") || url2.IsProtocol("https")) + { + m_multisession = true; + if(m_state->m_httpheader.GetValue("Server").find("Portable SDK for UPnP devices") != std::string::npos) + { + CLog::Log(LOGWARNING, + "CCurlFile::{} - <{}> Disabling multi session due to broken libupnp server", + __FUNCTION__, redactPath); + m_multisession = false; + } + } + + if(StringUtils::EqualsNoCase(m_state->m_httpheader.GetValue("Transfer-Encoding"), "chunked")) + m_state->m_fileSize = 0; + + if(m_state->m_fileSize <= 0) + m_seekable = false; + if (m_seekable) + { + if(url2.IsProtocol("http") + || url2.IsProtocol("https")) + { + // if server says explicitly it can't seek, respect that + if(StringUtils::EqualsNoCase(m_state->m_httpheader.GetValue("Accept-Ranges"),"none")) + m_seekable = false; + } + } + + std::string efurl = GetInfoString(CURLINFO_EFFECTIVE_URL); + if (!efurl.empty()) + { + if (m_url != efurl) + { + std::string redactEfpath = CURL::GetRedacted(efurl); + CLog::Log(LOGDEBUG, "CCurlFile::{} - <{}> Effective URL is {}", __FUNCTION__, redactPath, + redactEfpath); + } + m_url = efurl; + } + + return true; +} + +bool CCurlFile::OpenForWrite(const CURL& url, bool bOverWrite) +{ + if(m_opened) + return false; + + if (Exists(url) && !bOverWrite) + return false; + + CURL url2(url); + ParseAndCorrectUrl(url2); + + CLog::Log(LOGDEBUG, "CCurlFile::{} - Opening {}", __FUNCTION__, CURL::GetRedacted(m_url)); + + assert(m_state->m_easyHandle == NULL); + g_curlInterface.easy_acquire(url2.GetProtocol().c_str(), + url2.GetHostName().c_str(), + &m_state->m_easyHandle, + &m_state->m_multiHandle); + + // setup common curl options + SetCommonOptions(m_state); + SetRequestHeaders(m_state); + + std::string efurl = GetInfoString(CURLINFO_EFFECTIVE_URL); + if (!efurl.empty()) + m_url = efurl; + + m_opened = true; + m_forWrite = true; + m_inError = false; + m_writeOffset = 0; + + assert(m_state->m_multiHandle); + + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_UPLOAD, 1); + + g_curlInterface.multi_add_handle(m_state->m_multiHandle, m_state->m_easyHandle); + + m_state->SetReadBuffer(NULL, 0); + + return true; +} + +ssize_t CCurlFile::Write(const void* lpBuf, size_t uiBufSize) +{ + if (!(m_opened && m_forWrite) || m_inError) + return -1; + + assert(m_state->m_multiHandle); + + m_state->SetReadBuffer(lpBuf, uiBufSize); + m_state->m_isPaused = false; + g_curlInterface.easy_pause(m_state->m_easyHandle, CURLPAUSE_CONT); + + CURLMcode result = CURLM_OK; + + m_stillRunning = 1; + while (m_stillRunning && !m_state->m_isPaused) + { + while ((result = g_curlInterface.multi_perform(m_state->m_multiHandle, &m_stillRunning)) == CURLM_CALL_MULTI_PERFORM); + + if (!m_stillRunning) + break; + + if (result != CURLM_OK) + { + long code; + if(g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_RESPONSE_CODE, &code) == CURLE_OK ) + CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Unable to write curl resource with code {}", + __FUNCTION__, CURL::GetRedacted(m_url), code); + m_inError = true; + return -1; + } + } + + m_writeOffset += m_state->m_filePos; + return m_state->m_filePos; +} + +bool CCurlFile::CReadState::ReadString(char *szLine, int iLineLength) +{ + unsigned int want = (unsigned int)iLineLength; + + if((m_fileSize == 0 || m_filePos < m_fileSize) && FillBuffer(want) != FILLBUFFER_OK) + return false; + + // ensure only available data is considered + want = std::min(m_buffer.getMaxReadSize(), want); + + /* check if we finished prematurely */ + if (!m_stillRunning && (m_fileSize == 0 || m_filePos != m_fileSize) && !want) + { + if (m_fileSize != 0) + CLog::Log( + LOGWARNING, + "CCurlFile::{} - ({}) Transfer ended before entire file was retrieved pos {}, size {}", + __FUNCTION__, fmt::ptr(this), m_filePos, m_fileSize); + + return false; + } + + char* pLine = szLine; + do + { + if (!m_buffer.ReadData(pLine, 1)) + break; + + pLine++; + } while (((pLine - 1)[0] != '\n') && ((unsigned int)(pLine - szLine) < want)); + pLine[0] = 0; + m_filePos += (pLine - szLine); + return (pLine - szLine) > 0; +} + +bool CCurlFile::ReOpen(const CURL& url) +{ + Close(); + return Open(url); +} + +bool CCurlFile::Exists(const CURL& url) +{ + // if file is already running, get info from it + if( m_opened ) + { + CLog::Log(LOGWARNING, "CCurlFile::{} - <{}> Exist called on open file", __FUNCTION__, + url.GetRedacted()); + return true; + } + + CURL url2(url); + ParseAndCorrectUrl(url2); + + assert(m_state->m_easyHandle == NULL); + g_curlInterface.easy_acquire(url2.GetProtocol().c_str(), + url2.GetHostName().c_str(), + &m_state->m_easyHandle, NULL); + + SetCommonOptions(m_state); + SetRequestHeaders(m_state); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_TIMEOUT, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlconnecttimeout); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_NOBODY, 1); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_WRITEDATA, NULL); /* will cause write failure*/ + + if(url2.IsProtocol("ftp") || url2.IsProtocol("ftps")) + { + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FILETIME, 1); + // nocwd is less standard, will return empty list for non-existed remote dir on some ftp server, avoid it. + if (StringUtils::EndsWith(url2.GetFileName(), "/")) + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_SINGLECWD); + else + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_NOCWD); + } + + CURLcode result = g_curlInterface.easy_perform(m_state->m_easyHandle); + + if (result == CURLE_WRITE_ERROR || result == CURLE_OK) + { + g_curlInterface.easy_release(&m_state->m_easyHandle, NULL); + return true; + } + + if (result == CURLE_HTTP_RETURNED_ERROR) + { + long code; + if(g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_RESPONSE_CODE, &code) == CURLE_OK && code != 404 ) + { + if (code == 405) + { + // If we get a Method Not Allowed response, retry with a GET Request + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_NOBODY, 0); + + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FILETIME, 1); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_XFERINFOFUNCTION, transfer_abort_callback); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_NOPROGRESS, 0); + + curl_slist *list = NULL; + list = g_curlInterface.slist_append(list, "Range: bytes=0-1"); /* try to only request 1 byte */ + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_HTTPHEADER, list); + + CURLcode result = g_curlInterface.easy_perform(m_state->m_easyHandle); + g_curlInterface.slist_free_all(list); + + if (result == CURLE_WRITE_ERROR || result == CURLE_OK) + { + g_curlInterface.easy_release(&m_state->m_easyHandle, NULL); + return true; + } + + if (result == CURLE_HTTP_RETURNED_ERROR) + { + if (g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_RESPONSE_CODE, &code) == CURLE_OK && code != 404 ) + CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Failed: HTTP returned error {}", __FUNCTION__, + url.GetRedacted(), code); + } + } + else + CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Failed: HTTP returned error {}", __FUNCTION__, + url.GetRedacted(), code); + } + } + else if (result != CURLE_REMOTE_FILE_NOT_FOUND && result != CURLE_FTP_COULDNT_RETR_FILE) + { + CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Failed: {}({})", __FUNCTION__, url.GetRedacted(), + g_curlInterface.easy_strerror(result), result); + } + + errno = ENOENT; + g_curlInterface.easy_release(&m_state->m_easyHandle, NULL); + return false; +} + +int64_t CCurlFile::Seek(int64_t iFilePosition, int iWhence) +{ + int64_t nextPos = m_state->m_filePos; + + if(!m_seekable) + return -1; + + switch(iWhence) + { + case SEEK_SET: + nextPos = iFilePosition; + break; + case SEEK_CUR: + nextPos += iFilePosition; + break; + case SEEK_END: + if (m_state->m_fileSize) + nextPos = m_state->m_fileSize + iFilePosition; + else + return -1; + break; + default: + return -1; + } + + // We can't seek beyond EOF + if (m_state->m_fileSize && nextPos > m_state->m_fileSize) return -1; + + if(m_state->Seek(nextPos)) + return nextPos; + + if (m_multisession) + { + if (!m_oldState) + { + CURL url(m_url); + m_oldState = m_state; + m_state = new CReadState(); + m_state->m_fileSize = m_oldState->m_fileSize; + g_curlInterface.easy_acquire(url.GetProtocol().c_str(), + url.GetHostName().c_str(), + &m_state->m_easyHandle, + &m_state->m_multiHandle ); + } + else + { + CReadState *tmp; + tmp = m_state; + m_state = m_oldState; + m_oldState = tmp; + + if (m_state->Seek(nextPos)) + return nextPos; + + m_state->Disconnect(); + } + } + else + m_state->Disconnect(); + + // re-setup common curl options + SetCommonOptions(m_state); + + /* caller might have changed some headers (needed for daap)*/ + //! @todo daap is gone. is this needed for something else? + SetRequestHeaders(m_state); + + m_state->m_filePos = nextPos; + m_state->m_sendRange = true; + m_state->m_bRetry = m_allowRetry; + + long response = m_state->Connect(m_bufferSize); + if(response < 0 && (m_state->m_fileSize == 0 || m_state->m_fileSize != m_state->m_filePos)) + { + if(m_multisession) + { + if (m_oldState) + { + delete m_state; + m_state = m_oldState; + m_oldState = NULL; + } + // Retry without multisession + m_multisession = false; + return Seek(iFilePosition, iWhence); + } + else + { + m_seekable = false; + return -1; + } + } + + SetCorrectHeaders(m_state); + + return m_state->m_filePos; +} + +int64_t CCurlFile::GetLength() +{ + if (!m_opened) return 0; + return m_state->m_fileSize; +} + +int64_t CCurlFile::GetPosition() +{ + if (!m_opened) return 0; + return m_state->m_filePos; +} + +int CCurlFile::Stat(const CURL& url, struct __stat64* buffer) +{ + // if file is already running, get info from it + if( m_opened ) + { + CLog::Log(LOGWARNING, "CCurlFile::{} - <{}> Stat called on open file", __FUNCTION__, + url.GetRedacted()); + if (buffer) + { + *buffer = {}; + buffer->st_size = GetLength(); + buffer->st_mode = _S_IFREG; + } + return 0; + } + + CURL url2(url); + ParseAndCorrectUrl(url2); + + assert(m_state->m_easyHandle == NULL); + g_curlInterface.easy_acquire(url2.GetProtocol().c_str(), + url2.GetHostName().c_str(), + &m_state->m_easyHandle, NULL); + + SetCommonOptions(m_state); + SetRequestHeaders(m_state); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_TIMEOUT, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlconnecttimeout); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_NOBODY, 1); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FILETIME , 1); + + if(url2.IsProtocol("ftp")) + { + // nocwd is less standard, will return empty list for non-existed remote dir on some ftp server, avoid it. + if (StringUtils::EndsWith(url2.GetFileName(), "/")) + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_SINGLECWD); + else + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_NOCWD); + } + + CURLcode result = g_curlInterface.easy_perform(m_state->m_easyHandle); + + if(result == CURLE_HTTP_RETURNED_ERROR) + { + long code; + if(g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_RESPONSE_CODE, &code) == CURLE_OK && code == 404 ) + { + g_curlInterface.easy_release(&m_state->m_easyHandle, NULL); + errno = ENOENT; + return -1; + } + } + + if(result == CURLE_GOT_NOTHING + || result == CURLE_HTTP_RETURNED_ERROR + || result == CURLE_RECV_ERROR /* some silly shoutcast servers */ ) + { + /* some http servers and shoutcast servers don't give us any data on a head request */ + /* request normal and just bail out via progress meter callback after we received data */ + /* somehow curl doesn't reset CURLOPT_NOBODY properly so reset everything */ + SetCommonOptions(m_state); + SetRequestHeaders(m_state); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_TIMEOUT, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlconnecttimeout); + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FILETIME, 1); +#if LIBCURL_VERSION_NUM >= 0x072000 // 0.7.32 + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_XFERINFOFUNCTION, transfer_abort_callback); +#else + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_PROGRESSFUNCTION, transfer_abort_callback); +#endif + g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_NOPROGRESS, 0); + + result = g_curlInterface.easy_perform(m_state->m_easyHandle); + + } + + if( result != CURLE_ABORTED_BY_CALLBACK && result != CURLE_OK ) + { + g_curlInterface.easy_release(&m_state->m_easyHandle, NULL); + errno = ENOENT; + CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Failed: {}({})", __FUNCTION__, url.GetRedacted(), + g_curlInterface.easy_strerror(result), result); + return -1; + } + + double length; + result = g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &length); + if (result != CURLE_OK || length < 0.0) + { + if (url.IsProtocol("ftp")) + { + g_curlInterface.easy_release(&m_state->m_easyHandle, NULL); + CLog::Log(LOGINFO, "CCurlFile::{} - <{}> Content length failed: {}({})", __FUNCTION__, + url.GetRedacted(), g_curlInterface.easy_strerror(result), result); + errno = ENOENT; + return -1; + } + else + length = 0.0; + } + + SetCorrectHeaders(m_state); + + if (buffer) + { + *buffer = {}; + buffer->st_size = static_cast<int64_t>(length); + + // Note: CURLINFO_CONTENT_TYPE returns the last received content-type response header value. + // In case there is authentication required there might be multiple requests involved and if + // the last request which actually returns the data does not return a content-type header, but + // one of the preceding requests, CURLINFO_CONTENT_TYPE returns not the content type of the + // actual resource requested! m_state contains only the values of the last request, which is + // what we want here. + const std::string mimeType = m_state->m_httpheader.GetMimeType(); + if (mimeType.find("text/html") != std::string::npos) // consider html files directories + buffer->st_mode = _S_IFDIR; + else + buffer->st_mode = _S_IFREG; + + long filetime; + result = g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_FILETIME, &filetime); + if (result != CURLE_OK) + { + CLog::Log(LOGINFO, "CCurlFile::{} - <{}> Filetime failed: {}({})", __FUNCTION__, + url.GetRedacted(), g_curlInterface.easy_strerror(result), result); + } + else + { + if (filetime != -1) + buffer->st_mtime = filetime; + } + } + g_curlInterface.easy_release(&m_state->m_easyHandle, NULL); + return 0; +} + +ssize_t CCurlFile::CReadState::Read(void* lpBuf, size_t uiBufSize) +{ + /* only request 1 byte, for truncated reads (only if not eof) */ + if (m_fileSize == 0 || m_filePos < m_fileSize) + { + int8_t result = FillBuffer(1); + if (result == FILLBUFFER_FAIL) + return -1; // Fatal error + + if (result == FILLBUFFER_NO_DATA) + return 0; + } + + /* ensure only available data is considered */ + unsigned int want = std::min<unsigned int>(m_buffer.getMaxReadSize(), uiBufSize); + + /* xfer data to caller */ + if (m_buffer.ReadData((char *)lpBuf, want)) + { + m_filePos += want; + return want; + } + + /* check if we finished prematurely */ + if (!m_stillRunning && (m_fileSize == 0 || m_filePos != m_fileSize)) + { + CLog::Log(LOGWARNING, + "CCurlFile::CReadState::{} - ({}) Transfer ended before entire file was retrieved " + "pos {}, size {}", + __FUNCTION__, fmt::ptr(this), m_filePos, m_fileSize); + return -1; + } + + return 0; +} + +/* use to attempt to fill the read buffer up to requested number of bytes */ +int8_t CCurlFile::CReadState::FillBuffer(unsigned int want) +{ + int retry = 0; + fd_set fdread; + fd_set fdwrite; + fd_set fdexcep; + + // only attempt to fill buffer if transactions still running and buffer + // doesn't exceed required size already + while (m_buffer.getMaxReadSize() < want && m_buffer.getMaxWriteSize() > 0 ) + { + if (m_cancelled) + return FILLBUFFER_NO_DATA; + + /* if there is data in overflow buffer, try to use that first */ + if (m_overflowSize) + { + unsigned amount = std::min(m_buffer.getMaxWriteSize(), m_overflowSize); + m_buffer.WriteData(m_overflowBuffer, amount); + + if (amount < m_overflowSize) + memmove(m_overflowBuffer, m_overflowBuffer + amount, m_overflowSize - amount); + + m_overflowSize -= amount; + // Shrink memory: + m_overflowBuffer = (char*)realloc_simple(m_overflowBuffer, m_overflowSize); + continue; + } + + CURLMcode result = g_curlInterface.multi_perform(m_multiHandle, &m_stillRunning); + if (!m_stillRunning) + { + if (result == CURLM_OK) + { + /* if we still have stuff in buffer, we are fine */ + if (m_buffer.getMaxReadSize()) + return FILLBUFFER_OK; + + // check for errors + int msgs; + CURLMsg* msg; + bool bRetryNow = true; + bool bError = false; + while ((msg = g_curlInterface.multi_info_read(m_multiHandle, &msgs))) + { + if (msg->msg == CURLMSG_DONE) + { + if (msg->data.result == CURLE_OK) + return FILLBUFFER_OK; + + long httpCode = 0; + if (msg->data.result == CURLE_HTTP_RETURNED_ERROR) + { + g_curlInterface.easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &httpCode); + + // Don't log 404 not-found errors to prevent log-spam + if (httpCode != 404) + CLog::Log(LOGERROR, + "CCurlFile::CReadState::{} - ({}) Failed: HTTP returned code {}", + __FUNCTION__, fmt::ptr(this), httpCode); + } + else + { + CLog::Log(LOGERROR, "CCurlFile::CReadState::{} - ({}) Failed: {}({})", __FUNCTION__, + fmt::ptr(this), g_curlInterface.easy_strerror(msg->data.result), + msg->data.result); + } + + if ( (msg->data.result == CURLE_OPERATION_TIMEDOUT || + msg->data.result == CURLE_PARTIAL_FILE || + msg->data.result == CURLE_COULDNT_CONNECT || + msg->data.result == CURLE_RECV_ERROR) && + !m_bFirstLoop) + { + bRetryNow = false; // Leave it to caller whether the operation is retried + bError = true; + } + else if ( (msg->data.result == CURLE_HTTP_RANGE_ERROR || + httpCode == 416 /* = Requested Range Not Satisfiable */ || + httpCode == 406 /* = Not Acceptable (fixes issues with non compliant HDHomerun servers */) && + m_bFirstLoop && + m_filePos == 0 && + m_sendRange) + { + // If server returns a (possible) range error, disable range and retry (handled below) + bRetryNow = true; + bError = true; + m_sendRange = false; + } + else + { + // For all other errors, abort the operation + return FILLBUFFER_FAIL; + } + } + } + + // Check for an actual error, if not, just return no-data + if (!bError && !m_bLastError) + return FILLBUFFER_NO_DATA; + + // Close handle + if (m_multiHandle && m_easyHandle) + g_curlInterface.multi_remove_handle(m_multiHandle, m_easyHandle); + + // Reset all the stuff like we would in Disconnect() + m_buffer.Clear(); + free(m_overflowBuffer); + m_overflowBuffer = NULL; + m_overflowSize = 0; + m_bLastError = true; // Flag error for the next run + + // Retry immediately or leave it up to the caller? + if ((m_bRetry && retry < CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlretries) || (bRetryNow && retry == 0)) + { + retry++; + + // Connect + seek to current position (again) + SetResume(); + g_curlInterface.multi_add_handle(m_multiHandle, m_easyHandle); + + CLog::Log(LOGWARNING, "CCurlFile::CReadState::{} - ({}) Reconnect, (re)try {}", + __FUNCTION__, fmt::ptr(this), retry); + + // Return to the beginning of the loop: + continue; + } + + return FILLBUFFER_NO_DATA; // We failed but flag no data to caller, so it can retry the operation + } + return FILLBUFFER_FAIL; + } + + // We've finished out first loop + if(m_bFirstLoop && m_buffer.getMaxReadSize() > 0) + m_bFirstLoop = false; + + // No error this run + m_bLastError = false; + + switch (result) + { + case CURLM_OK: + { + int maxfd = -1; + FD_ZERO(&fdread); + FD_ZERO(&fdwrite); + FD_ZERO(&fdexcep); + + // get file descriptors from the transfers + g_curlInterface.multi_fdset(m_multiHandle, &fdread, &fdwrite, &fdexcep, &maxfd); + + long timeout = 0; + if (CURLM_OK != g_curlInterface.multi_timeout(m_multiHandle, &timeout) || timeout == -1 || timeout < 200) + timeout = 200; + + XbmcThreads::EndTime<> endTime{std::chrono::milliseconds(timeout)}; + int rc; + + do + { + /* On success the value of maxfd is guaranteed to be >= -1. We call + * select(maxfd + 1, ...); specially in case of (maxfd == -1) there are + * no fds ready yet so we call select(0, ...) --or Sleep() on Windows-- + * to sleep 100ms, which is the minimum suggested value in the + * curl_multi_fdset() doc. + */ + if (maxfd == -1) + { +#ifdef TARGET_WINDOWS + /* Windows does not support using select() for sleeping without a dummy + * socket. Instead use Windows' Sleep() and sleep for 100ms which is the + * minimum suggested value in the curl_multi_fdset() doc. + */ + KODI::TIME::Sleep(100ms); + rc = 0; +#else + /* Portable sleep for platforms other than Windows. */ + struct timeval wait = { 0, 100 * 1000 }; /* 100ms */ + rc = select(0, NULL, NULL, NULL, &wait); +#endif + } + else + { + unsigned int time_left = endTime.GetTimeLeft().count(); + struct timeval wait = { (int)time_left / 1000, ((int)time_left % 1000) * 1000 }; + rc = select(maxfd + 1, &fdread, &fdwrite, &fdexcep, &wait); + } +#ifdef TARGET_WINDOWS + } while(rc == SOCKET_ERROR && WSAGetLastError() == WSAEINTR); +#else + } while(rc == SOCKET_ERROR && errno == EINTR); +#endif + + if(rc == SOCKET_ERROR) + { +#ifdef TARGET_WINDOWS + char buf[256]; + strerror_s(buf, 256, WSAGetLastError()); + CLog::Log(LOGERROR, "CCurlFile::CReadState::{} - ({}) Failed with socket error:{}", + __FUNCTION__, fmt::ptr(this), buf); +#else + char const * str = strerror(errno); + CLog::Log(LOGERROR, "CCurlFile::CReadState::{} - ({}) Failed with socket error:{}", + __FUNCTION__, fmt::ptr(this), str); +#endif + + return FILLBUFFER_FAIL; + } + } + break; + case CURLM_CALL_MULTI_PERFORM: + { + // we don't keep calling here as that can easily overwrite our buffer which we want to avoid + // docs says we should call it soon after, but as long as we are reading data somewhere + // this aught to be soon enough. should stay in socket otherwise + continue; + } + break; + default: + { + CLog::Log(LOGERROR, + "CCurlFile::CReadState::{} - ({}) Multi perform failed with code {}, aborting", + __FUNCTION__, fmt::ptr(this), result); + return FILLBUFFER_FAIL; + } + break; + } + } + return FILLBUFFER_OK; +} + +void CCurlFile::CReadState::SetReadBuffer(const void* lpBuf, int64_t uiBufSize) +{ + m_readBuffer = const_cast<char*>((const char*)lpBuf); + m_fileSize = uiBufSize; + m_filePos = 0; +} + +void CCurlFile::ClearRequestHeaders() +{ + m_requestheaders.clear(); +} + +void CCurlFile::SetRequestHeader(const std::string& header, const std::string& value) +{ + m_requestheaders[header] = value; +} + +void CCurlFile::SetRequestHeader(const std::string& header, long value) +{ + m_requestheaders[header] = std::to_string(value); +} + +std::string CCurlFile::GetURL(void) +{ + return m_url; +} + +std::string CCurlFile::GetRedirectURL() +{ + return GetInfoString(CURLINFO_REDIRECT_URL); +} + +std::string CCurlFile::GetInfoString(int infoType) +{ + char* info{}; + CURLcode result = g_curlInterface.easy_getinfo(m_state->m_easyHandle, static_cast<CURLINFO> (infoType), &info); + if (result != CURLE_OK) + { + CLog::Log(LOGERROR, + "CCurlFile::{} - <{}> Info string request for type {} failed with result code {}", + __FUNCTION__, CURL::GetRedacted(m_url), infoType, result); + return ""; + } + return (info ? info : ""); +} + +/* STATIC FUNCTIONS */ +bool CCurlFile::GetHttpHeader(const CURL &url, CHttpHeader &headers) +{ + try + { + CCurlFile file; + if(file.Stat(url, NULL) == 0) + { + headers = file.GetHttpHeader(); + return true; + } + return false; + } + catch(...) + { + CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Exception thrown while trying to retrieve header", + __FUNCTION__, url.GetRedacted()); + return false; + } +} + +bool CCurlFile::GetMimeType(const CURL &url, std::string &content, const std::string &useragent) +{ + CCurlFile file; + if (!useragent.empty()) + file.SetUserAgent(useragent); + + struct __stat64 buffer; + std::string redactUrl = url.GetRedacted(); + if( file.Stat(url, &buffer) == 0 ) + { + if (buffer.st_mode == _S_IFDIR) + content = "x-directory/normal"; + else + content = file.GetProperty(XFILE::FILE_PROPERTY_MIME_TYPE); + CLog::Log(LOGDEBUG, "CCurlFile::{} - <{}> -> {}", __FUNCTION__, redactUrl, content); + return true; + } + CLog::Log(LOGDEBUG, "CCurlFile::{} - <{}> -> failed", __FUNCTION__, redactUrl); + content.clear(); + return false; +} + +bool CCurlFile::GetContentType(const CURL &url, std::string &content, const std::string &useragent) +{ + CCurlFile file; + if (!useragent.empty()) + file.SetUserAgent(useragent); + + struct __stat64 buffer; + std::string redactUrl = url.GetRedacted(); + if (file.Stat(url, &buffer) == 0) + { + if (buffer.st_mode == _S_IFDIR) + content = "x-directory/normal"; + else + content = file.GetProperty(XFILE::FILE_PROPERTY_CONTENT_TYPE, ""); + CLog::Log(LOGDEBUG, "CCurlFile::{} - <{}> -> {}", __FUNCTION__, redactUrl, content); + return true; + } + CLog::Log(LOGDEBUG, "CCurlFile::{} - <{}> -> failed", __FUNCTION__, redactUrl); + content.clear(); + return false; +} + +bool CCurlFile::GetCookies(const CURL &url, std::string &cookies) +{ + std::string cookiesStr; + curl_slist* curlCookies; + CURL_HANDLE* easyHandle; + CURLM* multiHandle; + + // get the cookies list + g_curlInterface.easy_acquire(url.GetProtocol().c_str(), + url.GetHostName().c_str(), + &easyHandle, &multiHandle); + if (CURLE_OK == g_curlInterface.easy_getinfo(easyHandle, CURLINFO_COOKIELIST, &curlCookies)) + { + // iterate over each cookie and format it into an RFC 2109 formatted Set-Cookie string + curl_slist* curlCookieIter = curlCookies; + while(curlCookieIter) + { + // tokenize the CURL cookie string + std::vector<std::string> valuesVec; + StringUtils::Tokenize(curlCookieIter->data, valuesVec, "\t"); + + // ensure the length is valid + if (valuesVec.size() < 7) + { + CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Invalid cookie: '{}'", __FUNCTION__, + url.GetRedacted(), curlCookieIter->data); + curlCookieIter = curlCookieIter->next; + continue; + } + + // create a http-header formatted cookie string + std::string cookieStr = valuesVec[5] + "=" + valuesVec[6] + + "; path=" + valuesVec[2] + + "; domain=" + valuesVec[0]; + + // append this cookie to the string containing all cookies + if (!cookiesStr.empty()) + cookiesStr += "\n"; + cookiesStr += cookieStr; + + // move on to the next cookie + curlCookieIter = curlCookieIter->next; + } + + // free the curl cookies + g_curlInterface.slist_free_all(curlCookies); + + // release our handles + g_curlInterface.easy_release(&easyHandle, &multiHandle); + + // if we have a non-empty cookie string, return it + if (!cookiesStr.empty()) + { + cookies = cookiesStr; + return true; + } + } + + // no cookies to return + return false; +} + +int CCurlFile::IoControl(EIoControl request, void* param) +{ + if (request == IOCTRL_SEEK_POSSIBLE) + return m_seekable ? 1 : 0; + + if (request == IOCTRL_SET_RETRY) + { + m_allowRetry = *(bool*) param; + return 0; + } + + return -1; +} + +const std::string CCurlFile::GetProperty(XFILE::FileProperty type, const std::string &name) const +{ + switch (type) + { + case FILE_PROPERTY_RESPONSE_PROTOCOL: + return m_state->m_httpheader.GetProtoLine(); + case FILE_PROPERTY_RESPONSE_HEADER: + return m_state->m_httpheader.GetValue(name); + case FILE_PROPERTY_CONTENT_TYPE: + return m_state->m_httpheader.GetValue("content-type"); + case FILE_PROPERTY_CONTENT_CHARSET: + return m_state->m_httpheader.GetCharset(); + case FILE_PROPERTY_MIME_TYPE: + return m_state->m_httpheader.GetMimeType(); + case FILE_PROPERTY_EFFECTIVE_URL: + { + char *url = nullptr; + g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_EFFECTIVE_URL, &url); + return url ? url : ""; + } + default: + return ""; + } +} + +const std::vector<std::string> CCurlFile::GetPropertyValues(XFILE::FileProperty type, const std::string &name) const +{ + if (type == FILE_PROPERTY_RESPONSE_HEADER) + { + return m_state->m_httpheader.GetValues(name); + } + std::vector<std::string> values; + std::string value = GetProperty(type, name); + if (!value.empty()) + { + values.emplace_back(value); + } + return values; +} + +double CCurlFile::GetDownloadSpeed() +{ +#if LIBCURL_VERSION_NUM >= 0x073a00 // 0.7.58.0 + double speed = 0.0; + if (g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_SPEED_DOWNLOAD, &speed) == CURLE_OK) + return speed; +#else + double time = 0.0, size = 0.0; + if (g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_TOTAL_TIME, &time) == CURLE_OK + && g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_SIZE_DOWNLOAD, &size) == CURLE_OK + && time > 0.0) + { + return size / time; + } +#endif + return 0.0; +} diff --git a/xbmc/filesystem/CurlFile.h b/xbmc/filesystem/CurlFile.h new file mode 100644 index 0000000..88f2923 --- /dev/null +++ b/xbmc/filesystem/CurlFile.h @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IFile.h" +#include "utils/HttpHeader.h" +#include "utils/RingBuffer.h" + +#include <map> +#include <string> + +typedef void CURL_HANDLE; +typedef void CURLM; +struct curl_slist; + +namespace XFILE +{ + class CCurlFile : public IFile + { + private: + typedef enum + { + PROXY_HTTP = 0, + PROXY_SOCKS4, + PROXY_SOCKS4A, + PROXY_SOCKS5, + PROXY_SOCKS5_REMOTE, + PROXY_HTTPS, + } ProxyType; + + public: + CCurlFile(); + ~CCurlFile() override; + bool Open(const CURL& url) override; + bool OpenForWrite(const CURL& url, bool bOverWrite = false) override; + bool ReOpen(const CURL& url) override; + bool Exists(const CURL& url) override; + int64_t Seek(int64_t iFilePosition, int iWhence=SEEK_SET) override; + int64_t GetPosition() override; + int64_t GetLength() override; + int Stat(const CURL& url, struct __stat64* buffer) override; + void Close() override; + bool ReadString(char *szLine, int iLineLength) override { return m_state->ReadString(szLine, iLineLength); } + ssize_t Read(void* lpBuf, size_t uiBufSize) override { return m_state->Read(lpBuf, uiBufSize); } + ssize_t Write(const void* lpBuf, size_t uiBufSize) override; + const std::string GetProperty(XFILE::FileProperty type, const std::string &name = "") const override; + const std::vector<std::string> GetPropertyValues(XFILE::FileProperty type, const std::string &name = "") const override; + int IoControl(EIoControl request, void* param) override; + double GetDownloadSpeed() override; + + bool Post(const std::string& strURL, const std::string& strPostData, std::string& strHTML); + bool Get(const std::string& strURL, std::string& strHTML); + bool ReadData(std::string& strHTML); + bool Download(const std::string& strURL, const std::string& strFileName, unsigned int* pdwSize = NULL); + bool IsInternet(); + void Cancel(); + void Reset(); + void SetUserAgent(const std::string& sUserAgent) { m_userAgent = sUserAgent; } + void SetProxy(const std::string &type, const std::string &host, uint16_t port, + const std::string &user, const std::string &password); + void SetCustomRequest(const std::string &request) { m_customrequest = request; } + void SetAcceptEncoding(const std::string& encoding) { m_acceptencoding = encoding; } + void SetAcceptCharset(const std::string& charset) { m_acceptCharset = charset; } + void SetTimeout(int connecttimeout) { m_connecttimeout = connecttimeout; } + void SetLowSpeedTime(int lowspeedtime) { m_lowspeedtime = lowspeedtime; } + void SetPostData(const std::string& postdata) { m_postdata = postdata; } + void SetReferer(const std::string& referer) { m_referer = referer; } + void SetCookie(const std::string& cookie) { m_cookie = cookie; } + void SetMimeType(const std::string& mimetype) { SetRequestHeader("Content-Type", mimetype); } + void SetRequestHeader(const std::string& header, const std::string& value); + void SetRequestHeader(const std::string& header, long value); + + void ClearRequestHeaders(); + void SetBufferSize(unsigned int size); + + const CHttpHeader& GetHttpHeader() const { return m_state->m_httpheader; } + std::string GetURL(void); + std::string GetRedirectURL(); + + /* static function that will get content type of a file */ + static bool GetHttpHeader(const CURL &url, CHttpHeader &headers); + static bool GetMimeType(const CURL &url, std::string &content, const std::string &useragent=""); + static bool GetContentType(const CURL &url, std::string &content, const std::string &useragent = ""); + + /* static function that will get cookies stored by CURL in RFC 2109 format */ + static bool GetCookies(const CURL &url, std::string &cookies); + + class CReadState + { + public: + CReadState(); + ~CReadState(); + CURL_HANDLE* m_easyHandle; + CURLM* m_multiHandle; + + CRingBuffer m_buffer; // our ringhold buffer + unsigned int m_bufferSize; + + char* m_overflowBuffer; // in the rare case we would overflow the above buffer + unsigned int m_overflowSize; // size of the overflow buffer + int m_stillRunning; // Is background url fetch still in progress + bool m_cancelled; + int64_t m_fileSize; + int64_t m_filePos; + bool m_bFirstLoop; + bool m_isPaused; + bool m_sendRange; + bool m_bLastError; + bool m_bRetry; + + char* m_readBuffer; + + /* returned http header */ + CHttpHeader m_httpheader; + bool IsHeaderDone(void) { return m_httpheader.IsHeaderDone(); } + + curl_slist* m_curlHeaderList; + curl_slist* m_curlAliasList; + + size_t ReadCallback(char *buffer, size_t size, size_t nitems); + size_t WriteCallback(char *buffer, size_t size, size_t nitems); + size_t HeaderCallback(void *ptr, size_t size, size_t nmemb); + + bool Seek(int64_t pos); + ssize_t Read(void* lpBuf, size_t uiBufSize); + bool ReadString(char *szLine, int iLineLength); + int8_t FillBuffer(unsigned int want); + void SetReadBuffer(const void* lpBuf, int64_t uiBufSize); + + void SetResume(void); + long Connect(unsigned int size); + void Disconnect(); + }; + + protected: + void ParseAndCorrectUrl(CURL &url); + void SetCommonOptions(CReadState* state, bool failOnError = true); + void SetRequestHeaders(CReadState* state); + void SetCorrectHeaders(CReadState* state); + bool Service(const std::string& strURL, std::string& strHTML); + std::string GetInfoString(int infoType); + + protected: + CReadState* m_state; + CReadState* m_oldState; + unsigned int m_bufferSize; + int64_t m_writeOffset = 0; + + std::string m_url; + std::string m_userAgent; + ProxyType m_proxytype = PROXY_HTTP; + std::string m_proxyhost; + uint16_t m_proxyport = 3128; + std::string m_proxyuser; + std::string m_proxypassword; + std::string m_customrequest; + std::string m_acceptencoding; + std::string m_acceptCharset; + std::string m_ftpauth; + std::string m_ftpport; + std::string m_binary; + std::string m_postdata; + std::string m_referer; + std::string m_cookie; + std::string m_username; + std::string m_password; + std::string m_httpauth; + std::string m_cipherlist; + bool m_ftppasvip; + int m_connecttimeout; + int m_redirectlimit; + int m_lowspeedtime; + bool m_opened; + bool m_forWrite; + bool m_inError; + bool m_seekable; + bool m_multisession; + bool m_skipshout; + bool m_postdataset; + bool m_allowRetry; + bool m_verifyPeer = true; + bool m_failOnError = true; + curl_slist* m_dnsCacheList = nullptr; + + CRingBuffer m_buffer; // our ringhold buffer + char* m_overflowBuffer; // in the rare case we would overflow the above buffer + unsigned int m_overflowSize = 0; // size of the overflow buffer + + int m_stillRunning; // Is background url fetch still in progress? + + typedef std::map<std::string, std::string> MAPHTTPHEADERS; + MAPHTTPHEADERS m_requestheaders; + + long m_httpresponse; + }; +} diff --git a/xbmc/filesystem/DAVCommon.cpp b/xbmc/filesystem/DAVCommon.cpp new file mode 100644 index 0000000..f200609 --- /dev/null +++ b/xbmc/filesystem/DAVCommon.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DAVCommon.h" + +#include "utils/StringUtils.h" +#include "utils/log.h" + +using namespace XFILE; + +/* + * Return true if pElement value is equal value without namespace. + * + * if pElement is <DAV:foo> and value is foo then ValueWithoutNamespace is true + */ +bool CDAVCommon::ValueWithoutNamespace(const TiXmlNode *pNode, const std::string& value) +{ + const TiXmlElement *pElement; + + if (!pNode) + { + return false; + } + + pElement = pNode->ToElement(); + + if (!pElement) + { + return false; + } + + std::vector<std::string> tag = StringUtils::Split(pElement->ValueStr(), ":", 2); + + if (tag.size() == 1 && tag[0] == value) + { + return true; + } + else if (tag.size() == 2 && tag[1] == value) + { + return true; + } + else if (tag.size() > 2) + { + CLog::Log(LOGERROR, "{} - Splitting {} failed, size(): {}, value: {}", __FUNCTION__, + pElement->Value(), (unsigned long int)tag.size(), value); + } + + return false; +} + +/* + * Search for <status> and return its content + */ +std::string CDAVCommon::GetStatusTag(const TiXmlElement *pElement) +{ + const TiXmlElement *pChild; + + for (pChild = pElement->FirstChildElement(); pChild != 0; pChild = pChild->NextSiblingElement()) + { + if (ValueWithoutNamespace(pChild, "status")) + { + return pChild->NoChildren() ? "" : pChild->FirstChild()->ValueStr(); + } + } + + return ""; +} + diff --git a/xbmc/filesystem/DAVCommon.h b/xbmc/filesystem/DAVCommon.h new file mode 100644 index 0000000..483de3b --- /dev/null +++ b/xbmc/filesystem/DAVCommon.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "utils/XBMCTinyXML.h" + +namespace XFILE +{ + class CDAVCommon + { + public: + static bool ValueWithoutNamespace(const TiXmlNode *pNode, const std::string& value); + static std::string GetStatusTag(const TiXmlElement *pElement); + }; +} diff --git a/xbmc/filesystem/DAVDirectory.cpp b/xbmc/filesystem/DAVDirectory.cpp new file mode 100644 index 0000000..48d4865 --- /dev/null +++ b/xbmc/filesystem/DAVDirectory.cpp @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DAVDirectory.h" + +#include "CurlFile.h" +#include "DAVCommon.h" +#include "DAVFile.h" +#include "FileItem.h" +#include "URL.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +using namespace XFILE; + +CDAVDirectory::CDAVDirectory(void) = default; +CDAVDirectory::~CDAVDirectory(void) = default; + +/* + * Parses a <response> + * + * <!ELEMENT response (href, ((href*, status)|(propstat+)), responsedescription?) > + * <!ELEMENT propstat (prop, status, responsedescription?) > + * + */ +void CDAVDirectory::ParseResponse(const TiXmlElement *pElement, CFileItem &item) +{ + const TiXmlElement *pResponseChild; + const TiXmlNode *pPropstatChild; + const TiXmlElement *pPropChild; + + /* Iterate response children elements */ + for (pResponseChild = pElement->FirstChildElement(); pResponseChild != 0; pResponseChild = pResponseChild->NextSiblingElement()) + { + if (CDAVCommon::ValueWithoutNamespace(pResponseChild, "href") && !pResponseChild->NoChildren()) + { + std::string path(pResponseChild->FirstChild()->ValueStr()); + URIUtils::RemoveSlashAtEnd(path); + item.SetPath(path); + } + else + if (CDAVCommon::ValueWithoutNamespace(pResponseChild, "propstat")) + { + if (CDAVCommon::GetStatusTag(pResponseChild->ToElement()).find("200 OK") != std::string::npos) + { + /* Iterate propstat children elements */ + for (pPropstatChild = pResponseChild->FirstChild(); pPropstatChild != 0; pPropstatChild = pPropstatChild->NextSibling()) + { + if (CDAVCommon::ValueWithoutNamespace(pPropstatChild, "prop")) + { + /* Iterate all properties available */ + for (pPropChild = pPropstatChild->FirstChildElement(); pPropChild != 0; pPropChild = pPropChild->NextSiblingElement()) + { + if (CDAVCommon::ValueWithoutNamespace(pPropChild, "getcontentlength") && !pPropChild->NoChildren()) + { + item.m_dwSize = strtoll(pPropChild->FirstChild()->Value(), NULL, 10); + } + else + if (CDAVCommon::ValueWithoutNamespace(pPropChild, "getlastmodified") && !pPropChild->NoChildren()) + { + struct tm timeDate = {}; + strptime(pPropChild->FirstChild()->Value(), "%a, %d %b %Y %T", &timeDate); + item.m_dateTime = mktime(&timeDate); + } + else + if (CDAVCommon::ValueWithoutNamespace(pPropChild, "displayname") && !pPropChild->NoChildren()) + { + item.SetLabel(CURL::Decode(pPropChild->FirstChild()->ValueStr())); + } + else + if (!item.m_dateTime.IsValid() && CDAVCommon::ValueWithoutNamespace(pPropChild, "creationdate") && !pPropChild->NoChildren()) + { + struct tm timeDate = {}; + strptime(pPropChild->FirstChild()->Value(), "%Y-%m-%dT%T", &timeDate); + item.m_dateTime = mktime(&timeDate); + } + else + if (CDAVCommon::ValueWithoutNamespace(pPropChild, "resourcetype")) + { + if (CDAVCommon::ValueWithoutNamespace(pPropChild->FirstChild(), "collection")) + { + item.m_bIsFolder = true; + } + } + } + } + } + } + } + } +} + +bool CDAVDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + CCurlFile dav; + std::string strRequest = "PROPFIND"; + + dav.SetCustomRequest(strRequest); + dav.SetMimeType("text/xml; charset=\"utf-8\""); + dav.SetRequestHeader("depth", 1); + dav.SetPostData( + "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" + " <D:propfind xmlns:D=\"DAV:\">" + " <D:prop>" + " <D:resourcetype/>" + " <D:getcontentlength/>" + " <D:getlastmodified/>" + " <D:creationdate/>" + " <D:displayname/>" + " </D:prop>" + " </D:propfind>"); + + if (!dav.Open(url)) + { + CLog::Log(LOGERROR, "{} - Unable to get dav directory ({})", __FUNCTION__, url.GetRedacted()); + return false; + } + + std::string strResponse; + dav.ReadData(strResponse); + + std::string fileCharset(dav.GetProperty(XFILE::FILE_PROPERTY_CONTENT_CHARSET)); + CXBMCTinyXML davResponse; + davResponse.Parse(strResponse, fileCharset); + + if (!davResponse.Parse(strResponse)) + { + CLog::Log(LOGERROR, "{} - Unable to process dav directory ({})", __FUNCTION__, + url.GetRedacted()); + dav.Close(); + return false; + } + + TiXmlNode *pChild; + // Iterate over all responses + for (pChild = davResponse.RootElement()->FirstChild(); pChild != 0; pChild = pChild->NextSibling()) + { + if (CDAVCommon::ValueWithoutNamespace(pChild, "response")) + { + CFileItem item; + ParseResponse(pChild->ToElement(), item); + const CURL& url2(url); + CURL url3(item.GetPath()); + + std::string itemPath(URIUtils::AddFileToFolder(url2.GetWithoutFilename(), url3.GetFileName())); + + if (item.GetLabel().empty()) + { + std::string name(itemPath); + URIUtils::RemoveSlashAtEnd(name); + item.SetLabel(CURL::Decode(URIUtils::GetFileName(name))); + } + + if (item.m_bIsFolder) + URIUtils::AddSlashAtEnd(itemPath); + + // Add back protocol options + if (!url2.GetProtocolOptions().empty()) + itemPath += "|" + url2.GetProtocolOptions(); + item.SetPath(itemPath); + + if (!item.IsURL(url)) + { + CFileItemPtr pItem(new CFileItem(item)); + items.Add(pItem); + } + } + } + + dav.Close(); + + return true; +} + +bool CDAVDirectory::Create(const CURL& url) +{ + CDAVFile dav; + std::string strRequest = "MKCOL"; + + dav.SetCustomRequest(strRequest); + + if (!dav.Execute(url)) + { + CLog::Log(LOGERROR, "{} - Unable to create dav directory ({}) - {}", __FUNCTION__, + url.GetRedacted(), dav.GetLastResponseCode()); + return false; + } + + dav.Close(); + + return true; +} + +bool CDAVDirectory::Exists(const CURL& url) +{ + CCurlFile dav; + + // Set the PROPFIND custom request else we may not find folders, depending + // on the server's configuration + std::string strRequest = "PROPFIND"; + dav.SetCustomRequest(strRequest); + dav.SetRequestHeader("depth", 0); + + return dav.Exists(url); +} + +bool CDAVDirectory::Remove(const CURL& url) +{ + CDAVFile dav; + std::string strRequest = "DELETE"; + + dav.SetCustomRequest(strRequest); + + if (!dav.Execute(url)) + { + CLog::Log(LOGERROR, "{} - Unable to delete dav directory ({}) - {}", __FUNCTION__, + url.GetRedacted(), dav.GetLastResponseCode()); + return false; + } + + dav.Close(); + + return true; +} diff --git a/xbmc/filesystem/DAVDirectory.h b/xbmc/filesystem/DAVDirectory.h new file mode 100644 index 0000000..d442dc3 --- /dev/null +++ b/xbmc/filesystem/DAVDirectory.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" + +class CFileItem; +class CFileItemList; +class TiXmlElement; + +namespace XFILE +{ + class CDAVDirectory : public IDirectory + { + public: + CDAVDirectory(void); + ~CDAVDirectory(void) override; + bool GetDirectory(const CURL& url, CFileItemList &items) override; + bool Create(const CURL& url) override; + bool Exists(const CURL& url) override; + bool Remove(const CURL& url) override; + DIR_CACHE_TYPE GetCacheType(const CURL& url) const override { return DIR_CACHE_ONCE; } + + private: + void ParseResponse(const TiXmlElement *pElement, CFileItem &item); + }; +} diff --git a/xbmc/filesystem/DAVFile.cpp b/xbmc/filesystem/DAVFile.cpp new file mode 100644 index 0000000..a0ff2fd --- /dev/null +++ b/xbmc/filesystem/DAVFile.cpp @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DAVFile.h" + +#include "DAVCommon.h" +#include "DllLibCurl.h" +#include "URL.h" +#include "utils/RegExp.h" +#include "utils/XBMCTinyXML.h" +#include "utils/log.h" + +using namespace XFILE; +using namespace XCURL; + +CDAVFile::CDAVFile(void) + : CCurlFile() +{ +} + +CDAVFile::~CDAVFile(void) = default; + +bool CDAVFile::Execute(const CURL& url) +{ + CURL url2(url); + ParseAndCorrectUrl(url2); + + CLog::Log(LOGDEBUG, "CDAVFile::Execute({}) {}", fmt::ptr(this), m_url); + + assert(!(!m_state->m_easyHandle ^ !m_state->m_multiHandle)); + if( m_state->m_easyHandle == NULL ) + g_curlInterface.easy_acquire(url2.GetProtocol().c_str(), + url2.GetHostName().c_str(), + &m_state->m_easyHandle, + &m_state->m_multiHandle); + + // setup common curl options + SetCommonOptions(m_state); + SetRequestHeaders(m_state); + + m_lastResponseCode = m_state->Connect(m_bufferSize); + if (m_lastResponseCode < 0 || m_lastResponseCode >= 400) + return false; + + char* efurl; + if (CURLE_OK == g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_EFFECTIVE_URL,&efurl) && efurl) + m_url = efurl; + + if (m_lastResponseCode == 207) + { + std::string strResponse; + ReadData(strResponse); + + CXBMCTinyXML davResponse; + davResponse.Parse(strResponse); + + if (!davResponse.Parse(strResponse)) + { + CLog::Log(LOGERROR, "CDAVFile::Execute - Unable to process dav response ({})", + CURL(m_url).GetRedacted()); + Close(); + return false; + } + + TiXmlNode *pChild; + // Iterate over all responses + for (pChild = davResponse.RootElement()->FirstChild(); pChild != 0; pChild = pChild->NextSibling()) + { + if (CDAVCommon::ValueWithoutNamespace(pChild, "response")) + { + std::string sRetCode = CDAVCommon::GetStatusTag(pChild->ToElement()); + CRegExp rxCode; + rxCode.RegComp("HTTP/.+\\s(\\d+)\\s.*"); + if (rxCode.RegFind(sRetCode) >= 0) + { + if (rxCode.GetSubCount()) + { + m_lastResponseCode = atoi(rxCode.GetMatch(1).c_str()); + if (m_lastResponseCode < 0 || m_lastResponseCode >= 400) + return false; + } + } + + } + } + } + + return true; +} + +bool CDAVFile::Delete(const CURL& url) +{ + if (m_opened) + return false; + + CDAVFile dav; + std::string strRequest = "DELETE"; + + dav.SetCustomRequest(strRequest); + + CLog::Log(LOGDEBUG, "CDAVFile::Delete - Execute DELETE ({})", url.GetRedacted()); + if (!dav.Execute(url)) + { + CLog::Log(LOGERROR, "CDAVFile::Delete - Unable to delete dav resource ({})", url.GetRedacted()); + return false; + } + + dav.Close(); + + return true; +} + +bool CDAVFile::Rename(const CURL& url, const CURL& urlnew) +{ + if (m_opened) + return false; + + CDAVFile dav; + + CURL url2(urlnew); + std::string strProtocol = url2.GetTranslatedProtocol(); + url2.SetProtocol(strProtocol); + + std::string strRequest = "MOVE"; + dav.SetCustomRequest(strRequest); + dav.SetRequestHeader("Destination", url2.GetWithoutUserDetails()); + + CLog::Log(LOGDEBUG, "CDAVFile::Rename - Execute MOVE ({} -> {})", url.GetRedacted(), + url2.GetRedacted()); + if (!dav.Execute(url)) + { + CLog::Log(LOGERROR, "CDAVFile::Rename - Unable to rename dav resource ({} -> {})", + url.GetRedacted(), url2.GetRedacted()); + return false; + } + + dav.Close(); + + return true; +} diff --git a/xbmc/filesystem/DAVFile.h b/xbmc/filesystem/DAVFile.h new file mode 100644 index 0000000..2103a45 --- /dev/null +++ b/xbmc/filesystem/DAVFile.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "CurlFile.h" + +namespace XFILE +{ + class CDAVFile : public CCurlFile + { + public: + CDAVFile(void); + ~CDAVFile(void) override; + + virtual bool Execute(const CURL& url); + + bool Delete(const CURL& url) override; + bool Rename(const CURL& url, const CURL& urlnew) override; + + virtual int GetLastResponseCode() { return m_lastResponseCode; } + + private: + int m_lastResponseCode = 0; + }; +} diff --git a/xbmc/filesystem/Directorization.h b/xbmc/filesystem/Directorization.h new file mode 100644 index 0000000..f413635 --- /dev/null +++ b/xbmc/filesystem/Directorization.h @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "FileItem.h" +#include "URL.h" +#include "utils/CharsetConverter.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +#include <string> +#include <utility> +#include <vector> + +namespace XFILE +{ + /** + * \brief Method definition to convert an entry to a CFileItemPtr. + * + * \param entry The entry to convert to a CFileItemPtr + * \param label The label of the entry + * \param path The path of the entry + * \param isFolder Whether the entry is a folder or not + * \return The CFileItemPtr object created from the given entry and data. + */ + template<class TEntry> + using DirectorizeEntryToFileItemFunction = CFileItemPtr(*)(const TEntry& entry, const std::string& label, const std::string& path, bool isFolder); + + template<class TEntry> + using DirectorizeEntry = std::pair<std::string, TEntry>; + template<class TEntry> + using DirectorizeEntries = std::vector<DirectorizeEntry<TEntry>>; + + /** + * \brief Analyzes the given entry list from the given URL and turns them into files and directories on one directory hierarchy. + * + * \param url URL of the directory hierarchy to build + * \param entries Entries to analyze and turn into files and directories + * \param converter Converter function to convert an entry into a CFileItemPtr + * \param items Resulting item list + */ + template<class TEntry> + static void Directorize(const CURL& url, const DirectorizeEntries<TEntry>& entries, DirectorizeEntryToFileItemFunction<TEntry> converter, CFileItemList& items) + { + if (url.Get().empty() || entries.empty()) + return; + + const std::string& options = url.GetOptions(); + const std::string& filePath = url.GetFileName(); + + CURL baseUrl(url); + baseUrl.SetOptions(""); // delete options to have a clean path to add stuff too + baseUrl.SetFileName(""); // delete filename too as our names later will contain it + + std::string basePath = baseUrl.Get(); + URIUtils::AddSlashAtEnd(basePath); + + std::vector<std::string> filePathTokens; + if (!filePath.empty()) + StringUtils::Tokenize(filePath, filePathTokens, "/"); + + bool fastLookup = items.GetFastLookup(); + items.SetFastLookup(true); + for (const auto& entry : entries) + { + std::string entryPath = entry.first; + std::string entryFileName = entryPath; + StringUtils::Replace(entryFileName, '\\', '/'); + + // skip the requested entry + if (entryFileName == filePath) + continue; + + // Disregard Apple Resource Fork data + std::size_t found = entryPath.find("__MACOSX"); + if (found != std::string::npos) + continue; + + std::vector<std::string> pathTokens; + StringUtils::Tokenize(entryFileName, pathTokens, "/"); + + // ignore any entries in lower directory hierarchies + if (pathTokens.size() < filePathTokens.size() + 1) + continue; + + // ignore any entries in different directory hierarchies + bool ignoreItem = false; + entryFileName.clear(); + for (auto filePathToken = filePathTokens.begin(); filePathToken != filePathTokens.end(); ++filePathToken) + { + if (*filePathToken != pathTokens[std::distance(filePathTokens.begin(), filePathToken)]) + { + ignoreItem = true; + break; + } + entryFileName = URIUtils::AddFileToFolder(entryFileName, *filePathToken); + } + if (ignoreItem) + continue; + + entryFileName = URIUtils::AddFileToFolder(entryFileName, pathTokens[filePathTokens.size()]); + char c = entryPath[entryFileName.size()]; + if (c == '/' || c == '\\') + URIUtils::AddSlashAtEnd(entryFileName); + + std::string itemPath = URIUtils::AddFileToFolder(basePath, entryFileName) + options; + bool isFolder = false; + if (URIUtils::HasSlashAtEnd(entryFileName)) // this is a directory + { + // check if the directory has already been added + if (items.Contains(itemPath)) // already added + continue; + + isFolder = true; + URIUtils::AddSlashAtEnd(itemPath); + } + + // determine the entry's filename + std::string label = pathTokens[filePathTokens.size()]; + g_charsetConverter.unknownToUTF8(label); + + // convert the entry into a CFileItem + CFileItemPtr item = converter(entry.second, label, itemPath, isFolder); + item->SetPath(itemPath); + item->m_bIsFolder = isFolder; + if (isFolder) + item->m_dwSize = 0; + + items.Add(item); + } + items.SetFastLookup(fastLookup); + } +} diff --git a/xbmc/filesystem/Directory.cpp b/xbmc/filesystem/Directory.cpp new file mode 100644 index 0000000..5436fd9 --- /dev/null +++ b/xbmc/filesystem/Directory.cpp @@ -0,0 +1,446 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "Directory.h" + +#include "DirectoryCache.h" +#include "DirectoryFactory.h" +#include "FileDirectoryFactory.h" +#include "FileItem.h" +#include "PasswordManager.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "commons/Exception.h" +#include "dialogs/GUIDialogBusy.h" +#include "guilib/GUIWindowManager.h" +#include "messaging/ApplicationMessenger.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/Job.h" +#include "utils/JobManager.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +using namespace XFILE; +using namespace std::chrono_literals; + +#define TIME_TO_BUSY_DIALOG 500 + +class CGetDirectory +{ +private: + + struct CResult + { + CResult(const CURL& dir, const CURL& listDir) : m_event(true), m_dir(dir), m_listDir(listDir), m_result(false) {} + CEvent m_event; + CFileItemList m_list; + CURL m_dir; + CURL m_listDir; + bool m_result; + }; + + struct CGetJob + : CJob + { + CGetJob(std::shared_ptr<IDirectory>& imp + , std::shared_ptr<CResult>& result) + : m_result(result) + , m_imp(imp) + {} + public: + bool DoWork() override + { + m_result->m_list.SetURL(m_result->m_listDir); + m_result->m_result = m_imp->GetDirectory(m_result->m_dir, m_result->m_list); + m_result->m_event.Set(); + return m_result->m_result; + } + + std::shared_ptr<CResult> m_result; + std::shared_ptr<IDirectory> m_imp; + }; + +public: + + CGetDirectory(std::shared_ptr<IDirectory>& imp, const CURL& dir, const CURL& listDir) + : m_result(new CResult(dir, listDir)) + { + m_id = CServiceBroker::GetJobManager()->AddJob(new CGetJob(imp, m_result), nullptr, + CJob::PRIORITY_HIGH); + if (m_id == 0) + { + CGetJob job(imp, m_result); + job.DoWork(); + } + } + ~CGetDirectory() { CServiceBroker::GetJobManager()->CancelJob(m_id); } + + CEvent& GetEvent() + { + return m_result->m_event; + } + + bool Wait(unsigned int timeout) + { + return m_result->m_event.Wait(std::chrono::milliseconds(timeout)); + } + + bool GetDirectory(CFileItemList& list) + { + /* if it was not finished or failed, return failure */ + if (!m_result->m_event.Wait(0ms) || !m_result->m_result) + { + list.Clear(); + return false; + } + + list.Copy(m_result->m_list); + return true; + } + std::shared_ptr<CResult> m_result; + unsigned int m_id; +}; + + +CDirectory::CDirectory() = default; + +CDirectory::~CDirectory() = default; + +bool CDirectory::GetDirectory(const std::string& strPath, CFileItemList &items, const std::string &strMask, int flags) +{ + CHints hints; + hints.flags = flags; + hints.mask = strMask; + const CURL pathToUrl(strPath); + return GetDirectory(pathToUrl, items, hints); +} + +bool CDirectory::GetDirectory(const std::string& strPath, + const std::shared_ptr<IDirectory>& pDirectory, + CFileItemList& items, + const std::string& strMask, + int flags) +{ + CHints hints; + hints.flags = flags; + hints.mask = strMask; + const CURL pathToUrl(strPath); + return GetDirectory(pathToUrl, pDirectory, items, hints); +} + +bool CDirectory::GetDirectory(const std::string& strPath, CFileItemList &items, const CHints &hints) +{ + const CURL pathToUrl(strPath); + return GetDirectory(pathToUrl, items, hints); +} + +bool CDirectory::GetDirectory(const CURL& url, CFileItemList &items, const std::string &strMask, int flags) +{ + CHints hints; + hints.flags = flags; + hints.mask = strMask; + return GetDirectory(url, items, hints); +} + +bool CDirectory::GetDirectory(const CURL& url, CFileItemList &items, const CHints &hints) +{ + CURL realURL = URIUtils::SubstitutePath(url); + std::shared_ptr<IDirectory> pDirectory(CDirectoryFactory::Create(realURL)); + return CDirectory::GetDirectory(url, pDirectory, items, hints); +} + +bool CDirectory::GetDirectory(const CURL& url, + const std::shared_ptr<IDirectory>& pDirectory, + CFileItemList& items, + const CHints& hints) +{ + try + { + CURL realURL = URIUtils::SubstitutePath(url); + if (!pDirectory) + return false; + + // check our cache for this path + if (g_directoryCache.GetDirectory(realURL.Get(), items, (hints.flags & DIR_FLAG_READ_CACHE) == DIR_FLAG_READ_CACHE)) + items.SetURL(url); + else + { + // need to clear the cache (in case the directory fetch fails) + // and (re)fetch the folder + if (!(hints.flags & DIR_FLAG_BYPASS_CACHE)) + g_directoryCache.ClearDirectory(realURL.Get()); + + pDirectory->SetFlags(hints.flags); + + bool result = false; + CURL authUrl = realURL; + + while (!result) + { + const std::string pathToUrl(url.Get()); + + // don't change auth if it's set explicitly + if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty()) + CPasswordManager::GetInstance().AuthenticateURL(authUrl); + + items.SetURL(url); + result = pDirectory->GetDirectory(authUrl, items); + + if (!result) + { + // @TODO ProcessRequirements() can bring up the keyboard input dialog + // filesystem must not depend on GUI + if (CServiceBroker::GetAppMessenger()->IsProcessThread() && + pDirectory->ProcessRequirements()) + { + authUrl.SetDomain(""); + authUrl.SetUserName(""); + authUrl.SetPassword(""); + continue; + } + + CLog::Log(LOGERROR, "{} - Error getting {}", __FUNCTION__, url.GetRedacted()); + return false; + } + } + + // hide credentials if necessary + if (CPasswordManager::GetInstance().IsURLSupported(realURL)) + { + bool hide = false; + // for explicitly credentials + if (!realURL.GetUserName().empty()) + { + // credentials was changed i.e. were stored in the password + // manager, in this case we can hide them from an item URL, + // otherwise we have to keep credentials in an item URL + if ( realURL.GetUserName() != authUrl.GetUserName() + || realURL.GetPassWord() != authUrl.GetPassWord() + || realURL.GetDomain() != authUrl.GetDomain()) + { + hide = true; + } + } + else + { + // hide credentials in any other cases + hide = true; + } + + if (hide) + { + for (int i = 0; i < items.Size(); ++i) + { + CFileItemPtr item = items[i]; + CURL itemUrl = item->GetURL(); + itemUrl.SetDomain(""); + itemUrl.SetUserName(""); + itemUrl.SetPassword(""); + item->SetPath(itemUrl.Get()); + } + } + } + + // cache the directory, if necessary + if (!(hints.flags & DIR_FLAG_BYPASS_CACHE)) + g_directoryCache.SetDirectory(realURL.Get(), items, pDirectory->GetCacheType(url)); + } + + // now filter for allowed files + if (!pDirectory->AllowAll()) + { + pDirectory->SetMask(hints.mask); + for (int i = 0; i < items.Size(); ++i) + { + CFileItemPtr item = items[i]; + if (!item->m_bIsFolder && !pDirectory->IsAllowed(item->GetURL())) + { + items.Remove(i); + i--; // don't confuse loop + } + } + } + // filter hidden files + //! @todo we shouldn't be checking the gui setting here, callers should use getHidden instead + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_SHOWHIDDEN) && !(hints.flags & DIR_FLAG_GET_HIDDEN)) + { + for (int i = 0; i < items.Size(); ++i) + { + if (items[i]->GetProperty("file:hidden").asBoolean()) + { + items.Remove(i); + i--; // don't confuse loop + } + } + } + + // Should any of the files we read be treated as a directory? + // Disable for database folders, as they already contain the extracted items + if (!(hints.flags & DIR_FLAG_NO_FILE_DIRS) && !items.IsMusicDb() && !items.IsVideoDb() && !items.IsSmartPlayList()) + FilterFileDirectories(items, hints.mask); + + // Correct items for path substitution + const std::string pathToUrl(url.Get()); + const std::string pathToUrl2(realURL.Get()); + if (pathToUrl != pathToUrl2) + { + for (int i = 0; i < items.Size(); ++i) + { + CFileItemPtr item = items[i]; + item->SetPath(URIUtils::SubstitutePath(item->GetPath(), true)); + } + } + + return true; + } + XBMCCOMMONS_HANDLE_UNCHECKED + catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); } + CLog::Log(LOGERROR, "{} - Error getting {}", __FUNCTION__, url.GetRedacted()); + return false; +} + +bool CDirectory::Create(const std::string& strPath) +{ + const CURL pathToUrl(strPath); + return Create(pathToUrl); +} + +bool CDirectory::Create(const CURL& url) +{ + try + { + CURL realURL = URIUtils::SubstitutePath(url); + + if (CPasswordManager::GetInstance().IsURLSupported(realURL) && realURL.GetUserName().empty()) + CPasswordManager::GetInstance().AuthenticateURL(realURL); + + std::unique_ptr<IDirectory> pDirectory(CDirectoryFactory::Create(realURL)); + if (pDirectory) + if(pDirectory->Create(realURL)) + return true; + } + XBMCCOMMONS_HANDLE_UNCHECKED + catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); } + CLog::Log(LOGERROR, "{} - Error creating {}", __FUNCTION__, url.GetRedacted()); + return false; +} + +bool CDirectory::Exists(const std::string& strPath, bool bUseCache /* = true */) +{ + const CURL pathToUrl(strPath); + return Exists(pathToUrl, bUseCache); +} + +bool CDirectory::Exists(const CURL& url, bool bUseCache /* = true */) +{ + try + { + CURL realURL = URIUtils::SubstitutePath(url); + if (bUseCache) + { + bool bPathInCache; + std::string realPath(realURL.Get()); + URIUtils::AddSlashAtEnd(realPath); + if (g_directoryCache.FileExists(realPath, bPathInCache)) + return true; + if (bPathInCache) + return false; + } + + if (CPasswordManager::GetInstance().IsURLSupported(realURL) && realURL.GetUserName().empty()) + CPasswordManager::GetInstance().AuthenticateURL(realURL); + + std::unique_ptr<IDirectory> pDirectory(CDirectoryFactory::Create(realURL)); + if (pDirectory) + return pDirectory->Exists(realURL); + } + XBMCCOMMONS_HANDLE_UNCHECKED + catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); } + CLog::Log(LOGERROR, "{} - Error checking for {}", __FUNCTION__, url.GetRedacted()); + return false; +} + +bool CDirectory::Remove(const std::string& strPath) +{ + const CURL pathToUrl(strPath); + return Remove(pathToUrl); +} + +bool CDirectory::RemoveRecursive(const std::string& strPath) +{ + return RemoveRecursive(CURL{ strPath }); +} + +bool CDirectory::Remove(const CURL& url) +{ + try + { + CURL realURL = URIUtils::SubstitutePath(url); + CURL authUrl = realURL; + if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty()) + CPasswordManager::GetInstance().AuthenticateURL(authUrl); + + std::unique_ptr<IDirectory> pDirectory(CDirectoryFactory::Create(realURL)); + if (pDirectory) + if(pDirectory->Remove(authUrl)) + { + g_directoryCache.ClearFile(realURL.Get()); + return true; + } + } + XBMCCOMMONS_HANDLE_UNCHECKED + catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); } + CLog::Log(LOGERROR, "{} - Error removing {}", __FUNCTION__, url.GetRedacted()); + return false; +} + +bool CDirectory::RemoveRecursive(const CURL& url) +{ + try + { + CURL realURL = URIUtils::SubstitutePath(url); + CURL authUrl = realURL; + if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty()) + CPasswordManager::GetInstance().AuthenticateURL(authUrl); + + std::unique_ptr<IDirectory> pDirectory(CDirectoryFactory::Create(realURL)); + if (pDirectory) + if(pDirectory->RemoveRecursive(authUrl)) + { + g_directoryCache.ClearFile(realURL.Get()); + return true; + } + } + XBMCCOMMONS_HANDLE_UNCHECKED + catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); } + CLog::Log(LOGERROR, "{} - Error removing {}", __FUNCTION__, url.GetRedacted()); + return false; +} + +void CDirectory::FilterFileDirectories(CFileItemList &items, const std::string &mask, + bool expandImages) +{ + for (int i=0; i< items.Size(); ++i) + { + CFileItemPtr pItem=items[i]; + auto mode = expandImages && pItem->IsDiscImage() ? EFILEFOLDER_TYPE_ONBROWSE : EFILEFOLDER_TYPE_ALWAYS; + if (!pItem->m_bIsFolder && pItem->IsFileFolder(mode)) + { + std::unique_ptr<IFileDirectory> pDirectory(CFileDirectoryFactory::Create(pItem->GetURL(),pItem.get(),mask)); + if (pDirectory) + pItem->m_bIsFolder = true; + else + if (pItem->m_bIsFolder) + { + items.Remove(i); + i--; // don't confuse loop + } + } + } +} diff --git a/xbmc/filesystem/Directory.h b/xbmc/filesystem/Directory.h new file mode 100644 index 0000000..0af92f3 --- /dev/null +++ b/xbmc/filesystem/Directory.h @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" + +#include <memory> +#include <string> + +namespace XFILE +{ +/*! + \ingroup filesystem + \brief Wrappers for \e IDirectory + */ +class CDirectory +{ +public: + CDirectory(void); + virtual ~CDirectory(void); + + class CHints + { + public: + std::string mask; + int flags = DIR_FLAG_DEFAULTS; + }; + + static bool GetDirectory(const CURL& url + , CFileItemList &items + , const std::string &strMask + , int flags); + + static bool GetDirectory(const CURL& url, + const std::shared_ptr<IDirectory>& pDirectory, + CFileItemList& items, + const CHints& hints); + + static bool GetDirectory(const CURL& url + , CFileItemList &items + , const CHints &hints); + + static bool Create(const CURL& url); + static bool Exists(const CURL& url, bool bUseCache = true); + static bool Remove(const CURL& url); + static bool RemoveRecursive(const CURL& url); + + static bool GetDirectory(const std::string& strPath + , CFileItemList &items + , const std::string &strMask + , int flags); + + static bool GetDirectory(const std::string& strPath, + const std::shared_ptr<IDirectory>& pDirectory, + CFileItemList& items, + const std::string& strMask, + int flags); + + static bool GetDirectory(const std::string& strPath + , CFileItemList &items + , const CHints &hints); + + static bool Create(const std::string& strPath); + static bool Exists(const std::string& strPath, bool bUseCache = true); + static bool Remove(const std::string& strPath); + static bool RemoveRecursive(const std::string& strPath); + + /*! \brief Filter files that act like directories from the list, replacing them with their directory counterparts + \param items The item list to filter + \param mask The mask to apply when filtering files + \param expandImages True to include disc images in file directory expansion + */ + static void FilterFileDirectories(CFileItemList &items, const std::string &mask, + bool expandImages=false); +}; +} diff --git a/xbmc/filesystem/DirectoryCache.cpp b/xbmc/filesystem/DirectoryCache.cpp new file mode 100644 index 0000000..d46f11b --- /dev/null +++ b/xbmc/filesystem/DirectoryCache.cpp @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryCache.h" + +#include "Directory.h" +#include "FileItem.h" +#include "URL.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +#include <algorithm> +#include <climits> +#include <mutex> + +// Maximum number of directories to keep in our cache +#define MAX_CACHED_DIRS 50 + +using namespace XFILE; + +CDirectoryCache::CDir::CDir(DIR_CACHE_TYPE cacheType) +{ + m_cacheType = cacheType; + m_lastAccess = 0; + m_Items = std::make_unique<CFileItemList>(); + m_Items->SetIgnoreURLOptions(true); + m_Items->SetFastLookup(true); +} + +CDirectoryCache::CDir::~CDir() = default; + +void CDirectoryCache::CDir::SetLastAccess(unsigned int &accessCounter) +{ + m_lastAccess = accessCounter++; +} + +CDirectoryCache::CDirectoryCache(void) +{ + m_accessCounter = 0; +#ifdef _DEBUG + m_cacheHits = 0; + m_cacheMisses = 0; +#endif +} + +CDirectoryCache::~CDirectoryCache(void) = default; + +bool CDirectoryCache::GetDirectory(const std::string& strPath, CFileItemList &items, bool retrieveAll) +{ + std::unique_lock<CCriticalSection> lock(m_cs); + + // Get rid of any URL options, else the compare may be wrong + std::string storedPath = CURL(strPath).GetWithoutOptions(); + URIUtils::RemoveSlashAtEnd(storedPath); + + auto i = m_cache.find(storedPath); + if (i != m_cache.end()) + { + CDir& dir = i->second; + if (dir.m_cacheType == XFILE::DIR_CACHE_ALWAYS || + (dir.m_cacheType == XFILE::DIR_CACHE_ONCE && retrieveAll)) + { + items.Copy(*dir.m_Items); + dir.SetLastAccess(m_accessCounter); +#ifdef _DEBUG + m_cacheHits+=items.Size(); +#endif + return true; + } + } + return false; +} + +void CDirectoryCache::SetDirectory(const std::string& strPath, const CFileItemList &items, DIR_CACHE_TYPE cacheType) +{ + if (cacheType == DIR_CACHE_NEVER) + return; // nothing to do + + // caches the given directory using a copy of the items, rather than the items + // themselves. The reason we do this is because there is often some further + // processing on the items (stacking, transparent rars/zips for instance) that + // alters the URL of the items. If we shared the pointers, we'd have problems + // as the URLs in the cache would have changed, so things such as + // CDirectoryCache::FileExists() would fail for files that really do exist (just their + // URL's have been altered). This is called from CFile::Exists() which causes + // all sorts of hassles. + // IDEALLY, any further processing on the item would actually create a new item + // instead of altering it, but we can't really enforce that in an easy way, so + // this is the best solution for now. + std::unique_lock<CCriticalSection> lock(m_cs); + + // Get rid of any URL options, else the compare may be wrong + std::string storedPath = CURL(strPath).GetWithoutOptions(); + URIUtils::RemoveSlashAtEnd(storedPath); + + ClearDirectory(storedPath); + + CheckIfFull(); + + CDir dir(cacheType); + dir.m_Items->Copy(items); + dir.SetLastAccess(m_accessCounter); + m_cache.emplace(std::make_pair(storedPath, std::move(dir))); +} + +void CDirectoryCache::ClearFile(const std::string& strFile) +{ + // Get rid of any URL options, else the compare may be wrong + std::string strFile2 = CURL(strFile).GetWithoutOptions(); + URIUtils::RemoveSlashAtEnd(strFile2); + + ClearDirectory(URIUtils::GetDirectory(strFile2)); +} + +void CDirectoryCache::ClearDirectory(const std::string& strPath) +{ + std::unique_lock<CCriticalSection> lock(m_cs); + + // Get rid of any URL options, else the compare may be wrong + std::string storedPath = CURL(strPath).GetWithoutOptions(); + URIUtils::RemoveSlashAtEnd(storedPath); + + m_cache.erase(storedPath); +} + +void CDirectoryCache::ClearSubPaths(const std::string& strPath) +{ + std::unique_lock<CCriticalSection> lock(m_cs); + + // Get rid of any URL options, else the compare may be wrong + std::string storedPath = CURL(strPath).GetWithoutOptions(); + + auto i = m_cache.begin(); + while (i != m_cache.end()) + { + if (URIUtils::PathHasParent(i->first, storedPath)) + m_cache.erase(i++); + else + i++; + } +} + +void CDirectoryCache::AddFile(const std::string& strFile) +{ + std::unique_lock<CCriticalSection> lock(m_cs); + + // Get rid of any URL options, else the compare may be wrong + std::string strPath = URIUtils::GetDirectory(CURL(strFile).GetWithoutOptions()); + URIUtils::RemoveSlashAtEnd(strPath); + + auto i = m_cache.find(strPath); + if (i != m_cache.end()) + { + CDir& dir = i->second; + CFileItemPtr item(new CFileItem(strFile, false)); + dir.m_Items->Add(item); + dir.SetLastAccess(m_accessCounter); + } +} + +bool CDirectoryCache::FileExists(const std::string& strFile, bool& bInCache) +{ + std::unique_lock<CCriticalSection> lock(m_cs); + bInCache = false; + + // Get rid of any URL options, else the compare may be wrong + std::string strPath = CURL(strFile).GetWithoutOptions(); + URIUtils::RemoveSlashAtEnd(strPath); + std::string storedPath = URIUtils::GetDirectory(strPath); + URIUtils::RemoveSlashAtEnd(storedPath); + + auto i = m_cache.find(storedPath); + if (i != m_cache.end()) + { + bInCache = true; + CDir& dir = i->second; + dir.SetLastAccess(m_accessCounter); +#ifdef _DEBUG + m_cacheHits++; +#endif + return (URIUtils::PathEquals(strPath, storedPath) || dir.m_Items->Contains(strFile)); + } +#ifdef _DEBUG + m_cacheMisses++; +#endif + return false; +} + +void CDirectoryCache::Clear() +{ + // this routine clears everything + std::unique_lock<CCriticalSection> lock(m_cs); + m_cache.clear(); +} + +void CDirectoryCache::InitCache(const std::set<std::string>& dirs) +{ + for (const std::string& strDir : dirs) + { + CFileItemList items; + CDirectory::GetDirectory(strDir, items, "", DIR_FLAG_NO_FILE_DIRS); + items.Clear(); + } +} + +void CDirectoryCache::ClearCache(std::set<std::string>& dirs) +{ + auto i = m_cache.begin(); + while (i != m_cache.end()) + { + if (dirs.find(i->first) != dirs.end()) + m_cache.erase(i++); + else + i++; + } +} + +void CDirectoryCache::CheckIfFull() +{ + std::unique_lock<CCriticalSection> lock(m_cs); + + // find the last accessed folder, and remove if the number of cached folders is too many + auto lastAccessed = m_cache.end(); + unsigned int numCached = 0; + for (auto i = m_cache.begin(); i != m_cache.end(); i++) + { + // ensure dirs that are always cached aren't cleared + if (i->second.m_cacheType != DIR_CACHE_ALWAYS) + { + if (lastAccessed == m_cache.end() || + i->second.GetLastAccess() < lastAccessed->second.GetLastAccess()) + lastAccessed = i; + numCached++; + } + } + if (lastAccessed != m_cache.end() && numCached >= MAX_CACHED_DIRS) + m_cache.erase(lastAccessed); +} + +#ifdef _DEBUG +void CDirectoryCache::PrintStats() const +{ + std::unique_lock<CCriticalSection> lock(m_cs); + CLog::Log(LOGDEBUG, "{} - total of {} cache hits, and {} cache misses", __FUNCTION__, m_cacheHits, + m_cacheMisses); + // run through and find the oldest and the number of items cached + unsigned int oldest = UINT_MAX; + unsigned int numItems = 0; + unsigned int numDirs = 0; + for (auto i = m_cache.begin(); i != m_cache.end(); i++) + { + const CDir& dir = i->second; + oldest = std::min(oldest, dir.GetLastAccess()); + numItems += dir.m_Items->Size(); + numDirs++; + } + CLog::Log(LOGDEBUG, "{} - {} folders cached, with {} items total. Oldest is {}, current is {}", + __FUNCTION__, numDirs, numItems, oldest, m_accessCounter); +} +#endif diff --git a/xbmc/filesystem/DirectoryCache.h b/xbmc/filesystem/DirectoryCache.h new file mode 100644 index 0000000..2058c78 --- /dev/null +++ b/xbmc/filesystem/DirectoryCache.h @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" +#include "threads/CriticalSection.h" + +#include <map> +#include <memory> +#include <set> + +class CFileItem; + +namespace XFILE +{ + class CDirectoryCache + { + class CDir + { + public: + explicit CDir(DIR_CACHE_TYPE cacheType); + CDir(CDir&& dir) = default; + CDir& operator=(CDir&& dir) = default; + virtual ~CDir(); + + void SetLastAccess(unsigned int &accessCounter); + unsigned int GetLastAccess() const { return m_lastAccess; } + + std::unique_ptr<CFileItemList> m_Items; + DIR_CACHE_TYPE m_cacheType; + private: + CDir(const CDir&) = delete; + CDir& operator=(const CDir&) = delete; + unsigned int m_lastAccess; + }; + public: + CDirectoryCache(void); + virtual ~CDirectoryCache(void); + bool GetDirectory(const std::string& strPath, CFileItemList &items, bool retrieveAll = false); + void SetDirectory(const std::string& strPath, const CFileItemList &items, DIR_CACHE_TYPE cacheType); + void ClearDirectory(const std::string& strPath); + void ClearFile(const std::string& strFile); + void ClearSubPaths(const std::string& strPath); + void Clear(); + void AddFile(const std::string& strFile); + bool FileExists(const std::string& strPath, bool& bInCache); +#ifdef _DEBUG + void PrintStats() const; +#endif + protected: + void InitCache(const std::set<std::string>& dirs); + void ClearCache(std::set<std::string>& dirs); + void CheckIfFull(); + + std::map<std::string, CDir> m_cache; + + mutable CCriticalSection m_cs; + + unsigned int m_accessCounter; + +#ifdef _DEBUG + unsigned int m_cacheHits; + unsigned int m_cacheMisses; +#endif + }; +} +extern XFILE::CDirectoryCache g_directoryCache; diff --git a/xbmc/filesystem/DirectoryFactory.cpp b/xbmc/filesystem/DirectoryFactory.cpp new file mode 100644 index 0000000..a3016f6 --- /dev/null +++ b/xbmc/filesystem/DirectoryFactory.cpp @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include <stdlib.h> +#include "network/Network.h" +#include "DirectoryFactory.h" +#include "SpecialProtocolDirectory.h" +#include "MultiPathDirectory.h" +#include "StackDirectory.h" +#include "FileDirectoryFactory.h" +#include "PlaylistDirectory.h" +#include "MusicDatabaseDirectory.h" +#include "MusicSearchDirectory.h" +#include "VideoDatabaseDirectory.h" +#include "FavouritesDirectory.h" +#include "LibraryDirectory.h" +#include "EventsDirectory.h" +#include "AddonsDirectory.h" +#include "SourcesDirectory.h" +#include "FTPDirectory.h" +#include "HTTPDirectory.h" +#include "DAVDirectory.h" +#if defined(HAS_UDFREAD) +#include "UDFDirectory.h" +#endif +#include "utils/log.h" +#include "network/WakeOnAccess.h" + +#ifdef TARGET_POSIX +#include "platform/posix/filesystem/PosixDirectory.h" +#elif defined(TARGET_WINDOWS) +#include "platform/win32/filesystem/Win32Directory.h" +#ifdef TARGET_WINDOWS_STORE +#include "platform/win10/filesystem/WinLibraryDirectory.h" +#endif +#endif +#ifdef HAS_FILESYSTEM_SMB +#ifdef TARGET_WINDOWS +#include "platform/win32/filesystem/Win32SMBDirectory.h" +#else +#include "platform/posix/filesystem/SMBDirectory.h" +#endif +#endif +#include "CDDADirectory.h" +#include "PluginDirectory.h" +#if defined(HAS_ISO9660PP) +#include "ISO9660Directory.h" +#endif +#ifdef HAS_UPNP +#include "UPnPDirectory.h" +#endif +#include "PVRDirectory.h" +#if defined(TARGET_ANDROID) +#include "platform/android/filesystem/APKDirectory.h" +#elif defined(TARGET_DARWIN_TVOS) +#include "platform/darwin/tvos/filesystem/TVOSDirectory.h" +#endif +#include "XbtDirectory.h" +#include "ZipDirectory.h" +#include "FileItem.h" +#include "URL.h" +#include "RSSDirectory.h" +#ifdef HAS_ZEROCONF +#include "ZeroconfDirectory.h" +#endif +#ifdef HAS_FILESYSTEM_NFS +#include "NFSDirectory.h" +#endif +#ifdef HAVE_LIBBLURAY +#include "BlurayDirectory.h" +#endif +#if defined(TARGET_ANDROID) +#include "platform/android/filesystem/AndroidAppDirectory.h" +#endif +#include "ResourceDirectory.h" +#include "ServiceBroker.h" +#include "addons/VFSEntry.h" +#include "utils/StringUtils.h" + +using namespace ADDON; + +using namespace XFILE; + +/*! + \brief Create a IDirectory object of the share type specified in \e strPath . + \param strPath Specifies the share type to access, can be a share or share with path. + \return IDirectory object to access the directories on the share. + \sa IDirectory + */ +IDirectory* CDirectoryFactory::Create(const CURL& url) +{ + if (!CWakeOnAccess::GetInstance().WakeUpHost(url)) + return NULL; + + CFileItem item(url.Get(), true); + IFileDirectory* pDir = CFileDirectoryFactory::Create(url, &item); + if (pDir) + return pDir; + + if (!url.GetProtocol().empty() && CServiceBroker::IsAddonInterfaceUp()) + { + for (const auto& vfsAddon : CServiceBroker::GetVFSAddonCache().GetAddonInstances()) + { + auto prots = StringUtils::Split(vfsAddon->GetProtocols(), "|"); + + if (vfsAddon->HasDirectories() && std::find(prots.begin(), prots.end(), url.GetProtocol()) != prots.end()) + return new CVFSEntryIDirectoryWrapper(vfsAddon); + } + } + +#ifdef TARGET_POSIX + if (url.GetProtocol().empty() || url.IsProtocol("file")) + { +#if defined(TARGET_DARWIN_TVOS) + if (CTVOSDirectory::WantsDirectory(url)) + return new CTVOSDirectory(); +#endif + return new CPosixDirectory(); + } +#elif defined(TARGET_WINDOWS) + if (url.GetProtocol().empty() || url.IsProtocol("file")) return new CWin32Directory(); +#else +#error Local directory access is not implemented for this platform +#endif + if (url.IsProtocol("special")) return new CSpecialProtocolDirectory(); + if (url.IsProtocol("sources")) return new CSourcesDirectory(); + if (url.IsProtocol("addons")) return new CAddonsDirectory(); +#if defined(HAS_DVD_DRIVE) + if (url.IsProtocol("cdda")) return new CCDDADirectory(); +#endif +#if defined(HAS_ISO9660PP) + if (url.IsProtocol("iso9660")) return new CISO9660Directory(); +#endif +#if defined(HAS_UDFREAD) + if (url.IsProtocol("udf")) return new CUDFDirectory(); +#endif + if (url.IsProtocol("plugin")) return new CPluginDirectory(); +#if defined(TARGET_ANDROID) + if (url.IsProtocol("apk")) return new CAPKDirectory(); +#endif + if (url.IsProtocol("zip")) return new CZipDirectory(); + if (url.IsProtocol("xbt")) return new CXbtDirectory(); + if (url.IsProtocol("multipath")) return new CMultiPathDirectory(); + if (url.IsProtocol("stack")) return new CStackDirectory(); + if (url.IsProtocol("playlistmusic")) return new CPlaylistDirectory(); + if (url.IsProtocol("playlistvideo")) return new CPlaylistDirectory(); + if (url.IsProtocol("musicdb")) return new CMusicDatabaseDirectory(); + if (url.IsProtocol("musicsearch")) return new CMusicSearchDirectory(); + if (url.IsProtocol("videodb")) return new CVideoDatabaseDirectory(); + if (url.IsProtocol("library")) return new CLibraryDirectory(); + if (url.IsProtocol("favourites")) return new CFavouritesDirectory(); +#if defined(TARGET_ANDROID) + if (url.IsProtocol("androidapp")) return new CAndroidAppDirectory(); +#endif +#ifdef HAVE_LIBBLURAY + if (url.IsProtocol("bluray")) return new CBlurayDirectory(); +#endif + if (url.IsProtocol("resource")) return new CResourceDirectory(); + if (url.IsProtocol("events")) return new CEventsDirectory(); +#ifdef TARGET_WINDOWS_STORE + if (CWinLibraryDirectory::IsValid(url)) return new CWinLibraryDirectory(); +#endif + + if (url.IsProtocol("ftp") || url.IsProtocol("ftps")) return new CFTPDirectory(); + if (url.IsProtocol("http") || url.IsProtocol("https")) return new CHTTPDirectory(); + if (url.IsProtocol("dav") || url.IsProtocol("davs")) return new CDAVDirectory(); +#ifdef HAS_FILESYSTEM_SMB +#ifdef TARGET_WINDOWS + if (url.IsProtocol("smb")) return new CWin32SMBDirectory(); +#else + if (url.IsProtocol("smb")) return new CSMBDirectory(); +#endif +#endif +#ifdef HAS_UPNP + if (url.IsProtocol("upnp")) return new CUPnPDirectory(); +#endif + if (url.IsProtocol("rss") || url.IsProtocol("rsss")) return new CRSSDirectory(); +#ifdef HAS_ZEROCONF + if (url.IsProtocol("zeroconf")) return new CZeroconfDirectory(); +#endif +#ifdef HAS_FILESYSTEM_NFS + if (url.IsProtocol("nfs")) return new CNFSDirectory(); +#endif + + if (url.IsProtocol("pvr")) + return new CPVRDirectory(); + + CLog::Log(LOGWARNING, "{} - unsupported protocol({}) in {}", __FUNCTION__, url.GetProtocol(), + url.GetRedacted()); + return NULL; +} + diff --git a/xbmc/filesystem/DirectoryFactory.h b/xbmc/filesystem/DirectoryFactory.h new file mode 100644 index 0000000..71d5f6f --- /dev/null +++ b/xbmc/filesystem/DirectoryFactory.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" + +namespace XFILE +{ +/*! + \ingroup filesystem + \brief Get access to a directory of a file system. + + The Factory can be used to create a directory object + for every file system accessable. \n + \n + Example: + + \verbatim + std::string strShare="iso9660://"; + + IDirectory* pDir=CDirectoryFactory::Create(strShare); + \endverbatim + The \e pDir pointer can be used to access a directory and retrieve it's content. + + When different types of shares have to be accessed use CVirtualDirectory. + \sa IDirectory + */ +class CDirectoryFactory +{ +public: + static IDirectory* Create(const CURL& url); +}; +} diff --git a/xbmc/filesystem/DirectoryHistory.cpp b/xbmc/filesystem/DirectoryHistory.cpp new file mode 100644 index 0000000..aecf285 --- /dev/null +++ b/xbmc/filesystem/DirectoryHistory.cpp @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryHistory.h" + +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +#include <algorithm> + +const std::string& CDirectoryHistory::CPathHistoryItem::GetPath(bool filter /* = false */) const +{ + if (filter && !m_strFilterPath.empty()) + return m_strFilterPath; + + return m_strPath; +} + +CDirectoryHistory::~CDirectoryHistory() +{ + m_vecHistory.clear(); + m_vecPathHistory.clear(); +} + +void CDirectoryHistory::RemoveSelectedItem(const std::string& strDirectory) +{ + HistoryMap::iterator iter = m_vecHistory.find(preparePath(strDirectory)); + if (iter != m_vecHistory.end()) + m_vecHistory.erase(iter); +} + +void CDirectoryHistory::SetSelectedItem(const std::string& strSelectedItem, const std::string& strDirectory) +{ + if (strSelectedItem.empty()) + return; + + std::string strDir = preparePath(strDirectory); + std::string strItem = preparePath(strSelectedItem, false); + + HistoryMap::iterator iter = m_vecHistory.find(strDir); + if (iter != m_vecHistory.end()) + { + iter->second.m_strItem = strItem; + return; + } + + CHistoryItem item; + item.m_strItem = strItem; + item.m_strDirectory = strDir; + m_vecHistory[strDir] = item; +} + +const std::string& CDirectoryHistory::GetSelectedItem(const std::string& strDirectory) const +{ + HistoryMap::const_iterator iter = m_vecHistory.find(preparePath(strDirectory)); + if (iter != m_vecHistory.end()) + return iter->second.m_strItem; + + return StringUtils::Empty; +} + +void CDirectoryHistory::AddPath(const std::string& strPath, const std::string &strFilterPath /* = "" */) +{ + if (!m_vecPathHistory.empty() && m_vecPathHistory.back().m_strPath == strPath) + { + if (!strFilterPath.empty()) + m_vecPathHistory.back().m_strFilterPath = strFilterPath; + return; + } + + CPathHistoryItem item; + item.m_strPath = strPath; + item.m_strFilterPath = strFilterPath; + m_vecPathHistory.push_back(item); +} + +void CDirectoryHistory::AddPathFront(const std::string& strPath, const std::string &strFilterPath /* = "" */) +{ + CPathHistoryItem item; + item.m_strPath = strPath; + item.m_strFilterPath = strFilterPath; + m_vecPathHistory.insert(m_vecPathHistory.begin(), item); +} + +std::string CDirectoryHistory::GetParentPath(bool filter /* = false */) +{ + if (m_vecPathHistory.empty()) + return ""; + + return m_vecPathHistory.back().GetPath(filter); +} + +bool CDirectoryHistory::IsInHistory(const std::string &path) const +{ + std::string slashEnded(path); + URIUtils::AddSlashAtEnd(slashEnded); + for (std::vector<CPathHistoryItem>::const_iterator i = m_vecPathHistory.begin(); i != m_vecPathHistory.end(); ++i) + { + std::string testPath(i->GetPath()); + URIUtils::AddSlashAtEnd(testPath); + if (slashEnded == testPath) + return true; + } + return false; +} + +std::string CDirectoryHistory::RemoveParentPath(bool filter /* = false */) +{ + if (m_vecPathHistory.empty()) + return ""; + + std::string strParent = GetParentPath(filter); + m_vecPathHistory.pop_back(); + return strParent; +} + +void CDirectoryHistory::ClearPathHistory() +{ + m_vecPathHistory.clear(); +} + +bool CDirectoryHistory::IsMusicSearchUrl(CPathHistoryItem &i) +{ + return StringUtils::StartsWith(i.GetPath(), "musicsearch://"); +} + +void CDirectoryHistory::ClearSearchHistory() +{ + m_vecPathHistory.erase(remove_if(m_vecPathHistory.begin(), m_vecPathHistory.end(), IsMusicSearchUrl), m_vecPathHistory.end()); +} + +void CDirectoryHistory::DumpPathHistory() +{ + // debug log + CLog::Log(LOGDEBUG,"Current m_vecPathHistory:"); + for (int i = 0; i < (int)m_vecPathHistory.size(); ++i) + CLog::Log(LOGDEBUG, " {:02}.[{}; {}]", i, m_vecPathHistory[i].m_strPath, + m_vecPathHistory[i].m_strFilterPath); +} + +std::string CDirectoryHistory::preparePath(const std::string &strDirectory, bool tolower /* = true */) +{ + std::string strDir = strDirectory; + if (tolower) + StringUtils::ToLower(strDir); + + URIUtils::RemoveSlashAtEnd(strDir); + + return strDir; +} diff --git a/xbmc/filesystem/DirectoryHistory.h b/xbmc/filesystem/DirectoryHistory.h new file mode 100644 index 0000000..49f1873 --- /dev/null +++ b/xbmc/filesystem/DirectoryHistory.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include <map> +#include <string> +#include <vector> + +class CDirectoryHistory +{ +public: + class CHistoryItem + { + public: + CHistoryItem() = default; + virtual ~CHistoryItem() = default; + std::string m_strItem; + std::string m_strDirectory; + }; + + class CPathHistoryItem + { + public: + CPathHistoryItem() = default; + virtual ~CPathHistoryItem() = default; + + const std::string& GetPath(bool filter = false) const; + + std::string m_strPath; + std::string m_strFilterPath; + }; + + CDirectoryHistory() = default; + virtual ~CDirectoryHistory(); + + void SetSelectedItem(const std::string& strSelectedItem, const std::string& strDirectory); + const std::string& GetSelectedItem(const std::string& strDirectory) const; + void RemoveSelectedItem(const std::string& strDirectory); + + void AddPath(const std::string& strPath, const std::string &m_strFilterPath = ""); + void AddPathFront(const std::string& strPath, const std::string &m_strFilterPath = ""); + std::string GetParentPath(bool filter = false); + std::string RemoveParentPath(bool filter = false); + void ClearPathHistory(); + void ClearSearchHistory(); + void DumpPathHistory(); + + /*! \brief Returns whether a path is in the history. + \param path to test + \return true if the path is in the history, false otherwise. + */ + bool IsInHistory(const std::string &path) const; + +private: + static std::string preparePath(const std::string &strDirectory, bool tolower = true); + + typedef std::map<std::string, CHistoryItem> HistoryMap; + HistoryMap m_vecHistory; + std::vector<CPathHistoryItem> m_vecPathHistory; ///< History of traversed directories + static bool IsMusicSearchUrl(CPathHistoryItem &i); +}; diff --git a/xbmc/filesystem/DllLibCurl.cpp b/xbmc/filesystem/DllLibCurl.cpp new file mode 100644 index 0000000..8367e9d --- /dev/null +++ b/xbmc/filesystem/DllLibCurl.cpp @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DllLibCurl.h" + +#include "threads/SystemClock.h" +#include "utils/log.h" + +#include <assert.h> +#include <mutex> + +namespace XCURL +{ +CURLcode DllLibCurl::global_init(long flags) +{ + return curl_global_init(flags); +} + +void DllLibCurl::global_cleanup() +{ + curl_global_cleanup(); +} + +CURL_HANDLE* DllLibCurl::easy_init() +{ + return curl_easy_init(); +} + +CURLcode DllLibCurl::easy_perform(CURL_HANDLE* handle) +{ + return curl_easy_perform(handle); +} + +CURLcode DllLibCurl::easy_pause(CURL_HANDLE* handle, int bitmask) +{ + return curl_easy_pause(handle, bitmask); +} + +void DllLibCurl::easy_reset(CURL_HANDLE* handle) +{ + curl_easy_reset(handle); +} + +void DllLibCurl::easy_cleanup(CURL_HANDLE* handle) +{ + curl_easy_cleanup(handle); +} + +CURL_HANDLE* DllLibCurl::easy_duphandle(CURL_HANDLE* handle) +{ + return curl_easy_duphandle(handle); +} + +CURLM* DllLibCurl::multi_init() +{ + return curl_multi_init(); +} + +CURLMcode DllLibCurl::multi_add_handle(CURLM* multi_handle, CURL_HANDLE* easy_handle) +{ + return curl_multi_add_handle(multi_handle, easy_handle); +} + +CURLMcode DllLibCurl::multi_perform(CURLM* multi_handle, int* running_handles) +{ + return curl_multi_perform(multi_handle, running_handles); +} + +CURLMcode DllLibCurl::multi_remove_handle(CURLM* multi_handle, CURL_HANDLE* easy_handle) +{ + return curl_multi_remove_handle(multi_handle, easy_handle); +} + +CURLMcode DllLibCurl::multi_fdset( + CURLM* multi_handle, fd_set* read_fd_set, fd_set* write_fd_set, fd_set* exc_fd_set, int* max_fd) +{ + return curl_multi_fdset(multi_handle, read_fd_set, write_fd_set, exc_fd_set, max_fd); +} + +CURLMcode DllLibCurl::multi_timeout(CURLM* multi_handle, long* timeout) +{ + return curl_multi_timeout(multi_handle, timeout); +} + +CURLMsg* DllLibCurl::multi_info_read(CURLM* multi_handle, int* msgs_in_queue) +{ + return curl_multi_info_read(multi_handle, msgs_in_queue); +} + +CURLMcode DllLibCurl::multi_cleanup(CURLM* handle) +{ + return curl_multi_cleanup(handle); +} + +curl_slist* DllLibCurl::slist_append(curl_slist* list, const char* to_append) +{ + return curl_slist_append(list, to_append); +} + +void DllLibCurl::slist_free_all(curl_slist* list) +{ + curl_slist_free_all(list); +} + +const char* DllLibCurl::easy_strerror(CURLcode code) +{ + return curl_easy_strerror(code); +} + +DllLibCurlGlobal::DllLibCurlGlobal() +{ + /* we handle this ourself */ + if (curl_global_init(CURL_GLOBAL_ALL)) + { + CLog::Log(LOGERROR, "Error initializing libcurl"); + } +} + +DllLibCurlGlobal::~DllLibCurlGlobal() +{ + // close libcurl + curl_global_cleanup(); +} + +void DllLibCurlGlobal::CheckIdle() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + /* 20 seconds idle time before closing handle */ + const unsigned int idletime = 30000; + + VEC_CURLSESSIONS::iterator it = m_sessions.begin(); + while (it != m_sessions.end()) + { + auto now = std::chrono::steady_clock::now(); + auto duration = + std::chrono::duration_cast<std::chrono::milliseconds>(now - it->m_idletimestamp); + + if (!it->m_busy && duration.count() > idletime) + { + CLog::Log(LOGDEBUG, "{} - Closing session to {}://{} (easy={}, multi={})", __FUNCTION__, + it->m_protocol, it->m_hostname, fmt::ptr(it->m_easy), fmt::ptr(it->m_multi)); + + if (it->m_multi && it->m_easy) + multi_remove_handle(it->m_multi, it->m_easy); + if (it->m_easy) + easy_cleanup(it->m_easy); + if (it->m_multi) + multi_cleanup(it->m_multi); + + it = m_sessions.erase(it); + continue; + } + ++it; + } +} + +void DllLibCurlGlobal::easy_acquire(const char* protocol, + const char* hostname, + CURL_HANDLE** easy_handle, + CURLM** multi_handle) +{ + assert(easy_handle != NULL); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + for (auto& it : m_sessions) + { + if (!it.m_busy) + { + /* allow reuse of requester is trying to connect to same host */ + /* curl will take care of any differences in username/password */ + if (it.m_protocol.compare(protocol) == 0 && it.m_hostname.compare(hostname) == 0) + { + it.m_busy = true; + if (easy_handle) + { + if (!it.m_easy) + it.m_easy = easy_init(); + + *easy_handle = it.m_easy; + } + + if (multi_handle) + { + if (!it.m_multi) + it.m_multi = multi_init(); + + *multi_handle = it.m_multi; + } + + return; + } + } + } + + SSession session = {}; + session.m_busy = true; + session.m_protocol = protocol; + session.m_hostname = hostname; + + if (easy_handle) + { + session.m_easy = easy_init(); + *easy_handle = session.m_easy; + } + + if (multi_handle) + { + session.m_multi = multi_init(); + *multi_handle = session.m_multi; + } + + m_sessions.push_back(session); + + CLog::Log(LOGDEBUG, "{} - Created session to {}://{}", __FUNCTION__, protocol, hostname); +} + +void DllLibCurlGlobal::easy_release(CURL_HANDLE** easy_handle, CURLM** multi_handle) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + CURL_HANDLE* easy = NULL; + CURLM* multi = NULL; + + if (easy_handle) + { + easy = *easy_handle; + *easy_handle = NULL; + } + + if (multi_handle) + { + multi = *multi_handle; + *multi_handle = NULL; + } + + for (auto& it : m_sessions) + { + if (it.m_easy == easy && (multi == nullptr || it.m_multi == multi)) + { + /* reset session so next caller doesn't reuse options, only connections */ + /* will reset verbose too so it won't print that it closed connections on cleanup*/ + easy_reset(easy); + it.m_busy = false; + it.m_idletimestamp = std::chrono::steady_clock::now(); + return; + } + } +} + +CURL_HANDLE* DllLibCurlGlobal::easy_duphandle(CURL_HANDLE* easy_handle) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + for (const auto& it : m_sessions) + { + if (it.m_easy == easy_handle) + { + SSession session = it; + session.m_easy = DllLibCurl::easy_duphandle(easy_handle); + m_sessions.push_back(session); + return session.m_easy; + } + } + return DllLibCurl::easy_duphandle(easy_handle); +} + +void DllLibCurlGlobal::easy_duplicate(CURL_HANDLE* easy, + const CURLM* multi, + CURL_HANDLE** easy_out, + CURLM** multi_out) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (easy_out && easy) + *easy_out = DllLibCurl::easy_duphandle(easy); + + if (multi_out && multi) + *multi_out = DllLibCurl::multi_init(); + + for (const auto& it : m_sessions) + { + if (it.m_easy == easy) + { + SSession session = it; + if (easy_out && easy) + session.m_easy = *easy_out; + else + session.m_easy = NULL; + + if (multi_out && multi) + session.m_multi = *multi_out; + else + session.m_multi = NULL; + + m_sessions.push_back(session); + return; + } + } +} +} // namespace XCURL diff --git a/xbmc/filesystem/DllLibCurl.h b/xbmc/filesystem/DllLibCurl.h new file mode 100644 index 0000000..760f28d --- /dev/null +++ b/xbmc/filesystem/DllLibCurl.h @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" + +#include <stdio.h> +#include <string> +#include <sys/time.h> +#include <sys/types.h> +#include <type_traits> +#include <vector> + +#define CURL CURL_HANDLE +#include <curl/curl.h> +#undef CURL + +namespace XCURL +{ + +class DllLibCurl +{ +public: + virtual ~DllLibCurl() = default; + + CURLcode global_init(long flags); + void global_cleanup(); + CURL_HANDLE* easy_init(); + template<typename... Args> + CURLcode easy_setopt(CURL_HANDLE* handle, CURLoption option, Args... args) + { + return curl_easy_setopt(handle, option, std::forward<Args>(args)...); + } + CURLcode easy_perform(CURL_HANDLE* handle); + CURLcode easy_pause(CURL_HANDLE* handle, int bitmask); + void easy_reset(CURL_HANDLE* handle); + template<typename... Args> + CURLcode easy_getinfo(CURL_HANDLE* curl, CURLINFO info, Args... args) + { + return curl_easy_getinfo(curl, info, std::forward<Args>(args)...); + } + void easy_cleanup(CURL_HANDLE* handle); + virtual CURL_HANDLE* easy_duphandle(CURL_HANDLE* handle); + CURLM* multi_init(void); + CURLMcode multi_add_handle(CURLM* multi_handle, CURL_HANDLE* easy_handle); + CURLMcode multi_perform(CURLM* multi_handle, int* running_handles); + CURLMcode multi_remove_handle(CURLM* multi_handle, CURL_HANDLE* easy_handle); + CURLMcode multi_fdset(CURLM* multi_handle, + fd_set* read_fd_set, + fd_set* write_fd_set, + fd_set* exc_fd_set, + int* max_fd); + CURLMcode multi_timeout(CURLM* multi_handle, long* timeout); + CURLMsg* multi_info_read(CURLM* multi_handle, int* msgs_in_queue); + CURLMcode multi_cleanup(CURLM* handle); + curl_slist* slist_append(curl_slist* list, const char* to_append); + void slist_free_all(curl_slist* list); + const char* easy_strerror(CURLcode code); +}; + +class DllLibCurlGlobal : public DllLibCurl +{ +public: + DllLibCurlGlobal(); + ~DllLibCurlGlobal(); + /* extend interface with buffered functions */ + void easy_acquire(const char* protocol, + const char* hostname, + CURL_HANDLE** easy_handle, + CURLM** multi_handle); + void easy_release(CURL_HANDLE** easy_handle, CURLM** multi_handle); + void easy_duplicate(CURL_HANDLE* easy, + const CURLM* multi, + CURL_HANDLE** easy_out, + CURLM** multi_out); + CURL_HANDLE* easy_duphandle(CURL_HANDLE* easy_handle) override; + void CheckIdle(); + + /* overloaded load and unload with reference counter */ + + /* structure holding a session info */ + typedef struct SSession + { + std::chrono::time_point<std::chrono::steady_clock> + m_idletimestamp; // timestamp of when this object when idle + std::string m_protocol; + std::string m_hostname; + bool m_busy; + CURL_HANDLE* m_easy; + CURLM* m_multi; + } SSession; + + typedef std::vector<SSession> VEC_CURLSESSIONS; + + VEC_CURLSESSIONS m_sessions; + CCriticalSection m_critSection; +}; +} // namespace XCURL + +extern XCURL::DllLibCurlGlobal g_curlInterface; diff --git a/xbmc/filesystem/EventsDirectory.cpp b/xbmc/filesystem/EventsDirectory.cpp new file mode 100644 index 0000000..d533475 --- /dev/null +++ b/xbmc/filesystem/EventsDirectory.cpp @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + + +#include "EventsDirectory.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "events/EventLog.h" +#include "utils/StringUtils.h" + +using namespace XFILE; + +bool CEventsDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + items.ClearProperties(); + items.SetContent("events"); + + auto log = CServiceBroker::GetEventLog(); + Events events; + + std::string hostname = url.GetHostName(); + if (hostname.empty()) + events = log->Get(); + else + { + bool includeHigherLevels = false; + // check if we should only retrieve events from a specific level or + // also from all higher levels + if (StringUtils::EndsWith(hostname, "+")) + { + includeHigherLevels = true; + + // remove the "+" from the end of the hostname + hostname = hostname.substr(0, hostname.size() - 1); + } + + EventLevel level = CEventLog::EventLevelFromString(hostname); + + // get the events of the specified level(s) + events = log->Get(level, includeHigherLevels); + } + + for (const auto& eventItem : events) + items.Add(EventToFileItem(eventItem)); + + return true; +} + +std::shared_ptr<CFileItem> CEventsDirectory::EventToFileItem( + const std::shared_ptr<const IEvent>& eventItem) +{ + if (!eventItem) + return CFileItemPtr(); + + CFileItemPtr item(new CFileItem(eventItem)); + + item->SetProperty(PROPERTY_EVENT_IDENTIFIER, eventItem->GetIdentifier()); + item->SetProperty(PROPERTY_EVENT_LEVEL, CEventLog::EventLevelToString(eventItem->GetLevel())); + item->SetProperty(PROPERTY_EVENT_DESCRIPTION, eventItem->GetDescription()); + + return item; +} diff --git a/xbmc/filesystem/EventsDirectory.h b/xbmc/filesystem/EventsDirectory.h new file mode 100644 index 0000000..adb26c3 --- /dev/null +++ b/xbmc/filesystem/EventsDirectory.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "filesystem/IDirectory.h" + +#include <memory> + +class CFileItem; +class CFileItemList; +class IEvent; + +#define PROPERTY_EVENT_IDENTIFIER "Event.ID" +#define PROPERTY_EVENT_LEVEL "Event.Level" +#define PROPERTY_EVENT_DESCRIPTION "Event.Description" + +namespace XFILE +{ + class CEventsDirectory : public IDirectory + { + public: + CEventsDirectory() = default; + ~CEventsDirectory() override = default; + + // implementations of IDirectory + bool GetDirectory(const CURL& url, CFileItemList& items) override; + bool Create(const CURL& url) override { return true; } + bool Exists(const CURL& url) override { return true; } + bool AllowAll() const override { return true; } + + static std::shared_ptr<CFileItem> EventToFileItem( + const std::shared_ptr<const IEvent>& activity); + }; +} diff --git a/xbmc/filesystem/FTPDirectory.cpp b/xbmc/filesystem/FTPDirectory.cpp new file mode 100644 index 0000000..46d3d7d --- /dev/null +++ b/xbmc/filesystem/FTPDirectory.cpp @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "FTPDirectory.h" + +#include "CurlFile.h" +#include "FTPParse.h" +#include "FileItem.h" +#include "URL.h" +#include "utils/CharsetConverter.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +#include <climits> + +using namespace XFILE; + +CFTPDirectory::CFTPDirectory(void) = default; +CFTPDirectory::~CFTPDirectory(void) = default; + +bool CFTPDirectory::GetDirectory(const CURL& url2, CFileItemList &items) +{ + CCurlFile reader; + + CURL url(url2); + + std::string path = url.GetFileName(); + if( !path.empty() && !StringUtils::EndsWith(path, "/") ) + { + path += "/"; + url.SetFileName(path); + } + + if (!reader.Open(url)) + return false; + + bool serverNotUseUTF8 = url.GetProtocolOption("utf8") == "0"; + + char buffer[MAX_PATH + 1024]; + while( reader.ReadString(buffer, sizeof(buffer)) ) + { + std::string strBuffer = buffer; + + StringUtils::RemoveCRLF(strBuffer); + + CFTPParse parse; + if (parse.FTPParse(strBuffer)) + { + if( parse.getName().length() == 0 ) + continue; + + if( parse.getFlagtrycwd() == 0 && parse.getFlagtryretr() == 0 ) + continue; + + /* buffer name */ + std::string name; + name.assign(parse.getName()); + + if( name == ".." || name == "." ) + continue; + + // server returned filename could in utf8 or non-utf8 encoding + // we need utf8, so convert it to utf8 anyway + g_charsetConverter.unknownToUTF8(name); + + // convert got empty result, ignore it + if (name.empty()) + continue; + + if (serverNotUseUTF8 || name != parse.getName()) + // non-utf8 name path, tag it with protocol option. + // then we can talk to server with the same encoding in CurlFile according to this tag. + url.SetProtocolOption("utf8", "0"); + else + url.RemoveProtocolOption("utf8"); + + CFileItemPtr pItem(new CFileItem(name)); + + pItem->m_bIsFolder = parse.getFlagtrycwd() != 0; + std::string filePath = path + name; + if (pItem->m_bIsFolder) + URIUtils::AddSlashAtEnd(filePath); + + /* qualify the url with host and all */ + url.SetFileName(filePath); + pItem->SetPath(url.Get()); + + pItem->m_dwSize = parse.getSize(); + pItem->m_dateTime=parse.getTime(); + + items.Add(pItem); + } + } + + return true; +} + +bool CFTPDirectory::Exists(const CURL& url) +{ + // make sure ftp dir ends with slash, + // curl need to known it's a dir to check ftp directory existence. + std::string file = url.Get(); + URIUtils::AddSlashAtEnd(file); + + CCurlFile ftp; + CURL url2(file); + return ftp.Exists(url2); +} diff --git a/xbmc/filesystem/FTPDirectory.h b/xbmc/filesystem/FTPDirectory.h new file mode 100644 index 0000000..0a0c6db --- /dev/null +++ b/xbmc/filesystem/FTPDirectory.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" + +namespace XFILE +{ + class CFTPDirectory : public IDirectory + { + public: + CFTPDirectory(void); + ~CFTPDirectory(void) override; + bool GetDirectory(const CURL& url, CFileItemList &items) override; + bool Exists(const CURL& url) override; + private: + }; +} + diff --git a/xbmc/filesystem/FTPParse.cpp b/xbmc/filesystem/FTPParse.cpp new file mode 100644 index 0000000..fb5e035 --- /dev/null +++ b/xbmc/filesystem/FTPParse.cpp @@ -0,0 +1,504 @@ +/* + * Copyright (C) 2010-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "FTPParse.h" + +#include <cmath> + +#include <pcrecpp.h> + +CFTPParse::CFTPParse() +{ + m_flagtrycwd = 0; + m_flagtryretr = 0; + m_size = 0; +} + +std::string CFTPParse::getName() +{ + return m_name; +} + +int CFTPParse::getFlagtrycwd() +{ + return m_flagtrycwd; +} + +int CFTPParse::getFlagtryretr() +{ + return m_flagtryretr; +} + +uint64_t CFTPParse::getSize() +{ + return m_size; +} + +time_t CFTPParse::getTime() +{ + return m_time; +} + +void CFTPParse::setTime(const std::string& str) +{ + /* Variables used to capture patterns via the regexes */ + std::string month; + std::string day; + std::string year; + std::string hour; + std::string minute; + std::string second; + std::string am_or_pm; + + /* time struct used to set the time_t variable */ + struct tm time_struct = {}; + + /* Regex to read Unix, NetWare and NetPresenz time format */ + pcrecpp::RE unix_re("^([A-Za-z]{3})" // month + "\\s+(\\d{1,2})" // day of month + "\\s+([:\\d]{4,5})$" // time of day or year + ); + + /* Regex to read MultiNet time format */ + pcrecpp::RE multinet_re("^(\\d{1,2})" // day of month + "-([A-Za-z]{3})" // month + "-(\\d{4})" // year + "\\s+(\\d{2})" // hour + ":(\\d{2})" // minute + "(:(\\d{2}))?$" // second + ); + + /* Regex to read MSDOS time format */ + pcrecpp::RE msdos_re("^(\\d{2})" // month + "-(\\d{2})" // day of month + "-(\\d{2})" // year + "\\s+(\\d{2})" // hour + ":(\\d{2})" // minute + "([AP]M)$" // AM or PM + ); + + if (unix_re.FullMatch(str, &month, &day, &year)) + { + /* set the month */ + if (pcrecpp::RE("jan", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 0; + else if (pcrecpp::RE("feb", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 1; + else if (pcrecpp::RE("mar", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 2; + else if (pcrecpp::RE("apr", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 3; + else if (pcrecpp::RE("may", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 4; + else if (pcrecpp::RE("jun", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 5; + else if (pcrecpp::RE("jul", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 6; + else if (pcrecpp::RE("aug", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 7; + else if (pcrecpp::RE("sep", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 8; + else if (pcrecpp::RE("oct", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 9; + else if (pcrecpp::RE("nov", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 10; + else if (pcrecpp::RE("dec", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 11; + + /* set the day of the month */ + time_struct.tm_mday = atoi(day.c_str()); + + time_t t = time(NULL); + struct tm *current_time; +#ifdef LOCALTIME_R + struct tm result = {}; + current_time = localtime_r(&t, &result); +#else + current_time = localtime(&t); +#endif + if (pcrecpp::RE("(\\d{2}):(\\d{2})").FullMatch(year, &hour, &minute)) + { + /* set the hour and minute */ + time_struct.tm_hour = atoi(hour.c_str()); + time_struct.tm_min = atoi(minute.c_str()); + + /* set the year */ + if ((current_time->tm_mon - time_struct.tm_mon < 0) || + ((current_time->tm_mon - time_struct.tm_mon == 0) && + (current_time->tm_mday - time_struct.tm_mday < 0))) + time_struct.tm_year = current_time->tm_year - 1; + else + time_struct.tm_year = current_time->tm_year; + } + else + { + /* set the year */ + time_struct.tm_year = atoi(year.c_str()) - 1900; + } + + /* set the day of the week */ + time_struct.tm_wday = getDayOfWeek(time_struct.tm_mon + 1, + time_struct.tm_mday, + time_struct.tm_year + 1900); + } + else if (multinet_re.FullMatch(str, &day, &month, &year, + &hour, &minute, (void*)NULL, &second)) + { + /* set the month */ + if (pcrecpp::RE("jan", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 0; + else if (pcrecpp::RE("feb", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 1; + else if (pcrecpp::RE("mar", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 2; + else if (pcrecpp::RE("apr", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 3; + else if (pcrecpp::RE("may", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 4; + else if (pcrecpp::RE("jun", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 5; + else if (pcrecpp::RE("jul", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 6; + else if (pcrecpp::RE("aug", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 7; + else if (pcrecpp::RE("sep", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 8; + else if (pcrecpp::RE("oct", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 9; + else if (pcrecpp::RE("nov", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 10; + else if (pcrecpp::RE("dec", + pcrecpp::RE_Options().set_caseless(true)).FullMatch(month)) + time_struct.tm_mon = 11; + + /* set the day of the month and year */ + time_struct.tm_mday = atoi(day.c_str()); + time_struct.tm_year = atoi(year.c_str()) - 1900; + + /* set the hour and minute */ + time_struct.tm_hour = atoi(hour.c_str()); + time_struct.tm_min = atoi(minute.c_str()); + + /* set the second if given*/ + if (second.length() > 0) + time_struct.tm_sec = atoi(second.c_str()); + + /* set the day of the week */ + time_struct.tm_wday = getDayOfWeek(time_struct.tm_mon + 1, + time_struct.tm_mday, + time_struct.tm_year + 1900); + } + else if (msdos_re.FullMatch(str, &month, &day, + &year, &hour, &minute, &am_or_pm)) + { + /* set the month and the day of the month */ + time_struct.tm_mon = atoi(month.c_str()) - 1; + time_struct.tm_mday = atoi(day.c_str()); + + /* set the year */ + time_struct.tm_year = atoi(year.c_str()); + if (time_struct.tm_year < 70) + time_struct.tm_year += 100; + + /* set the hour */ + time_struct.tm_hour = atoi(hour.c_str()); + if (time_struct.tm_hour == 12) + time_struct.tm_hour -= 12; + if (pcrecpp::RE("PM").FullMatch(am_or_pm)) + time_struct.tm_hour += 12; + + /* set the minute */ + time_struct.tm_min = atoi(minute.c_str()); + + /* set the day of the week */ + time_struct.tm_wday = getDayOfWeek(time_struct.tm_mon + 1, + time_struct.tm_mday, + time_struct.tm_year + 1900); + } + + /* now set m_time */ + m_time = mktime(&time_struct); +} + +int CFTPParse::getDayOfWeek(int month, int date, int year) +{ + /* Here we use the Doomsday rule to calculate the day of the week */ + + /* First determine the anchor day */ + int anchor; + if (year >= 1900 && year < 2000) + anchor = 3; + else if (year >= 2000 && year < 2100) + anchor = 2; + else if (year >= 2100 && year < 2200) + anchor = 0; + else if (year >= 2200 && year < 2300) + anchor = 5; + else // must have been given an invalid year :-/ + return -1; + + /* Now determine the doomsday */ + int y = year % 100; + int dday = + ((y/12 + (y % 12) + ((y % 12)/4)) % 7) + anchor; + + /* Determine if the given year is a leap year */ + int leap_year = 0; + if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)) + leap_year = 1; + + /* Now select a doomsday for the given month */ + int mdday = 1; + if (month == 1) + { + if (leap_year) + mdday = 4; + else + mdday = 3; + } + if (month == 2) + { + if (leap_year) + mdday = 1; + else + mdday = 7; + } + if (month == 3) + mdday = 7; + if (month == 4) + mdday = 4; + if (month == 5) + mdday = 9; + if (month == 6) + mdday = 6; + if (month == 7) + mdday = 11; + if (month == 8) + mdday = 8; + if (month == 9) + mdday = 5; + if (month == 10) + mdday = 10; + if (month == 11) + mdday = 9; + if (month == 12) + mdday = 12; + + /* Now calculate the day of the week + * Sunday = 0, Monday = 1, Tuesday = 2, Wednesday = 3, Thursday = 4, + * Friday = 5, Saturday = 6. + */ + int day_of_week = ((date - 1) % 7) - ((mdday - 1) % 7) + dday; + if (day_of_week >= 7) + day_of_week -= 7; + + return day_of_week; +} + +int CFTPParse::FTPParse(const std::string& str) +{ + /* Various variable to capture patterns via the regexes */ + std::string permissions; + std::string link_count; + std::string owner; + std::string group; + std::string size; + std::string date; + std::string name; + std::string type; + std::string stuff; + std::string facts; + std::string version; + std::string file_id; + + /* Regex for standard Unix listing formats */ + pcrecpp::RE unix_re("^([-bcdlps])" // type + "([-rwxXsStT]{9})" // permissions + "\\s+(\\d+)" // hard link count + "\\s+(\\w+)" // owner + "\\s+(\\w+)" // group + "\\s+(\\d+)" // size + "\\s+([A-Za-z]{3}\\s+\\d{1,2}\\s+[:\\d]{4,5})" // modification date + "\\s+(.+)$" // name + ); + + /* Regex for NetWare listing formats */ + /* See http://www.novell.com/documentation/oes/ftp_enu/data/a3ep22p.html#fbhbaijf */ + pcrecpp::RE netware_re("^([-d])" // type + "\\s+(\\[[-SRWCIEMFA]{8}\\])" // rights + "\\s+(\\w+)" // owner + "\\s+(\\d+)" // size + "\\s+([A-Za-z]{3}\\s+\\d{1,2}\\s+[:\\d]{4,5})" // time + "\\s+(.+)$" // name + ); + + /* Regex for NetPresenz */ + /* See http://files.stairways.com/other/ftp-list-specs-info.txt */ + /* Here we will capture permissions and size if given */ + pcrecpp::RE netpresenz_re("^([-dl])" // type + "([-rwx]{9}|)" // permissions + "\\s+(.*)" // stuff + "\\s+(\\d+|)" // size + "\\s+([A-Za-z]{3}\\s+\\d{1,2}\\s+[:\\d]{4,5})" // modification date + "\\s+(.+)$" // name + ); + + /* Regex for EPLF */ + /* See http://cr.yp.to/ftp/list/eplf.html */ + /* SAVE: "(/,|r,|s\\d+,|m\\d+,|i[\\d!#@$%^&*()]+(\\.[\\d!#@$%^&*()]+|),)+" */ + pcrecpp::RE eplf_re("^\\+" // initial "plus" sign + "([^\\s]+)" // facts + "\\s(.+)$" // name + ); + + /* Regex for MultiNet */ + /* Best documentation found was + * http://www-sld.slac.stanford.edu/SLDWWW/workbook/vms_files.html */ + pcrecpp::RE multinet_re("^([^;]+)" // name + ";(\\d+)" // version + "\\s+([\\d/]+)" // file id + "\\s+(\\d{1,2}-[A-Za-z]{3}-\\d{4}\\s+\\d{2}:\\d{2}(:\\d{2})?)" // date + "\\s+\\[([^\\]]+)\\]" // owner,group + "\\s+\\(([^\\)]+)\\)$" // permissions + ); + + /* Regex for MSDOS */ + pcrecpp::RE msdos_re("^(\\d{2}-\\d{2}-\\d{2}\\s+\\d{2}:\\d{2}[AP]M)" // date + "\\s+(<DIR>|[\\d]+)" // dir or size + "\\s+(.+)$" // name + ); + + if (unix_re.FullMatch(str, &type, &permissions, &link_count, &owner, &group, &size, &date, &name)) + { + m_name = name; + m_size = (uint64_t)strtod(size.c_str(), NULL); + if (pcrecpp::RE("d").FullMatch(type)) + m_flagtrycwd = 1; + if (pcrecpp::RE("-").FullMatch(type)) + m_flagtryretr = 1; + if (pcrecpp::RE("l").FullMatch(type)) + { + m_flagtrycwd = m_flagtryretr = 1; + // handle symlink + size_t found = m_name.find(" -> "); + if (found != std::string::npos) + m_name = m_name.substr(0, found); + } + setTime(date); + + return 1; + } + if (netware_re.FullMatch(str, &type, &permissions, &owner, &size, &date, &name)) + { + m_name = name; + m_size = (uint64_t)strtod(size.c_str(), NULL); + if (pcrecpp::RE("d").FullMatch(type)) + m_flagtrycwd = 1; + if (pcrecpp::RE("-").FullMatch(type)) + m_flagtryretr = 1; + setTime(date); + + return 1; + } + if (netpresenz_re.FullMatch(str, &type, &permissions, &stuff, &size, &date, &name)) + { + m_name = name; + m_size = (uint64_t)strtod(size.c_str(), NULL); + if (pcrecpp::RE("d").FullMatch(type)) + m_flagtrycwd = 1; + if (pcrecpp::RE("-").FullMatch(type)) + m_flagtryretr = 1; + if (pcrecpp::RE("l").FullMatch(type)) + { + m_flagtrycwd = m_flagtryretr = 1; + // handle symlink + size_t found = m_name.find(" -> "); + if (found != std::string::npos) + m_name = m_name.substr(0, found); + } + setTime(date); + + return 1; + } + if (eplf_re.FullMatch(str, &facts, &name)) + { + /* Get the type, size, and date from the facts */ + pcrecpp::RE("(\\+|,)(r|/),").PartialMatch(facts, (void*)NULL, &type); + pcrecpp::RE("(\\+|,)s(\\d+),").PartialMatch(facts, (void*)NULL, &size); + pcrecpp::RE("(\\+|,)m(\\d+),").PartialMatch(facts, (void*)NULL, &date); + + m_name = name; + m_size = (uint64_t)strtod(size.c_str(), NULL); + if (pcrecpp::RE("/").FullMatch(type)) + m_flagtrycwd = 1; + if (pcrecpp::RE("r").FullMatch(type)) + m_flagtryretr = 1; + /* eplf stores the date in time_t format already */ + m_time = atoi(date.c_str()); + + return 1; + } + if (multinet_re.FullMatch(str, &name, &version, &file_id, &date, (void*)NULL, &owner, &permissions)) + { + if (pcrecpp::RE("\\.DIR$").PartialMatch(name)) + { + name.resize(name.size() - 4); + m_flagtrycwd = 1; + } + else + m_flagtryretr = 1; + m_name = name; + setTime(date); + /* Multinet doesn't provide a size */ + m_size = 0; + + return 1; + } + if (msdos_re.FullMatch(str, &date, &size, &name)) + { + m_name = name; + if (pcrecpp::RE("<DIR>").FullMatch(size)) + { + m_flagtrycwd = 1; + m_size = 0; + } + else + { + m_flagtryretr = 1; + m_size = (uint64_t)strtod(size.c_str(), NULL); + } + setTime(date); + + return 1; + } + + return 0; +} diff --git a/xbmc/filesystem/FTPParse.h b/xbmc/filesystem/FTPParse.h new file mode 100644 index 0000000..5f56981 --- /dev/null +++ b/xbmc/filesystem/FTPParse.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2010-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include <ctime> +#include <stdint.h> +#include <string> + +class CFTPParse +{ +public: + CFTPParse(); + int FTPParse(const std::string& str); + std::string getName(); + int getFlagtrycwd(); + int getFlagtryretr(); + uint64_t getSize(); + time_t getTime(); +private: + std::string m_name; // not necessarily 0-terminated + int m_flagtrycwd; // 0 if cwd is definitely pointless, 1 otherwise + int m_flagtryretr; // 0 if retr is definitely pointless, 1 otherwise + uint64_t m_size; // number of octets + time_t m_time = 0; // modification time + void setTime(const std::string& str); // Method used to set m_time from a string + int getDayOfWeek(int month, int date, int year); // Method to get day of week +}; diff --git a/xbmc/filesystem/FavouritesDirectory.cpp b/xbmc/filesystem/FavouritesDirectory.cpp new file mode 100644 index 0000000..fe46874 --- /dev/null +++ b/xbmc/filesystem/FavouritesDirectory.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "FavouritesDirectory.h" + +#include "Directory.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "favourites/FavouritesService.h" +#include "profiles/ProfileManager.h" +#include "utils/FileUtils.h" +#include "utils/URIUtils.h" + +namespace XFILE +{ + +bool CFavouritesDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + items.Clear(); + CServiceBroker::GetFavouritesService().GetAll(items); + return true; +} + +bool CFavouritesDirectory::Exists(const CURL& url) +{ + if (url.IsProtocol("favourites")) + { + if (CFileUtils::Exists("special://xbmc/system/favourites.xml")) + return true; + + const std::string favouritesXml = URIUtils::AddFileToFolder(m_profileManager->GetProfileUserDataFolder(), "favourites.xml"); + + return CFileUtils::Exists(favouritesXml); + } + + return XFILE::CDirectory::Exists(url); +} +} // namespace XFILE diff --git a/xbmc/filesystem/FavouritesDirectory.h b/xbmc/filesystem/FavouritesDirectory.h new file mode 100644 index 0000000..fe7d5b6 --- /dev/null +++ b/xbmc/filesystem/FavouritesDirectory.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" + +class CFileItemList; +class CFileItem; + +namespace XFILE +{ + + class CFavouritesDirectory : public IDirectory + { + public: + CFavouritesDirectory() = default; + + bool GetDirectory(const CURL& url, CFileItemList &items) override; + bool Exists(const CURL& url) override; + }; + +} diff --git a/xbmc/filesystem/File.cpp b/xbmc/filesystem/File.cpp new file mode 100644 index 0000000..9e8a266 --- /dev/null +++ b/xbmc/filesystem/File.cpp @@ -0,0 +1,1224 @@ +/* + * Copyright (c) 2002 Frodo + * Portions Copyright (c) by the authors of ffmpeg and xvid + * Copyright (C) 2002-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "File.h" + +#include "Directory.h" +#include "DirectoryCache.h" +#include "FileCache.h" +#include "FileFactory.h" +#include "IFile.h" +#include "PasswordManager.h" +#include "ServiceBroker.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPowerHandling.h" +#include "commons/Exception.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/BitstreamStats.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +using namespace XFILE; + +////////////////////////////////////////////////////////////////////// +// Construction/Destruction +////////////////////////////////////////////////////////////////////// +#ifndef __GNUC__ +#pragma warning (disable:4244) +#endif + +//********************************************************************************************* +CFile::CFile() = default; + +//********************************************************************************************* +CFile::~CFile() +{ + Close(); +} + +//********************************************************************************************* + +bool CFile::Copy(const std::string& strFileName, const std::string& strDest, XFILE::IFileCallback* pCallback, void* pContext) +{ + const CURL pathToUrl(strFileName); + const CURL pathToUrlDest(strDest); + return Copy(pathToUrl, pathToUrlDest, pCallback, pContext); +} + +bool CFile::Copy(const CURL& url2, const CURL& dest, XFILE::IFileCallback* pCallback, void* pContext) +{ + CFile file; + + const std::string pathToUrl(dest.Get()); + if (pathToUrl.empty()) + return false; + + // special case for zips - ignore caching + CURL url(url2); + if (StringUtils::StartsWith(url.Get(), "zip://") || URIUtils::IsInAPK(url.Get())) + url.SetOptions("?cache=no"); + if (file.Open(url.Get(), READ_TRUNCATED | READ_CHUNKED)) + { + + CFile newFile; + if (URIUtils::IsHD(pathToUrl)) // create possible missing dirs + { + std::vector<std::string> tokens; + std::string strDirectory = URIUtils::GetDirectory(pathToUrl); + URIUtils::RemoveSlashAtEnd(strDirectory); // for the test below + if (!(strDirectory.size() == 2 && strDirectory[1] == ':')) + { + CURL url(strDirectory); + std::string pathsep; +#ifndef TARGET_POSIX + pathsep = "\\"; +#else + pathsep = "/"; +#endif + // Try to use the recursive creation first, if it fails + // it might not be implemented for that subsystem so let's + // fall back to the old method in that case + if (!CDirectory::Create(url)) + { + StringUtils::Tokenize(url.GetFileName(), tokens, pathsep); + std::string strCurrPath; + // Handle special + if (!url.GetProtocol().empty()) + { + pathsep = "/"; + strCurrPath += url.GetProtocol() + "://"; + } // If the directory has a / at the beginning, don't forget it + else if (strDirectory[0] == pathsep[0]) + strCurrPath += pathsep; + + for (const std::string& iter : tokens) + { + strCurrPath += iter + pathsep; + CDirectory::Create(strCurrPath); + } + } + } + } + if (CFile::Exists(dest)) + CFile::Delete(dest); + if (!newFile.OpenForWrite(dest, true)) // overwrite always + { + file.Close(); + return false; + } + + int iBufferSize = DetermineChunkSize(file.GetChunkSize(), 128 * 1024); + + std::vector<char> buffer(iBufferSize); + ssize_t iRead, iWrite; + + unsigned long long llFileSize = file.GetLength(); + unsigned long long llPos = 0; + + CStopWatch timer; + timer.StartZero(); + float start = 0.0f; + auto& components = CServiceBroker::GetAppComponents(); + const auto appPower = components.GetComponent<CApplicationPowerHandling>(); + while (true) + { + appPower->ResetScreenSaver(); + + iRead = file.Read(buffer.data(), buffer.size()); + if (iRead == 0) break; + else if (iRead < 0) + { + CLog::Log(LOGERROR, "{} - Failed read from file {}", __FUNCTION__, url.GetRedacted()); + llFileSize = (uint64_t)-1; + break; + } + + /* write data and make sure we managed to write it all */ + iWrite = 0; + while(iWrite < iRead) + { + ssize_t iWrite2 = newFile.Write(buffer.data() + iWrite, iRead - iWrite); + if(iWrite2 <=0) + break; + iWrite+=iWrite2; + } + + if (iWrite != iRead) + { + CLog::Log(LOGERROR, "{} - Failed write to file {}", __FUNCTION__, dest.GetRedacted()); + llFileSize = (uint64_t)-1; + break; + } + + llPos += iRead; + + // calculate the current and average speeds + float end = timer.GetElapsedSeconds(); + + if (pCallback && end - start > 0.5f && end) + { + start = end; + + float averageSpeed = llPos / end; + int ipercent = 0; + if(llFileSize) + ipercent = 100 * llPos / llFileSize; + + if(!pCallback->OnFileCallback(pContext, ipercent, averageSpeed)) + { + CLog::Log(LOGERROR, "{} - User aborted copy", __FUNCTION__); + llFileSize = (uint64_t)-1; + break; + } + } + } + + /* close both files */ + newFile.Close(); + file.Close(); + + /* verify that we managed to completed the file */ + if (llFileSize && llPos != llFileSize) + { + CFile::Delete(dest); + return false; + } + return true; + } + return false; +} + +//********************************************************************************************* + +bool CFile::CURLCreate(const std::string &url) +{ + m_curl.Parse(url); + return true; +} + +bool CFile::CURLAddOption(XFILE::CURLOPTIONTYPE type, const char* name, const char * value) +{ + switch (type){ + case XFILE::CURL_OPTION_CREDENTIALS: + { + m_curl.SetUserName(name); + m_curl.SetPassword(value); + break; + } + case XFILE::CURL_OPTION_PROTOCOL: + case XFILE::CURL_OPTION_HEADER: + { + m_curl.SetProtocolOption(name, value); + break; + } + case XFILE::CURL_OPTION_OPTION: + { + m_curl.SetOption(name, value); + break; + } + default: + return false; + } + return true; +} + +bool CFile::CURLOpen(unsigned int flags) +{ + return Open(m_curl, flags); +} + +bool CFile::Open(const std::string& strFileName, const unsigned int flags) +{ + const CURL pathToUrl(strFileName); + return Open(pathToUrl, flags); +} + +bool CFile::Open(const CURL& file, const unsigned int flags) +{ + if (m_pFile) + { + if ((flags & READ_REOPEN) == 0) + { + CLog::Log(LOGERROR, "File::Open - already open: {}", file.GetRedacted()); + return false; + } + else + { + return m_pFile->ReOpen(URIUtils::SubstitutePath(file)); + } + } + + m_flags = flags; + try + { + bool bPathInCache; + + CURL url(URIUtils::SubstitutePath(file)), url2(url); + + if (url2.IsProtocol("apk") || url2.IsProtocol("zip") ) + url2.SetOptions(""); + + if (!g_directoryCache.FileExists(url2.Get(), bPathInCache) ) + { + if (bPathInCache) + return false; + } + + /* + * There are 5 buffer modes available (configurable in as.xml) + * 0) Buffer all internet filesystems (like 2 but additionally also ftp, webdav, etc.) + * 1) Buffer all filesystems (including local) + * 2) Only buffer true internet filesystems (streams) (http, etc.) + * 3) No buffer + * 4) Buffer all remote (non-local) filesystems + */ + if (!(m_flags & READ_NO_CACHE)) + { + const std::string pathToUrl(url.Get()); + if (URIUtils::IsDVD(pathToUrl) || URIUtils::IsBluray(pathToUrl) || + (m_flags & READ_AUDIO_VIDEO)) + { + const unsigned int iCacheBufferMode = + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cacheBufferMode; + if ((iCacheBufferMode == CACHE_BUFFER_MODE_INTERNET && + URIUtils::IsInternetStream(pathToUrl, true)) || + (iCacheBufferMode == CACHE_BUFFER_MODE_TRUE_INTERNET && + URIUtils::IsInternetStream(pathToUrl, false)) || + (iCacheBufferMode == CACHE_BUFFER_MODE_NETWORK && + URIUtils::IsNetworkFilesystem(pathToUrl)) || + (iCacheBufferMode == CACHE_BUFFER_MODE_ALL && + (URIUtils::IsNetworkFilesystem(pathToUrl) || URIUtils::IsHD(pathToUrl)))) + { + m_flags |= READ_CACHED; + } + } + + if (m_flags & READ_CACHED) + { + m_pFile = std::make_unique<CFileCache>(m_flags); + + if (!m_pFile) + return false; + + return m_pFile->Open(url); + } + } + + m_pFile.reset(CFileFactory::CreateLoader(url)); + + if (!m_pFile) + return false; + + CURL authUrl(url); + if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty()) + CPasswordManager::GetInstance().AuthenticateURL(authUrl); + + try + { + if (!m_pFile->Open(authUrl)) + return false; + } + catch (CRedirectException *pRedirectEx) + { + // the file implementation decided this item should use a different implementation. + // the exception will contain the new implementation. + CLog::Log(LOGDEBUG, "File::Open - redirecting implementation for {}", file.GetRedacted()); + if (pRedirectEx && pRedirectEx->m_pNewFileImp) + { + std::unique_ptr<CURL> pNewUrl(pRedirectEx->m_pNewUrl); + m_pFile.reset(pRedirectEx->m_pNewFileImp); + delete pRedirectEx; + + if (pNewUrl) + { + CURL newAuthUrl(*pNewUrl); + if (CPasswordManager::GetInstance().IsURLSupported(newAuthUrl) && newAuthUrl.GetUserName().empty()) + CPasswordManager::GetInstance().AuthenticateURL(newAuthUrl); + + if (!m_pFile->Open(newAuthUrl)) + return false; + } + else + { + if (!m_pFile->Open(authUrl)) + return false; + } + } + } + catch (...) + { + CLog::Log(LOGERROR, "File::Open - unknown exception when opening {}", file.GetRedacted()); + return false; + } + + if (m_pFile->GetChunkSize() && !(m_flags & READ_CHUNKED)) + { + m_pBuffer = std::make_unique<CFileStreamBuffer>(0); + m_pBuffer->Attach(m_pFile.get()); + } + + if (m_flags & READ_BITRATE) + { + m_bitStreamStats = std::make_unique<BitstreamStats>(); + m_bitStreamStats->Start(); + } + + return true; + } + XBMCCOMMONS_HANDLE_UNCHECKED + catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); } + CLog::Log(LOGERROR, "{} - Error opening {}", __FUNCTION__, file.GetRedacted()); + return false; +} + +bool CFile::OpenForWrite(const std::string& strFileName, bool bOverWrite) +{ + const CURL pathToUrl(strFileName); + return OpenForWrite(pathToUrl, bOverWrite); +} + +bool CFile::OpenForWrite(const CURL& file, bool bOverWrite) +{ + try + { + CURL url = URIUtils::SubstitutePath(file); + CURL authUrl = url; + if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty()) + CPasswordManager::GetInstance().AuthenticateURL(authUrl); + + m_pFile.reset(CFileFactory::CreateLoader(url)); + + if (m_pFile && m_pFile->OpenForWrite(authUrl, bOverWrite)) + { + // add this file to our directory cache (if it's stored) + g_directoryCache.AddFile(url.Get()); + return true; + } + return false; + } + XBMCCOMMONS_HANDLE_UNCHECKED + catch(...) + { + CLog::Log(LOGERROR, "{} - Unhandled exception opening {}", __FUNCTION__, file.GetRedacted()); + } + CLog::Log(LOGERROR, "{} - Error opening {}", __FUNCTION__, file.GetRedacted()); + return false; +} + +int CFile::DetermineChunkSize(const int srcChunkSize, const int reqChunkSize) +{ + // Determine cache chunk size: if source chunk size is bigger than 1 + // use source chunk size else use requested chunk size + return (srcChunkSize > 1 ? srcChunkSize : reqChunkSize); +} + +bool CFile::Exists(const std::string& strFileName, bool bUseCache /* = true */) +{ + const CURL pathToUrl(strFileName); + return Exists(pathToUrl, bUseCache); +} + +bool CFile::Exists(const CURL& file, bool bUseCache /* = true */) +{ + CURL url(URIUtils::SubstitutePath(file)); + CURL authUrl = url; + if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty()) + CPasswordManager::GetInstance().AuthenticateURL(authUrl); + + try + { + if (bUseCache) + { + bool bPathInCache; + if (g_directoryCache.FileExists(url.Get(), bPathInCache)) + return true; + if (bPathInCache) + return false; + } + + std::unique_ptr<IFile> pFile(CFileFactory::CreateLoader(url)); + if (!pFile) + return false; + + return pFile->Exists(authUrl); + } + XBMCCOMMONS_HANDLE_UNCHECKED + catch (CRedirectException *pRedirectEx) + { + // the file implementation decided this item should use a different implementation. + // the exception will contain the new implementation and optional a redirected URL. + CLog::Log(LOGDEBUG, "File::Exists - redirecting implementation for {}", file.GetRedacted()); + if (pRedirectEx && pRedirectEx->m_pNewFileImp) + { + std::unique_ptr<IFile> pImp(pRedirectEx->m_pNewFileImp); + std::unique_ptr<CURL> pNewUrl(pRedirectEx->m_pNewUrl); + delete pRedirectEx; + + if (pImp) + { + if (pNewUrl) + { + if (bUseCache) + { + bool bPathInCache; + if (g_directoryCache.FileExists(pNewUrl->Get(), bPathInCache)) + return true; + if (bPathInCache) + return false; + } + CURL newAuthUrl = *pNewUrl; + if (CPasswordManager::GetInstance().IsURLSupported(newAuthUrl) && newAuthUrl.GetUserName().empty()) + CPasswordManager::GetInstance().AuthenticateURL(newAuthUrl); + + return pImp->Exists(newAuthUrl); + } + else + { + return pImp->Exists(authUrl); + } + } + } + } + catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); } + CLog::Log(LOGERROR, "{} - Error checking for {}", __FUNCTION__, file.GetRedacted()); + return false; +} + +int CFile::Stat(struct __stat64 *buffer) +{ + if (!buffer) + return -1; + + if (!m_pFile) + { + *buffer = {}; + errno = ENOENT; + return -1; + } + + return m_pFile->Stat(buffer); +} + +int CFile::Stat(const std::string& strFileName, struct __stat64* buffer) +{ + const CURL pathToUrl(strFileName); + return Stat(pathToUrl, buffer); +} + +int CFile::Stat(const CURL& file, struct __stat64* buffer) +{ + if (!buffer) + return -1; + + CURL url(URIUtils::SubstitutePath(file)); + CURL authUrl = url; + if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty()) + CPasswordManager::GetInstance().AuthenticateURL(authUrl); + + try + { + std::unique_ptr<IFile> pFile(CFileFactory::CreateLoader(url)); + if (!pFile) + return -1; + return pFile->Stat(authUrl, buffer); + } + XBMCCOMMONS_HANDLE_UNCHECKED + catch (CRedirectException *pRedirectEx) + { + // the file implementation decided this item should use a different implementation. + // the exception will contain the new implementation and optional a redirected URL. + CLog::Log(LOGDEBUG, "File::Stat - redirecting implementation for {}", file.GetRedacted()); + if (pRedirectEx && pRedirectEx->m_pNewFileImp) + { + std::unique_ptr<IFile> pImp(pRedirectEx->m_pNewFileImp); + std::unique_ptr<CURL> pNewUrl(pRedirectEx->m_pNewUrl); + delete pRedirectEx; + + if (pNewUrl) + { + if (pImp) + { + CURL newAuthUrl = *pNewUrl; + if (CPasswordManager::GetInstance().IsURLSupported(newAuthUrl) && newAuthUrl.GetUserName().empty()) + CPasswordManager::GetInstance().AuthenticateURL(newAuthUrl); + + if (!pImp->Stat(newAuthUrl, buffer)) + { + return 0; + } + } + } + else + { + if (pImp.get() && !pImp->Stat(authUrl, buffer)) + { + return 0; + } + } + } + } + catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); } + CLog::Log(LOGERROR, "{} - Error statting {}", __FUNCTION__, file.GetRedacted()); + return -1; +} + +ssize_t CFile::Read(void *lpBuf, size_t uiBufSize) +{ + if (!m_pFile) + return -1; + if (lpBuf == NULL && uiBufSize != 0) + return -1; + + if (uiBufSize > SSIZE_MAX) + uiBufSize = SSIZE_MAX; + + if (uiBufSize == 0) + { + // "test" read with zero size + // some VFSs don't handle correctly null buffer pointer + // provide valid buffer pointer for them + char dummy; + return m_pFile->Read(&dummy, 0); + } + + if(m_pBuffer) + { + if(m_flags & READ_TRUNCATED) + { + const ssize_t nBytes = m_pBuffer->sgetn( + (char *)lpBuf, std::min<std::streamsize>((std::streamsize)uiBufSize, + m_pBuffer->in_avail())); + if (m_bitStreamStats && nBytes>0) + m_bitStreamStats->AddSampleBytes(nBytes); + return nBytes; + } + else + { + const ssize_t nBytes = m_pBuffer->sgetn((char*)lpBuf, uiBufSize); + if (m_bitStreamStats && nBytes>0) + m_bitStreamStats->AddSampleBytes(nBytes); + return nBytes; + } + } + + try + { + if(m_flags & READ_TRUNCATED) + { + const ssize_t nBytes = m_pFile->Read(lpBuf, uiBufSize); + if (m_bitStreamStats && nBytes>0) + m_bitStreamStats->AddSampleBytes(nBytes); + return nBytes; + } + else + { + ssize_t done = 0; + while((uiBufSize-done) > 0) + { + const ssize_t curr = m_pFile->Read((char*)lpBuf+done, uiBufSize-done); + if (curr <= 0) + { + if (curr < 0 && done == 0) + return -1; + + break; + } + done+=curr; + } + if (m_bitStreamStats && done > 0) + m_bitStreamStats->AddSampleBytes(done); + return done; + } + } + XBMCCOMMONS_HANDLE_UNCHECKED + catch(...) + { + CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); + return -1; + } + return 0; +} + +//********************************************************************************************* +void CFile::Close() +{ + try + { + if (m_pFile) + m_pFile->Close(); + + m_pBuffer.reset(); + m_pFile.reset(); + } + XBMCCOMMONS_HANDLE_UNCHECKED + catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); } +} + +void CFile::Flush() +{ + try + { + if (m_pFile) + m_pFile->Flush(); + } + XBMCCOMMONS_HANDLE_UNCHECKED + catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); } +} + +//********************************************************************************************* +int64_t CFile::Seek(int64_t iFilePosition, int iWhence) +{ + if (!m_pFile) + return -1; + + if (m_pBuffer) + { + if(iWhence == SEEK_CUR) + return m_pBuffer->pubseekoff(iFilePosition, std::ios_base::cur); + else if(iWhence == SEEK_END) + return m_pBuffer->pubseekoff(iFilePosition, std::ios_base::end); + else if(iWhence == SEEK_SET) + return m_pBuffer->pubseekoff(iFilePosition, std::ios_base::beg); + } + + try + { + return m_pFile->Seek(iFilePosition, iWhence); + } + XBMCCOMMONS_HANDLE_UNCHECKED + catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); } + return -1; +} + +//********************************************************************************************* +int CFile::Truncate(int64_t iSize) +{ + if (!m_pFile) + return -1; + + try + { + return m_pFile->Truncate(iSize); + } + XBMCCOMMONS_HANDLE_UNCHECKED + catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); } + return -1; +} + +//********************************************************************************************* +int64_t CFile::GetLength() +{ + try + { + if (m_pFile) + return m_pFile->GetLength(); + return 0; + } + XBMCCOMMONS_HANDLE_UNCHECKED + catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); } + return 0; +} + +//********************************************************************************************* +int64_t CFile::GetPosition() const +{ + if (!m_pFile) + return -1; + + if (m_pBuffer) + return m_pBuffer->pubseekoff(0, std::ios_base::cur); + + try + { + return m_pFile->GetPosition(); + } + XBMCCOMMONS_HANDLE_UNCHECKED + catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); } + return -1; +} + + +//********************************************************************************************* +bool CFile::ReadString(char *szLine, int iLineLength) +{ + if (!m_pFile || !szLine) + return false; + + if (m_pBuffer) + { + typedef CFileStreamBuffer::traits_type traits; + CFileStreamBuffer::int_type aByte = m_pBuffer->sgetc(); + + if(aByte == traits::eof()) + return false; + + while(iLineLength>0) + { + aByte = m_pBuffer->sbumpc(); + + if(aByte == traits::eof()) + break; + + if(aByte == traits::to_int_type('\n')) + { + if(m_pBuffer->sgetc() == traits::to_int_type('\r')) + m_pBuffer->sbumpc(); + break; + } + + if(aByte == traits::to_int_type('\r')) + { + if(m_pBuffer->sgetc() == traits::to_int_type('\n')) + m_pBuffer->sbumpc(); + break; + } + + *szLine = traits::to_char_type(aByte); + szLine++; + iLineLength--; + } + + // if we have no space for terminating character we failed + if(iLineLength==0) + return false; + + *szLine = 0; + + return true; + } + + try + { + return m_pFile->ReadString(szLine, iLineLength); + } + XBMCCOMMONS_HANDLE_UNCHECKED + catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); } + return false; +} + +ssize_t CFile::Write(const void* lpBuf, size_t uiBufSize) +{ + if (!m_pFile) + return -1; + if (lpBuf == NULL && uiBufSize != 0) + return -1; + + try + { + if (uiBufSize == 0 && lpBuf == NULL) + { // "test" write with zero size + // some VFSs don't handle correctly null buffer pointer + // provide valid buffer pointer for them + const char dummyBuf = 0; + return m_pFile->Write(&dummyBuf, 0); + } + + return m_pFile->Write(lpBuf, uiBufSize); + } + XBMCCOMMONS_HANDLE_UNCHECKED + catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); } + return -1; +} + +bool CFile::Delete(const std::string& strFileName) +{ + const CURL pathToUrl(strFileName); + return Delete(pathToUrl); +} + +bool CFile::Delete(const CURL& file) +{ + try + { + CURL url(URIUtils::SubstitutePath(file)); + CURL authUrl = url; + if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty()) + CPasswordManager::GetInstance().AuthenticateURL(authUrl); + + std::unique_ptr<IFile> pFile(CFileFactory::CreateLoader(url)); + if (!pFile) + return false; + + if(pFile->Delete(authUrl)) + { + g_directoryCache.ClearFile(url.Get()); + return true; + } + } + XBMCCOMMONS_HANDLE_UNCHECKED + catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); } + if (Exists(file)) + CLog::Log(LOGERROR, "{} - Error deleting file {}", __FUNCTION__, file.GetRedacted()); + return false; +} + +bool CFile::Rename(const std::string& strFileName, const std::string& strNewFileName) +{ + const CURL pathToUrl(strFileName); + const CURL pathToUrlNew(strNewFileName); + return Rename(pathToUrl, pathToUrlNew); +} + +bool CFile::Rename(const CURL& file, const CURL& newFile) +{ + try + { + CURL url(URIUtils::SubstitutePath(file)); + CURL urlnew(URIUtils::SubstitutePath(newFile)); + + CURL authUrl = url; + if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty()) + CPasswordManager::GetInstance().AuthenticateURL(authUrl); + CURL authUrlNew = urlnew; + if (CPasswordManager::GetInstance().IsURLSupported(authUrlNew) && authUrlNew.GetUserName().empty()) + CPasswordManager::GetInstance().AuthenticateURL(authUrlNew); + + std::unique_ptr<IFile> pFile(CFileFactory::CreateLoader(url)); + if (!pFile) + return false; + + if(pFile->Rename(authUrl, authUrlNew)) + { + g_directoryCache.ClearFile(url.Get()); + g_directoryCache.AddFile(urlnew.Get()); + return true; + } + } + XBMCCOMMONS_HANDLE_UNCHECKED + catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception ", __FUNCTION__); } + CLog::Log(LOGERROR, "{} - Error renaming file {}", __FUNCTION__, file.GetRedacted()); + return false; +} + +bool CFile::SetHidden(const std::string& fileName, bool hidden) +{ + const CURL pathToUrl(fileName); + return SetHidden(pathToUrl, hidden); +} + +bool CFile::SetHidden(const CURL& file, bool hidden) +{ + try + { + CURL url(URIUtils::SubstitutePath(file)); + CURL authUrl = url; + if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty()) + CPasswordManager::GetInstance().AuthenticateURL(authUrl); + + std::unique_ptr<IFile> pFile(CFileFactory::CreateLoader(url)); + if (!pFile) + return false; + + return pFile->SetHidden(authUrl, hidden); + } + catch(...) + { + CLog::Log(LOGERROR, "{}({}) - Unhandled exception", __FUNCTION__, file.GetRedacted()); + } + return false; +} + +int CFile::IoControl(EIoControl request, void* param) +{ + int result = -1; + if (!m_pFile) + return -1; + result = m_pFile->IoControl(request, param); + + if(result == -1 && request == IOCTRL_SEEK_POSSIBLE) + { + if(m_pFile->GetLength() >= 0 && m_pFile->Seek(0, SEEK_CUR) >= 0) + return 1; + else + return 0; + } + + return result; +} + +int CFile::GetChunkSize() +{ + if (m_pFile) + return m_pFile->GetChunkSize(); + return 0; +} + +const std::string CFile::GetProperty(XFILE::FileProperty type, const std::string &name) const +{ + if (!m_pFile) + return ""; + return m_pFile->GetProperty(type, name); +} + +const std::vector<std::string> CFile::GetPropertyValues(XFILE::FileProperty type, const std::string &name) const +{ + if (!m_pFile) + { + return std::vector<std::string>(); + } + return m_pFile->GetPropertyValues(type, name); +} + +ssize_t CFile::LoadFile(const std::string& filename, std::vector<uint8_t>& outputBuffer) +{ + const CURL pathToUrl(filename); + return LoadFile(pathToUrl, outputBuffer); +} + +ssize_t CFile::LoadFile(const CURL& file, std::vector<uint8_t>& outputBuffer) +{ + static const size_t max_file_size = 0x7FFFFFFF; + static const size_t min_chunk_size = 64 * 1024U; + static const size_t max_chunk_size = 2048 * 1024U; + + outputBuffer.clear(); + + if (!Open(file, READ_TRUNCATED)) + return 0; + + /* + GetLength() will typically return values that fall into three cases: + 1. The real filesize. This is the typical case. + 2. Zero. This is the case for some http:// streams for example. + 3. Some value smaller than the real filesize. This is the case for an expanding file. + + In order to handle all three cases, we read the file in chunks, relying on Read() + returning 0 at EOF. To minimize (re)allocation of the buffer, the chunksize in + cases 1 and 3 is set to one byte larger than the value returned by GetLength(). + The chunksize in case 2 is set to the lowest value larger than min_chunk_size aligned + to GetChunkSize(). + + We fill the buffer entirely before reallocation. Thus, reallocation never occurs in case 1 + as the buffer is larger than the file, so we hit EOF before we hit the end of buffer. + + To minimize reallocation, we double the chunksize each read while chunksize is lower + than max_chunk_size. + */ + int64_t filesize = GetLength(); + if (filesize > (int64_t)max_file_size) + return 0; /* file is too large for this function */ + + size_t chunksize = (filesize > 0) ? static_cast<size_t>(filesize + 1) + : static_cast<size_t>(DetermineChunkSize(GetChunkSize(), + min_chunk_size)); + size_t total_read = 0; + while (true) + { + if (total_read == outputBuffer.size()) + { // (re)alloc + if (outputBuffer.size() + chunksize > max_file_size) + { + outputBuffer.clear(); + return -1; + } + outputBuffer.resize(outputBuffer.size() + chunksize); + if (chunksize < max_chunk_size) + chunksize *= 2; + } + ssize_t read = Read(outputBuffer.data() + total_read, outputBuffer.size() - total_read); + if (read < 0) + { + outputBuffer.clear(); + return -1; + } + total_read += read; + if (!read) + break; + } + + outputBuffer.resize(total_read); + + return total_read; +} + +double CFile::GetDownloadSpeed() +{ + if (m_pFile) + return m_pFile->GetDownloadSpeed(); + return 0.0; +} + +//********************************************************************************************* +//*************** Stream IO for CFile objects ************************************************* +//********************************************************************************************* +CFileStreamBuffer::~CFileStreamBuffer() +{ + sync(); + Detach(); +} + +CFileStreamBuffer::CFileStreamBuffer(int backsize) + : std::streambuf() + , m_file(NULL) + , m_buffer(NULL) + , m_backsize(backsize) +{ +} + +void CFileStreamBuffer::Attach(IFile *file) +{ + m_file = file; + + m_frontsize = CFile::DetermineChunkSize(m_file->GetChunkSize(), 64 * 1024); + + m_buffer = new char[m_frontsize+m_backsize]; + setg(0,0,0); + setp(0,0); +} + +void CFileStreamBuffer::Detach() +{ + setg(0,0,0); + setp(0,0); + delete[] m_buffer; + m_buffer = NULL; +} + +CFileStreamBuffer::int_type CFileStreamBuffer::underflow() +{ + if(gptr() < egptr()) + return traits_type::to_int_type(*gptr()); + + if(!m_file) + return traits_type::eof(); + + size_t backsize = 0; + if(m_backsize) + { + backsize = (size_t)std::min<ptrdiff_t>((ptrdiff_t)m_backsize, egptr()-eback()); + memmove(m_buffer, egptr()-backsize, backsize); + } + + ssize_t size = m_file->Read(m_buffer+backsize, m_frontsize); + + if (size == 0) + return traits_type::eof(); + else if (size < 0) + { + CLog::LogF(LOGWARNING, "Error reading file - assuming eof"); + return traits_type::eof(); + } + + setg(m_buffer, m_buffer+backsize, m_buffer+backsize+size); + return traits_type::to_int_type(*gptr()); +} + +CFileStreamBuffer::pos_type CFileStreamBuffer::seekoff( + off_type offset, + std::ios_base::seekdir way, + std::ios_base::openmode mode) +{ + // calculate relative offset + off_type aheadbytes = (egptr() - gptr()); + off_type pos = m_file->GetPosition() - aheadbytes; + off_type offset2; + if(way == std::ios_base::cur) + offset2 = offset; + else if(way == std::ios_base::beg) + offset2 = offset - pos; + else if(way == std::ios_base::end) + offset2 = offset + m_file->GetLength() - pos; + else + return std::streampos(-1); + + // a non seek shouldn't modify our buffer + if(offset2 == 0) + return pos; + + // try to seek within buffer + if(gptr()+offset2 >= eback() && gptr()+offset2 < egptr()) + { + gbump(offset2); + return pos + offset2; + } + + // reset our buffer pointer, will + // start buffering on next read + setg(0,0,0); + setp(0,0); + + int64_t position = -1; + if(way == std::ios_base::cur) + position = m_file->Seek(offset - aheadbytes, SEEK_CUR); + else if(way == std::ios_base::end) + position = m_file->Seek(offset, SEEK_END); + else + position = m_file->Seek(offset, SEEK_SET); + + if(position<0) + return std::streampos(-1); + + return position; +} + +CFileStreamBuffer::pos_type CFileStreamBuffer::seekpos( + pos_type pos, + std::ios_base::openmode mode) +{ + return seekoff(pos, std::ios_base::beg, mode); +} + +std::streamsize CFileStreamBuffer::showmanyc() +{ + underflow(); + return egptr() - gptr(); +} + +CFileStream::CFileStream(int backsize /*= 0*/) : std::istream(&m_buffer), m_buffer(backsize) +{ +} + +CFileStream::~CFileStream() +{ + Close(); +} + + +bool CFileStream::Open(const CURL& filename) +{ + Close(); + + CURL url(URIUtils::SubstitutePath(filename)); + m_file.reset(CFileFactory::CreateLoader(url)); + + CURL authUrl = url; + if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty()) + CPasswordManager::GetInstance().AuthenticateURL(authUrl); + + if(m_file && m_file->Open(authUrl)) + { + m_buffer.Attach(m_file.get()); + return true; + } + + setstate(failbit); + return false; +} + +int64_t CFileStream::GetLength() +{ + return m_file->GetLength(); +} + +void CFileStream::Close() +{ + if(!m_file) + return; + + m_buffer.Detach(); +} + +bool CFileStream::Open(const std::string& filename) +{ + const CURL pathToUrl(filename); + return Open(pathToUrl); +} diff --git a/xbmc/filesystem/File.h b/xbmc/filesystem/File.h new file mode 100644 index 0000000..a5f991a --- /dev/null +++ b/xbmc/filesystem/File.h @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2002 Frodo + * Portions Copyright (c) by the authors of ffmpeg and xvid + * Copyright (C) 2002-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +// File.h: interface for the CFile class. +// +////////////////////////////////////////////////////////////////////// + +#include "IFileTypes.h" +#include "URL.h" + +#include <iostream> +#include <memory> +#include <stdio.h> +#include <string> +#include <vector> + +#include "PlatformDefs.h" + +class BitstreamStats; + +namespace XFILE +{ + +class IFile; + +class CFileStreamBuffer; + +class CFile +{ +public: + CFile(); + ~CFile(); + + bool CURLCreate(const std::string &url); + bool CURLAddOption(XFILE::CURLOPTIONTYPE type, const char* name, const char * value); + bool CURLOpen(unsigned int flags); + + /** + * Attempt to open an IFile instance. + * @param file reference to CCurl file description + * @param flags see IFileTypes.h + * @return true on success, false otherwise + * + * Remarks: Open can only be called once. Calling + * Open() on an already opened file will fail + * except if flag READ_REOPEN is set and the underlying + * file has an implementation of ReOpen(). + */ + bool Open(const CURL& file, const unsigned int flags = 0); + bool Open(const std::string& strFileName, const unsigned int flags = 0); + + bool OpenForWrite(const CURL& file, bool bOverWrite = false); + bool OpenForWrite(const std::string& strFileName, bool bOverWrite = false); + + ssize_t LoadFile(const CURL& file, std::vector<uint8_t>& outputBuffer); + + /** + * Attempt to read bufSize bytes from currently opened file into buffer bufPtr. + * @param bufPtr pointer to buffer + * @param bufSize size of the buffer + * @return number of successfully read bytes if any bytes were read and stored in + * buffer, zero if no bytes are available to read (end of file was reached) + * or undetectable error occur, -1 in case of any explicit error + */ + ssize_t Read(void* bufPtr, size_t bufSize); + bool ReadString(char *szLine, int iLineLength); + /** + * Attempt to write bufSize bytes from buffer bufPtr into currently opened file. + * @param bufPtr pointer to buffer + * @param bufSize size of the buffer + * @return number of successfully written bytes if any bytes were written, + * zero if no bytes were written and no detectable error occur, + * -1 in case of any explicit error + */ + ssize_t Write(const void* bufPtr, size_t bufSize); + void Flush(); + int64_t Seek(int64_t iFilePosition, int iWhence = SEEK_SET); + int Truncate(int64_t iSize); + int64_t GetPosition() const; + int64_t GetLength(); + void Close(); + int GetChunkSize(); + const std::string GetProperty(XFILE::FileProperty type, const std::string &name = "") const; + const std::vector<std::string> GetPropertyValues(XFILE::FileProperty type, const std::string &name = "") const; + ssize_t LoadFile(const std::string& filename, std::vector<uint8_t>& outputBuffer); + + static int DetermineChunkSize(const int srcChunkSize, const int reqChunkSize); + + const std::unique_ptr<BitstreamStats>& GetBitstreamStats() const { return m_bitStreamStats; } + + int IoControl(EIoControl request, void* param); + + IFile* GetImplementation() const { return m_pFile.get(); } + + // CURL interface + static bool Exists(const CURL& file, bool bUseCache = true); + static bool Delete(const CURL& file); + /** + * Fills struct __stat64 with information about file specified by filename + * For st_mode function will set correctly _S_IFDIR (directory) flag and may set + * _S_IREAD (read permission), _S_IWRITE (write permission) flags if such + * information is available. Function may set st_size (file size), st_atime, + * st_mtime, st_ctime (access, modification, creation times). + * Any other flags and members of __stat64 that didn't updated with actual file + * information will be set to zero (st_nlink can be set ether to 1 or zero). + * @param file specifies requested file + * @param buffer pointer to __stat64 buffer to receive information about file + * @return zero of success, -1 otherwise. + */ + static int Stat(const CURL& file, struct __stat64* buffer); + static bool Rename(const CURL& file, const CURL& urlNew); + static bool Copy(const CURL& file, const CURL& dest, XFILE::IFileCallback* pCallback = NULL, void* pContext = NULL); + static bool SetHidden(const CURL& file, bool hidden); + + // string interface + static bool Exists(const std::string& strFileName, bool bUseCache = true); + /** + * Fills struct __stat64 with information about file specified by filename + * For st_mode function will set correctly _S_IFDIR (directory) flag and may set + * _S_IREAD (read permission), _S_IWRITE (write permission) flags if such + * information is available. Function may set st_size (file size), st_atime, + * st_mtime, st_ctime (access, modification, creation times). + * Any other flags and members of __stat64 that didn't updated with actual file + * information will be set to zero (st_nlink can be set ether to 1 or zero). + * @param strFileName specifies requested file + * @param buffer pointer to __stat64 buffer to receive information about file + * @return zero of success, -1 otherwise. + */ + static int Stat(const std::string& strFileName, struct __stat64* buffer); + /** + * Fills struct __stat64 with information about currently open file + * For st_mode function will set correctly _S_IFDIR (directory) flag and may set + * _S_IREAD (read permission), _S_IWRITE (write permission) flags if such + * information is available. Function may set st_size (file size), st_atime, + * st_mtime, st_ctime (access, modification, creation times). + * Any other flags and members of __stat64 that didn't updated with actual file + * information will be set to zero (st_nlink can be set ether to 1 or zero). + * @param buffer pointer to __stat64 buffer to receive information about file + * @return zero of success, -1 otherwise. + */ + int Stat(struct __stat64 *buffer); + static bool Delete(const std::string& strFileName); + static bool Rename(const std::string& strFileName, const std::string& strNewFileName); + static bool Copy(const std::string& strFileName, const std::string& strDest, XFILE::IFileCallback* pCallback = NULL, void* pContext = NULL); + static bool SetHidden(const std::string& fileName, bool hidden); + double GetDownloadSpeed(); + +private: + unsigned int m_flags = 0; + CURL m_curl; + std::unique_ptr<IFile> m_pFile; + std::unique_ptr<CFileStreamBuffer> m_pBuffer; + std::unique_ptr<BitstreamStats> m_bitStreamStats; +}; + +// streambuf for file io, only supports buffered input currently +class CFileStreamBuffer + : public std::streambuf +{ +public: + ~CFileStreamBuffer() override; + explicit CFileStreamBuffer(int backsize = 0); + + void Attach(IFile *file); + void Detach(); + +private: + int_type underflow() override; + std::streamsize showmanyc() override; + pos_type seekoff(off_type, std::ios_base::seekdir,std::ios_base::openmode = std::ios_base::in | std::ios_base::out) override; + pos_type seekpos(pos_type, std::ios_base::openmode = std::ios_base::in | std::ios_base::out) override; + + IFile* m_file; + char* m_buffer; + int m_backsize; + int m_frontsize = 0; +}; + +// very basic file input stream +class CFileStream + : public std::istream +{ +public: + explicit CFileStream(int backsize = 0); + ~CFileStream() override; + + bool Open(const std::string& filename); + bool Open(const CURL& filename); + void Close(); + + int64_t GetLength(); +private: + CFileStreamBuffer m_buffer; + std::unique_ptr<IFile> m_file; +}; + +} diff --git a/xbmc/filesystem/FileCache.cpp b/xbmc/filesystem/FileCache.cpp new file mode 100644 index 0000000..4f2e07b --- /dev/null +++ b/xbmc/filesystem/FileCache.cpp @@ -0,0 +1,630 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "FileCache.h" + +#include "CircularCache.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "threads/Thread.h" +#include "utils/log.h" + +#include <mutex> + +#if !defined(TARGET_WINDOWS) +#include "platform/posix/ConvUtils.h" +#endif + +#include <algorithm> +#include <cassert> +#include <chrono> +#include <inttypes.h> +#include <memory> + +#ifdef TARGET_POSIX +#include "platform/posix/ConvUtils.h" +#endif + +using namespace XFILE; +using namespace std::chrono_literals; + +class CWriteRate +{ +public: + CWriteRate() + { + m_stamp = std::chrono::steady_clock::now(); + m_pos = 0; + m_size = 0; + m_time = std::chrono::milliseconds(0); + } + + void Reset(int64_t pos, bool bResetAll = true) + { + m_stamp = std::chrono::steady_clock::now(); + m_pos = pos; + + if (bResetAll) + { + m_size = 0; + m_time = std::chrono::milliseconds(0); + } + } + + uint32_t Rate(int64_t pos, uint32_t time_bias = 0) + { + auto ts = std::chrono::steady_clock::now(); + + m_size += (pos - m_pos); + m_time += std::chrono::duration_cast<std::chrono::milliseconds>(ts - m_stamp); + m_pos = pos; + m_stamp = ts; + + if (m_time == std::chrono::milliseconds(0)) + return 0; + + return static_cast<uint32_t>(1000 * (m_size / (m_time.count() + time_bias))); + } + +private: + std::chrono::time_point<std::chrono::steady_clock> m_stamp; + int64_t m_pos; + std::chrono::milliseconds m_time; + int64_t m_size; +}; + + +CFileCache::CFileCache(const unsigned int flags) + : CThread("FileCache"), + m_seekPossible(0), + m_nSeekResult(0), + m_seekPos(0), + m_readPos(0), + m_writePos(0), + m_chunkSize(0), + m_writeRate(0), + m_writeRateActual(0), + m_writeRateLowSpeed(0), + m_forwardCacheSize(0), + m_bFilling(false), + m_fileSize(0), + m_flags(flags) +{ +} + +CFileCache::~CFileCache() +{ + Close(); +} + +IFile *CFileCache::GetFileImp() +{ + return m_source.GetImplementation(); +} + +bool CFileCache::Open(const CURL& url) +{ + Close(); + + std::unique_lock<CCriticalSection> lock(m_sync); + + m_sourcePath = url.GetRedacted(); + + CLog::Log(LOGDEBUG, "CFileCache::{} - <{}> opening", __FUNCTION__, m_sourcePath); + + // opening the source file. + if (!m_source.Open(url.Get(), READ_NO_CACHE | READ_TRUNCATED | READ_CHUNKED)) + { + CLog::Log(LOGERROR, "CFileCache::{} - <{}> failed to open", __FUNCTION__, m_sourcePath); + Close(); + return false; + } + + m_source.IoControl(IOCTRL_SET_CACHE, this); + + bool retry = false; + m_source.IoControl(IOCTRL_SET_RETRY, &retry); // We already handle retrying ourselves + + // check if source can seek + m_seekPossible = m_source.IoControl(IOCTRL_SEEK_POSSIBLE, NULL); + + // Determine the best chunk size we can use + m_chunkSize = CFile::DetermineChunkSize( + m_source.GetChunkSize(), + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cacheChunkSize); + CLog::Log(LOGDEBUG, + "CFileCache::{} - <{}> source chunk size is {}, setting cache chunk size to {}", + __FUNCTION__, m_sourcePath, m_source.GetChunkSize(), m_chunkSize); + + m_fileSize = m_source.GetLength(); + + if (!m_pCache) + { + if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cacheMemSize == 0) + { + // Use cache on disk + m_pCache = std::unique_ptr<CSimpleFileCache>(new CSimpleFileCache()); // C++14 - Replace with std::make_unique + m_forwardCacheSize = 0; + } + else + { + size_t cacheSize; + if (m_fileSize > 0 && m_fileSize < CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cacheMemSize && !(m_flags & READ_AUDIO_VIDEO)) + { + // Cap cache size by filesize, but not for audio/video files as those may grow. + // We don't need to take into account READ_MULTI_STREAM here as that's only used for audio/video + cacheSize = m_fileSize; + + // Cap chunk size by cache size + if (m_chunkSize > cacheSize) + m_chunkSize = cacheSize; + } + else + { + cacheSize = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cacheMemSize; + + // NOTE: READ_MULTI_STREAM is only used with READ_AUDIO_VIDEO + if (m_flags & READ_MULTI_STREAM) + { + // READ_MULTI_STREAM requires double buffering, so use half the amount of memory for each buffer + cacheSize /= 2; + } + + // Make sure cache can at least hold 2 chunks + if (cacheSize < m_chunkSize * 2) + cacheSize = m_chunkSize * 2; + } + + if (m_flags & READ_MULTI_STREAM) + CLog::Log(LOGDEBUG, "CFileCache::{} - <{}> using double memory cache each sized {} bytes", + __FUNCTION__, m_sourcePath, cacheSize); + else + CLog::Log(LOGDEBUG, "CFileCache::{} - <{}> using single memory cache sized {} bytes", + __FUNCTION__, m_sourcePath, cacheSize); + + const size_t back = cacheSize / 4; + const size_t front = cacheSize - back; + + m_pCache = std::unique_ptr<CCircularCache>(new CCircularCache(front, back)); // C++14 - Replace with std::make_unique + m_forwardCacheSize = front; + } + + if (m_flags & READ_MULTI_STREAM) + { + // If READ_MULTI_STREAM flag is set: Double buffering is required + m_pCache = std::unique_ptr<CDoubleCache>(new CDoubleCache(m_pCache.release())); // C++14 - Replace with std::make_unique + } + } + + // open cache strategy + if (!m_pCache || m_pCache->Open() != CACHE_RC_OK) + { + CLog::Log(LOGERROR, "CFileCache::{} - <{}> failed to open cache", __FUNCTION__, m_sourcePath); + Close(); + return false; + } + + m_readPos = 0; + m_writePos = 0; + m_writeRate = 1024 * 1024; + m_writeRateActual = 0; + m_writeRateLowSpeed = 0; + m_bFilling = true; + m_seekEvent.Reset(); + m_seekEnded.Reset(); + + CThread::Create(false); + + return true; +} + +void CFileCache::Process() +{ + if (!m_pCache) + { + CLog::Log(LOGERROR, "CFileCache::{} - <{}> sanity failed. no cache strategy", __FUNCTION__, + m_sourcePath); + return; + } + + // create our read buffer + std::unique_ptr<char[]> buffer(new char[m_chunkSize]); + if (buffer == nullptr) + { + CLog::Log(LOGERROR, "CFileCache::{} - <{}> failed to allocate read buffer", __FUNCTION__, + m_sourcePath); + return; + } + + CWriteRate limiter; + CWriteRate average; + + while (!m_bStop) + { + // Update filesize + m_fileSize = m_source.GetLength(); + + // check for seek events + if (m_seekEvent.Wait(0ms)) + { + m_seekEvent.Reset(); + const int64_t cacheMaxPos = m_pCache->CachedDataEndPosIfSeekTo(m_seekPos); + const bool cacheReachEOF = (cacheMaxPos == m_fileSize); + + bool sourceSeekFailed = false; + if (!cacheReachEOF) + { + m_nSeekResult = m_source.Seek(cacheMaxPos, SEEK_SET); + if (m_nSeekResult != cacheMaxPos) + { + CLog::Log(LOGERROR, "CFileCache::{} - <{}> error {} seeking. Seek returned {}", + __FUNCTION__, m_sourcePath, GetLastError(), m_nSeekResult); + m_seekPossible = m_source.IoControl(IOCTRL_SEEK_POSSIBLE, NULL); + sourceSeekFailed = true; + } + } + + if (!sourceSeekFailed) + { + const bool bCompleteReset = m_pCache->Reset(m_seekPos); + m_readPos = m_seekPos; + m_writePos = m_pCache->CachedDataEndPos(); + assert(m_writePos == cacheMaxPos); + average.Reset(m_writePos, bCompleteReset); // Can only recalculate new average from scratch after a full reset (empty cache) + limiter.Reset(m_writePos); + m_nSeekResult = m_seekPos; + if (bCompleteReset) + { + CLog::Log(LOGDEBUG, + "CFileCache::{} - <{}> cache completely reset for seek to position {}", + __FUNCTION__, m_sourcePath, m_seekPos); + m_bFilling = true; + m_writeRateLowSpeed = 0; + } + } + + m_seekEnded.Set(); + } + + while (m_writeRate) + { + if (m_writePos - m_readPos < m_writeRate * CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cacheReadFactor) + { + limiter.Reset(m_writePos); + break; + } + + if (limiter.Rate(m_writePos) < m_writeRate * CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cacheReadFactor) + break; + + if (m_seekEvent.Wait(100ms)) + { + if (!m_bStop) + m_seekEvent.Set(); + break; + } + } + + const int64_t maxWrite = m_pCache->GetMaxWriteSize(m_chunkSize); + int64_t maxSourceRead = m_chunkSize; + // Cap source read size by space available between current write position and EOF + if (m_fileSize != 0) + maxSourceRead = std::min(maxSourceRead, m_fileSize - m_writePos); + + /* Only read from source if there's enough write space in the cache + * else we may keep disposing data and seeking back on (slow) source + */ + if (maxWrite < maxSourceRead) + { + // Wait until sufficient cache write space is available + m_pCache->m_space.Wait(5ms); + continue; + } + + ssize_t iRead = 0; + if (maxSourceRead > 0) + iRead = m_source.Read(buffer.get(), maxSourceRead); + if (iRead <= 0) + { + // Check for actual EOF and retry as long as we still have data in our cache + if (m_writePos < m_fileSize && m_pCache->WaitForData(0, 0ms) > 0) + { + CLog::Log(LOGWARNING, "CFileCache::{} - <{}> source read returned {}! Will retry", + __FUNCTION__, m_sourcePath, iRead); + + // Wait a bit: + if (m_seekEvent.Wait(2000ms)) + { + if (!m_bStop) + m_seekEvent.Set(); // hack so that later we realize seek is needed + } + + // and retry: + continue; // while (!m_bStop) + } + else + { + if (iRead < 0) + CLog::Log(LOGERROR, + "{} - <{}> source read failed with {}!", __FUNCTION__, m_sourcePath, iRead); + else if (m_fileSize == 0) + CLog::Log(LOGDEBUG, + "CFileCache::{} - <{}> source read didn't return any data! Hit eof(?)", + __FUNCTION__, m_sourcePath); + else if (m_writePos < m_fileSize) + CLog::Log(LOGERROR, + "CFileCache::{} - <{}> source read didn't return any data before eof!", + __FUNCTION__, m_sourcePath); + else + CLog::Log(LOGDEBUG, "CFileCache::{} - <{}> source read hit eof", __FUNCTION__, + m_sourcePath); + + m_pCache->EndOfInput(); + + // The thread event will now also cause the wait of an event to return a false. + if (AbortableWait(m_seekEvent) == WAIT_SIGNALED) + { + m_pCache->ClearEndOfInput(); + if (!m_bStop) + m_seekEvent.Set(); // hack so that later we realize seek is needed + } + else + break; // while (!m_bStop) + } + } + + int iTotalWrite = 0; + while (!m_bStop && (iTotalWrite < iRead)) + { + int iWrite = 0; + iWrite = m_pCache->WriteToCache(buffer.get() + iTotalWrite, iRead - iTotalWrite); + + // write should always work. all handling of buffering and errors should be + // done inside the cache strategy. only if unrecoverable error happened, WriteToCache would return error and we break. + if (iWrite < 0) + { + CLog::Log(LOGERROR, "CFileCache::{} - <{}> error writing to cache", __FUNCTION__, + m_sourcePath); + m_bStop = true; + break; + } + else if (iWrite == 0) + { + m_pCache->m_space.Wait(5ms); + } + + iTotalWrite += iWrite; + + // check if seek was asked. otherwise if cache is full we'll freeze. + if (m_seekEvent.Wait(0ms)) + { + if (!m_bStop) + m_seekEvent.Set(); // make sure we get the seek event later. + break; + } + } + + m_writePos += iTotalWrite; + + // under estimate write rate by a second, to + // avoid uncertainty at start of caching + m_writeRateActual = average.Rate(m_writePos, 1000); + + /* NOTE: We can only reliably test for low speed condition, when the cache is *really* + * filling. This is because as soon as it's full the average- + * rate will become approximately the current-rate which can flag false + * low read-rate conditions. + */ + if (m_bFilling && m_forwardCacheSize != 0) + { + const int64_t forward = m_pCache->WaitForData(0, 0ms); + if (forward + m_chunkSize >= m_forwardCacheSize) + { + if (m_writeRateActual < m_writeRate) + m_writeRateLowSpeed = m_writeRateActual; + + m_bFilling = false; + } + } + } +} + +void CFileCache::OnExit() +{ + m_bStop = true; + + // make sure cache is set to mark end of file (read may be waiting). + if (m_pCache) + m_pCache->EndOfInput(); + + // just in case someone's waiting... + m_seekEnded.Set(); +} + +bool CFileCache::Exists(const CURL& url) +{ + return CFile::Exists(url.Get()); +} + +int CFileCache::Stat(const CURL& url, struct __stat64* buffer) +{ + return CFile::Stat(url.Get(), buffer); +} + +ssize_t CFileCache::Read(void* lpBuf, size_t uiBufSize) +{ + std::unique_lock<CCriticalSection> lock(m_sync); + if (!m_pCache) + { + CLog::Log(LOGERROR, "CFileCache::{} - <{}> sanity failed. no cache strategy!", __FUNCTION__, + m_sourcePath); + return -1; + } + int64_t iRc; + + if (uiBufSize > SSIZE_MAX) + uiBufSize = SSIZE_MAX; + +retry: + // attempt to read + iRc = m_pCache->ReadFromCache((char *)lpBuf, uiBufSize); + if (iRc > 0) + { + m_readPos += iRc; + return (int)iRc; + } + + if (iRc == CACHE_RC_WOULD_BLOCK) + { + // just wait for some data to show up + iRc = m_pCache->WaitForData(1, 10s); + if (iRc > 0) + goto retry; + } + + if (iRc == CACHE_RC_TIMEOUT) + { + CLog::Log(LOGWARNING, "CFileCache::{} - <{}> timeout waiting for data", __FUNCTION__, + m_sourcePath); + return -1; + } + + if (iRc == 0) + return 0; + + // unknown error code + CLog::Log(LOGERROR, "CFileCache::{} - <{}> cache strategy returned unknown error code {}", + __FUNCTION__, m_sourcePath, (int)iRc); + return -1; +} + +int64_t CFileCache::Seek(int64_t iFilePosition, int iWhence) +{ + std::unique_lock<CCriticalSection> lock(m_sync); + + if (!m_pCache) + { + CLog::Log(LOGERROR, "CFileCache::{} - <{}> sanity failed. no cache strategy!", __FUNCTION__, + m_sourcePath); + return -1; + } + + int64_t iCurPos = m_readPos; + int64_t iTarget = iFilePosition; + if (iWhence == SEEK_END) + iTarget = m_fileSize + iTarget; + else if (iWhence == SEEK_CUR) + iTarget = iCurPos + iTarget; + else if (iWhence != SEEK_SET) + return -1; + + if (iTarget == m_readPos) + return m_readPos; + + if ((m_nSeekResult = m_pCache->Seek(iTarget)) != iTarget) + { + if (m_seekPossible == 0) + return m_nSeekResult; + + // Never request closer to end than one chunk. Speeds up tag reading + m_seekPos = std::min(iTarget, std::max((int64_t)0, m_fileSize - m_chunkSize)); + + m_seekEvent.Set(); + while (!m_seekEnded.Wait(100ms)) + { + // SeekEnded will never be set if FileCache thread is not running + if (!CThread::IsRunning()) + return -1; + } + + /* wait for any remaining data */ + if(m_seekPos < iTarget) + { + CLog::Log(LOGDEBUG, "CFileCache::{} - <{}> waiting for position {}", __FUNCTION__, + m_sourcePath, iTarget); + if (m_pCache->WaitForData(static_cast<uint32_t>(iTarget - m_seekPos), 10s) < + iTarget - m_seekPos) + { + CLog::Log(LOGWARNING, "CFileCache::{} - <{}> failed to get remaining data", __FUNCTION__, + m_sourcePath); + return -1; + } + m_pCache->Seek(iTarget); + } + m_readPos = iTarget; + m_seekEvent.Reset(); + } + else + m_readPos = iTarget; + + return iTarget; +} + +void CFileCache::Close() +{ + StopThread(); + + std::unique_lock<CCriticalSection> lock(m_sync); + if (m_pCache) + m_pCache->Close(); + + m_source.Close(); +} + +int64_t CFileCache::GetPosition() +{ + return m_readPos; +} + +int64_t CFileCache::GetLength() +{ + return m_fileSize; +} + +void CFileCache::StopThread(bool bWait /*= true*/) +{ + m_bStop = true; + //Process could be waiting for seekEvent + m_seekEvent.Set(); + CThread::StopThread(bWait); +} + +const std::string CFileCache::GetProperty(XFILE::FileProperty type, const std::string &name) const +{ + if (!m_source.GetImplementation()) + return IFile::GetProperty(type, name); + + return m_source.GetImplementation()->GetProperty(type, name); +} + +int CFileCache::IoControl(EIoControl request, void* param) +{ + if (request == IOCTRL_CACHE_STATUS) + { + SCacheStatus* status = (SCacheStatus*)param; + status->forward = m_pCache->WaitForData(0, 0ms); + status->maxrate = m_writeRate; + status->currate = m_writeRateActual; + status->lowrate = m_writeRateLowSpeed; + m_writeRateLowSpeed = 0; // Reset low speed condition + return 0; + } + + if (request == IOCTRL_CACHE_SETRATE) + { + m_writeRate = *static_cast<uint32_t*>(param); + return 0; + } + + if (request == IOCTRL_SEEK_POSSIBLE) + return m_seekPossible; + + return -1; +} diff --git a/xbmc/filesystem/FileCache.h b/xbmc/filesystem/FileCache.h new file mode 100644 index 0000000..df7c839 --- /dev/null +++ b/xbmc/filesystem/FileCache.h @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "CacheStrategy.h" +#include "File.h" +#include "IFile.h" +#include "threads/CriticalSection.h" +#include "threads/Thread.h" + +#include <atomic> +#include <memory> + +namespace XFILE +{ + + class CFileCache : public IFile, public CThread + { + public: + explicit CFileCache(const unsigned int flags); + ~CFileCache() override; + + // CThread methods + void Process() override; + void OnExit() override; + void StopThread(bool bWait = true) override; + + // IFIle methods + bool Open(const CURL& url) override; + void Close() override; + bool Exists(const CURL& url) override; + int Stat(const CURL& url, struct __stat64* buffer) override; + + ssize_t Read(void* lpBuf, size_t uiBufSize) override; + + int64_t Seek(int64_t iFilePosition, int iWhence) override; + int64_t GetPosition() override; + int64_t GetLength() override; + + int IoControl(EIoControl request, void* param) override; + + IFile *GetFileImp(); + + const std::string GetProperty(XFILE::FileProperty type, const std::string &name = "") const override; + + const std::vector<std::string> GetPropertyValues(XFILE::FileProperty type, const std::string& name = "") const override + { + return std::vector<std::string>(); + } + + private: + std::unique_ptr<CCacheStrategy> m_pCache; + int m_seekPossible; + CFile m_source; + std::string m_sourcePath; + CEvent m_seekEvent; + CEvent m_seekEnded; + int64_t m_nSeekResult; + int64_t m_seekPos; + int64_t m_readPos; + int64_t m_writePos; + unsigned m_chunkSize; + uint32_t m_writeRate; + uint32_t m_writeRateActual; + uint32_t m_writeRateLowSpeed; + int64_t m_forwardCacheSize; + bool m_bFilling; + std::atomic<int64_t> m_fileSize; + unsigned int m_flags; + CCriticalSection m_sync; + }; + +} diff --git a/xbmc/filesystem/FileDirectoryFactory.cpp b/xbmc/filesystem/FileDirectoryFactory.cpp new file mode 100644 index 0000000..a4e0f3f --- /dev/null +++ b/xbmc/filesystem/FileDirectoryFactory.cpp @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "FileDirectoryFactory.h" + +#if defined(HAS_ISO9660PP) +#include "ISO9660Directory.h" +#endif +#if defined(HAS_UDFREAD) +#include "UDFDirectory.h" +#endif +#include "RSSDirectory.h" +#include "UDFDirectory.h" +#include "utils/URIUtils.h" +#if defined(TARGET_ANDROID) +#include "platform/android/filesystem/APKDirectory.h" +#endif +#include "AudioBookFileDirectory.h" +#include "Directory.h" +#include "FileItem.h" +#include "PlaylistFileDirectory.h" +#include "ServiceBroker.h" +#include "SmartPlaylistDirectory.h" +#include "URL.h" +#include "XbtDirectory.h" +#include "ZipDirectory.h" +#include "addons/AudioDecoder.h" +#include "addons/ExtsMimeSupportList.h" +#include "addons/VFSEntry.h" +#include "addons/addoninfo/AddonInfo.h" +#include "playlists/PlayListFactory.h" +#include "playlists/SmartPlayList.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +using namespace ADDON; +using namespace KODI::ADDONS; +using namespace XFILE; +using namespace PLAYLIST; + +CFileDirectoryFactory::CFileDirectoryFactory(void) = default; + +CFileDirectoryFactory::~CFileDirectoryFactory(void) = default; + +// return NULL + set pItem->m_bIsFolder to remove it completely from list. +IFileDirectory* CFileDirectoryFactory::Create(const CURL& url, CFileItem* pItem, const std::string& strMask) +{ + if (url.IsProtocol("stack")) // disqualify stack as we need to work with each of the parts instead + return NULL; + + /** + * Check available binary addons which can contain files with underlaid + * folders / files. + * Currently in vfs and audiodecoder addons. + * + * @note The file extensions are absolutely necessary for these in order to + * identify the associated add-on. + */ + /**@{*/ + + // Get file extensions to find addon related to it. + std::string strExtension = URIUtils::GetExtension(url); + StringUtils::ToLower(strExtension); + + if (!strExtension.empty() && CServiceBroker::IsAddonInterfaceUp()) + { + /*! + * Scan here about audiodecoder addons. + * + * @note: Do not check audio decoder files that are already open, they cannot + * contain any further sub-folders. + */ + if (!StringUtils::EndsWith(strExtension, KODI_ADDON_AUDIODECODER_TRACK_EXT)) + { + auto addonInfos = CServiceBroker::GetExtsMimeSupportList().GetExtensionSupportedAddonInfos( + strExtension, CExtsMimeSupportList::FilterSelect::hasTracks); + for (const auto& addonInfo : addonInfos) + { + std::unique_ptr<CAudioDecoder> result = std::make_unique<CAudioDecoder>(addonInfo.second); + if (!result->CreateDecoder() || !result->ContainsFiles(url)) + { + CLog::Log(LOGINFO, + "CFileDirectoryFactory::{}: Addon '{}' support extension '{}' but creation " + "failed (seems not supported), trying other addons and Kodi", + __func__, addonInfo.second->ID(), strExtension); + continue; + } + return result.release(); + } + } + + /*! + * Scan here about VFS addons. + */ + for (const auto& vfsAddon : CServiceBroker::GetVFSAddonCache().GetAddonInstances()) + { + if (vfsAddon->HasFileDirectories()) + { + auto exts = StringUtils::Split(vfsAddon->GetExtensions(), "|"); + if (std::find(exts.begin(), exts.end(), strExtension) != exts.end()) + { + CVFSEntryIFileDirectoryWrapper* wrap = new CVFSEntryIFileDirectoryWrapper(vfsAddon); + if (wrap->ContainsFiles(url)) + { + if (wrap->m_items.Size() == 1) + { + // one STORED file - collapse it down + *pItem = *wrap->m_items[0]; + } + else + { + // compressed or more than one file -> create a dir + pItem->SetPath(wrap->m_items.GetPath()); + } + + // Check for folder, if yes return also wrap. + // Needed to fix for e.g. RAR files with only one file inside + pItem->m_bIsFolder = URIUtils::HasSlashAtEnd(pItem->GetPath()); + if (pItem->m_bIsFolder) + return wrap; + } + else + { + pItem->m_bIsFolder = true; + } + + delete wrap; + return nullptr; + } + } + } + } + /**@}*/ + + if (pItem->IsRSS()) + return new CRSSDirectory(); + + + if (pItem->IsDiscImage()) + { +#if defined(HAS_ISO9660PP) + CISO9660Directory* iso = new CISO9660Directory(); + if (iso->Exists(pItem->GetURL())) + return iso; + + delete iso; +#endif + +#if defined(HAS_UDFREAD) + return new CUDFDirectory(); +#endif + + return nullptr; + } + +#if defined(TARGET_ANDROID) + if (url.IsFileType("apk")) + { + CURL zipURL = URIUtils::CreateArchivePath("apk", url); + + CFileItemList items; + CDirectory::GetDirectory(zipURL, items, strMask, DIR_FLAG_DEFAULTS); + if (items.Size() == 0) // no files + pItem->m_bIsFolder = true; + else if (items.Size() == 1 && items[0]->m_idepth == 0 && !items[0]->m_bIsFolder) + { + // one STORED file - collapse it down + *pItem = *items[0]; + } + else + { // compressed or more than one file -> create a apk dir + pItem->SetURL(zipURL); + return new CAPKDirectory; + } + return NULL; + } +#endif + if (url.IsFileType("zip")) + { + CURL zipURL = URIUtils::CreateArchivePath("zip", url); + + CFileItemList items; + CDirectory::GetDirectory(zipURL, items, strMask, DIR_FLAG_DEFAULTS); + if (items.Size() == 0) // no files + pItem->m_bIsFolder = true; + else if (items.Size() == 1 && items[0]->m_idepth == 0 && !items[0]->m_bIsFolder) + { + // one STORED file - collapse it down + *pItem = *items[0]; + } + else + { // compressed or more than one file -> create a zip dir + pItem->SetURL(zipURL); + return new CZipDirectory; + } + return NULL; + } + if (url.IsFileType("xbt")) + { + CURL xbtUrl = URIUtils::CreateArchivePath("xbt", url); + pItem->SetURL(xbtUrl); + + return new CXbtDirectory(); + } + if (url.IsFileType("xsp")) + { // XBMC Smart playlist - just XML renamed to XSP + // read the name of the playlist in + CSmartPlaylist playlist; + if (playlist.OpenAndReadName(url)) + { + pItem->SetLabel(playlist.GetName()); + pItem->SetLabelPreformatted(true); + } + IFileDirectory* pDir=new CSmartPlaylistDirectory; + return pDir; // treat as directory + } + if (CPlayListFactory::IsPlaylist(url)) + { // Playlist file + // currently we only return the directory if it contains + // more than one file. Reason is that .pls and .m3u may be used + // for links to http streams etc. + IFileDirectory *pDir = new CPlaylistFileDirectory(); + CFileItemList items; + if (pDir->GetDirectory(url, items)) + { + if (items.Size() > 1) + return pDir; + } + delete pDir; + return NULL; + } + + if (pItem->IsAudioBook()) + { + if (!pItem->HasMusicInfoTag() || pItem->GetEndOffset() <= 0) + { + std::unique_ptr<CAudioBookFileDirectory> pDir(new CAudioBookFileDirectory); + if (pDir->ContainsFiles(url)) + return pDir.release(); + } + return NULL; + } + return NULL; +} + diff --git a/xbmc/filesystem/FileDirectoryFactory.h b/xbmc/filesystem/FileDirectoryFactory.h new file mode 100644 index 0000000..b5b4575 --- /dev/null +++ b/xbmc/filesystem/FileDirectoryFactory.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IFileDirectory.h" + +#include <string> + +class CFileItem; + +namespace XFILE +{ +class CFileDirectoryFactory +{ +public: + CFileDirectoryFactory(void); + virtual ~CFileDirectoryFactory(void); + static IFileDirectory* Create(const CURL& url, CFileItem* pItem, const std::string& strMask=""); +}; +} diff --git a/xbmc/filesystem/FileFactory.cpp b/xbmc/filesystem/FileFactory.cpp new file mode 100644 index 0000000..b248255 --- /dev/null +++ b/xbmc/filesystem/FileFactory.cpp @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "network/Network.h" +#include "FileFactory.h" +#ifdef TARGET_POSIX +#include "platform/posix/filesystem/PosixFile.h" +#elif defined(TARGET_WINDOWS) +#include "platform/win32/filesystem/Win32File.h" +#ifdef TARGET_WINDOWS_STORE +#include "platform/win10/filesystem/WinLibraryFile.h" +#endif +#endif // TARGET_WINDOWS +#include "CurlFile.h" +#include "DAVFile.h" +#include "ShoutcastFile.h" +#ifdef HAS_FILESYSTEM_SMB +#ifdef TARGET_WINDOWS +#include "platform/win32/filesystem/Win32SMBFile.h" +#else +#include "platform/posix/filesystem/SMBFile.h" +#endif +#endif +#include "CDDAFile.h" +#if defined(HAS_ISO9660PP) +#include "ISO9660File.h" +#endif +#if defined(TARGET_ANDROID) +#include "platform/android/filesystem/APKFile.h" +#endif +#include "XbtFile.h" +#include "ZipFile.h" +#ifdef HAS_FILESYSTEM_NFS +#include "NFSFile.h" +#endif +#if defined(TARGET_ANDROID) +#include "platform/android/filesystem/AndroidAppFile.h" +#endif +#if defined(TARGET_DARWIN_TVOS) +#include "platform/darwin/tvos/filesystem/TVOSFile.h" +#endif // TARGET_DARWIN_TVOS +#ifdef HAS_UPNP +#include "UPnPFile.h" +#endif +#ifdef HAVE_LIBBLURAY +#include "BlurayFile.h" +#endif +#include "PipeFile.h" +#include "MusicDatabaseFile.h" +#include "VideoDatabaseFile.h" +#include "PluginFile.h" +#include "SpecialProtocolFile.h" +#include "MultiPathFile.h" +#if defined(HAS_UDFREAD) +#include "UDFFile.h" +#endif +#include "ImageFile.h" +#include "ResourceFile.h" +#include "URL.h" +#include "utils/log.h" +#include "network/WakeOnAccess.h" +#include "utils/StringUtils.h" +#include "ServiceBroker.h" +#include "addons/VFSEntry.h" + +using namespace ADDON; +using namespace XFILE; + +CFileFactory::CFileFactory() = default; + +CFileFactory::~CFileFactory() = default; + +IFile* CFileFactory::CreateLoader(const std::string& strFileName) +{ + CURL url(strFileName); + return CreateLoader(url); +} + +IFile* CFileFactory::CreateLoader(const CURL& url) +{ + if (!CWakeOnAccess::GetInstance().WakeUpHost(url)) + return NULL; + + if (!url.GetProtocol().empty() && CServiceBroker::IsAddonInterfaceUp()) + { + for (const auto& vfsAddon : CServiceBroker::GetVFSAddonCache().GetAddonInstances()) + { + auto prots = StringUtils::Split(vfsAddon->GetProtocols(), "|"); + + if (vfsAddon->HasFiles() && std::find(prots.begin(), prots.end(), url.GetProtocol()) != prots.end()) + return new CVFSEntryIFileWrapper(vfsAddon); + } + } + +#if defined(TARGET_ANDROID) + if (url.IsProtocol("apk")) return new CAPKFile(); +#endif + if (url.IsProtocol("zip")) return new CZipFile(); + else if (url.IsProtocol("xbt")) return new CXbtFile(); + else if (url.IsProtocol("musicdb")) return new CMusicDatabaseFile(); + else if (url.IsProtocol("videodb")) return new CVideoDatabaseFile(); + else if (url.IsProtocol("plugin")) return new CPluginFile(); + else if (url.IsProtocol("library")) return nullptr; + else if (url.IsProtocol("pvr")) return nullptr; + else if (url.IsProtocol("special")) return new CSpecialProtocolFile(); + else if (url.IsProtocol("multipath")) return new CMultiPathFile(); + else if (url.IsProtocol("image")) return new CImageFile(); +#ifdef TARGET_POSIX + else if (url.IsProtocol("file") || url.GetProtocol().empty()) + { +#if defined(TARGET_DARWIN_TVOS) + if (CTVOSFile::WantsFile(url)) + return new CTVOSFile(); +#endif + return new CPosixFile(); + } +#elif defined(TARGET_WINDOWS) + else if (url.IsProtocol("file") || url.GetProtocol().empty()) + { +#ifdef TARGET_WINDOWS_STORE + if (CWinLibraryFile::IsInAccessList(url)) + return new CWinLibraryFile(); +#endif + return new CWin32File(); + } +#endif // TARGET_WINDOWS +#if defined(HAS_DVD_DRIVE) + else if (url.IsProtocol("cdda")) return new CFileCDDA(); +#endif +#if defined(HAS_ISO9660PP) + else if (url.IsProtocol("iso9660")) + return new CISO9660File(); +#endif +#if defined(HAS_UDFREAD) + else if(url.IsProtocol("udf")) + return new CUDFFile(); +#endif +#if defined(TARGET_ANDROID) + else if (url.IsProtocol("androidapp")) return new CFileAndroidApp(); +#endif + else if (url.IsProtocol("pipe")) return new CPipeFile(); +#ifdef HAVE_LIBBLURAY + else if (url.IsProtocol("bluray")) return new CBlurayFile(); +#endif + else if (url.IsProtocol("resource")) return new CResourceFile(); +#ifdef TARGET_WINDOWS_STORE + else if (CWinLibraryFile::IsValid(url)) return new CWinLibraryFile(); +#endif + + if (url.IsProtocol("ftp") + || url.IsProtocol("ftps") + || url.IsProtocol("rss") + || url.IsProtocol("rsss") + || url.IsProtocol("http") + || url.IsProtocol("https")) return new CCurlFile(); + else if (url.IsProtocol("dav") || url.IsProtocol("davs")) return new CDAVFile(); + else if (url.IsProtocol("shout") || url.IsProtocol("shouts")) return new CShoutcastFile(); +#ifdef HAS_FILESYSTEM_SMB +#ifdef TARGET_WINDOWS + else if (url.IsProtocol("smb")) return new CWin32SMBFile(); +#else + else if (url.IsProtocol("smb")) return new CSMBFile(); +#endif +#endif +#ifdef HAS_FILESYSTEM_NFS + else if (url.IsProtocol("nfs")) return new CNFSFile(); +#endif +#ifdef HAS_UPNP + else if (url.IsProtocol("upnp")) return new CUPnPFile(); +#endif + + CLog::Log(LOGWARNING, "{} - unsupported protocol({}) in {}", __FUNCTION__, url.GetProtocol(), + url.GetRedacted()); + return NULL; +} diff --git a/xbmc/filesystem/FileFactory.h b/xbmc/filesystem/FileFactory.h new file mode 100644 index 0000000..ec0fd10 --- /dev/null +++ b/xbmc/filesystem/FileFactory.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +// FileFactory1.h: interface for the CFileFactory class. +// +////////////////////////////////////////////////////////////////////// + +#include "IFile.h" + +#include <string> + +namespace XFILE +{ +class CFileFactory +{ +public: + CFileFactory(); + virtual ~CFileFactory(); + static IFile* CreateLoader(const std::string& strFileName); + static IFile* CreateLoader(const CURL& url); +}; +} diff --git a/xbmc/filesystem/HTTPDirectory.cpp b/xbmc/filesystem/HTTPDirectory.cpp new file mode 100644 index 0000000..0097b7f --- /dev/null +++ b/xbmc/filesystem/HTTPDirectory.cpp @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "HTTPDirectory.h" + +#include "CurlFile.h" +#include "FileItem.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/CharsetConverter.h" +#include "utils/HTMLUtil.h" +#include "utils/RegExp.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +#include <climits> + +using namespace XFILE; + +CHTTPDirectory::CHTTPDirectory(void) = default; +CHTTPDirectory::~CHTTPDirectory(void) = default; + +bool CHTTPDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + CCurlFile http; + + const std::string& strBasePath = url.GetFileName(); + + if(!http.Open(url)) + { + CLog::Log(LOGERROR, "{} - Unable to get http directory ({})", __FUNCTION__, url.GetRedacted()); + return false; + } + + CRegExp reItem(true); // HTML is case-insensitive + reItem.RegComp("<a href=\"([^\"]*)\"[^>]*>\\s*(.*?)\\s*</a>(.+?)(?=<a|</tr|$)"); + + CRegExp reDateTimeHtml(true); + reDateTimeHtml.RegComp( + "<td align=\"right\">([0-9]{2})-([A-Z]{3})-([0-9]{4}) ([0-9]{2}):([0-9]{2}) +</td>"); + + CRegExp reDateTimeLighttp(true); + reDateTimeLighttp.RegComp( + "<td class=\"m\">([0-9]{4})-([A-Z]{3})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})</td>"); + + CRegExp reDateTimeNginx(true); + reDateTimeNginx.RegComp("([0-9]{2})-([A-Z]{3})-([0-9]{4}) ([0-9]{2}):([0-9]{2})"); + + CRegExp reDateTimeNginxFancy(true); + reDateTimeNginxFancy.RegComp( + "<td class=\"date\">([0-9]{4})-([A-Z]{3})-([0-9]{2}) ([0-9]{2}):([0-9]{2})</td>"); + + CRegExp reDateTimeApacheNewFormat(true); + reDateTimeApacheNewFormat.RegComp( + "<td align=\"right\">([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}) +</td>"); + + CRegExp reDateTime(true); + reDateTime.RegComp("([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2})"); + + CRegExp reSizeHtml(true); + reSizeHtml.RegComp("> *([0-9.]+) *(B|K|M|G| )(iB)?</td>"); + + CRegExp reSize(true); + reSize.RegComp(" +([0-9]+)(B|K|M|G)?(?=\\s|<|$)"); + + /* read response from server into string buffer */ + std::string strBuffer; + if (http.ReadData(strBuffer) && strBuffer.length() > 0) + { + /* if Content-Length is found and its not text/html, URL is pointing to file so don't treat URL as HTTPDirectory */ + if (!http.GetHttpHeader().GetValue("Content-Length").empty() && + !StringUtils::StartsWithNoCase(http.GetHttpHeader().GetValue("Content-type"), "text/html")) + { + return false; + } + + std::string fileCharset(http.GetProperty(XFILE::FILE_PROPERTY_CONTENT_CHARSET)); + if (!fileCharset.empty() && fileCharset != "UTF-8") + { + std::string converted; + if (g_charsetConverter.ToUtf8(fileCharset, strBuffer, converted) && !converted.empty()) + strBuffer = converted; + } + + unsigned int bufferOffset = 0; + while (bufferOffset < strBuffer.length()) + { + int matchOffset = reItem.RegFind(strBuffer.c_str(), bufferOffset); + if (matchOffset < 0) + break; + + bufferOffset = matchOffset + reItem.GetSubLength(0); + + std::string strLink = reItem.GetMatch(1); + std::string strName = reItem.GetMatch(2); + std::string strMetadata = reItem.GetMatch(3); + StringUtils::Trim(strMetadata); + + if(strLink[0] == '/') + strLink = strLink.substr(1); + + std::string strNameTemp = StringUtils::Trim(strName); + + std::wstring wName, wLink, wConverted; + if (fileCharset.empty()) + g_charsetConverter.unknownToUTF8(strNameTemp); + g_charsetConverter.utf8ToW(strNameTemp, wName, false); + HTML::CHTMLUtil::ConvertHTMLToW(wName, wConverted); + g_charsetConverter.wToUTF8(wConverted, strNameTemp); + URIUtils::RemoveSlashAtEnd(strNameTemp); + + std::string strLinkBase = strLink; + std::string strLinkOptions; + + // split link with url options + size_t pos = strLinkBase.find('?'); + if (pos != std::string::npos) + { + strLinkOptions = strLinkBase.substr(pos); + strLinkBase.erase(pos); + } + + // strip url fragment from the link + pos = strLinkBase.find('#'); + if (pos != std::string::npos) + { + strLinkBase.erase(pos); + } + + // Convert any HTTP character entities (e.g.: "&") to percentage encoding + // (e.g.: "%xx") as some web servers (Apache) put these in HTTP Directory Indexes + // this is also needed as CURL objects interpret them incorrectly due to the ; + // also being allowed as URL option separator + if (fileCharset.empty()) + g_charsetConverter.unknownToUTF8(strLinkBase); + g_charsetConverter.utf8ToW(strLinkBase, wLink, false); + HTML::CHTMLUtil::ConvertHTMLToW(wLink, wConverted); + g_charsetConverter.wToUTF8(wConverted, strLinkBase); + + // encoding + and ; to URL encode if it is not already encoded by http server used on the remote server (example: Apache) + // more characters may be added here when required when required by certain http servers + pos = strLinkBase.find_first_of("+;"); + while (pos != std::string::npos) + { + std::stringstream convert; + convert << '%' << std::hex << int(strLinkBase.at(pos)); + strLinkBase.replace(pos, 1, convert.str()); + pos = strLinkBase.find_first_of("+;"); + } + + std::string strLinkTemp = strLinkBase; + + URIUtils::RemoveSlashAtEnd(strLinkTemp); + strLinkTemp = CURL::Decode(strLinkTemp); + + if (StringUtils::EndsWith(strNameTemp, "..>") && + StringUtils::StartsWith(strLinkTemp, strNameTemp.substr(0, strNameTemp.length() - 3))) + strName = strNameTemp = strLinkTemp; + + /* Per RFC 1808 § 5.3, relative paths containing a colon ":" should be either prefixed with + * "./" or escaped (as "%3A"). This handles the prefix case, the escaping should be handled by + * the CURL::Decode above + * - https://tools.ietf.org/html/rfc1808#section-5.3 + */ + auto NameMatchesLink([](const std::string& name, const std::string& link) -> bool + { + return (name == link) || + ((std::string::npos != name.find(':')) && (std::string{"./"}.append(name) == link)); + }); + + // we detect http directory items by its display name and its stripped link + // if same, we consider it as a valid item. + if (strLinkTemp != ".." && strLinkTemp != "" && NameMatchesLink(strNameTemp, strLinkTemp)) + { + CFileItemPtr pItem(new CFileItem(strNameTemp)); + pItem->SetProperty("IsHTTPDirectory", true); + CURL url2(url); + + url2.SetFileName(strBasePath + strLinkBase); + url2.SetOptions(strLinkOptions); + pItem->SetURL(url2); + + if(URIUtils::HasSlashAtEnd(pItem->GetPath(), true)) + pItem->m_bIsFolder = true; + + std::string day, month, year, hour, minute; + int monthNum = 0; + + if (reDateTimeHtml.RegFind(strMetadata.c_str()) >= 0) + { + day = reDateTimeHtml.GetMatch(1); + month = reDateTimeHtml.GetMatch(2); + year = reDateTimeHtml.GetMatch(3); + hour = reDateTimeHtml.GetMatch(4); + minute = reDateTimeHtml.GetMatch(5); + } + else if (reDateTimeNginxFancy.RegFind(strMetadata.c_str()) >= 0) + { + day = reDateTimeNginxFancy.GetMatch(3); + month = reDateTimeNginxFancy.GetMatch(2); + year = reDateTimeNginxFancy.GetMatch(1); + hour = reDateTimeNginxFancy.GetMatch(4); + minute = reDateTimeNginxFancy.GetMatch(5); + } + else if (reDateTimeNginx.RegFind(strMetadata.c_str()) >= 0) + { + day = reDateTimeNginx.GetMatch(1); + month = reDateTimeNginx.GetMatch(2); + year = reDateTimeNginx.GetMatch(3); + hour = reDateTimeNginx.GetMatch(4); + minute = reDateTimeNginx.GetMatch(5); + } + else if (reDateTimeLighttp.RegFind(strMetadata.c_str()) >= 0) + { + day = reDateTimeLighttp.GetMatch(3); + month = reDateTimeLighttp.GetMatch(2); + year = reDateTimeLighttp.GetMatch(1); + hour = reDateTimeLighttp.GetMatch(4); + minute = reDateTimeLighttp.GetMatch(5); + } + else if (reDateTimeApacheNewFormat.RegFind(strMetadata.c_str()) >= 0) + { + day = reDateTimeApacheNewFormat.GetMatch(3); + monthNum = atoi(reDateTimeApacheNewFormat.GetMatch(2).c_str()); + year = reDateTimeApacheNewFormat.GetMatch(1); + hour = reDateTimeApacheNewFormat.GetMatch(4); + minute = reDateTimeApacheNewFormat.GetMatch(5); + } + else if (reDateTime.RegFind(strMetadata.c_str()) >= 0) + { + day = reDateTime.GetMatch(3); + monthNum = atoi(reDateTime.GetMatch(2).c_str()); + year = reDateTime.GetMatch(1); + hour = reDateTime.GetMatch(4); + minute = reDateTime.GetMatch(5); + } + + if (month.length() > 0) + monthNum = CDateTime::MonthStringToMonthNum(month); + + if (day.length() > 0 && monthNum > 0 && year.length() > 0) + { + pItem->m_dateTime = CDateTime(atoi(year.c_str()), monthNum, atoi(day.c_str()), atoi(hour.c_str()), atoi(minute.c_str()), 0); + } + + if (!pItem->m_bIsFolder) + { + if (reSizeHtml.RegFind(strMetadata.c_str()) >= 0) + { + double Size = atof(reSizeHtml.GetMatch(1).c_str()); + std::string strUnit(reSizeHtml.GetMatch(2)); + + if (strUnit == "K") + Size = Size * 1024; + else if (strUnit == "M") + Size = Size * 1024 * 1024; + else if (strUnit == "G") + Size = Size * 1024 * 1024 * 1024; + + pItem->m_dwSize = (int64_t)Size; + } + else if (reSize.RegFind(strMetadata.c_str()) >= 0) + { + double Size = atof(reSize.GetMatch(1).c_str()); + std::string strUnit(reSize.GetMatch(2)); + + if (strUnit == "K") + Size = Size * 1024; + else if (strUnit == "M") + Size = Size * 1024 * 1024; + else if (strUnit == "G") + Size = Size * 1024 * 1024 * 1024; + + pItem->m_dwSize = (int64_t)Size; + } + else + if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bHTTPDirectoryStatFilesize) // As a fallback get the size by stat-ing the file (slow) + { + CCurlFile file; + file.Open(url); + pItem->m_dwSize=file.GetLength(); + file.Close(); + } + } + items.Add(pItem); + } + } + } + http.Close(); + + items.SetProperty("IsHTTPDirectory", true); + + return true; +} + +bool CHTTPDirectory::Exists(const CURL &url) +{ + CCurlFile http; + struct __stat64 buffer; + + if( http.Stat(url, &buffer) != 0 ) + { + return false; + } + + if (buffer.st_mode == _S_IFDIR) + return true; + + return false; +} diff --git a/xbmc/filesystem/HTTPDirectory.h b/xbmc/filesystem/HTTPDirectory.h new file mode 100644 index 0000000..02a27d9 --- /dev/null +++ b/xbmc/filesystem/HTTPDirectory.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" + +namespace XFILE +{ + class CHTTPDirectory : public IDirectory + { + public: + CHTTPDirectory(void); + ~CHTTPDirectory(void) override; + bool GetDirectory(const CURL& url, CFileItemList &items) override; + bool Exists(const CURL& url) override; + DIR_CACHE_TYPE GetCacheType(const CURL& url) const override { return DIR_CACHE_ONCE; } + + private: + }; +} diff --git a/xbmc/filesystem/IDirectory.cpp b/xbmc/filesystem/IDirectory.cpp new file mode 100644 index 0000000..b2ad59a --- /dev/null +++ b/xbmc/filesystem/IDirectory.cpp @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "IDirectory.h" + +#include "PasswordManager.h" +#include "URL.h" +#include "guilib/GUIKeyboardFactory.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +using namespace KODI::MESSAGING; +using namespace XFILE; + +const CProfileManager *IDirectory::m_profileManager = nullptr; + +void IDirectory::RegisterProfileManager(const CProfileManager &profileManager) +{ + m_profileManager = &profileManager; +} + +void IDirectory::UnregisterProfileManager() +{ + m_profileManager = nullptr; +} + +IDirectory::IDirectory() +{ + m_flags = DIR_FLAG_DEFAULTS; +} + +IDirectory::~IDirectory(void) = default; + +/*! + \brief Test if file have an allowed extension, as specified with SetMask() + \param strFile File to test + \return \e true if file is allowed + \note If extension is ".ifo", filename format must be "vide_ts.ifo" or + "vts_##_0.ifo". If extension is ".dat", filename format must be + "AVSEQ##(#).DAT", "ITEM###(#).DAT" or "MUSIC##(#).DAT". + */ +bool IDirectory::IsAllowed(const CURL& url) const +{ + if (m_strFileMask.empty()) + return true; + + // Check if strFile have an allowed extension + if (!URIUtils::HasExtension(url, m_strFileMask)) + return false; + + // We should ignore all non dvd/vcd related ifo and dat files. + if (URIUtils::HasExtension(url, ".ifo")) + { + std::string fileName = URIUtils::GetFileName(url); + + // Allow filenames of the form video_ts.ifo or vts_##_0.ifo + + return StringUtils::EqualsNoCase(fileName, "video_ts.ifo") || + (fileName.length() == 12 && + StringUtils::StartsWithNoCase(fileName, "vts_") && + StringUtils::EndsWithNoCase(fileName, "_0.ifo")); + } + + if (URIUtils::HasExtension(url, ".dat")) + { + std::string fileName = URIUtils::GetFileName(url); + std::string folder = URIUtils::GetDirectory(fileName); + URIUtils::RemoveSlashAtEnd(folder); + folder = URIUtils::GetFileName(folder); + if (StringUtils::EqualsNoCase(folder, "vcd") || + StringUtils::EqualsNoCase(folder, "mpegav") || + StringUtils::EqualsNoCase(folder, "cdda")) + { + // Allow filenames of the form AVSEQ##(#).DAT, ITEM###(#).DAT + // and MUSIC##(#).DAT + return (fileName.length() == 11 || fileName.length() == 12) && + (StringUtils::StartsWithNoCase(fileName, "AVSEQ") || + StringUtils::StartsWithNoCase(fileName, "MUSIC") || + StringUtils::StartsWithNoCase(fileName, "ITEM")); + } + } + return true; +} + +/*! + \brief Set a mask of extensions for the files in the directory. + \param strMask Mask of file extensions that are allowed. + + The mask has to look like the following: \n + \verbatim + .m4a|.flac|.aac| + \endverbatim + So only *.m4a, *.flac, *.aac files will be retrieved with GetDirectory(). + */ +void IDirectory::SetMask(const std::string& strMask) +{ + m_strFileMask = strMask; + // ensure it's completed with a | so that filtering is easy. + StringUtils::ToLower(m_strFileMask); + if (m_strFileMask.size() && m_strFileMask[m_strFileMask.size() - 1] != '|') + m_strFileMask += '|'; +} + +/*! + \brief Set the flags for this directory handler. + \param flags - \sa XFILE::DIR_FLAG for a description. + */ +void IDirectory::SetFlags(int flags) +{ + m_flags = flags; +} + +bool IDirectory::ProcessRequirements() +{ + std::string type = m_requirements["type"].asString(); + if (type == "keyboard") + { + std::string input; + if (CGUIKeyboardFactory::ShowAndGetInput(input, m_requirements["heading"], false, m_requirements["hidden"].asBoolean())) + { + m_requirements["input"] = input; + return true; + } + } + else if (type == "authenticate") + { + CURL url(m_requirements["url"].asString()); + if (CPasswordManager::GetInstance().PromptToAuthenticateURL(url)) + { + m_requirements.clear(); + return true; + } + } + else if (type == "error") + { + HELPERS::ShowOKDialogLines(CVariant{m_requirements["heading"]}, CVariant{m_requirements["line1"]}, CVariant{m_requirements["line2"]}, CVariant{m_requirements["line3"]}); + } + m_requirements.clear(); + return false; +} + +bool IDirectory::GetKeyboardInput(const CVariant &heading, std::string &input, bool hiddenInput) +{ + if (!m_requirements["input"].asString().empty()) + { + input = m_requirements["input"].asString(); + return true; + } + m_requirements.clear(); + m_requirements["type"] = "keyboard"; + m_requirements["heading"] = heading; + m_requirements["hidden"] = hiddenInput; + return false; +} + +void IDirectory::SetErrorDialog(const CVariant &heading, const CVariant &line1, const CVariant &line2, const CVariant &line3) +{ + m_requirements.clear(); + m_requirements["type"] = "error"; + m_requirements["heading"] = heading; + m_requirements["line1"] = line1; + m_requirements["line2"] = line2; + m_requirements["line3"] = line3; +} + +void IDirectory::RequireAuthentication(const CURL &url) +{ + m_requirements.clear(); + m_requirements["type"] = "authenticate"; + m_requirements["url"] = url.Get(); +} diff --git a/xbmc/filesystem/IDirectory.h b/xbmc/filesystem/IDirectory.h new file mode 100644 index 0000000..5667dc3 --- /dev/null +++ b/xbmc/filesystem/IDirectory.h @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "utils/Variant.h" + +#include <string> + +class CFileItemList; +class CProfileManager; +class CURL; + +namespace XFILE +{ + enum DIR_CACHE_TYPE + { + DIR_CACHE_NEVER = 0, ///< Never cache this directory to memory + DIR_CACHE_ONCE, ///< Cache this directory to memory for each fetch (so that FileExists() checks are fast) + DIR_CACHE_ALWAYS ///< Always cache this directory to memory, so that each additional fetch of this folder will utilize the cache (until it's cleared) + }; + + /*! \brief Available directory flags + The defaults are to allow file directories, no prompting, retrieve file information, hide hidden files, and utilise the directory cache + based on the implementation's wishes. + */ + enum DIR_FLAG + { + DIR_FLAG_DEFAULTS = 0, + DIR_FLAG_NO_FILE_DIRS = (2 << 0), ///< Don't convert files (zip, rar etc.) to directories + DIR_FLAG_ALLOW_PROMPT = (2 << 1), ///< Allow prompting for further info (passwords etc.) + DIR_FLAG_NO_FILE_INFO = (2 << 2), ///< Don't read additional file info (stat for example) + DIR_FLAG_GET_HIDDEN = (2 << 3), ///< Get hidden files + DIR_FLAG_READ_CACHE = (2 << 4), ///< Force reading from the directory cache (if available) + DIR_FLAG_BYPASS_CACHE = (2 << 5) ///< Completely bypass the directory cache (no reading, no writing) + }; +/*! + \ingroup filesystem + \brief Interface to the directory on a file system. + + This Interface is retrieved from CDirectoryFactory and can be used to + access the directories on a filesystem. + \sa CDirectoryFactory + */ +class IDirectory +{ +public: + static void RegisterProfileManager(const CProfileManager &profileManager); + static void UnregisterProfileManager(); + + IDirectory(); + virtual ~IDirectory(void); + /*! + \brief Get the \e items of the directory \e strPath. + \param url Directory to read. + \param items Retrieves the directory entries. + \return Returns \e true, if successful. + \sa CDirectoryFactory + */ + virtual bool GetDirectory(const CURL& url, CFileItemList &items) = 0; + /*! + \brief Retrieve the progress of the current directory fetch (if possible). + \return the progress as a float in the range 0..100. + \sa GetDirectory, CancelDirectory + */ + virtual float GetProgress() const { return 0.0f; } + /*! + \brief Cancel the current directory fetch (if possible). + \sa GetDirectory + */ + virtual void CancelDirectory() {} + /*! + \brief Create the directory + \param url Directory to create. + \return Returns \e true, if directory is created or if it already exists + \sa CDirectoryFactory + */ + virtual bool Create(const CURL& url) { return false; } + /*! + \brief Check for directory existence + \param url Directory to check. + \return Returns \e true, if directory exists + \sa CDirectoryFactory + */ + virtual bool Exists(const CURL& url) { return false; } + /*! + \brief Removes the directory + \param url Directory to remove. + \return Returns \e false if not successful + */ + virtual bool Remove(const CURL& url) { return false; } + + /*! + \brief Recursively removes the directory + \param url Directory to remove. + \return Returns \e false if not successful + */ + virtual bool RemoveRecursive(const CURL& url) { return false; } + + /*! + \brief Whether this file should be listed + \param url File to test. + \return Returns \e true if the file should be listed + */ + virtual bool IsAllowed(const CURL& url) const; + + /*! \brief Whether to allow all files/folders to be listed. + \return Returns \e true if all files/folder should be listed. + */ + virtual bool AllowAll() const { return false; } + + /*! + \brief How this directory should be cached + \param url Directory at hand. + \return Returns the cache type. + */ + virtual DIR_CACHE_TYPE GetCacheType(const CURL& url) const { return DIR_CACHE_ONCE; } + + void SetMask(const std::string& strMask); + void SetFlags(int flags); + + /*! \brief Process additional requirements before the directory fetch is performed. + Some directory fetches may require authentication, keyboard input etc. The IDirectory subclass + should call GetKeyboardInput, SetErrorDialog or RequireAuthentication and then return false + from the GetDirectory method. CDirectory will then prompt for input from the user, before + re-calling the GetDirectory method. + \sa GetKeyboardInput, SetErrorDialog, RequireAuthentication + */ + bool ProcessRequirements(); + +protected: + /*! \brief Prompt the user for some keyboard input + Call this method from the GetDirectory method to retrieve additional input from the user. + If this function returns false then no input has been received, and the GetDirectory call + should return false. + \param heading an integer or string heading for the keyboard dialog + \param input [out] the returned input (if available). + \return true if keyboard input has been received. False if it hasn't. + \sa ProcessRequirements + */ + bool GetKeyboardInput(const CVariant &heading, std::string &input, bool hiddenInput = false); + + /*! \brief Show an error dialog on failure of GetDirectory call + Call this method from the GetDirectory method to set an error message to be shown to the user + \param heading an integer or string heading for the error dialog. + \param line1 the first line to be displayed (integer or string). + \param line2 the first line to be displayed (integer or string). + \param line3 the first line to be displayed (integer or string). + \sa ProcessRequirements + */ + void SetErrorDialog(const CVariant &heading, const CVariant &line1, const CVariant &line2 = 0, const CVariant &line3 = 0); + + /*! \brief Prompt the user for authentication of a URL. + Call this method from the GetDirectory method when authentication is required from the user, before returning + false from the GetDirectory call. The user will be prompted for authentication, and GetDirectory will be + re-called. + \param url the URL to authenticate. + \sa ProcessRequirements + */ + void RequireAuthentication(const CURL& url); + + static const CProfileManager *m_profileManager; + + std::string m_strFileMask; ///< Holds the file mask specified by SetMask() + + int m_flags; ///< Directory flags - see DIR_FLAG + + CVariant m_requirements; +}; +} diff --git a/xbmc/filesystem/IFile.cpp b/xbmc/filesystem/IFile.cpp new file mode 100644 index 0000000..eddffea --- /dev/null +++ b/xbmc/filesystem/IFile.cpp @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2002 Frodo + * Portions Copyright (c) by the authors of ffmpeg and xvid + * Copyright (C) 2002-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "IFile.h" + +#include "URL.h" + +#include <cstring> +#include <errno.h> + +using namespace XFILE; + +////////////////////////////////////////////////////////////////////// +// Construction/Destruction +////////////////////////////////////////////////////////////////////// + +IFile::IFile() = default; + +IFile::~IFile() = default; + +int IFile::Stat(struct __stat64* buffer) +{ + if (buffer) + *buffer = {}; + + errno = ENOENT; + return -1; +} +bool IFile::ReadString(char *szLine, int iLineLength) +{ + if(Seek(0, SEEK_CUR) < 0) return false; + + int64_t iFilePos = GetPosition(); + int iBytesRead = Read( (unsigned char*)szLine, iLineLength - 1); + if (iBytesRead <= 0) + return false; + + szLine[iBytesRead] = 0; + + for (int i = 0; i < iBytesRead; i++) + { + if ('\n' == szLine[i]) + { + if ('\r' == szLine[i + 1]) + { + szLine[i + 1] = 0; + Seek(iFilePos + i + 2, SEEK_SET); + } + else + { + // end of line + szLine[i + 1] = 0; + Seek(iFilePos + i + 1, SEEK_SET); + } + break; + } + else if ('\r' == szLine[i]) + { + if ('\n' == szLine[i + 1]) + { + szLine[i + 1] = 0; + Seek(iFilePos + i + 2, SEEK_SET); + } + else + { + // end of line + szLine[i + 1] = 0; + Seek(iFilePos + i + 1, SEEK_SET); + } + break; + } + } + return true; +} + +CRedirectException::CRedirectException() : + m_pNewFileImp(NULL), m_pNewUrl(NULL) +{ +} + +CRedirectException::CRedirectException(IFile *pNewFileImp, CURL *pNewUrl) : + m_pNewFileImp(pNewFileImp), m_pNewUrl(pNewUrl) +{ +} diff --git a/xbmc/filesystem/IFile.h b/xbmc/filesystem/IFile.h new file mode 100644 index 0000000..d76d6ed --- /dev/null +++ b/xbmc/filesystem/IFile.h @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2002 Frodo + * Portions Copyright (c) by the authors of ffmpeg and xvid + * Copyright (C) 2002-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +// IFile.h: interface for the IFile class. +// +////////////////////////////////////////////////////////////////////// + +#include "PlatformDefs.h" // for __stat64, ssize_t + +#include <stdio.h> +#include <stdint.h> +#include <sys/stat.h> +#include <string> +#include <vector> + +#if !defined(SIZE_MAX) || !defined(SSIZE_MAX) +#include <limits.h> +#ifndef SIZE_MAX +#define SIZE_MAX UINTPTR_MAX +#endif // ! SIZE_MAX +#ifndef SSIZE_MAX +#define SSIZE_MAX INTPTR_MAX +#endif // ! SSIZE_MAX +#endif // ! SIZE_MAX || ! SSIZE_MAX + +#include "IFileTypes.h" + +class CURL; + +namespace XFILE +{ + +class IFile +{ +public: + IFile(); + virtual ~IFile(); + + virtual bool Open(const CURL& url) = 0; + virtual bool OpenForWrite(const CURL& url, bool bOverWrite = false) { return false; } + virtual bool ReOpen(const CURL& url) { return false; } + virtual bool Exists(const CURL& url) = 0; + /** + * Fills struct __stat64 with information about file specified by url. + * For st_mode function will set correctly _S_IFDIR (directory) flag and may set + * _S_IREAD (read permission), _S_IWRITE (write permission) flags if such + * information is available. Function may set st_size (file size), st_atime, + * st_mtime, st_ctime (access, modification, creation times). + * Any other flags and members of __stat64 that didn't updated with actual file + * information will be set to zero (st_nlink can be set ether to 1 or zero). + * @param url specifies requested file + * @param buffer pointer to __stat64 buffer to receive information about file + * @return zero of success, -1 otherwise. + */ + virtual int Stat(const CURL& url, struct __stat64* buffer) = 0; + /** + * Fills struct __stat64 with information about currently open file + * For st_mode function will set correctly _S_IFDIR (directory) flag and may set + * _S_IREAD (read permission), _S_IWRITE (write permission) flags if such + * information is available. Function may set st_size (file size), st_atime, + * st_mtime, st_ctime (access, modification, creation times). + * Any other flags and members of __stat64 that didn't updated with actual file + * information will be set to zero (st_nlink can be set ether to 1 or zero). + * @param buffer pointer to __stat64 buffer to receive information about file + * @return zero of success, -1 otherwise. + */ + virtual int Stat(struct __stat64* buffer); + /** + * Attempt to read bufSize bytes from currently opened file into buffer bufPtr. + * @param bufPtr pointer to buffer + * @param bufSize size of the buffer + * @return number of successfully read bytes if any bytes were read and stored in + * buffer, zero if no bytes are available to read (end of file was reached) + * or undetectable error occur, -1 in case of any explicit error + */ + virtual ssize_t Read(void* bufPtr, size_t bufSize) = 0; + /** + * Attempt to write bufSize bytes from buffer bufPtr into currently opened file. + * @param bufPtr pointer to buffer + * @param bufSize size of the buffer + * @return number of successfully written bytes if any bytes were written, + * zero if no bytes were written and no detectable error occur, + * -1 in case of any explicit error + */ + virtual ssize_t Write(const void* bufPtr, size_t bufSize) { return -1;} + virtual bool ReadString(char *szLine, int iLineLength); + virtual int64_t Seek(int64_t iFilePosition, int iWhence = SEEK_SET) = 0; + virtual void Close() = 0; + virtual int64_t GetPosition() = 0; + virtual int64_t GetLength() = 0; + virtual void Flush() { } + virtual int Truncate(int64_t size) { return -1; } + + /* Returns the minimum size that can be read from input stream. * + * For example cdrom access where access could be sector based. * + * This will cause file system to buffer read requests, to * + * to meet the requirement of CFile. * + * It can also be used to indicate a file system is non buffered * + * but accepts any read size, have it return the value 1 */ + virtual int GetChunkSize() {return 0;} + virtual double GetDownloadSpeed() { return 0.0; } + + virtual bool Delete(const CURL& url) { return false; } + virtual bool Rename(const CURL& url, const CURL& urlnew) { return false; } + virtual bool SetHidden(const CURL& url, bool hidden) { return false; } + + virtual int IoControl(EIoControl request, void* param) { return -1; } + + virtual const std::string GetProperty(XFILE::FileProperty type, const std::string &name = "") const + { + return type == XFILE::FILE_PROPERTY_CONTENT_TYPE ? "application/octet-stream" : ""; + }; + + virtual const std::vector<std::string> GetPropertyValues(XFILE::FileProperty type, const std::string &name = "") const + { + std::vector<std::string> values; + std::string value = GetProperty(type, name); + if (!value.empty()) + { + values.emplace_back(value); + } + return values; + } +}; + +class CRedirectException +{ +public: + IFile *m_pNewFileImp; + CURL *m_pNewUrl; + + CRedirectException(); + + CRedirectException(IFile *pNewFileImp, CURL *pNewUrl=NULL); +}; + +} diff --git a/xbmc/filesystem/IFileDirectory.h b/xbmc/filesystem/IFileDirectory.h new file mode 100644 index 0000000..357ed2e --- /dev/null +++ b/xbmc/filesystem/IFileDirectory.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" + +namespace XFILE +{ +class IFileDirectory : public IDirectory +{ +public: + ~IFileDirectory(void) override = default; + virtual bool ContainsFiles(const CURL& url)=0; +}; + +} diff --git a/xbmc/filesystem/IFileTypes.h b/xbmc/filesystem/IFileTypes.h new file mode 100644 index 0000000..524d871 --- /dev/null +++ b/xbmc/filesystem/IFileTypes.h @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include <stdint.h> + +namespace XFILE +{ + +/* indicate that caller can handle truncated reads, where function returns before entire buffer has been filled */ + static const unsigned int READ_TRUNCATED = 0x01; + +/* indicate that that caller support read in the minimum defined chunk size, this disables internal cache then */ + static const unsigned int READ_CHUNKED = 0x02; + +/* use cache to access this file */ + static const unsigned int READ_CACHED = 0x04; + +/* open without caching. regardless to file type. */ + static const unsigned int READ_NO_CACHE = 0x08; + +/* calculate bitrate for file while reading */ + static const unsigned int READ_BITRATE = 0x10; + +/* indicate to the caller we will seek between multiple streams in the file frequently */ + static const unsigned int READ_MULTI_STREAM = 0x20; + +/* indicate to the caller file is audio and/or video (and e.g. may grow) */ + static const unsigned int READ_AUDIO_VIDEO = 0x40; + +/* indicate that caller will do write operations before reading */ + static const unsigned int READ_AFTER_WRITE = 0x80; + +/* indicate that caller want to reopen a file if its already open */ + static const unsigned int READ_REOPEN = 0x100; + +struct SNativeIoControl +{ + unsigned long int request; + void* param; +}; + +struct SCacheStatus +{ + uint64_t forward; /**< number of bytes cached forward of current position */ + uint32_t maxrate; /**< maximum allowed read(fill) rate (bytes/second) */ + uint32_t currate; /**< average read rate (bytes/second) since last position change */ + uint32_t lowrate; /**< low speed read rate (bytes/second) (if any, else 0) */ +}; + +typedef enum { + IOCTRL_NATIVE = 1, /**< SNativeIoControl structure, containing what should be passed to native ioctrl */ + IOCTRL_SEEK_POSSIBLE = 2, /**< return 0 if known not to work, 1 if it should work */ + IOCTRL_CACHE_STATUS = 3, /**< SCacheStatus structure */ + IOCTRL_CACHE_SETRATE = 4, /**< unsigned int with speed limit for caching in bytes per second */ + IOCTRL_SET_CACHE = 8, /**< CFileCache */ + IOCTRL_SET_RETRY = 16, /**< Enable/disable retry within the protocol handler (if supported) */ +} EIoControl; + +enum CURLOPTIONTYPE +{ + CURL_OPTION_OPTION, /**< Set a general option */ + CURL_OPTION_PROTOCOL, /**< Set a protocol option (see below) */ + CURL_OPTION_CREDENTIALS,/**< Set User and password */ + CURL_OPTION_HEADER /**< Add a Header */ +}; + +/** + * The following names for CURL_OPTION_PROTOCOL are possible: + * + * accept-charset: Set the "accept-charset" header + * acceptencoding or encoding: Set the "accept-encoding" header + * active-remote: Set the "active-remote" header + * auth: Set the authentication method. Possible values: any, anysafe, digest, ntlm + * connection-timeout: Set the connection timeout in seconds + * cookie: Set the "cookie" header + * customrequest: Set a custom HTTP request like DELETE + * noshout: Set to true if kodi detects a stream as shoutcast by mistake. + * postdata: Set the post body (value needs to be base64 encoded). (Implicitly sets the request to POST) + * referer: Set the "referer" header + * user-agent: Set the "user-agent" header + * seekable: Set the stream seekable. 1: enable, 0: disable + * sslcipherlist: Set list of accepted SSL ciphers. + */ + +enum FileProperty +{ + FILE_PROPERTY_RESPONSE_PROTOCOL, /**< Get response protocol line */ + FILE_PROPERTY_RESPONSE_HEADER, /**< Get response Header value */ + FILE_PROPERTY_CONTENT_TYPE, /**< Get file content-type */ + FILE_PROPERTY_CONTENT_CHARSET, /**< Get file content charset */ + FILE_PROPERTY_MIME_TYPE, /**< Get file mime type */ + FILE_PROPERTY_EFFECTIVE_URL /**< Get effective URL for redirected streams */ +}; + +class IFileCallback +{ +public: + virtual bool OnFileCallback(void* pContext, int ipercent, float avgSpeed) = 0; + virtual ~IFileCallback() = default; +}; +} diff --git a/xbmc/filesystem/ISO9660Directory.cpp b/xbmc/filesystem/ISO9660Directory.cpp new file mode 100644 index 0000000..894db53 --- /dev/null +++ b/xbmc/filesystem/ISO9660Directory.cpp @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "ISO9660Directory.h" + +#include "FileItem.h" +#include "URL.h" +#include "utils/URIUtils.h" + +#include <cdio++/iso9660.hpp> + +using namespace XFILE; + +bool CISO9660Directory::GetDirectory(const CURL& url, CFileItemList& items) +{ + CURL url2(url); + if (!url2.IsProtocol("iso9660")) + { + url2.Reset(); + url2.SetProtocol("iso9660"); + url2.SetHostName(url.Get()); + } + + std::string strRoot(url2.Get()); + std::string strSub(url2.GetFileName()); + + URIUtils::AddSlashAtEnd(strRoot); + URIUtils::AddSlashAtEnd(strSub); + + std::unique_ptr<ISO9660::IFS> iso(new ISO9660::IFS); + + if (!iso->open(url2.GetHostName().c_str())) + return false; + + std::vector<ISO9660::Stat*> isoFiles; + + if (iso->readdir(strSub.c_str(), isoFiles)) + { + for (const auto file : isoFiles) + { + std::string filename(file->p_stat->filename); + + if (file->p_stat->type == 2) + { + if (filename != "." && filename != "..") + { + CFileItemPtr pItem(new CFileItem(filename)); + std::string strDir(strRoot + filename); + URIUtils::AddSlashAtEnd(strDir); + pItem->SetPath(strDir); + pItem->m_bIsFolder = true; + items.Add(pItem); + } + } + else + { + CFileItemPtr pItem(new CFileItem(filename)); + pItem->SetPath(strRoot + filename); + pItem->m_bIsFolder = false; + pItem->m_dwSize = file->p_stat->size; + items.Add(pItem); + } + } + + isoFiles.clear(); + return true; + } + + return false; +} + +bool CISO9660Directory::Exists(const CURL& url) +{ + CFileItemList items; + return GetDirectory(url, items); +} diff --git a/xbmc/filesystem/ISO9660Directory.h b/xbmc/filesystem/ISO9660Directory.h new file mode 100644 index 0000000..6c624ff --- /dev/null +++ b/xbmc/filesystem/ISO9660Directory.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IFileDirectory.h" + +namespace XFILE +{ + +class CISO9660Directory : public IFileDirectory +{ +public: + CISO9660Directory() = default; + ~CISO9660Directory() = default; + bool GetDirectory(const CURL& url, CFileItemList& items) override; + bool Exists(const CURL& url) override; + bool ContainsFiles(const CURL& url) override { return true; } +}; +} diff --git a/xbmc/filesystem/ISO9660File.cpp b/xbmc/filesystem/ISO9660File.cpp new file mode 100644 index 0000000..83de0cc --- /dev/null +++ b/xbmc/filesystem/ISO9660File.cpp @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "ISO9660File.h" + +#include "URL.h" + +#include <cmath> + +using namespace XFILE; + +CISO9660File::CISO9660File() : m_iso(new ISO9660::IFS()) +{ +} + +bool CISO9660File::Open(const CURL& url) +{ + if (m_iso && m_stat) + return true; + + if (!m_iso->open(url.GetHostName().c_str())) + return false; + + m_stat.reset(m_iso->stat(url.GetFileName().c_str())); + + if (!m_stat) + return false; + + if (!m_stat->p_stat) + return false; + + m_start = m_stat->p_stat->lsn; + m_current = 0; + + return true; +} + +int CISO9660File::Stat(const CURL& url, struct __stat64* buffer) +{ + if (!m_iso) + return -1; + + if (!m_stat) + return -1; + + if (!m_stat->p_stat) + return -1; + + if (!buffer) + return -1; + + *buffer = {}; + buffer->st_size = m_stat->p_stat->size; + + switch (m_stat->p_stat->type) + { + case 2: + buffer->st_mode = S_IFDIR; + break; + case 1: + default: + buffer->st_mode = S_IFREG; + break; + } + + return 0; +} + +ssize_t CISO9660File::Read(void* buffer, size_t size) +{ + const int maxSize = std::min(size, static_cast<size_t>(GetLength())); + const int blocks = std::ceil(maxSize / ISO_BLOCKSIZE); + + if (m_current > std::ceil(GetLength() / ISO_BLOCKSIZE)) + return -1; + + auto read = m_iso->seek_read(buffer, m_start + m_current, blocks); + + m_current += blocks; + + return read; +} + +int64_t CISO9660File::Seek(int64_t filePosition, int whence) +{ + int block = std::floor(filePosition / ISO_BLOCKSIZE); + + switch (whence) + { + case SEEK_SET: + m_current = block; + break; + case SEEK_CUR: + m_current += block; + break; + case SEEK_END: + m_current = std::ceil(GetLength() / ISO_BLOCKSIZE) + block; + break; + } + + return m_current * ISO_BLOCKSIZE; +} + +int64_t CISO9660File::GetLength() +{ + return m_stat->p_stat->size; +} + +int64_t CISO9660File::GetPosition() +{ + return m_current * ISO_BLOCKSIZE; +} + +bool CISO9660File::Exists(const CURL& url) +{ + return Open(url); +} + +int CISO9660File::GetChunkSize() +{ + return ISO_BLOCKSIZE; +} diff --git a/xbmc/filesystem/ISO9660File.h b/xbmc/filesystem/ISO9660File.h new file mode 100644 index 0000000..5754605 --- /dev/null +++ b/xbmc/filesystem/ISO9660File.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IFile.h" + +#include <memory> + +#include <cdio++/iso9660.hpp> + +namespace XFILE +{ + +class CISO9660File : public IFile +{ +public: + CISO9660File(); + ~CISO9660File() override = default; + + bool Open(const CURL& url) override; + void Close() override {} + + int Stat(const CURL& url, struct __stat64* buffer) override; + + ssize_t Read(void* buffer, size_t size) override; + int64_t Seek(int64_t filePosition, int whence) override; + + int64_t GetLength() override; + int64_t GetPosition() override; + + bool Exists(const CURL& url) override; + + int GetChunkSize() override; + +private: + std::unique_ptr<ISO9660::IFS> m_iso; + std::unique_ptr<ISO9660::Stat> m_stat; + + int32_t m_start; + int32_t m_current; +}; + +} // namespace XFILE diff --git a/xbmc/filesystem/ImageFile.cpp b/xbmc/filesystem/ImageFile.cpp new file mode 100644 index 0000000..bb1e23a --- /dev/null +++ b/xbmc/filesystem/ImageFile.cpp @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "ImageFile.h" + +#include "ServiceBroker.h" +#include "TextureCache.h" +#include "URL.h" + +using namespace XFILE; + +CImageFile::CImageFile(void) = default; + +CImageFile::~CImageFile(void) +{ + Close(); +} + +bool CImageFile::Open(const CURL& url) +{ + std::string file = url.Get(); + bool needsRecaching = false; + std::string cachedFile = + CServiceBroker::GetTextureCache()->CheckCachedImage(file, needsRecaching); + if (cachedFile.empty()) + { // not in the cache, so cache it + cachedFile = CServiceBroker::GetTextureCache()->CacheImage(file); + } + if (!cachedFile.empty()) + { // in the cache, return what we have + if (m_file.Open(cachedFile)) + return true; + } + return false; +} + +bool CImageFile::Exists(const CURL& url) +{ + bool needsRecaching = false; + std::string cachedFile = + CServiceBroker::GetTextureCache()->CheckCachedImage(url.Get(), needsRecaching); + if (!cachedFile.empty()) + { + if (CFile::Exists(cachedFile, false)) + return true; + else + // Remove from cache so it gets cached again on next Open() + CServiceBroker::GetTextureCache()->ClearCachedImage(url.Get()); + } + + // need to check if the original can be cached on demand and that the file exists + if (!CTextureCache::CanCacheImageURL(url)) + return false; + + return CFile::Exists(url.GetHostName()); +} + +int CImageFile::Stat(const CURL& url, struct __stat64* buffer) +{ + bool needsRecaching = false; + std::string cachedFile = + CServiceBroker::GetTextureCache()->CheckCachedImage(url.Get(), needsRecaching); + if (!cachedFile.empty()) + return CFile::Stat(cachedFile, buffer); + + /* + Doesn't exist in the cache yet. We have 3 options here: + 1. Cache the file and do the Stat() on the cached file. + 2. Do the Stat() on the original file. + 3. Return -1; + Only 1 will return valid results, at the cost of being time consuming. ATM we do 3 under + the theory that the only user of this is the webinterface currently, where Stat() is not + required. + */ + return -1; +} + +ssize_t CImageFile::Read(void* lpBuf, size_t uiBufSize) +{ + return m_file.Read(lpBuf, uiBufSize); +} + +int64_t CImageFile::Seek(int64_t iFilePosition, int iWhence /*=SEEK_SET*/) +{ + return m_file.Seek(iFilePosition, iWhence); +} + +void CImageFile::Close() +{ + m_file.Close(); +} + +int64_t CImageFile::GetPosition() +{ + return m_file.GetPosition(); +} + +int64_t CImageFile::GetLength() +{ + return m_file.GetLength(); +} diff --git a/xbmc/filesystem/ImageFile.h b/xbmc/filesystem/ImageFile.h new file mode 100644 index 0000000..ebf6d7b --- /dev/null +++ b/xbmc/filesystem/ImageFile.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "File.h" +#include "IFile.h" + +namespace XFILE +{ + class CImageFile: public IFile + { + public: + CImageFile(); + ~CImageFile() override; + bool Open(const CURL& url) override; + bool Exists(const CURL& url) override; + int Stat(const CURL& url, struct __stat64* buffer) override; + + ssize_t Read(void* lpBuf, size_t uiBufSize) override; + int64_t Seek(int64_t iFilePosition, int iWhence = SEEK_SET) override; + void Close() override; + int64_t GetPosition() override; + int64_t GetLength() override; + + protected: + CFile m_file; + }; +} diff --git a/xbmc/filesystem/LibraryDirectory.cpp b/xbmc/filesystem/LibraryDirectory.cpp new file mode 100644 index 0000000..8f81495 --- /dev/null +++ b/xbmc/filesystem/LibraryDirectory.cpp @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "LibraryDirectory.h" + +#include "Directory.h" +#include "FileItem.h" +#include "GUIInfoManager.h" +#include "SmartPlaylistDirectory.h" +#include "URL.h" +#include "guilib/GUIControlFactory.h" // for label parsing +#include "guilib/TextureManager.h" +#include "playlists/SmartPlayList.h" +#include "profiles/ProfileManager.h" +#include "utils/FileUtils.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/XMLUtils.h" +#include "utils/log.h" + +using namespace XFILE; + +CLibraryDirectory::CLibraryDirectory(void) = default; + +CLibraryDirectory::~CLibraryDirectory(void) = default; + +bool CLibraryDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + std::string libNode = GetNode(url); + if (libNode.empty()) + return false; + + if (URIUtils::HasExtension(libNode, ".xml")) + { // a filter or folder node + TiXmlElement *node = LoadXML(libNode); + if (node) + { + std::string type = XMLUtils::GetAttribute(node, "type"); + if (type == "filter") + { + CSmartPlaylist playlist; + std::string type, label; + XMLUtils::GetString(node, "content", type); + if (type.empty()) + { + CLog::Log(LOGERROR, "<content> tag must not be empty for type=\"filter\" node '{}'", + libNode); + return false; + } + if (XMLUtils::GetString(node, "label", label)) + label = CGUIControlFactory::FilterLabel(label); + playlist.SetType(type); + playlist.SetName(label); + if (playlist.LoadFromXML(node) && + CSmartPlaylistDirectory::GetDirectory(playlist, items)) + { + items.SetProperty("library.filter", "true"); + items.SetPath(items.GetProperty("path.db").asString()); + return true; + } + } + else if (type == "folder") + { + std::string label; + if (XMLUtils::GetString(node, "label", label)) + label = CGUIControlFactory::FilterLabel(label); + items.SetLabel(label); + std::string path; + XMLUtils::GetPath(node, "path", path); + if (!path.empty()) + { + URIUtils::AddSlashAtEnd(path); + return CDirectory::GetDirectory(path, items, m_strFileMask, m_flags); + } + } + } + return false; + } + + // just a plain node - read the folder for XML nodes and other folders + CFileItemList nodes; + if (!CDirectory::GetDirectory(libNode, nodes, ".xml", DIR_FLAG_NO_FILE_DIRS)) + return false; + + // iterate over our nodes + std::string basePath = url.Get(); + for (int i = 0; i < nodes.Size(); i++) + { + const TiXmlElement *node = NULL; + std::string xml = nodes[i]->GetPath(); + if (nodes[i]->m_bIsFolder) + node = LoadXML(URIUtils::AddFileToFolder(xml, "index.xml")); + else + { + node = LoadXML(xml); + if (node && URIUtils::GetFileName(xml) == "index.xml") + { // set the label on our items + std::string label; + if (XMLUtils::GetString(node, "label", label)) + label = CGUIControlFactory::FilterLabel(label); + items.SetLabel(label); + continue; + } + } + if (node) + { + std::string label, icon; + if (XMLUtils::GetString(node, "label", label)) + label = CGUIControlFactory::FilterLabel(label); + XMLUtils::GetString(node, "icon", icon); + int order = 0; + node->Attribute("order", &order); + + // create item + URIUtils::RemoveSlashAtEnd(xml); + std::string folder = URIUtils::GetFileName(xml); + CFileItemPtr item(new CFileItem(URIUtils::AddFileToFolder(basePath, folder), true)); + + item->SetLabel(label); + if (!icon.empty() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture(icon)) + item->SetArt("icon", icon); + item->m_iprogramCount = order; + items.Add(item); + } + } + items.Sort(SortByPlaylistOrder, SortOrderAscending); + return true; +} + +TiXmlElement *CLibraryDirectory::LoadXML(const std::string &xmlFile) +{ + if (!CFileUtils::Exists(xmlFile)) + return nullptr; + + if (!m_doc.LoadFile(xmlFile)) + return nullptr; + + TiXmlElement *xml = m_doc.RootElement(); + if (!xml || xml->ValueStr() != "node") + return nullptr; + + // check the condition + std::string condition = XMLUtils::GetAttribute(xml, "visible"); + CGUIComponent* gui = CServiceBroker::GetGUI(); + if (condition.empty() || + (gui && gui->GetInfoManager().EvaluateBool(condition, INFO::DEFAULT_CONTEXT))) + return xml; + + return nullptr; +} + +bool CLibraryDirectory::Exists(const CURL& url) +{ + return !GetNode(url).empty(); +} + +std::string CLibraryDirectory::GetNode(const CURL& url) +{ + std::string libDir = URIUtils::AddFileToFolder(m_profileManager->GetLibraryFolder(), url.GetHostName() + "/"); + if (!CDirectory::Exists(libDir)) + libDir = URIUtils::AddFileToFolder("special://xbmc/system/library/", url.GetHostName() + "/"); + + libDir = URIUtils::AddFileToFolder(libDir, url.GetFileName()); + + // is this a virtual node (aka actual folder on disk?) + if (CDirectory::Exists(libDir)) + return libDir; + + // maybe it's an XML node? + std::string xmlNode = libDir; + URIUtils::RemoveSlashAtEnd(xmlNode); + + if (CFileUtils::Exists(xmlNode)) + return xmlNode; + + return ""; +} diff --git a/xbmc/filesystem/LibraryDirectory.h b/xbmc/filesystem/LibraryDirectory.h new file mode 100644 index 0000000..af76dd4 --- /dev/null +++ b/xbmc/filesystem/LibraryDirectory.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" +#include "utils/XBMCTinyXML.h" + +namespace XFILE +{ + class CLibraryDirectory : public IDirectory + { + public: + CLibraryDirectory(); + ~CLibraryDirectory() override; + bool GetDirectory(const CURL& url, CFileItemList &items) override; + bool Exists(const CURL& url) override; + bool AllowAll() const override { return true; } + private: + /*! \brief parse the given path and return the node corresponding to this path + \param path the library:// path to parse + \return path to the XML file or directory corresponding to this path + */ + std::string GetNode(const CURL& path); + + /*! \brief load the XML file and return a pointer to the <node> root element. + Checks visible attribute and only returns non-NULL for valid nodes that should be visible. + \param xmlFile the XML file to load and parse + \return the TiXmlElement pointer to the node, if it should be visible. + */ + TiXmlElement *LoadXML(const std::string &xmlFile); + + CXBMCTinyXML m_doc; + }; +} diff --git a/xbmc/filesystem/MultiPathDirectory.cpp b/xbmc/filesystem/MultiPathDirectory.cpp new file mode 100644 index 0000000..4cdcfcc --- /dev/null +++ b/xbmc/filesystem/MultiPathDirectory.cpp @@ -0,0 +1,309 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MultiPathDirectory.h" + +#include "Directory.h" +#include "FileItem.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "Util.h" +#include "dialogs/GUIDialogProgress.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "threads/SystemClock.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" + +using namespace XFILE; + +using namespace std::chrono_literals; + +// +// multipath://{path1}/{path2}/{path3}/.../{path-N} +// +// unlike the older virtualpath:// protocol, sub-folders are combined together into a new +// multipath:// style url. +// + +CMultiPathDirectory::CMultiPathDirectory() = default; + +CMultiPathDirectory::~CMultiPathDirectory() = default; + +bool CMultiPathDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + CLog::Log(LOGDEBUG, "CMultiPathDirectory::GetDirectory({})", url.GetRedacted()); + + std::vector<std::string> vecPaths; + if (!GetPaths(url, vecPaths)) + return false; + + XbmcThreads::EndTime<> progressTime(3000ms); // 3 seconds before showing progress bar + CGUIDialogProgress* dlgProgress = NULL; + + unsigned int iFailures = 0; + for (unsigned int i = 0; i < vecPaths.size(); ++i) + { + // show the progress dialog if we have passed our time limit + if (progressTime.IsTimePast() && !dlgProgress) + { + dlgProgress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS); + if (dlgProgress) + { + dlgProgress->SetHeading(CVariant{15310}); + dlgProgress->SetLine(0, CVariant{15311}); + dlgProgress->SetLine(1, CVariant{""}); + dlgProgress->SetLine(2, CVariant{""}); + dlgProgress->Open(); + dlgProgress->ShowProgressBar(true); + dlgProgress->SetProgressMax((int)vecPaths.size()*2); + dlgProgress->Progress(); + } + } + if (dlgProgress) + { + CURL url(vecPaths[i]); + dlgProgress->SetLine(1, CVariant{url.GetWithoutUserDetails()}); + dlgProgress->SetProgressAdvance(); + dlgProgress->Progress(); + } + + CFileItemList tempItems; + CLog::Log(LOGDEBUG, "Getting Directory ({})", CURL::GetRedacted(vecPaths[i])); + if (CDirectory::GetDirectory(vecPaths[i], tempItems, m_strFileMask, m_flags)) + items.Append(tempItems); + else + { + CLog::Log(LOGERROR, "Error Getting Directory ({})", CURL::GetRedacted(vecPaths[i])); + iFailures++; + } + + if (dlgProgress) + { + dlgProgress->SetProgressAdvance(); + dlgProgress->Progress(); + } + } + + if (dlgProgress) + dlgProgress->Close(); + + if (iFailures == vecPaths.size()) + return false; + + // merge like-named folders into a sub multipath:// style url + MergeItems(items); + + return true; +} + +bool CMultiPathDirectory::Exists(const CURL& url) +{ + CLog::Log(LOGDEBUG, "Testing Existence ({})", url.GetRedacted()); + + std::vector<std::string> vecPaths; + if (!GetPaths(url, vecPaths)) + return false; + + for (unsigned int i = 0; i < vecPaths.size(); ++i) + { + CLog::Log(LOGDEBUG, "Testing Existence ({})", CURL::GetRedacted(vecPaths[i])); + if (CDirectory::Exists(vecPaths[i])) + return true; + } + return false; +} + +bool CMultiPathDirectory::Remove(const CURL& url) +{ + std::vector<std::string> vecPaths; + if (!GetPaths(url, vecPaths)) + return false; + + bool success = false; + for (unsigned int i = 0; i < vecPaths.size(); ++i) + { + if (CDirectory::Remove(vecPaths[i])) + success = true; + } + return success; +} + +std::string CMultiPathDirectory::GetFirstPath(const std::string &strPath) +{ + size_t pos = strPath.find('/', 12); + if (pos != std::string::npos) + return CURL::Decode(strPath.substr(12, pos - 12)); + return ""; +} + +bool CMultiPathDirectory::GetPaths(const CURL& url, std::vector<std::string>& vecPaths) +{ + const std::string pathToUrl(url.Get()); + return GetPaths(pathToUrl, vecPaths); +} + +bool CMultiPathDirectory::GetPaths(const std::string& path, std::vector<std::string>& paths) +{ + paths.clear(); + + // remove multipath:// from path and any trailing / (so that the last path doesn't get any more than it originally had) + std::string path1 = path.substr(12); + path1.erase(path1.find_last_not_of('/')+1); + + // split on "/" + std::vector<std::string> temp = StringUtils::Split(path1, '/'); + if (temp.empty()) + return false; + + // URL decode each item + paths.resize(temp.size()); + std::transform(temp.begin(), temp.end(), paths.begin(), CURL::Decode); + return true; +} + +bool CMultiPathDirectory::HasPath(const std::string& strPath, const std::string& strPathToFind) +{ + // remove multipath:// from path and any trailing / (so that the last path doesn't get any more than it originally had) + std::string strPath1 = strPath.substr(12); + URIUtils::RemoveSlashAtEnd(strPath1); + + // split on "/" + std::vector<std::string> vecTemp = StringUtils::Split(strPath1, '/'); + if (vecTemp.empty()) + return false; + + // check each item + for (unsigned int i = 0; i < vecTemp.size(); i++) + { + if (CURL::Decode(vecTemp[i]) == strPathToFind) + return true; + } + return false; +} + +std::string CMultiPathDirectory::ConstructMultiPath(const CFileItemList& items, const std::vector<int> &stack) +{ + // we replace all instances of comma's with double comma's, then separate + // the paths using " , " + //CLog::Log(LOGDEBUG, "Building multipath"); + std::string newPath = "multipath://"; + //CLog::Log(LOGDEBUG, "-- adding path: {}", strPath); + for (unsigned int i = 0; i < stack.size(); ++i) + AddToMultiPath(newPath, items[stack[i]]->GetPath()); + + //CLog::Log(LOGDEBUG, "Final path: {}", newPath); + return newPath; +} + +void CMultiPathDirectory::AddToMultiPath(std::string& strMultiPath, const std::string& strPath) +{ + URIUtils::AddSlashAtEnd(strMultiPath); + //CLog::Log(LOGDEBUG, "-- adding path: {}", strPath); + strMultiPath += CURL::Encode(strPath); + strMultiPath += "/"; +} + +std::string CMultiPathDirectory::ConstructMultiPath(const std::vector<std::string> &vecPaths) +{ + // we replace all instances of comma's with double comma's, then separate + // the paths using " , " + //CLog::Log(LOGDEBUG, "Building multipath"); + std::string newPath = "multipath://"; + //CLog::Log(LOGDEBUG, "-- adding path: {}", strPath); + for (std::vector<std::string>::const_iterator path = vecPaths.begin(); path != vecPaths.end(); ++path) + AddToMultiPath(newPath, *path); + //CLog::Log(LOGDEBUG, "Final path: {}", newPath); + return newPath; +} + +std::string CMultiPathDirectory::ConstructMultiPath(const std::set<std::string> &setPaths) +{ + std::string newPath = "multipath://"; + for (const std::string& path : setPaths) + AddToMultiPath(newPath, path); + + return newPath; +} + +void CMultiPathDirectory::MergeItems(CFileItemList &items) +{ + CLog::Log(LOGDEBUG, "CMultiPathDirectory::MergeItems, items = {}", items.Size()); + auto start = std::chrono::steady_clock::now(); + if (items.Size() == 0) + return; + // sort items by label + // folders are before files in this sort method + items.Sort(SortByLabel, SortOrderAscending); + int i = 0; + + // if first item in the sorted list is a file, just abort + if (!items.Get(i)->m_bIsFolder) + return; + + while (i + 1 < items.Size()) + { + // there are no more folders left, so exit the loop + CFileItemPtr pItem1 = items.Get(i); + if (!pItem1->m_bIsFolder) + break; + + std::vector<int> stack; + stack.push_back(i); + CLog::Log(LOGDEBUG, "Testing path: [{:03}] {}", i, CURL::GetRedacted(pItem1->GetPath())); + + int j = i + 1; + do + { + CFileItemPtr pItem2 = items.Get(j); + if (pItem2->GetLabel() != pItem1->GetLabel()) + break; + + // ignore any filefolders which may coincidently have + // the same label as a true folder + if (!pItem2->IsFileFolder()) + { + stack.push_back(j); + CLog::Log(LOGDEBUG, " Adding path: [{:03}] {}", j, CURL::GetRedacted(pItem2->GetPath())); + } + j++; + } + while (j < items.Size()); + + // do we have anything to combine? + if (stack.size() > 1) + { + // we have a multipath so remove the items and add the new item + std::string newPath = ConstructMultiPath(items, stack); + for (unsigned int k = stack.size() - 1; k > 0; --k) + items.Remove(stack[k]); + pItem1->SetPath(newPath); + CLog::Log(LOGDEBUG, " New path: {}", CURL::GetRedacted(pItem1->GetPath())); + } + + i++; + } + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + CLog::Log(LOGDEBUG, "CMultiPathDirectory::MergeItems, items = {}, took {} ms", items.Size(), + duration.count()); +} + +bool CMultiPathDirectory::SupportsWriteFileOperations(const std::string &strPath) +{ + std::vector<std::string> paths; + GetPaths(strPath, paths); + for (unsigned int i = 0; i < paths.size(); ++i) + if (CUtil::SupportsWriteFileOperations(paths[i])) + return true; + return false; +} diff --git a/xbmc/filesystem/MultiPathDirectory.h b/xbmc/filesystem/MultiPathDirectory.h new file mode 100644 index 0000000..7f92217 --- /dev/null +++ b/xbmc/filesystem/MultiPathDirectory.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" + +#include <set> +#include <string> +#include <vector> + +namespace XFILE +{ +class CMultiPathDirectory : + public IDirectory +{ +public: + CMultiPathDirectory(void); + ~CMultiPathDirectory(void) override; + bool GetDirectory(const CURL& url, CFileItemList &items) override; + bool Exists(const CURL& url) override; + bool Remove(const CURL& url) override; + + static std::string GetFirstPath(const std::string &strPath); + static bool SupportsWriteFileOperations(const std::string &strPath); + static bool GetPaths(const CURL& url, std::vector<std::string>& vecPaths); + static bool GetPaths(const std::string& path, std::vector<std::string>& paths); + static bool HasPath(const std::string& strPath, const std::string& strPathToFind); + static std::string ConstructMultiPath(const std::vector<std::string> &vecPaths); + static std::string ConstructMultiPath(const std::set<std::string> &setPaths); + +private: + void MergeItems(CFileItemList &items); + static void AddToMultiPath(std::string& strMultiPath, const std::string& strPath); + std::string ConstructMultiPath(const CFileItemList& items, const std::vector<int> &stack); +}; +} diff --git a/xbmc/filesystem/MultiPathFile.cpp b/xbmc/filesystem/MultiPathFile.cpp new file mode 100644 index 0000000..1f1bf96 --- /dev/null +++ b/xbmc/filesystem/MultiPathFile.cpp @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MultiPathFile.h" + +#include "MultiPathDirectory.h" +#include "URL.h" +#include "utils/URIUtils.h" + +using namespace XFILE; + +CMultiPathFile::CMultiPathFile(void) + : COverrideFile(false) +{ } + +CMultiPathFile::~CMultiPathFile(void) = default; + +bool CMultiPathFile::Open(const CURL& url) +{ + // grab the filename off the url + std::string path, fileName; + URIUtils::Split(url.Get(), path, fileName); + std::vector<std::string> vecPaths; + if (!CMultiPathDirectory::GetPaths(path, vecPaths)) + return false; + + for (unsigned int i = 0; i < vecPaths.size(); i++) + { + std::string filePath = vecPaths[i]; + filePath = URIUtils::AddFileToFolder(filePath, fileName); + if (m_file.Open(filePath)) + return true; + } + return false; +} + +bool CMultiPathFile::Exists(const CURL& url) +{ + // grab the filename off the url + std::string path, fileName; + URIUtils::Split(url.Get(), path, fileName); + std::vector<std::string> vecPaths; + if (!CMultiPathDirectory::GetPaths(path, vecPaths)) + return false; + + for (unsigned int i = 0; i < vecPaths.size(); i++) + { + std::string filePath = vecPaths[i]; + filePath = URIUtils::AddFileToFolder(filePath, fileName); + if (CFile::Exists(filePath)) + return true; + } + return false; +} + +int CMultiPathFile::Stat(const CURL& url, struct __stat64* buffer) +{ + // grab the filename off the url + std::string path, fileName; + URIUtils::Split(url.Get(), path, fileName); + std::vector<std::string> vecPaths; + if (!CMultiPathDirectory::GetPaths(path, vecPaths)) + return false; + + for (unsigned int i = 0; i < vecPaths.size(); i++) + { + std::string filePath = vecPaths[i]; + filePath = URIUtils::AddFileToFolder(filePath, fileName); + int ret = CFile::Stat(filePath, buffer); + if (ret == 0) + return ret; + } + return -1; +} + +std::string CMultiPathFile::TranslatePath(const CURL& url) +{ + return url.Get(); +} diff --git a/xbmc/filesystem/MultiPathFile.h b/xbmc/filesystem/MultiPathFile.h new file mode 100644 index 0000000..edff559 --- /dev/null +++ b/xbmc/filesystem/MultiPathFile.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "filesystem/OverrideFile.h" + +namespace XFILE +{ + class CMultiPathFile : public COverrideFile + { + public: + CMultiPathFile(void); + ~CMultiPathFile(void) override; + bool Open(const CURL& url) override; + bool Exists(const CURL& url) override; + int Stat(const CURL& url, struct __stat64* buffer) override; + + protected: + std::string TranslatePath(const CURL &url) override; + }; +} diff --git a/xbmc/filesystem/MusicDatabaseDirectory.cpp b/xbmc/filesystem/MusicDatabaseDirectory.cpp new file mode 100644 index 0000000..f998d55 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory.cpp @@ -0,0 +1,344 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicDatabaseDirectory.h" + +#include "FileItem.h" +#include "MusicDatabaseDirectory/QueryParams.h" +#include "ServiceBroker.h" +#include "filesystem/File.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/TextureManager.h" +#include "music/MusicDatabase.h" +#include "music/MusicDbUrl.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/Crc32.h" +#include "utils/LegacyPathTranslation.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +using namespace XFILE; +using namespace MUSICDATABASEDIRECTORY; + +CMusicDatabaseDirectory::CMusicDatabaseDirectory(void) = default; + +CMusicDatabaseDirectory::~CMusicDatabaseDirectory(void) = default; + +bool CMusicDatabaseDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + std::string path = CLegacyPathTranslation::TranslateMusicDbPath(url); + + // Adjust path to control navigation from albums to discs or directly to songs + CQueryParams params; + NODE_TYPE type; + NODE_TYPE childtype; + GetDirectoryNodeInfo(path, type, childtype, params); + if (childtype == NODE_TYPE_DISC) + { + bool bFlatten = false; + if (params.GetAlbumId() < 0) + bFlatten = true; // Showing *all albums next always songs + else + { + // Option to show discs for ordinary albums (not just boxed sets) + bFlatten = !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_SHOWDISCS); + CMusicDatabase musicdatabase; + if (musicdatabase.Open()) + { + if (bFlatten) // Check for boxed set + bFlatten = !musicdatabase.IsAlbumBoxset(params.GetAlbumId()); + if (!bFlatten) + { // Check we will get more than 1 disc when path filter options applied + int iDiscTotal = musicdatabase.GetDiscsCount(path); + bFlatten = iDiscTotal <= 1; + } + } + musicdatabase.Close(); + } + if (bFlatten) + { // Skip discs level and go directly to songs + CMusicDbUrl musicUrl; + if (!musicUrl.FromString(path)) + return false; + musicUrl.AppendPath("-2/"); // Flattened so adjust list label etc. + path = musicUrl.ToString(); + } + } + + items.SetPath(path); + items.m_dwSize = -1; // No size + + std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path)); + + if (!pNode) + return false; + + bool bResult = pNode->GetChilds(items); + for (int i=0;i<items.Size();++i) + { + CFileItemPtr item = items[i]; + if (item->m_bIsFolder && !item->HasArt("icon") && !item->HasArt("thumb")) + { + std::string strImage = GetIcon(item->GetPath()); + if (!strImage.empty() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture(strImage)) + item->SetArt("icon", strImage); + } + } + if (items.GetLabel().empty()) + items.SetLabel(pNode->GetLocalizedName()); + + return bResult; +} + +NODE_TYPE CMusicDatabaseDirectory::GetDirectoryChildType(const std::string& strPath) +{ + std::string path = CLegacyPathTranslation::TranslateMusicDbPath(strPath); + std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path)); + + if (!pNode) + return NODE_TYPE_NONE; + + return pNode->GetChildType(); +} + +NODE_TYPE CMusicDatabaseDirectory::GetDirectoryType(const std::string& strPath) +{ + std::string path = CLegacyPathTranslation::TranslateMusicDbPath(strPath); + std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path)); + + if (!pNode) + return NODE_TYPE_NONE; + + return pNode->GetType(); +} + +NODE_TYPE CMusicDatabaseDirectory::GetDirectoryParentType(const std::string& strPath) +{ + std::string path = CLegacyPathTranslation::TranslateMusicDbPath(strPath); + std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path)); + + if (!pNode) + return NODE_TYPE_NONE; + + CDirectoryNode* pParentNode=pNode->GetParent(); + + if (!pParentNode) + return NODE_TYPE_NONE; + + return pParentNode->GetChildType(); +} + +bool CMusicDatabaseDirectory::GetDirectoryNodeInfo(const std::string& strPath, + MUSICDATABASEDIRECTORY::NODE_TYPE& type, + MUSICDATABASEDIRECTORY::NODE_TYPE& childtype, + MUSICDATABASEDIRECTORY::CQueryParams& params) +{ + std::string path = CLegacyPathTranslation::TranslateMusicDbPath(strPath); + if (!CDirectoryNode::GetNodeInfo(path, type, childtype, params)) + return false; + + return true; +} + +bool CMusicDatabaseDirectory::IsArtistDir(const std::string& strDirectory) +{ + NODE_TYPE node=GetDirectoryType(strDirectory); + return (node==NODE_TYPE_ARTIST); +} + +void CMusicDatabaseDirectory::ClearDirectoryCache(const std::string& strDirectory) +{ + std::string path = CLegacyPathTranslation::TranslateMusicDbPath(strDirectory); + URIUtils::RemoveSlashAtEnd(path); + + uint32_t crc = Crc32::ComputeFromLowerCase(path); + + std::string strFileName = StringUtils::Format("special://temp/archive_cache/{:08x}.fi", crc); + CFile::Delete(strFileName); +} + +bool CMusicDatabaseDirectory::IsAllItem(const std::string& strDirectory) +{ + //Last query parameter, ignoring any appended options, is -1 or -2 + CURL url(strDirectory); + if (StringUtils::EndsWith(url.GetWithoutOptions(), "/-1/") || // any albumid + StringUtils::EndsWith(url.GetWithoutOptions(), "/-1/-2/")) // any albumid + flattened + return true; + return false; +} + +bool CMusicDatabaseDirectory::GetLabel(const std::string& strDirectory, std::string& strLabel) +{ + strLabel = ""; + + std::string path = CLegacyPathTranslation::TranslateMusicDbPath(strDirectory); + std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path)); + if (!pNode) + return false; + + // first see if there's any filter criteria + CQueryParams params; + CDirectoryNode::GetDatabaseInfo(path, params); + + CMusicDatabase musicdatabase; + if (!musicdatabase.Open()) + return false; + + // get genre + if (params.GetGenreId() >= 0) + strLabel += musicdatabase.GetGenreById(params.GetGenreId()); + + // get artist + if (params.GetArtistId() >= 0) + { + if (!strLabel.empty()) + strLabel += " / "; + strLabel += musicdatabase.GetArtistById(params.GetArtistId()); + } + + // get album + if (params.GetAlbumId() >= 0) + { + if (!strLabel.empty()) + strLabel += " / "; + strLabel += musicdatabase.GetAlbumById(params.GetAlbumId()); + } + + if (strLabel.empty()) + { + switch (pNode->GetChildType()) + { + case NODE_TYPE_TOP100: + strLabel = g_localizeStrings.Get(271); // Top 100 + break; + case NODE_TYPE_GENRE: + strLabel = g_localizeStrings.Get(135); // Genres + break; + case NODE_TYPE_SOURCE: + strLabel = g_localizeStrings.Get(39030); // Sources + break; + case NODE_TYPE_ROLE: + strLabel = g_localizeStrings.Get(38033); // Roles + break; + case NODE_TYPE_ARTIST: + strLabel = g_localizeStrings.Get(133); // Artists + break; + case NODE_TYPE_ALBUM: + strLabel = g_localizeStrings.Get(132); // Albums + break; + case NODE_TYPE_ALBUM_RECENTLY_ADDED: + case NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS: + strLabel = g_localizeStrings.Get(359); // Recently Added Albums + break; + case NODE_TYPE_ALBUM_RECENTLY_PLAYED: + case NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS: + strLabel = g_localizeStrings.Get(517); // Recently Played Albums + break; + case NODE_TYPE_ALBUM_TOP100: + case NODE_TYPE_ALBUM_TOP100_SONGS: + strLabel = g_localizeStrings.Get(10505); // Top 100 Albums + break; + case NODE_TYPE_SINGLES: + strLabel = g_localizeStrings.Get(1050); // Singles + break; + case NODE_TYPE_SONG: + strLabel = g_localizeStrings.Get(134); // Songs + break; + case NODE_TYPE_SONG_TOP100: + strLabel = g_localizeStrings.Get(10504); // Top 100 Songs + break; + case NODE_TYPE_YEAR: + strLabel = g_localizeStrings.Get(652); // Years + break; + case NODE_TYPE_OVERVIEW: + strLabel = ""; + break; + default: + return false; + } + } + + return true; +} + +bool CMusicDatabaseDirectory::ContainsSongs(const std::string &path) +{ + MUSICDATABASEDIRECTORY::NODE_TYPE type = GetDirectoryChildType(path); + if (type == MUSICDATABASEDIRECTORY::NODE_TYPE_SONG) return true; + if (type == MUSICDATABASEDIRECTORY::NODE_TYPE_SINGLES) return true; + if (type == MUSICDATABASEDIRECTORY::NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS) return true; + if (type == MUSICDATABASEDIRECTORY::NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS) return true; + if (type == MUSICDATABASEDIRECTORY::NODE_TYPE_ALBUM_TOP100_SONGS) return true; + if (type == MUSICDATABASEDIRECTORY::NODE_TYPE_SONG_TOP100) return true; + if (type == MUSICDATABASEDIRECTORY::NODE_TYPE_DISC) return true; + return false; +} + +bool CMusicDatabaseDirectory::Exists(const CURL& url) +{ + std::string path = CLegacyPathTranslation::TranslateMusicDbPath(url); + std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path)); + + if (!pNode) + return false; + + if (pNode->GetChildType() == MUSICDATABASEDIRECTORY::NODE_TYPE_NONE) + return false; + + return true; +} + +bool CMusicDatabaseDirectory::CanCache(const std::string& strPath) +{ + std::string path = CLegacyPathTranslation::TranslateMusicDbPath(strPath); + std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path)); + if (!pNode) + return false; + return pNode->CanCache(); +} + +std::string CMusicDatabaseDirectory::GetIcon(const std::string &strDirectory) +{ + switch (GetDirectoryChildType(strDirectory)) + { + case NODE_TYPE_ARTIST: + return "DefaultMusicArtists.png"; + case NODE_TYPE_GENRE: + return "DefaultMusicGenres.png"; + case NODE_TYPE_SOURCE: + return "DefaultMusicSources.png"; + case NODE_TYPE_ROLE: + return "DefaultMusicRoles.png"; + case NODE_TYPE_TOP100: + return "DefaultMusicTop100.png"; + case NODE_TYPE_ALBUM: + return "DefaultMusicAlbums.png"; + case NODE_TYPE_ALBUM_RECENTLY_ADDED: + case NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS: + return "DefaultMusicRecentlyAdded.png"; + case NODE_TYPE_ALBUM_RECENTLY_PLAYED: + case NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS: + return "DefaultMusicRecentlyPlayed.png"; + case NODE_TYPE_SINGLES: + case NODE_TYPE_SONG: + return "DefaultMusicSongs.png"; + case NODE_TYPE_ALBUM_TOP100: + case NODE_TYPE_ALBUM_TOP100_SONGS: + return "DefaultMusicTop100Albums.png"; + case NODE_TYPE_SONG_TOP100: + return "DefaultMusicTop100Songs.png"; + case NODE_TYPE_YEAR: + return "DefaultMusicYears.png"; + default: + break; + } + + return ""; +} diff --git a/xbmc/filesystem/MusicDatabaseDirectory.h b/xbmc/filesystem/MusicDatabaseDirectory.h new file mode 100644 index 0000000..dc52464 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" +#include "MusicDatabaseDirectory/DirectoryNode.h" +#include "MusicDatabaseDirectory/QueryParams.h" + +namespace XFILE +{ + class CMusicDatabaseDirectory : public IDirectory + { + public: + CMusicDatabaseDirectory(void); + ~CMusicDatabaseDirectory(void) override; + bool GetDirectory(const CURL& url, CFileItemList &items) override; + bool AllowAll() const override { return true; } + bool Exists(const CURL& url) override; + static MUSICDATABASEDIRECTORY::NODE_TYPE GetDirectoryChildType(const std::string& strPath); + static MUSICDATABASEDIRECTORY::NODE_TYPE GetDirectoryType(const std::string& strPath); + static MUSICDATABASEDIRECTORY::NODE_TYPE GetDirectoryParentType(const std::string& strPath); + static bool GetDirectoryNodeInfo(const std::string& strPath, MUSICDATABASEDIRECTORY::NODE_TYPE& type, MUSICDATABASEDIRECTORY::NODE_TYPE& childtype, MUSICDATABASEDIRECTORY::CQueryParams& params); + bool IsArtistDir(const std::string& strDirectory); + void ClearDirectoryCache(const std::string& strDirectory); + static bool IsAllItem(const std::string& strDirectory); + static bool GetLabel(const std::string& strDirectory, std::string& strLabel); + bool ContainsSongs(const std::string &path); + static bool CanCache(const std::string& strPath); + static std::string GetIcon(const std::string& strDirectory); + }; +} diff --git a/xbmc/filesystem/MusicDatabaseDirectory/CMakeLists.txt b/xbmc/filesystem/MusicDatabaseDirectory/CMakeLists.txt new file mode 100644 index 0000000..6eafcba --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/CMakeLists.txt @@ -0,0 +1,39 @@ +set(SOURCES DirectoryNodeAlbum.cpp + DirectoryNodeAlbumRecentlyAdded.cpp + DirectoryNodeAlbumRecentlyAddedSong.cpp + DirectoryNodeAlbumRecentlyPlayed.cpp + DirectoryNodeAlbumRecentlyPlayedSong.cpp + DirectoryNodeAlbumTop100.cpp + DirectoryNodeAlbumTop100Song.cpp + DirectoryNodeArtist.cpp + DirectoryNodeDiscs.cpp + DirectoryNode.cpp + DirectoryNodeGrouped.cpp + DirectoryNodeOverview.cpp + DirectoryNodeRoot.cpp + DirectoryNodeSingles.cpp + DirectoryNodeSong.cpp + DirectoryNodeSongTop100.cpp + DirectoryNodeTop100.cpp + QueryParams.cpp) + +set(HEADERS DirectoryNode.h + DirectoryNodeAlbum.h + DirectoryNodeAlbumRecentlyAdded.h + DirectoryNodeAlbumRecentlyAddedSong.h + DirectoryNodeAlbumRecentlyPlayed.h + DirectoryNodeAlbumRecentlyPlayedSong.h + DirectoryNodeAlbumTop100.h + DirectoryNodeAlbumTop100Song.h + DirectoryNodeArtist.h + DirectoryNodeDiscs.h + DirectoryNodeGrouped.h + DirectoryNodeOverview.h + DirectoryNodeRoot.h + DirectoryNodeSingles.h + DirectoryNodeSong.h + DirectoryNodeSongTop100.h + DirectoryNodeTop100.h + QueryParams.h) + +core_add_library(musicdatabasedirectory) diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNode.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNode.cpp new file mode 100644 index 0000000..00e1bce --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNode.cpp @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNode.h" + +#include "DirectoryNodeAlbum.h" +#include "DirectoryNodeAlbumRecentlyAdded.h" +#include "DirectoryNodeAlbumRecentlyAddedSong.h" +#include "DirectoryNodeAlbumRecentlyPlayed.h" +#include "DirectoryNodeAlbumRecentlyPlayedSong.h" +#include "DirectoryNodeAlbumTop100.h" +#include "DirectoryNodeAlbumTop100Song.h" +#include "DirectoryNodeArtist.h" +#include "DirectoryNodeDiscs.h" +#include "DirectoryNodeGrouped.h" +#include "DirectoryNodeOverview.h" +#include "DirectoryNodeRoot.h" +#include "DirectoryNodeSingles.h" +#include "DirectoryNodeSong.h" +#include "DirectoryNodeSongTop100.h" +#include "DirectoryNodeTop100.h" +#include "FileItem.h" +#include "QueryParams.h" +#include "URL.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +using namespace XFILE::MUSICDATABASEDIRECTORY; + +// Constructor is protected use ParseURL() +CDirectoryNode::CDirectoryNode(NODE_TYPE Type, const std::string& strName, CDirectoryNode* pParent) +{ + m_Type=Type; + m_strName=strName; + m_pParent=pParent; +} + +CDirectoryNode::~CDirectoryNode() +{ + delete m_pParent; +} + +// Parses a given path and returns the current node of the path +CDirectoryNode* CDirectoryNode::ParseURL(const std::string& strPath) +{ + CURL url(strPath); + + std::string strDirectory=url.GetFileName(); + URIUtils::RemoveSlashAtEnd(strDirectory); + + std::vector<std::string> Path = StringUtils::Split(strDirectory, '/'); + Path.insert(Path.begin(), ""); + + CDirectoryNode* pNode = nullptr; + CDirectoryNode* pParent = nullptr; + NODE_TYPE NodeType = NODE_TYPE_ROOT; + + for (int i=0; i < static_cast<int>(Path.size()); ++i) + { + pNode = CreateNode(NodeType, Path[i], pParent); + NodeType = pNode ? pNode->GetChildType() : NODE_TYPE_NONE; + pParent = pNode; + } + + // Add all the additional URL options to the last node + if (pNode) + pNode->AddOptions(url.GetOptions()); + + return pNode; +} + +// returns the database ids of the path, +void CDirectoryNode::GetDatabaseInfo(const std::string& strPath, CQueryParams& params) +{ + std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(strPath)); + + if (!pNode) + return; + + pNode->CollectQueryParams(params); +} + +bool CDirectoryNode::GetNodeInfo(const std::string& strPath, + NODE_TYPE& type, + NODE_TYPE& childtype, + CQueryParams& params) +{ + std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(strPath)); + if (!pNode) + return false; + + type = pNode->GetType(); + childtype = pNode->GetChildType(); + pNode->CollectQueryParams(params); + + return true; +} + +// Create a node object +CDirectoryNode* CDirectoryNode::CreateNode(NODE_TYPE Type, const std::string& strName, CDirectoryNode* pParent) +{ + switch (Type) + { + case NODE_TYPE_ROOT: + return new CDirectoryNodeRoot(strName, pParent); + case NODE_TYPE_OVERVIEW: + return new CDirectoryNodeOverview(strName, pParent); + case NODE_TYPE_GENRE: + case NODE_TYPE_SOURCE: + case NODE_TYPE_ROLE: + case NODE_TYPE_YEAR: + return new CDirectoryNodeGrouped(Type, strName, pParent); + case NODE_TYPE_DISC: + return new CDirectoryNodeDiscs(strName, pParent); + case NODE_TYPE_ARTIST: + return new CDirectoryNodeArtist(strName, pParent); + case NODE_TYPE_ALBUM: + return new CDirectoryNodeAlbum(strName, pParent); + case NODE_TYPE_SONG: + return new CDirectoryNodeSong(strName, pParent); + case NODE_TYPE_SINGLES: + return new CDirectoryNodeSingles(strName, pParent); + case NODE_TYPE_TOP100: + return new CDirectoryNodeTop100(strName, pParent); + case NODE_TYPE_ALBUM_TOP100: + return new CDirectoryNodeAlbumTop100(strName, pParent); + case NODE_TYPE_ALBUM_TOP100_SONGS: + return new CDirectoryNodeAlbumTop100Song(strName, pParent); + case NODE_TYPE_SONG_TOP100: + return new CDirectoryNodeSongTop100(strName, pParent); + case NODE_TYPE_ALBUM_RECENTLY_ADDED: + return new CDirectoryNodeAlbumRecentlyAdded(strName, pParent); + case NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS: + return new CDirectoryNodeAlbumRecentlyAddedSong(strName, pParent); + case NODE_TYPE_ALBUM_RECENTLY_PLAYED: + return new CDirectoryNodeAlbumRecentlyPlayed(strName, pParent); + case NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS: + return new CDirectoryNodeAlbumRecentlyPlayedSong(strName, pParent); + default: + break; + } + + return nullptr; +} + +// Current node name +const std::string& CDirectoryNode::GetName() const +{ + return m_strName; +} + +int CDirectoryNode::GetID() const +{ + return atoi(m_strName.c_str()); +} + +std::string CDirectoryNode::GetLocalizedName() const +{ + return ""; +} + +// Current node type +NODE_TYPE CDirectoryNode::GetType() const +{ + return m_Type; +} + +// Return the parent directory node or NULL, if there is no +CDirectoryNode* CDirectoryNode::GetParent() const +{ + return m_pParent; +} + +void CDirectoryNode::RemoveParent() +{ + m_pParent = nullptr; +} + +// should be overloaded by a derived class +// to get the content of a node. Will be called +// by GetChilds() of a parent node +bool CDirectoryNode::GetContent(CFileItemList& items) const +{ + return false; +} + +// Creates a musicdb url +std::string CDirectoryNode::BuildPath() const +{ + std::vector<std::string> array; + + if (!m_strName.empty()) + array.insert(array.begin(), m_strName); + + CDirectoryNode* pParent=m_pParent; + while (pParent != nullptr) + { + const std::string& strNodeName=pParent->GetName(); + if (!strNodeName.empty()) + array.insert(array.begin(), strNodeName); + + pParent=pParent->GetParent(); + } + + std::string strPath="musicdb://"; + for (int i = 0; i < static_cast<int>(array.size()); ++i) + strPath+=array[i]+"/"; + + std::string options = m_options.GetOptionsString(); + if (!options.empty()) + strPath += "?" + options; + + return strPath; +} + +void CDirectoryNode::AddOptions(const std::string &options) +{ + if (options.empty()) + return; + + m_options.AddOptions(options); +} + +// Collects Query params from this and all parent nodes. If a NODE_TYPE can +// be used as a database parameter, it will be added to the +// params object. +void CDirectoryNode::CollectQueryParams(CQueryParams& params) const +{ + params.SetQueryParam(m_Type, m_strName); + + CDirectoryNode* pParent=m_pParent; + while (pParent != nullptr) + { + params.SetQueryParam(pParent->GetType(), pParent->GetName()); + pParent=pParent->GetParent(); + } +} + +// Should be overloaded by a derived class. +// Returns the NODE_TYPE of the child nodes. +NODE_TYPE CDirectoryNode::GetChildType() const +{ + return NODE_TYPE_NONE; +} + +// Get the child fileitems of this node +bool CDirectoryNode::GetChilds(CFileItemList& items) +{ + if (CanCache() && items.Load()) + return true; + + std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::CreateNode(GetChildType(), "", this)); + + bool bSuccess=false; + if (pNode) + { + pNode->m_options = m_options; + bSuccess=pNode->GetContent(items); + if (bSuccess) + { + if (CanCache()) + items.SetCacheToDisc(CFileItemList::CACHE_ALWAYS); + } + else + items.Clear(); + + pNode->RemoveParent(); + } + + return bSuccess; +} + + +bool CDirectoryNode::CanCache() const +{ + // JM: No need to cache these views, as caching is added in the mediawindow baseclass for anything that takes + // longer than a second + return false; +} diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNode.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNode.h new file mode 100644 index 0000000..63d11d1 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNode.h @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "utils/UrlOptions.h" + +class CFileItemList; + +namespace XFILE +{ + namespace MUSICDATABASEDIRECTORY + { + class CQueryParams; + + typedef enum _NODE_TYPE + { + NODE_TYPE_NONE=0, + NODE_TYPE_ROOT, + NODE_TYPE_OVERVIEW, + NODE_TYPE_TOP100, + NODE_TYPE_ROLE, + NODE_TYPE_SOURCE, + NODE_TYPE_GENRE, + NODE_TYPE_ARTIST, + NODE_TYPE_ALBUM, + NODE_TYPE_ALBUM_RECENTLY_ADDED, + NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS, + NODE_TYPE_ALBUM_RECENTLY_PLAYED, + NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS, + NODE_TYPE_ALBUM_TOP100, + NODE_TYPE_ALBUM_TOP100_SONGS, + NODE_TYPE_SONG, + NODE_TYPE_SONG_TOP100, + NODE_TYPE_YEAR, + NODE_TYPE_SINGLES, + NODE_TYPE_DISC, + } NODE_TYPE; + + typedef struct { + NODE_TYPE node; + std::string id; + int label; + } Node; + + class CDirectoryNode + { + public: + static CDirectoryNode* ParseURL(const std::string& strPath); + static void GetDatabaseInfo(const std::string& strPath, CQueryParams& params); + static bool GetNodeInfo(const std::string& strPath, NODE_TYPE& type, NODE_TYPE& childtype, CQueryParams& params); + virtual ~CDirectoryNode(); + + NODE_TYPE GetType() const; + + bool GetChilds(CFileItemList& items); + virtual NODE_TYPE GetChildType() const; + virtual std::string GetLocalizedName() const; + + CDirectoryNode* GetParent() const; + virtual bool CanCache() const; + + std::string BuildPath() const; + + protected: + CDirectoryNode(NODE_TYPE Type, const std::string& strName, CDirectoryNode* pParent); + static CDirectoryNode* CreateNode(NODE_TYPE Type, const std::string& strName, CDirectoryNode* pParent); + + void AddOptions(const std::string &options); + void CollectQueryParams(CQueryParams& params) const; + + const std::string& GetName() const; + int GetID() const; + void RemoveParent(); + + virtual bool GetContent(CFileItemList& items) const; + + private: + NODE_TYPE m_Type; + std::string m_strName; + CDirectoryNode* m_pParent; + CUrlOptions m_options; + }; + } +} + + diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbum.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbum.cpp new file mode 100644 index 0000000..f60a025 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbum.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeAlbum.h" + +#include "QueryParams.h" +#include "guilib/LocalizeStrings.h" +#include "music/MusicDatabase.h" + +using namespace XFILE::MUSICDATABASEDIRECTORY; + +CDirectoryNodeAlbum::CDirectoryNodeAlbum(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_ALBUM, strName, pParent) +{ + +} + +NODE_TYPE CDirectoryNodeAlbum::GetChildType() const +{ + return NODE_TYPE_DISC; +} + +std::string CDirectoryNodeAlbum::GetLocalizedName() const +{ + if (GetID() == -1) + return g_localizeStrings.Get(15102); // All Albums + CMusicDatabase db; + if (db.Open()) + return db.GetAlbumById(GetID()); + return ""; +} + +bool CDirectoryNodeAlbum::GetContent(CFileItemList& items) const +{ + CMusicDatabase musicdatabase; + if (!musicdatabase.Open()) + return false; + + CQueryParams params; + CollectQueryParams(params); + + bool bSuccess=musicdatabase.GetAlbumsNav(BuildPath(), items, params.GetGenreId(), params.GetArtistId()); + + musicdatabase.Close(); + + return bSuccess; +} diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbum.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbum.h new file mode 100644 index 0000000..60fb233 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbum.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace MUSICDATABASEDIRECTORY + { + class CDirectoryNodeAlbum : public CDirectoryNode + { + public: + CDirectoryNodeAlbum(const std::string& strName, CDirectoryNode* pParent); + protected: + NODE_TYPE GetChildType() const override; + bool GetContent(CFileItemList& items) const override; + std::string GetLocalizedName() const override; + }; + } +} + + diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAdded.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAdded.cpp new file mode 100644 index 0000000..f274b59 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAdded.cpp @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeAlbumRecentlyAdded.h" + +#include "FileItem.h" +#include "guilib/LocalizeStrings.h" +#include "music/MusicDatabase.h" +#include "utils/StringUtils.h" + +using namespace XFILE::MUSICDATABASEDIRECTORY; + +CDirectoryNodeAlbumRecentlyAdded::CDirectoryNodeAlbumRecentlyAdded(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_ALBUM_RECENTLY_ADDED, strName, pParent) +{ + +} + +NODE_TYPE CDirectoryNodeAlbumRecentlyAdded::GetChildType() const +{ + if (GetName()=="-1") + return NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS; + + return NODE_TYPE_DISC; +} + +std::string CDirectoryNodeAlbumRecentlyAdded::GetLocalizedName() const +{ + if (GetID() == -1) + return g_localizeStrings.Get(15102); // All Albums + CMusicDatabase db; + if (db.Open()) + return db.GetAlbumById(GetID()); + return ""; +} + +bool CDirectoryNodeAlbumRecentlyAdded::GetContent(CFileItemList& items) const +{ + CMusicDatabase musicdatabase; + if (!musicdatabase.Open()) + return false; + + VECALBUMS albums; + if (!musicdatabase.GetRecentlyAddedAlbums(albums)) + { + musicdatabase.Close(); + return false; + } + + for (int i=0; i<(int)albums.size(); ++i) + { + CAlbum& album=albums[i]; + std::string strDir = StringUtils::Format("{}{}/", BuildPath(), album.idAlbum); + CFileItemPtr pItem(new CFileItem(strDir, album)); + items.Add(pItem); + } + + musicdatabase.Close(); + return true; +} diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAdded.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAdded.h new file mode 100644 index 0000000..a1b4dc1 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAdded.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace MUSICDATABASEDIRECTORY + { + class CDirectoryNodeAlbumRecentlyAdded : public CDirectoryNode + { + public: + CDirectoryNodeAlbumRecentlyAdded(const std::string& strName, CDirectoryNode* pParent); + protected: + NODE_TYPE GetChildType() const override; + bool GetContent(CFileItemList& items) const override; + std::string GetLocalizedName() const override; + }; + } +} + + diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAddedSong.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAddedSong.cpp new file mode 100644 index 0000000..8c8d786 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAddedSong.cpp @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeAlbumRecentlyAddedSong.h" + +#include "music/MusicDatabase.h" + +using namespace XFILE::MUSICDATABASEDIRECTORY; + +CDirectoryNodeAlbumRecentlyAddedSong::CDirectoryNodeAlbumRecentlyAddedSong(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS, strName, pParent) +{ + +} + +bool CDirectoryNodeAlbumRecentlyAddedSong::GetContent(CFileItemList& items) const +{ + CMusicDatabase musicdatabase; + if (!musicdatabase.Open()) + return false; + + std::string strBaseDir=BuildPath(); + bool bSuccess=musicdatabase.GetRecentlyAddedAlbumSongs(strBaseDir, items); + + musicdatabase.Close(); + + return bSuccess; +} diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAddedSong.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAddedSong.h new file mode 100644 index 0000000..9cc46c1 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAddedSong.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace MUSICDATABASEDIRECTORY + { + class CDirectoryNodeAlbumRecentlyAddedSong : public CDirectoryNode + { + public: + CDirectoryNodeAlbumRecentlyAddedSong(const std::string& strName, CDirectoryNode* pParent); + protected: + bool GetContent(CFileItemList& items) const override; + }; + } +} + + diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayed.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayed.cpp new file mode 100644 index 0000000..77940c2 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayed.cpp @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeAlbumRecentlyPlayed.h" + +#include "FileItem.h" +#include "guilib/LocalizeStrings.h" +#include "music/MusicDatabase.h" +#include "utils/StringUtils.h" + +using namespace XFILE::MUSICDATABASEDIRECTORY; + +CDirectoryNodeAlbumRecentlyPlayed::CDirectoryNodeAlbumRecentlyPlayed(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_ALBUM_RECENTLY_PLAYED, strName, pParent) +{ + +} + +NODE_TYPE CDirectoryNodeAlbumRecentlyPlayed::GetChildType() const +{ + if (GetName()=="-1") + return NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS; + + return NODE_TYPE_DISC; +} + +std::string CDirectoryNodeAlbumRecentlyPlayed::GetLocalizedName() const +{ + if (GetID() == -1) + return g_localizeStrings.Get(15102); // All Albums + CMusicDatabase db; + if (db.Open()) + return db.GetAlbumById(GetID()); + return ""; +} + +bool CDirectoryNodeAlbumRecentlyPlayed::GetContent(CFileItemList& items) const +{ + CMusicDatabase musicdatabase; + if (!musicdatabase.Open()) + return false; + + VECALBUMS albums; + if (!musicdatabase.GetRecentlyPlayedAlbums(albums)) + { + musicdatabase.Close(); + return false; + } + + for (int i=0; i<(int)albums.size(); ++i) + { + CAlbum& album=albums[i]; + std::string strDir = StringUtils::Format("{}{}/", BuildPath(), album.idAlbum); + CFileItemPtr pItem(new CFileItem(strDir, album)); + items.Add(pItem); + } + + musicdatabase.Close(); + return true; +} diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayed.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayed.h new file mode 100644 index 0000000..4691c89 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayed.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace MUSICDATABASEDIRECTORY + { + class CDirectoryNodeAlbumRecentlyPlayed : public CDirectoryNode + { + public: + CDirectoryNodeAlbumRecentlyPlayed(const std::string& strName, CDirectoryNode* pParent); + protected: + NODE_TYPE GetChildType() const override; + bool GetContent(CFileItemList& items) const override; + std::string GetLocalizedName() const override; + }; + } +} + + diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayedSong.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayedSong.cpp new file mode 100644 index 0000000..7a01af0 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayedSong.cpp @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeAlbumRecentlyPlayedSong.h" + +#include "music/MusicDatabase.h" + +using namespace XFILE::MUSICDATABASEDIRECTORY; + +CDirectoryNodeAlbumRecentlyPlayedSong::CDirectoryNodeAlbumRecentlyPlayedSong(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS, strName, pParent) +{ + +} + +bool CDirectoryNodeAlbumRecentlyPlayedSong::GetContent(CFileItemList& items) const +{ + CMusicDatabase musicdatabase; + if (!musicdatabase.Open()) + return false; + + std::string strBaseDir=BuildPath(); + bool bSuccess=musicdatabase.GetRecentlyPlayedAlbumSongs(strBaseDir, items); + + musicdatabase.Close(); + + return bSuccess; +} diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayedSong.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayedSong.h new file mode 100644 index 0000000..3ffab08 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayedSong.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace MUSICDATABASEDIRECTORY + { + class CDirectoryNodeAlbumRecentlyPlayedSong : public CDirectoryNode + { + public: + CDirectoryNodeAlbumRecentlyPlayedSong(const std::string& strName, CDirectoryNode* pParent); + protected: + bool GetContent(CFileItemList& items) const override; + }; + } +} + + diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100.cpp new file mode 100644 index 0000000..88ffaff --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeAlbumTop100.h" + +#include "FileItem.h" +#include "music/MusicDatabase.h" +#include "utils/StringUtils.h" + +using namespace XFILE::MUSICDATABASEDIRECTORY; + +CDirectoryNodeAlbumTop100::CDirectoryNodeAlbumTop100(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_ALBUM_TOP100, strName, pParent) +{ + +} + +NODE_TYPE CDirectoryNodeAlbumTop100::GetChildType() const +{ + if (GetName()=="-1") + return NODE_TYPE_ALBUM_TOP100_SONGS; + + return NODE_TYPE_SONG; +} + +std::string CDirectoryNodeAlbumTop100::GetLocalizedName() const +{ + CMusicDatabase db; + if (db.Open()) + return db.GetAlbumById(GetID()); + return ""; +} + +bool CDirectoryNodeAlbumTop100::GetContent(CFileItemList& items) const +{ + CMusicDatabase musicdatabase; + if (!musicdatabase.Open()) + return false; + + VECALBUMS albums; + if (!musicdatabase.GetTop100Albums(albums)) + { + musicdatabase.Close(); + return false; + } + + for (int i=0; i<(int)albums.size(); ++i) + { + CAlbum& album=albums[i]; + std::string strDir = StringUtils::Format("{}{}/", BuildPath(), album.idAlbum); + CFileItemPtr pItem(new CFileItem(strDir, album)); + items.Add(pItem); + } + + musicdatabase.Close(); + + return true; +} diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100.h new file mode 100644 index 0000000..de117bd --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace MUSICDATABASEDIRECTORY + { + class CDirectoryNodeAlbumTop100 : public CDirectoryNode + { + public: + CDirectoryNodeAlbumTop100(const std::string& strName, CDirectoryNode* pParent); + protected: + NODE_TYPE GetChildType() const override; + bool GetContent(CFileItemList& items) const override; + std::string GetLocalizedName() const override; + }; + } +} + + diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100Song.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100Song.cpp new file mode 100644 index 0000000..109af60 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100Song.cpp @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeAlbumTop100Song.h" + +#include "music/MusicDatabase.h" + +using namespace XFILE::MUSICDATABASEDIRECTORY; + +CDirectoryNodeAlbumTop100Song::CDirectoryNodeAlbumTop100Song(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_ALBUM_TOP100_SONGS, strName, pParent) +{ + +} + +bool CDirectoryNodeAlbumTop100Song::GetContent(CFileItemList& items) const +{ + CMusicDatabase musicdatabase; + if (!musicdatabase.Open()) + return false; + + std::string strBaseDir=BuildPath(); + bool bSuccess=musicdatabase.GetTop100AlbumSongs(strBaseDir, items); + + musicdatabase.Close(); + + return bSuccess; +} diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100Song.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100Song.h new file mode 100644 index 0000000..18351d7 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100Song.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace MUSICDATABASEDIRECTORY + { + class CDirectoryNodeAlbumTop100Song : public CDirectoryNode + { + public: + CDirectoryNodeAlbumTop100Song(const std::string& strName, CDirectoryNode* pParent); + protected: + bool GetContent(CFileItemList& items) const override; + }; + } +} + + diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeArtist.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeArtist.cpp new file mode 100644 index 0000000..b087fa2 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeArtist.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeArtist.h" + +#include "QueryParams.h" +#include "ServiceBroker.h" +#include "guilib/LocalizeStrings.h" +#include "music/MusicDatabase.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" + +using namespace XFILE::MUSICDATABASEDIRECTORY; + +CDirectoryNodeArtist::CDirectoryNodeArtist(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_ARTIST, strName, pParent) +{ + +} + +NODE_TYPE CDirectoryNodeArtist::GetChildType() const +{ + return NODE_TYPE_ALBUM; +} + +std::string CDirectoryNodeArtist::GetLocalizedName() const +{ + if (GetID() == -1) + return g_localizeStrings.Get(15103); // All Artists + CMusicDatabase db; + if (db.Open()) + return db.GetArtistById(GetID()); + return ""; +} + +bool CDirectoryNodeArtist::GetContent(CFileItemList& items) const +{ + CMusicDatabase musicdatabase; + if (!musicdatabase.Open()) + return false; + + CQueryParams params; + CollectQueryParams(params); + + bool bSuccess = musicdatabase.GetArtistsNav(BuildPath(), items, !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS), params.GetGenreId()); + + musicdatabase.Close(); + + return bSuccess; +} diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeArtist.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeArtist.h new file mode 100644 index 0000000..59b8c73 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeArtist.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace MUSICDATABASEDIRECTORY + { + class CDirectoryNodeArtist : public CDirectoryNode + { + public: + CDirectoryNodeArtist(const std::string& strName, CDirectoryNode* pParent); + protected: + NODE_TYPE GetChildType() const override; + bool GetContent(CFileItemList& items) const override; + std::string GetLocalizedName() const override; + }; + } +} + + diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeDiscs.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeDiscs.cpp new file mode 100644 index 0000000..42cf3bb --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeDiscs.cpp @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2005-2019 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeDiscs.h" + +#include "QueryParams.h" +#include "guilib/LocalizeStrings.h" +#include "music/MusicDatabase.h" + +using namespace XFILE::MUSICDATABASEDIRECTORY; + +CDirectoryNodeDiscs::CDirectoryNodeDiscs(const std::string& strName, + CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_DISC, strName, pParent) +{ +} + +NODE_TYPE CDirectoryNodeDiscs::GetChildType() const +{ + return NODE_TYPE_SONG; +} + +std::string CDirectoryNodeDiscs::GetLocalizedName() const +{ + CQueryParams params; + CollectQueryParams(params); + std::string title; + CMusicDatabase db; + if (db.Open()) + title = db.GetAlbumDiscTitle(params.GetAlbumId(), params.GetDisc()); + db.Close(); + if (title.empty()) + title = g_localizeStrings.Get(15102); // All Albums + + return title; +} + +bool CDirectoryNodeDiscs::GetContent(CFileItemList& items) const +{ + CMusicDatabase musicdatabase; + if (!musicdatabase.Open()) + return false; + + CQueryParams params; + CollectQueryParams(params); + + bool bSuccess = musicdatabase.GetDiscsNav(BuildPath(), items, params.GetAlbumId()); + + musicdatabase.Close(); + + return bSuccess; +} diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeDiscs.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeDiscs.h new file mode 100644 index 0000000..5cf4f6a --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeDiscs.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2005-2019 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ +namespace MUSICDATABASEDIRECTORY +{ +class CDirectoryNodeDiscs : public CDirectoryNode +{ +public: + CDirectoryNodeDiscs(const std::string& strName, CDirectoryNode* pParent); + +protected: + NODE_TYPE GetChildType() const override; + bool GetContent(CFileItemList& items) const override; + std::string GetLocalizedName() const override; +}; +} // namespace MUSICDATABASEDIRECTORY +} // namespace XFILE diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeGrouped.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeGrouped.cpp new file mode 100644 index 0000000..65875f7 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeGrouped.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeGrouped.h" + +#include "music/MusicDatabase.h" + +using namespace XFILE::MUSICDATABASEDIRECTORY; + +CDirectoryNodeGrouped::CDirectoryNodeGrouped(NODE_TYPE type, const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(type, strName, pParent) +{ } + +NODE_TYPE CDirectoryNodeGrouped::GetChildType() const +{ + if (GetType() == NODE_TYPE_YEAR) + return NODE_TYPE_ALBUM; + + return NODE_TYPE_ARTIST; +} + +std::string CDirectoryNodeGrouped::GetLocalizedName() const +{ + CMusicDatabase db; + if (db.Open()) + return db.GetItemById(GetContentType(), GetID()); + return ""; +} + +bool CDirectoryNodeGrouped::GetContent(CFileItemList& items) const +{ + CMusicDatabase musicdatabase; + if (!musicdatabase.Open()) + return false; + + return musicdatabase.GetItems(BuildPath(), GetContentType(), items); +} + +std::string CDirectoryNodeGrouped::GetContentType() const +{ + switch (GetType()) + { + case NODE_TYPE_GENRE: + return "genres"; + case NODE_TYPE_SOURCE: + return "sources"; + case NODE_TYPE_ROLE: + return "roles"; + case NODE_TYPE_YEAR: + return "years"; + default: + break; + } + + return ""; +} diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeGrouped.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeGrouped.h new file mode 100644 index 0000000..38e7fd9 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeGrouped.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace MUSICDATABASEDIRECTORY + { + class CDirectoryNodeGrouped : public CDirectoryNode + { + public: + CDirectoryNodeGrouped(NODE_TYPE type, const std::string& strName, CDirectoryNode* pParent); + protected: + NODE_TYPE GetChildType() const override; + bool GetContent(CFileItemList& items) const override; + std::string GetLocalizedName() const override; + + private: + std::string GetContentType() const; + }; + } +} + + diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeOverview.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeOverview.cpp new file mode 100644 index 0000000..fefeb6d --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeOverview.cpp @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeOverview.h" + +#include "FileItem.h" +#include "guilib/LocalizeStrings.h" +#include "music/MusicDatabase.h" +#include "utils/StringUtils.h" + +namespace XFILE +{ + namespace MUSICDATABASEDIRECTORY + { + Node OverviewChildren[] = { + { NODE_TYPE_GENRE, "genres", 135 }, + { NODE_TYPE_ARTIST, "artists", 133 }, + { NODE_TYPE_ALBUM, "albums", 132 }, + { NODE_TYPE_SINGLES, "singles", 1050 }, + { NODE_TYPE_SONG, "songs", 134 }, + { NODE_TYPE_YEAR, "years", 652 }, + { NODE_TYPE_TOP100, "top100", 271 }, + { NODE_TYPE_ALBUM_RECENTLY_ADDED, "recentlyaddedalbums", 359 }, + { NODE_TYPE_ALBUM_RECENTLY_PLAYED, "recentlyplayedalbums", 517 }, + { NODE_TYPE_ALBUM, "compilations", 521 }, + { NODE_TYPE_ROLE, "roles", 38033 }, + { NODE_TYPE_SOURCE, "sources", 39031 }, + { NODE_TYPE_DISC, "discs", 14087 }, + { NODE_TYPE_YEAR, "originalyears", 38078 }, + }; + }; +}; + +using namespace XFILE::MUSICDATABASEDIRECTORY; + +CDirectoryNodeOverview::CDirectoryNodeOverview(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_OVERVIEW, strName, pParent) +{ + +} + +NODE_TYPE CDirectoryNodeOverview::GetChildType() const +{ + for (const Node& node : OverviewChildren) + if (GetName() == node.id) + return node.node; + return NODE_TYPE_NONE; +} + +std::string CDirectoryNodeOverview::GetLocalizedName() const +{ + for (const Node& node : OverviewChildren) + if (GetName() == node.id) + return g_localizeStrings.Get(node.label); + return ""; +} + +bool CDirectoryNodeOverview::GetContent(CFileItemList& items) const +{ + CMusicDatabase musicDatabase; + musicDatabase.Open(); + + bool hasSingles = (musicDatabase.GetSinglesCount() > 0); + bool hasCompilations = (musicDatabase.GetCompilationAlbumsCount() > 0); + + for (unsigned int i = 0; i < sizeof(OverviewChildren) / sizeof(Node); ++i) + { + if (i == 3 && !hasSingles) + continue; + if (i == 9 && !hasCompilations) + continue; + + CFileItemPtr pItem(new CFileItem(g_localizeStrings.Get(OverviewChildren[i].label))); + std::string strDir = StringUtils::Format("{}/", OverviewChildren[i].id); + pItem->SetPath(BuildPath() + strDir); + pItem->m_bIsFolder = true; + pItem->SetCanQueue(false); + items.Add(pItem); + } + + return true; +} diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeOverview.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeOverview.h new file mode 100644 index 0000000..057a9e8 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeOverview.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace MUSICDATABASEDIRECTORY + { + class CDirectoryNodeOverview : public CDirectoryNode + { + public: + CDirectoryNodeOverview(const std::string& strName, CDirectoryNode* pParent); + protected: + NODE_TYPE GetChildType() const override; + bool GetContent(CFileItemList& items) const override; + std::string GetLocalizedName() const override; + }; + } +} + + diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeRoot.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeRoot.cpp new file mode 100644 index 0000000..31cc761 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeRoot.cpp @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeRoot.h" + +using namespace XFILE::MUSICDATABASEDIRECTORY; + +CDirectoryNodeRoot::CDirectoryNodeRoot(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_ROOT, strName, pParent) +{ + +} + +NODE_TYPE CDirectoryNodeRoot::GetChildType() const +{ + return NODE_TYPE_OVERVIEW; +} diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeRoot.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeRoot.h new file mode 100644 index 0000000..c4128a3 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeRoot.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace MUSICDATABASEDIRECTORY + { + class CDirectoryNodeRoot : public CDirectoryNode + { + public: + CDirectoryNodeRoot(const std::string& strName, CDirectoryNode* pParent); + protected: + NODE_TYPE GetChildType() const override; + }; + } +} + + diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSingles.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSingles.cpp new file mode 100644 index 0000000..8deae27 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSingles.cpp @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeSingles.h" + +#include "music/MusicDatabase.h" + +using namespace XFILE::MUSICDATABASEDIRECTORY; + +CDirectoryNodeSingles::CDirectoryNodeSingles(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_SINGLES, strName, pParent) +{ + +} + +bool CDirectoryNodeSingles::GetContent(CFileItemList& items) const +{ + CMusicDatabase musicdatabase; + if (!musicdatabase.Open()) + return false; + + bool bSuccess = musicdatabase.GetSongsFullByWhere(BuildPath(), CDatabase::Filter(), items, SortDescription(), true); + + musicdatabase.Close(); + + return bSuccess; +} diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSingles.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSingles.h new file mode 100644 index 0000000..cc297b7 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSingles.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace MUSICDATABASEDIRECTORY + { + class CDirectoryNodeSingles : public CDirectoryNode + { + public: + CDirectoryNodeSingles(const std::string& strName, CDirectoryNode* pParent); + protected: + bool GetContent(CFileItemList& items) const override; + }; + } +} + + diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSong.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSong.cpp new file mode 100644 index 0000000..4c43f90 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSong.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeSong.h" + +#include "QueryParams.h" +#include "music/MusicDatabase.h" + +using namespace XFILE::MUSICDATABASEDIRECTORY; + +CDirectoryNodeSong::CDirectoryNodeSong(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_SONG, strName, pParent) +{ + +} + +bool CDirectoryNodeSong::GetContent(CFileItemList& items) const +{ + CMusicDatabase musicdatabase; + if (!musicdatabase.Open()) + return false; + + CQueryParams params; + CollectQueryParams(params); + + std::string strBaseDir=BuildPath(); + bool bSuccess=musicdatabase.GetSongsNav(strBaseDir, items, params.GetGenreId(), params.GetArtistId(), params.GetAlbumId()); + + musicdatabase.Close(); + + return bSuccess; +} diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSong.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSong.h new file mode 100644 index 0000000..668014b --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSong.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace MUSICDATABASEDIRECTORY + { + class CDirectoryNodeSong : public CDirectoryNode + { + public: + CDirectoryNodeSong(const std::string& strEntryName, CDirectoryNode* pParent); + protected: + bool GetContent(CFileItemList& items) const override; + }; + } +} + + diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSongTop100.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSongTop100.cpp new file mode 100644 index 0000000..a03a8ff --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSongTop100.cpp @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeSongTop100.h" + +#include "music/MusicDatabase.h" + +using namespace XFILE::MUSICDATABASEDIRECTORY; + +CDirectoryNodeSongTop100::CDirectoryNodeSongTop100(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_SONG_TOP100, strName, pParent) +{ + +} + +bool CDirectoryNodeSongTop100::GetContent(CFileItemList& items) const +{ + CMusicDatabase musicdatabase; + if (!musicdatabase.Open()) + return false; + + std::string strBaseDir=BuildPath(); + bool bSuccess=musicdatabase.GetTop100(strBaseDir, items); + + musicdatabase.Close(); + + return bSuccess; +} diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSongTop100.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSongTop100.h new file mode 100644 index 0000000..ca34a72 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSongTop100.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace MUSICDATABASEDIRECTORY + { + class CDirectoryNodeSongTop100 : public CDirectoryNode + { + public: + CDirectoryNodeSongTop100(const std::string& strName, CDirectoryNode* pParent); + protected: + bool GetContent(CFileItemList& items) const override; + }; + } +} + + diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeTop100.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeTop100.cpp new file mode 100644 index 0000000..fff9228 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeTop100.cpp @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeTop100.h" + +#include "FileItem.h" +#include "guilib/LocalizeStrings.h" +#include "utils/StringUtils.h" + +using namespace XFILE::MUSICDATABASEDIRECTORY; + +Node Top100Children[] = { + { NODE_TYPE_SONG_TOP100, "songs", 10504 }, + { NODE_TYPE_ALBUM_TOP100, "albums", 10505 }, + }; + +CDirectoryNodeTop100::CDirectoryNodeTop100(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_TOP100, strName, pParent) +{ + +} + +NODE_TYPE CDirectoryNodeTop100::GetChildType() const +{ + for (const Node& node : Top100Children) + if (GetName() == node.id) + return node.node; + + return NODE_TYPE_NONE; +} + +std::string CDirectoryNodeTop100::GetLocalizedName() const +{ + for (const Node& node : Top100Children) + if (GetName() == node.id) + return g_localizeStrings.Get(node.label); + return ""; +} + +bool CDirectoryNodeTop100::GetContent(CFileItemList& items) const +{ + for (const Node& node : Top100Children) + { + CFileItemPtr pItem(new CFileItem(g_localizeStrings.Get(node.label))); + std::string strDir = StringUtils::Format("{}/", node.id); + pItem->SetPath(BuildPath() + strDir); + pItem->m_bIsFolder = true; + items.Add(pItem); + } + + return true; +} diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeTop100.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeTop100.h new file mode 100644 index 0000000..bf5068f --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeTop100.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace MUSICDATABASEDIRECTORY + { + class CDirectoryNodeTop100 : public CDirectoryNode + { + public: + CDirectoryNodeTop100(const std::string& strName, CDirectoryNode* pParent); + protected: + NODE_TYPE GetChildType() const override; + bool GetContent(CFileItemList& items) const override; + std::string GetLocalizedName() const override; + }; + } +} + + diff --git a/xbmc/filesystem/MusicDatabaseDirectory/QueryParams.cpp b/xbmc/filesystem/MusicDatabaseDirectory/QueryParams.cpp new file mode 100644 index 0000000..3353631 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/QueryParams.cpp @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "QueryParams.h" + +#include <stdlib.h> + +using namespace XFILE::MUSICDATABASEDIRECTORY; + +CQueryParams::CQueryParams() +{ + m_idArtist=-1; + m_idAlbum=-1; + m_idGenre=-1; + m_idSong=-1; + m_year=-1; + m_disc = -1; +} + +void CQueryParams::SetQueryParam(NODE_TYPE NodeType, const std::string& strNodeName) +{ + int idDb = atoi(strNodeName.c_str()); + + switch (NodeType) + { + case NODE_TYPE_GENRE: + m_idGenre=idDb; + break; + case NODE_TYPE_YEAR: + m_year=idDb; + break; + case NODE_TYPE_ARTIST: + m_idArtist=idDb; + break; + case NODE_TYPE_DISC: + m_disc = idDb; + break; + case NODE_TYPE_ALBUM_RECENTLY_PLAYED: + case NODE_TYPE_ALBUM_RECENTLY_ADDED: + case NODE_TYPE_ALBUM_TOP100: + case NODE_TYPE_ALBUM: + m_idAlbum=idDb; + break; + case NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS: + case NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS: + case NODE_TYPE_ALBUM_TOP100_SONGS: + case NODE_TYPE_SONG: + case NODE_TYPE_SONG_TOP100: + m_idSong=idDb; + default: + break; + } +} diff --git a/xbmc/filesystem/MusicDatabaseDirectory/QueryParams.h b/xbmc/filesystem/MusicDatabaseDirectory/QueryParams.h new file mode 100644 index 0000000..abfbdea --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseDirectory/QueryParams.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace MUSICDATABASEDIRECTORY + { + class CQueryParams + { + public: + CQueryParams(); + int GetArtistId() { return m_idArtist; } + int GetAlbumId() { return m_idAlbum; } + int GetGenreId() { return m_idGenre; } + int GetSongId() { return m_idSong; } + int GetYear() { return m_year; } + int GetDisc() { return m_disc; } + + protected: + void SetQueryParam(NODE_TYPE NodeType, const std::string& strNodeName); + + friend class CDirectoryNode; + private: + int m_idArtist; + int m_idAlbum; + int m_idGenre; + int m_idSong; + int m_year; + int m_disc; + }; + } +} + + diff --git a/xbmc/filesystem/MusicDatabaseFile.cpp b/xbmc/filesystem/MusicDatabaseFile.cpp new file mode 100644 index 0000000..0e41ad4 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseFile.cpp @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicDatabaseFile.h" + +#include "URL.h" +#include "music/MusicDatabase.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +using namespace XFILE; + +CMusicDatabaseFile::CMusicDatabaseFile(void) = default; + +CMusicDatabaseFile::~CMusicDatabaseFile(void) +{ + Close(); +} + +std::string CMusicDatabaseFile::TranslateUrl(const CURL& url) +{ + CMusicDatabase musicDatabase; + if (!musicDatabase.Open()) + return ""; + + std::string strFileName=URIUtils::GetFileName(url.Get()); + std::string strExtension = URIUtils::GetExtension(strFileName); + URIUtils::RemoveExtension(strFileName); + + if (!StringUtils::IsNaturalNumber(strFileName)) + return ""; + + int idSong = atoi(strFileName.c_str()); + + CSong song; + if (!musicDatabase.GetSong(idSong, song)) + return ""; + + StringUtils::ToLower(strExtension); + if (!URIUtils::HasExtension(song.strFileName, strExtension)) + return ""; + + return song.strFileName; +} + +bool CMusicDatabaseFile::Open(const CURL& url) +{ + return m_file.Open(TranslateUrl(url)); +} + +bool CMusicDatabaseFile::Exists(const CURL& url) +{ + return !TranslateUrl(url).empty(); +} + +int CMusicDatabaseFile::Stat(const CURL& url, struct __stat64* buffer) +{ + return m_file.Stat(TranslateUrl(url), buffer); +} + +ssize_t CMusicDatabaseFile::Read(void* lpBuf, size_t uiBufSize) +{ + return m_file.Read(lpBuf, uiBufSize); +} + +int64_t CMusicDatabaseFile::Seek(int64_t iFilePosition, int iWhence /*=SEEK_SET*/) +{ + return m_file.Seek(iFilePosition, iWhence); +} + +void CMusicDatabaseFile::Close() +{ + m_file.Close(); +} + +int64_t CMusicDatabaseFile::GetPosition() +{ + return m_file.GetPosition(); +} + +int64_t CMusicDatabaseFile::GetLength() +{ + return m_file.GetLength(); +} + diff --git a/xbmc/filesystem/MusicDatabaseFile.h b/xbmc/filesystem/MusicDatabaseFile.h new file mode 100644 index 0000000..1e259f0 --- /dev/null +++ b/xbmc/filesystem/MusicDatabaseFile.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "File.h" +#include "IFile.h" + +namespace XFILE +{ +class CMusicDatabaseFile : public IFile +{ +public: + CMusicDatabaseFile(void); + ~CMusicDatabaseFile(void) override; + bool Open(const CURL& url) override; + bool Exists(const CURL& url) override; + int Stat(const CURL& url, struct __stat64* buffer) override; + + ssize_t Read(void* lpBuf, size_t uiBufSize) override; + int64_t Seek(int64_t iFilePosition, int iWhence = SEEK_SET) override; + void Close() override; + int64_t GetPosition() override; + int64_t GetLength() override; + + static std::string TranslateUrl(const CURL& url); +protected: + CFile m_file; +}; +} diff --git a/xbmc/filesystem/MusicFileDirectory.cpp b/xbmc/filesystem/MusicFileDirectory.cpp new file mode 100644 index 0000000..b3c7749 --- /dev/null +++ b/xbmc/filesystem/MusicFileDirectory.cpp @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicFileDirectory.h" + +#include "FileItem.h" +#include "URL.h" +#include "guilib/LocalizeStrings.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +using namespace MUSIC_INFO; +using namespace XFILE; + +CMusicFileDirectory::CMusicFileDirectory(void) = default; + +CMusicFileDirectory::~CMusicFileDirectory(void) = default; + +bool CMusicFileDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + std::string strPath=url.Get(); + + std::string strFileName; + strFileName = URIUtils::GetFileName(strPath); + URIUtils::RemoveExtension(strFileName); + + int iStreams = GetTrackCount(strPath); + + URIUtils::AddSlashAtEnd(strPath); + + for (int i=0; i<iStreams; ++i) + { + std::string strLabel = + StringUtils::Format("{} - {} {:02}", strFileName, g_localizeStrings.Get(554), i + 1); + CFileItemPtr pItem(new CFileItem(strLabel)); + strLabel = StringUtils::Format("{}{}-{}.{}", strPath, strFileName, i + 1, m_strExt); + pItem->SetPath(strLabel); + + /* + * Try fist to load tag about related stream track. If them fails or not + * available, take base tag for all streams (in this case the item names + * are all the same). + */ + MUSIC_INFO::CMusicInfoTag tag; + if (Load(strLabel, tag, nullptr)) + *pItem->GetMusicInfoTag() = tag; + else if (m_tag.Loaded()) + *pItem->GetMusicInfoTag() = m_tag; + + /* + * Check track number not set and take stream entry number about. + * NOTE: Audio decoder addons can also give a own track number. + */ + if (pItem->GetMusicInfoTag()->GetTrackNumber() == 0) + pItem->GetMusicInfoTag()->SetTrackNumber(i+1); + items.Add(pItem); + } + + return true; +} + +bool CMusicFileDirectory::Exists(const CURL& url) +{ + return true; +} + +bool CMusicFileDirectory::ContainsFiles(const CURL &url) +{ + const std::string pathToUrl(url.Get()); + if (GetTrackCount(pathToUrl) > 1) + return true; + + return false; +} diff --git a/xbmc/filesystem/MusicFileDirectory.h b/xbmc/filesystem/MusicFileDirectory.h new file mode 100644 index 0000000..283e40e --- /dev/null +++ b/xbmc/filesystem/MusicFileDirectory.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IFileDirectory.h" +#include "music/tags/MusicInfoTag.h" + +namespace XFILE +{ + class CMusicFileDirectory : public IFileDirectory + { + public: + CMusicFileDirectory(void); + ~CMusicFileDirectory(void) override; + bool GetDirectory(const CURL& url, CFileItemList &items) override; + bool Exists(const CURL& url) override; + bool ContainsFiles(const CURL& url) override; + bool AllowAll() const override { return true; } + protected: + virtual bool Load(const std::string& strFileName, + MUSIC_INFO::CMusicInfoTag& tag, + EmbeddedArt* art = nullptr) { return false; } + virtual int GetTrackCount(const std::string& strPath) = 0; + std::string m_strExt; + MUSIC_INFO::CMusicInfoTag m_tag; + }; +} diff --git a/xbmc/filesystem/MusicSearchDirectory.cpp b/xbmc/filesystem/MusicSearchDirectory.cpp new file mode 100644 index 0000000..1d1868a --- /dev/null +++ b/xbmc/filesystem/MusicSearchDirectory.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "MusicSearchDirectory.h" + +#include "FileItem.h" +#include "URL.h" +#include "guilib/LocalizeStrings.h" +#include "music/MusicDatabase.h" +#include "utils/log.h" + +using namespace XFILE; + +CMusicSearchDirectory::CMusicSearchDirectory(void) = default; + +CMusicSearchDirectory::~CMusicSearchDirectory(void) = default; + +bool CMusicSearchDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + // break up our path + // format is: musicsearch://<url encoded search string> + const std::string& search(url.GetHostName()); + + if (search.empty()) + return false; + + // and retrieve the search details + items.SetURL(url); + auto start = std::chrono::steady_clock::now(); + CMusicDatabase db; + db.Open(); + db.Search(search, items); + db.Close(); + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + CLog::Log(LOGDEBUG, "{} ({}) took {} ms", __FUNCTION__, url.GetRedacted(), duration.count()); + + items.SetLabel(g_localizeStrings.Get(137)); // Search + return true; +} + +bool CMusicSearchDirectory::Exists(const CURL& url) +{ + return true; +} diff --git a/xbmc/filesystem/MusicSearchDirectory.h b/xbmc/filesystem/MusicSearchDirectory.h new file mode 100644 index 0000000..67e51f3 --- /dev/null +++ b/xbmc/filesystem/MusicSearchDirectory.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" + +namespace XFILE +{ + class CMusicSearchDirectory : public IDirectory + { + public: + CMusicSearchDirectory(void); + ~CMusicSearchDirectory(void) override; + bool GetDirectory(const CURL& url, CFileItemList &items) override; + bool Exists(const CURL& url) override; + bool AllowAll() const override { return true; } + }; +} diff --git a/xbmc/filesystem/NFSDirectory.cpp b/xbmc/filesystem/NFSDirectory.cpp new file mode 100644 index 0000000..7feba53 --- /dev/null +++ b/xbmc/filesystem/NFSDirectory.cpp @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#ifdef TARGET_WINDOWS +#include <mutex> + +#include <sys\stat.h> +#endif + +#include "FileItem.h" +#include "NFSDirectory.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/XTimeUtils.h" +#include "utils/log.h" + +#ifdef TARGET_WINDOWS +#include <sys\stat.h> +#endif + +using namespace XFILE; +#include <limits.h> +#include <nfsc/libnfs.h> +#include <nfsc/libnfs-raw-nfs.h> + +#if defined(TARGET_WINDOWS) +#define S_IFLNK 0120000 +#define S_ISBLK(m) (0) +#define S_ISSOCK(m) (0) +#define S_ISLNK(m) ((m & S_IFLNK) != 0) +#define S_ISCHR(m) ((m & _S_IFCHR) != 0) +#define S_ISDIR(m) ((m & _S_IFDIR) != 0) +#define S_ISFIFO(m) ((m & _S_IFIFO) != 0) +#define S_ISREG(m) ((m & _S_IFREG) != 0) +#endif + +CNFSDirectory::CNFSDirectory(void) +{ + gNfsConnection.AddActiveConnection(); +} + +CNFSDirectory::~CNFSDirectory(void) +{ + gNfsConnection.AddIdleConnection(); +} + +bool CNFSDirectory::GetDirectoryFromExportList(const std::string& strPath, CFileItemList &items) +{ + CURL url(strPath); + std::string nonConstStrPath(strPath); + std::list<std::string> exportList=gNfsConnection.GetExportList(url); + + for (const std::string& it : exportList) + { + const std::string& currentExport(it); + URIUtils::RemoveSlashAtEnd(nonConstStrPath); + + CFileItemPtr pItem(new CFileItem(currentExport)); + std::string path(nonConstStrPath + currentExport); + URIUtils::AddSlashAtEnd(path); + pItem->SetPath(path); + pItem->m_dateTime = 0; + + pItem->m_bIsFolder = true; + items.Add(pItem); + } + + return exportList.empty() ? false : true; +} + +bool CNFSDirectory::GetServerList(CFileItemList &items) +{ + struct nfs_server_list *srvrs; + struct nfs_server_list *srv; + bool ret = false; + + srvrs = nfs_find_local_servers(); + + for (srv=srvrs; srv; srv = srv->next) + { + std::string currentExport(srv->addr); + + CFileItemPtr pItem(new CFileItem(currentExport)); + std::string path("nfs://" + currentExport); + URIUtils::AddSlashAtEnd(path); + pItem->m_dateTime=0; + + pItem->SetPath(path); + pItem->m_bIsFolder = true; + items.Add(pItem); + ret = true; //added at least one entry + } + free_nfs_srvr_list(srvrs); + + return ret; +} + +bool CNFSDirectory::ResolveSymlink( const std::string &dirName, struct nfsdirent *dirent, CURL &resolvedUrl) +{ + std::unique_lock<CCriticalSection> lock(gNfsConnection); + int ret = 0; + bool retVal = true; + std::string fullpath = dirName; + char resolvedLink[MAX_PATH]; + + URIUtils::AddSlashAtEnd(fullpath); + fullpath.append(dirent->name); + + resolvedUrl.Reset(); + resolvedUrl.SetPort(2049); + resolvedUrl.SetProtocol("nfs"); + resolvedUrl.SetHostName(gNfsConnection.GetConnectedIp()); + + ret = nfs_readlink(gNfsConnection.GetNfsContext(), fullpath.c_str(), resolvedLink, MAX_PATH); + + if(ret == 0) + { + nfs_stat_64 tmpBuffer = {}; + fullpath = dirName; + URIUtils::AddSlashAtEnd(fullpath); + fullpath.append(resolvedLink); + + //special case - if link target is absolute it could be even another export + //intervolume symlinks baby ... + if(resolvedLink[0] == '/') + { + //use the special stat function for using an extra context + //because we are inside of a dir traversal + //and just can't change the global nfs context here + //without destroying something... + fullpath = resolvedLink; + resolvedUrl.SetFileName(fullpath); + ret = gNfsConnection.stat(resolvedUrl, &tmpBuffer); + } + else + { + ret = nfs_stat64(gNfsConnection.GetNfsContext(), fullpath.c_str(), &tmpBuffer); + resolvedUrl.SetFileName(gNfsConnection.GetConnectedExport() + fullpath); + } + + if (ret != 0) + { + CLog::Log(LOGERROR, "NFS: Failed to stat({}) on link resolve {}", fullpath, + nfs_get_error(gNfsConnection.GetNfsContext())); + retVal = false; + } + else + { + dirent->inode = tmpBuffer.nfs_ino; + dirent->mode = tmpBuffer.nfs_mode; + dirent->size = tmpBuffer.nfs_size; + dirent->atime.tv_sec = tmpBuffer.nfs_atime; + dirent->mtime.tv_sec = tmpBuffer.nfs_mtime; + dirent->ctime.tv_sec = tmpBuffer.nfs_ctime; + + //map stat mode to nf3type + if (S_ISBLK(tmpBuffer.nfs_mode)) + { + dirent->type = NF3BLK; + } + else if (S_ISCHR(tmpBuffer.nfs_mode)) + { + dirent->type = NF3CHR; + } + else if (S_ISDIR(tmpBuffer.nfs_mode)) + { + dirent->type = NF3DIR; + } + else if (S_ISFIFO(tmpBuffer.nfs_mode)) + { + dirent->type = NF3FIFO; + } + else if (S_ISREG(tmpBuffer.nfs_mode)) + { + dirent->type = NF3REG; + } + else if (S_ISLNK(tmpBuffer.nfs_mode)) + { + dirent->type = NF3LNK; + } + else if (S_ISSOCK(tmpBuffer.nfs_mode)) + { + dirent->type = NF3SOCK; + } + } + } + else + { + CLog::Log(LOGERROR, "Failed to readlink({}) {}", fullpath, + nfs_get_error(gNfsConnection.GetNfsContext())); + retVal = false; + } + return retVal; +} + +bool CNFSDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + // We accept nfs://server/path[/file]]]] + int ret = 0; + KODI::TIME::FileTime fileTime, localTime; + std::unique_lock<CCriticalSection> lock(gNfsConnection); + std::string strDirName=""; + std::string myStrPath(url.Get()); + URIUtils::AddSlashAtEnd(myStrPath); //be sure the dir ends with a slash + + if(!gNfsConnection.Connect(url,strDirName)) + { + //connect has failed - so try to get the exported filesystems if no path is given to the url + if(url.GetShareName().empty()) + { + if(url.GetHostName().empty()) + { + return GetServerList(items); + } + else + { + return GetDirectoryFromExportList(myStrPath, items); + } + } + else + { + return false; + } + } + + struct nfsdir *nfsdir = NULL; + struct nfsdirent *nfsdirent = NULL; + + ret = nfs_opendir(gNfsConnection.GetNfsContext(), strDirName.c_str(), &nfsdir); + + if(ret != 0) + { + CLog::Log(LOGERROR, "Failed to open({}) {}", strDirName, + nfs_get_error(gNfsConnection.GetNfsContext())); + return false; + } + lock.unlock(); + + while((nfsdirent = nfs_readdir(gNfsConnection.GetNfsContext(), nfsdir)) != NULL) + { + struct nfsdirent tmpDirent = *nfsdirent; + std::string strName = tmpDirent.name; + std::string path(myStrPath + strName); + int64_t iSize = 0; + bool bIsDir = false; + int64_t lTimeDate = 0; + + //reslove symlinks + if(tmpDirent.type == NF3LNK) + { + CURL linkUrl; + //resolve symlink changes tmpDirent and strName + if(!ResolveSymlink(strDirName,&tmpDirent,linkUrl)) + { + continue; + } + + path = linkUrl.Get(); + } + + iSize = tmpDirent.size; + bIsDir = tmpDirent.type == NF3DIR; + lTimeDate = tmpDirent.mtime.tv_sec; + + if (!StringUtils::EqualsNoCase(strName,".") && !StringUtils::EqualsNoCase(strName,"..") + && !StringUtils::EqualsNoCase(strName,"lost+found")) + { + if(lTimeDate == 0) // if modification date is missing, use create date + { + lTimeDate = tmpDirent.ctime.tv_sec; + } + + long long ll = lTimeDate & 0xffffffff; + ll *= 10000000ll; + ll += 116444736000000000ll; + fileTime.lowDateTime = (DWORD)(ll & 0xffffffff); + fileTime.highDateTime = (DWORD)(ll >> 32); + KODI::TIME::FileTimeToLocalFileTime(&fileTime, &localTime); + + CFileItemPtr pItem(new CFileItem(tmpDirent.name)); + pItem->m_dateTime=localTime; + pItem->m_dwSize = iSize; + + if (bIsDir) + { + URIUtils::AddSlashAtEnd(path); + pItem->m_bIsFolder = true; + } + else + { + pItem->m_bIsFolder = false; + } + + if (strName[0] == '.') + { + pItem->SetProperty("file:hidden", true); + } + pItem->SetPath(path); + items.Add(pItem); + } + } + + lock.lock(); + nfs_closedir(gNfsConnection.GetNfsContext(), nfsdir);//close the dir + lock.unlock(); + return true; +} + +bool CNFSDirectory::Create(const CURL& url2) +{ + int ret = 0; + bool success=true; + + std::unique_lock<CCriticalSection> lock(gNfsConnection); + std::string folderName(url2.Get()); + URIUtils::RemoveSlashAtEnd(folderName);//mkdir fails if a slash is at the end!!! + CURL url(folderName); + folderName = ""; + + if(!gNfsConnection.Connect(url,folderName)) + return false; + + ret = nfs_mkdir(gNfsConnection.GetNfsContext(), folderName.c_str()); + + success = (ret == 0 || -EEXIST == ret); + if(!success) + CLog::Log(LOGERROR, "NFS: Failed to create({}) {}", folderName, + nfs_get_error(gNfsConnection.GetNfsContext())); + return success; +} + +bool CNFSDirectory::Remove(const CURL& url2) +{ + int ret = 0; + + std::unique_lock<CCriticalSection> lock(gNfsConnection); + std::string folderName(url2.Get()); + URIUtils::RemoveSlashAtEnd(folderName);//rmdir fails if a slash is at the end!!! + CURL url(folderName); + folderName = ""; + + if(!gNfsConnection.Connect(url,folderName)) + return false; + + ret = nfs_rmdir(gNfsConnection.GetNfsContext(), folderName.c_str()); + + if(ret != 0 && errno != ENOENT) + { + CLog::Log(LOGERROR, "{} - Error( {} )", __FUNCTION__, + nfs_get_error(gNfsConnection.GetNfsContext())); + return false; + } + return true; +} + +bool CNFSDirectory::Exists(const CURL& url2) +{ + int ret = 0; + + std::unique_lock<CCriticalSection> lock(gNfsConnection); + std::string folderName(url2.Get()); + URIUtils::RemoveSlashAtEnd(folderName);//remove slash at end or URIUtils::GetFileName won't return what we want... + CURL url(folderName); + folderName = ""; + + if(!gNfsConnection.Connect(url,folderName)) + return false; + + nfs_stat_64 info; + ret = nfs_stat64(gNfsConnection.GetNfsContext(), folderName.c_str(), &info); + + if (ret != 0) + { + return false; + } + return S_ISDIR(info.nfs_mode) ? true : false; +} diff --git a/xbmc/filesystem/NFSDirectory.h b/xbmc/filesystem/NFSDirectory.h new file mode 100644 index 0000000..684ad8c --- /dev/null +++ b/xbmc/filesystem/NFSDirectory.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" +#include "NFSFile.h" + +struct nfsdirent; + +namespace XFILE +{ + class CNFSDirectory : public IDirectory + { + public: + CNFSDirectory(void); + ~CNFSDirectory(void) override; + bool GetDirectory(const CURL& url, CFileItemList &items) override; + DIR_CACHE_TYPE GetCacheType(const CURL& url) const override { return DIR_CACHE_ONCE; } + bool Create(const CURL& url) override; + bool Exists(const CURL& url) override; + bool Remove(const CURL& url) override; + private: + bool GetServerList(CFileItemList &items); + bool GetDirectoryFromExportList(const std::string& strPath, CFileItemList &items); + bool ResolveSymlink( const std::string &dirName, struct nfsdirent *dirent, CURL &resolvedUrl); + }; +} + diff --git a/xbmc/filesystem/NFSFile.cpp b/xbmc/filesystem/NFSFile.cpp new file mode 100644 index 0000000..5e0c148 --- /dev/null +++ b/xbmc/filesystem/NFSFile.cpp @@ -0,0 +1,962 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +// FileNFS.cpp: implementation of the CNFSFile class. +// +////////////////////////////////////////////////////////////////////// + +#include "NFSFile.h" + +#include "ServiceBroker.h" +#include "network/DNSNameCache.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +#include <inttypes.h> +#include <mutex> + +#include <nfsc/libnfs-raw-mount.h> +#include <nfsc/libnfs.h> + +#ifdef TARGET_WINDOWS +#include <fcntl.h> +#include <sys\stat.h> +#endif + +#if defined(TARGET_WINDOWS) +#define S_IRGRP 0 +#define S_IROTH 0 +#define S_IWUSR _S_IWRITE +#define S_IRUSR _S_IREAD +#endif + +using namespace XFILE; + +using namespace std::chrono_literals; + +namespace +{ +// Default "lease_time" on most Linux NFSv4 servers are 90s. +// See: https://linux-nfs.org/wiki/index.php/NFS_lock_recovery_notes +// Keep alive interval should be always less than lease_time to avoid client session expires + +constexpr auto CONTEXT_TIMEOUT = 60s; // 2/3 parts of lease_time +constexpr auto KEEP_ALIVE_TIMEOUT = 45s; // half of lease_time +constexpr auto IDLE_TIMEOUT = 30s; // close fast unused contexts when no active connections + +constexpr int NFS4ERR_EXPIRED = -11; // client session expired due idle time greater than lease_time + +constexpr auto SETTING_NFS_VERSION = "nfs.version"; +} // unnamed namespace + +CNfsConnection::CNfsConnection() + : m_pNfsContext(NULL), + m_exportPath(""), + m_hostName(""), + m_resolvedHostName(""), + m_IdleTimeout(std::chrono::steady_clock::now() + IDLE_TIMEOUT) +{ +} + +CNfsConnection::~CNfsConnection() +{ + Deinit(); +} + +void CNfsConnection::resolveHost(const CURL& url) +{ + // resolve if hostname has changed + CDNSNameCache::Lookup(url.GetHostName(), m_resolvedHostName); +} + +std::list<std::string> CNfsConnection::GetExportList(const CURL& url) +{ + std::list<std::string> retList; + + struct exportnode *exportlist, *tmp; +#ifdef HAS_NFS_MOUNT_GETEXPORTS_TIMEOUT + exportlist = mount_getexports_timeout( + m_resolvedHostName.c_str(), + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_nfsTimeout * 1000); +#else + exportlist = mount_getexports(m_resolvedHostName.c_str()); +#endif + + for (tmp = exportlist; tmp != NULL; tmp = tmp->ex_next) + { + std::string exportStr = std::string(tmp->ex_dir); + + retList.push_back(exportStr); + } + + mount_free_export_list(exportlist); + retList.sort(); + retList.reverse(); + + return retList; +} + +void CNfsConnection::clearMembers() +{ + // NOTE - DON'T CLEAR m_exportList HERE! + // splitUrlIntoExportAndPath checks for m_exportList.empty() + // and would query the server in an excessive unwanted fashion + // also don't clear m_KeepAliveTimeouts here because we + // would loose any "paused" file handles during export change + m_exportPath.clear(); + m_hostName.clear(); + m_writeChunkSize = 0; + m_readChunkSize = 0; + m_pNfsContext = NULL; +} + +void CNfsConnection::destroyOpenContexts() +{ + std::unique_lock<CCriticalSection> lock(openContextLock); + for (auto& it : m_openContextMap) + { + nfs_destroy_context(it.second.pContext); + } + m_openContextMap.clear(); +} + +void CNfsConnection::destroyContext(const std::string &exportName) +{ + std::unique_lock<CCriticalSection> lock(openContextLock); + tOpenContextMap::iterator it = m_openContextMap.find(exportName.c_str()); + if (it != m_openContextMap.end()) + { + nfs_destroy_context(it->second.pContext); + m_openContextMap.erase(it); + } +} + +struct nfs_context *CNfsConnection::getContextFromMap(const std::string &exportname, bool forceCacheHit/* = false*/) +{ + struct nfs_context *pRet = NULL; + std::unique_lock<CCriticalSection> lock(openContextLock); + + tOpenContextMap::iterator it = m_openContextMap.find(exportname.c_str()); + if (it != m_openContextMap.end()) + { + //check if context has timed out already + auto now = std::chrono::steady_clock::now(); + auto duration = + std::chrono::duration_cast<std::chrono::milliseconds>(now - it->second.lastAccessedTime); + if (duration < CONTEXT_TIMEOUT || forceCacheHit) + { + //its not timedout yet or caller wants the cached entry regardless of timeout + //refresh access time of that + //context and return it + if (!forceCacheHit) // only log it if this isn't the resetkeepalive on each read ;) + CLog::Log(LOGDEBUG, "NFS: Refreshing context for {}, old: {}, new: {}", exportname, + it->second.lastAccessedTime.time_since_epoch().count(), + now.time_since_epoch().count()); + it->second.lastAccessedTime = now; + pRet = it->second.pContext; + } + else + { + //context is timed out + //destroy it and return NULL + CLog::Log(LOGDEBUG, "NFS: Old context timed out - destroying it"); + nfs_destroy_context(it->second.pContext); + m_openContextMap.erase(it); + } + } + return pRet; +} + +CNfsConnection::ContextStatus CNfsConnection::getContextForExport(const std::string& exportname) +{ + CNfsConnection::ContextStatus ret = CNfsConnection::ContextStatus::INVALID; + + clearMembers(); + + m_pNfsContext = getContextFromMap(exportname); + + if(!m_pNfsContext) + { + CLog::Log(LOGDEBUG, "NFS: Context for {} not open - get a new context.", exportname); + m_pNfsContext = nfs_init_context(); + + if(!m_pNfsContext) + { + CLog::Log(LOGERROR,"NFS: Error initcontext in getContextForExport."); + } + else + { + struct contextTimeout tmp; + std::unique_lock<CCriticalSection> lock(openContextLock); + setOptions(m_pNfsContext); + tmp.pContext = m_pNfsContext; + tmp.lastAccessedTime = std::chrono::steady_clock::now(); + m_openContextMap[exportname] = tmp; //add context to list of all contexts + ret = CNfsConnection::ContextStatus::NEW; + } + } + else + { + ret = CNfsConnection::ContextStatus::CACHED; + CLog::Log(LOGDEBUG,"NFS: Using cached context."); + } + m_lastAccessedTime = std::chrono::steady_clock::now(); + + return ret; +} + +bool CNfsConnection::splitUrlIntoExportAndPath(const CURL& url, std::string &exportPath, std::string &relativePath) +{ + //refresh exportlist if empty or hostname change + if(m_exportList.empty() || !StringUtils::EqualsNoCase(url.GetHostName(), m_hostName)) + { + const auto settingsComponent = CServiceBroker::GetSettingsComponent(); + if (!settingsComponent) + return false; + + const auto settings = settingsComponent->GetSettings(); + if (!settings) + return false; + + const int nfsVersion = settings->GetInt(SETTING_NFS_VERSION); + + if (nfsVersion == 4) + m_exportList = {"/"}; + else + m_exportList = GetExportList(url); + } + + return splitUrlIntoExportAndPath(url, exportPath, relativePath, m_exportList); +} + +bool CNfsConnection::splitUrlIntoExportAndPath(const CURL& url,std::string &exportPath, std::string &relativePath, std::list<std::string> &exportList) +{ + bool ret = false; + + if(!exportList.empty()) + { + relativePath = ""; + exportPath = ""; + + std::string path = url.GetFileName(); + + //GetFileName returns path without leading "/" + //but we need it because the export paths start with "/" + //and path.Find(*it) wouldn't work else + if(path[0] != '/') + { + path = "/" + path; + } + + for (const std::string& it : exportList) + { + //if path starts with the current export path + if (URIUtils::PathHasParent(path, it)) + { + /* It's possible that PathHasParent() may not find the correct match first/ + * As an example, if /path/ & and /path/sub/ are exported, but + * the user specifies the path /path/subdir/ (from /path/ export). + * If the path is longer than the exportpath, make sure / is next. + */ + if ((path.length() > it.length()) && (path[it.length()] != '/') && it != "/") + continue; + exportPath = it; + //handle special case where root is exported + //in that case we don't want to strip off to + //much from the path + if( exportPath == path ) + relativePath = "//"; + else if( exportPath == "/" ) + relativePath = "//" + path.substr(exportPath.length()); + else + relativePath = "//" + path.substr(exportPath.length()+1); + ret = true; + break; + } + } + } + return ret; +} + +bool CNfsConnection::Connect(const CURL& url, std::string &relativePath) +{ + std::unique_lock<CCriticalSection> lock(*this); + int nfsRet = 0; + std::string exportPath; + + resolveHost(url); + bool ret = splitUrlIntoExportAndPath(url, exportPath, relativePath); + + auto now = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_lastAccessedTime); + + if ((ret && (exportPath != m_exportPath || url.GetHostName() != m_hostName)) || + duration > CONTEXT_TIMEOUT) + { + CNfsConnection::ContextStatus contextRet = getContextForExport(url.GetHostName() + exportPath); + + // we need a new context because sharename or hostname has changed + if (contextRet == CNfsConnection::ContextStatus::INVALID) + { + return false; + } + + // new context was created - we need to mount it + if (contextRet == CNfsConnection::ContextStatus::NEW) + { + //we connect to the directory of the path. This will be the "root" path of this connection then. + //So all fileoperations are relative to this mountpoint... + nfsRet = nfs_mount(m_pNfsContext, m_resolvedHostName.c_str(), exportPath.c_str()); + + if(nfsRet != 0) + { + CLog::Log(LOGERROR, "NFS: Failed to mount nfs share: {} ({})", exportPath, + nfs_get_error(m_pNfsContext)); + destroyContext(url.GetHostName() + exportPath); + return false; + } + CLog::Log(LOGDEBUG, "NFS: Connected to server {} and export {}", url.GetHostName(), + exportPath); + } + m_exportPath = exportPath; + m_hostName = url.GetHostName(); + + // read chunksize only works after mount + m_readChunkSize = nfs_get_readmax(m_pNfsContext); + m_writeChunkSize = nfs_get_writemax(m_pNfsContext); + + if (m_readChunkSize == 0) + { + CLog::Log(LOGDEBUG, "NFS Server did not return max read chunksize - Using 128K default"); + m_readChunkSize = 128 * 1024; // 128K + } + if (m_writeChunkSize == 0) + { + CLog::Log(LOGDEBUG, "NFS Server did not return max write chunksize - Using 128K default"); + m_writeChunkSize = 128 * 1024; // 128K + } + + if (contextRet == CNfsConnection::ContextStatus::NEW) + { + CLog::Log(LOGDEBUG, "NFS: chunks: r/w {}/{}", (int)m_readChunkSize, (int)m_writeChunkSize); + } + } + return ret; +} + +void CNfsConnection::Deinit() +{ + if(m_pNfsContext) + { + destroyOpenContexts(); + m_pNfsContext = NULL; + } + clearMembers(); + // clear any keep alive timeouts on deinit + m_KeepAliveTimeouts.clear(); +} + +/* This is called from CApplication::ProcessSlow() and is used to tell if nfs have been idle for too long */ +void CNfsConnection::CheckIfIdle() +{ + /* We check if there are open connections. This is done without a lock to not halt the mainthread. It should be thread safe as + worst case scenario is that m_OpenConnections could read 0 and then changed to 1 if this happens it will enter the if which will lead to another check, which is locked. */ + if (m_OpenConnections == 0 && m_pNfsContext != NULL) + { /* I've set the the maximum IDLE time to be 1 min and 30 sec. */ + std::unique_lock<CCriticalSection> lock(*this); + if (m_OpenConnections == 0 /* check again - when locked */) + { + const auto now = std::chrono::steady_clock::now(); + + if (m_IdleTimeout < now) + { + CLog::Log(LOGINFO, "NFS is idle. Closing the remaining connections."); + gNfsConnection.Deinit(); + } + } + } + + if( m_pNfsContext != NULL ) + { + std::unique_lock<CCriticalSection> lock(keepAliveLock); + + const auto now = std::chrono::steady_clock::now(); + + //handle keep alive on opened files + for (auto& it : m_KeepAliveTimeouts) + { + if (it.second.refreshTime < now) + { + keepAlive(it.second.exportPath, it.first); + //reset timeout + resetKeepAlive(it.second.exportPath, it.first); + } + } + } +} + +//remove file handle from keep alive list on file close +void CNfsConnection::removeFromKeepAliveList(struct nfsfh *_pFileHandle) +{ + std::unique_lock<CCriticalSection> lock(keepAliveLock); + m_KeepAliveTimeouts.erase(_pFileHandle); +} + +//reset timeouts on read +void CNfsConnection::resetKeepAlive(const std::string& _exportPath, struct nfsfh* _pFileHandle) +{ + std::unique_lock<CCriticalSection> lock(keepAliveLock); + //refresh last access time of the context aswell + struct nfs_context *pContext = getContextFromMap(_exportPath, true); + + // if we keep alive using m_pNfsContext we need to mark + // its last access time too here + if (m_pNfsContext == pContext) + { + m_lastAccessedTime = std::chrono::steady_clock::now(); + } + + //adds new keys - refreshes existing ones + m_KeepAliveTimeouts[_pFileHandle].exportPath = _exportPath; + m_KeepAliveTimeouts[_pFileHandle].refreshTime = m_lastAccessedTime + KEEP_ALIVE_TIMEOUT; +} + +//keep alive the filehandles nfs connection +//by blindly doing a read 32bytes - seek back to where +//we were before +void CNfsConnection::keepAlive(const std::string& _exportPath, struct nfsfh* _pFileHandle) +{ + uint64_t offset = 0; + char buffer[32]; + // this also refreshes the last accessed time for the context + // true forces a cachehit regardless the context is timedout + // on this call we are sure its not timedout even if the last accessed + // time suggests it. + struct nfs_context *pContext = getContextFromMap(_exportPath, true); + + if (!pContext)// this should normally never happen - paranoia + pContext = m_pNfsContext; + + CLog::LogF(LOGDEBUG, "sending keep alive after {}s.", + std::chrono::duration_cast<std::chrono::seconds>(KEEP_ALIVE_TIMEOUT).count()); + + std::unique_lock<CCriticalSection> lock(*this); + + nfs_lseek(pContext, _pFileHandle, 0, SEEK_CUR, &offset); + + int bytes = nfs_read(pContext, _pFileHandle, 32, buffer); + if (bytes < 0) + { + CLog::LogF(LOGERROR, "nfs_read - Error ({}, {})", bytes, nfs_get_error(pContext)); + return; + } + + nfs_lseek(pContext, _pFileHandle, offset, SEEK_SET, &offset); +} + +int CNfsConnection::stat(const CURL& url, nfs_stat_64* statbuff) +{ + std::unique_lock<CCriticalSection> lock(*this); + int nfsRet = 0; + std::string exportPath; + std::string relativePath; + struct nfs_context *pTmpContext = NULL; + + resolveHost(url); + + if(splitUrlIntoExportAndPath(url, exportPath, relativePath)) + { + pTmpContext = nfs_init_context(); + + if(pTmpContext) + { + setOptions(pTmpContext); + //we connect to the directory of the path. This will be the "root" path of this connection then. + //So all fileoperations are relative to this mountpoint... + nfsRet = nfs_mount(pTmpContext, m_resolvedHostName.c_str(), exportPath.c_str()); + + if(nfsRet == 0) + { + nfsRet = nfs_stat64(pTmpContext, relativePath.c_str(), statbuff); + } + else + { + CLog::Log(LOGERROR, "NFS: Failed to mount nfs share: {} ({})", exportPath, + nfs_get_error(m_pNfsContext)); + } + + nfs_destroy_context(pTmpContext); + CLog::Log(LOGDEBUG, "NFS: Connected to server {} and export {} in tmpContext", + url.GetHostName(), exportPath); + } + } + return nfsRet; +} + +/* The following two function is used to keep track on how many Opened files/directories there are. +needed for unloading the dylib*/ +void CNfsConnection::AddActiveConnection() +{ + std::unique_lock<CCriticalSection> lock(*this); + m_OpenConnections++; +} + +void CNfsConnection::AddIdleConnection() +{ + std::unique_lock<CCriticalSection> lock(*this); + m_OpenConnections--; + /* If we close a file we reset the idle timer so that we don't have any weird behaviours if a user + leaves the movie paused for a long while and then press stop */ + const auto now = std::chrono::steady_clock::now(); + m_IdleTimeout = now + IDLE_TIMEOUT; +} + + +void CNfsConnection::setOptions(struct nfs_context* context) +{ + const auto settingsComponent = CServiceBroker::GetSettingsComponent(); + if (!settingsComponent) + return; + + const auto advancedSettings = settingsComponent->GetAdvancedSettings(); + if (!advancedSettings) + return; + +#ifdef HAS_NFS_SET_TIMEOUT + uint32_t timeout = advancedSettings->m_nfsTimeout; + nfs_set_timeout(context, timeout > 0 ? timeout * 1000 : -1); +#endif + int retries = advancedSettings->m_nfsRetries; + nfs_set_autoreconnect(context, retries); + + const auto settings = settingsComponent->GetSettings(); + if (!settings) + return; + + const int nfsVersion = settings->GetInt(SETTING_NFS_VERSION); + + int ret = nfs_set_version(context, nfsVersion); + if (ret != 0) + { + CLog::Log(LOGERROR, "NFS: Failed to set nfs version: {} ({})", nfsVersion, + nfs_get_error(context)); + return; + } + + CLog::Log(LOGDEBUG, "NFS: version: {}", nfsVersion); +} + +CNfsConnection gNfsConnection; + +CNFSFile::CNFSFile() +: m_pFileHandle(NULL) +, m_pNfsContext(NULL) +{ + gNfsConnection.AddActiveConnection(); +} + +CNFSFile::~CNFSFile() +{ + Close(); + gNfsConnection.AddIdleConnection(); +} + +int64_t CNFSFile::GetPosition() +{ + int ret = 0; + uint64_t offset = 0; + std::unique_lock<CCriticalSection> lock(gNfsConnection); + + if (gNfsConnection.GetNfsContext() == NULL || m_pFileHandle == NULL) return 0; + + ret = nfs_lseek(gNfsConnection.GetNfsContext(), m_pFileHandle, 0, SEEK_CUR, &offset); + + if (ret < 0) + { + CLog::Log(LOGERROR, "NFS: Failed to lseek({})", nfs_get_error(gNfsConnection.GetNfsContext())); + } + return offset; +} + +int64_t CNFSFile::GetLength() +{ + if (m_pFileHandle == NULL) return 0; + return m_fileSize; +} + +bool CNFSFile::Open(const CURL& url) +{ + Close(); + // we can't open files like nfs://file.f or nfs://server/file.f + // if a file matches the if below return false, it can't exist on a nfs share. + if (!IsValidFile(url.GetFileName())) + { + CLog::Log(LOGINFO, "NFS: Bad URL : '{}'", url.GetFileName()); + return false; + } + + std::string filename; + + std::unique_lock<CCriticalSection> lock(gNfsConnection); + + if (!gNfsConnection.Connect(url, filename)) + return false; + + m_pNfsContext = gNfsConnection.GetNfsContext(); + m_exportPath = gNfsConnection.GetContextMapId(); + + int ret = nfs_open(m_pNfsContext, filename.c_str(), O_RDONLY, &m_pFileHandle); + + if (ret == NFS4ERR_EXPIRED) // client session expired due no activity/keep alive + { + CLog::Log(LOGERROR, + "CNFSFile::Open: Unable to open file - trying again with a new context: error: '{}'", + nfs_get_error(m_pNfsContext)); + + gNfsConnection.Deinit(); + m_pNfsContext = gNfsConnection.GetNfsContext(); + m_exportPath = gNfsConnection.GetContextMapId(); + ret = nfs_open(m_pNfsContext, filename.c_str(), O_RDONLY, &m_pFileHandle); + } + + if (ret != 0) + { + CLog::Log(LOGERROR, "CNFSFile::Open: Unable to open file: '{}' error: '{}'", url.GetFileName(), + nfs_get_error(m_pNfsContext)); + + m_pNfsContext = nullptr; + m_exportPath.clear(); + return false; + } + + CLog::Log(LOGDEBUG, "CNFSFile::Open - opened {}", url.GetFileName()); + m_url=url; + + struct __stat64 tmpBuffer; + + if( Stat(&tmpBuffer) ) + { + m_url.Reset(); + Close(); + return false; + } + + m_fileSize = tmpBuffer.st_size;//cache the size of this file + // We've successfully opened the file! + return true; +} + +bool CNFSFile::Exists(const CURL& url) +{ + return Stat(url,NULL) == 0; +} + +int CNFSFile::Stat(struct __stat64* buffer) +{ + return Stat(m_url,buffer); +} + + +int CNFSFile::Stat(const CURL& url, struct __stat64* buffer) +{ + int ret = 0; + std::unique_lock<CCriticalSection> lock(gNfsConnection); + std::string filename; + + if(!gNfsConnection.Connect(url,filename)) + return -1; + + nfs_stat_64 tmpBuffer = {}; + + ret = nfs_stat64(gNfsConnection.GetNfsContext(), filename.c_str(), &tmpBuffer); + + //if buffer == NULL we where called from Exists - in that case don't spam the log with errors + if (ret != 0 && buffer != NULL) + { + CLog::Log(LOGERROR, "NFS: Failed to stat({}) {}", url.GetFileName(), + nfs_get_error(gNfsConnection.GetNfsContext())); + ret = -1; + } + else + { + if (buffer) + { + *buffer = {}; + buffer->st_dev = tmpBuffer.nfs_dev; + buffer->st_ino = tmpBuffer.nfs_ino; + buffer->st_mode = tmpBuffer.nfs_mode; + buffer->st_nlink = tmpBuffer.nfs_nlink; + buffer->st_uid = tmpBuffer.nfs_uid; + buffer->st_gid = tmpBuffer.nfs_gid; + buffer->st_rdev = tmpBuffer.nfs_rdev; + buffer->st_size = tmpBuffer.nfs_size; + buffer->st_atime = tmpBuffer.nfs_atime; + buffer->st_mtime = tmpBuffer.nfs_mtime; + buffer->st_ctime = tmpBuffer.nfs_ctime; + } + } + return ret; +} + +ssize_t CNFSFile::Read(void *lpBuf, size_t uiBufSize) +{ + if (uiBufSize > SSIZE_MAX) + uiBufSize = SSIZE_MAX; + + ssize_t numberOfBytesRead = 0; + std::unique_lock<CCriticalSection> lock(gNfsConnection); + + if (m_pFileHandle == NULL || m_pNfsContext == NULL ) + return -1; + + numberOfBytesRead = nfs_read(m_pNfsContext, m_pFileHandle, uiBufSize, (char *)lpBuf); + + lock.unlock(); //no need to keep the connection lock after that + + gNfsConnection.resetKeepAlive(m_exportPath, m_pFileHandle);//triggers keep alive timer reset for this filehandle + + //something went wrong ... + if (numberOfBytesRead < 0) + CLog::Log(LOGERROR, "{} - Error( {}, {} )", __FUNCTION__, (int64_t)numberOfBytesRead, + nfs_get_error(m_pNfsContext)); + + return numberOfBytesRead; +} + +int64_t CNFSFile::Seek(int64_t iFilePosition, int iWhence) +{ + int ret = 0; + uint64_t offset = 0; + + std::unique_lock<CCriticalSection> lock(gNfsConnection); + if (m_pFileHandle == NULL || m_pNfsContext == NULL) return -1; + + + ret = nfs_lseek(m_pNfsContext, m_pFileHandle, iFilePosition, iWhence, &offset); + if (ret < 0) + { + CLog::Log(LOGERROR, "{} - Error( seekpos: {}, whence: {}, fsize: {}, {})", __FUNCTION__, + iFilePosition, iWhence, m_fileSize, nfs_get_error(m_pNfsContext)); + return -1; + } + return (int64_t)offset; +} + +int CNFSFile::Truncate(int64_t iSize) +{ + int ret = 0; + + std::unique_lock<CCriticalSection> lock(gNfsConnection); + if (m_pFileHandle == NULL || m_pNfsContext == NULL) return -1; + + + ret = nfs_ftruncate(m_pNfsContext, m_pFileHandle, iSize); + if (ret < 0) + { + CLog::Log(LOGERROR, "{} - Error( ftruncate: {}, fsize: {}, {})", __FUNCTION__, iSize, + m_fileSize, nfs_get_error(m_pNfsContext)); + return -1; + } + return ret; +} + +void CNFSFile::Close() +{ + std::unique_lock<CCriticalSection> lock(gNfsConnection); + + if (m_pFileHandle != NULL && m_pNfsContext != NULL) + { + int ret = 0; + CLog::Log(LOGDEBUG, "CNFSFile::Close closing file {}", m_url.GetFileName()); + // remove it from keep alive list before closing + // so keep alive code doesn't process it anymore + gNfsConnection.removeFromKeepAliveList(m_pFileHandle); + ret = nfs_close(m_pNfsContext, m_pFileHandle); + + if (ret < 0) + { + CLog::Log(LOGERROR, "Failed to close({}) - {}", m_url.GetFileName(), + nfs_get_error(m_pNfsContext)); + } + m_pFileHandle = NULL; + m_pNfsContext = NULL; + m_fileSize = 0; + m_exportPath.clear(); + } +} + +//this was a bitch! +//for nfs write to work we have to write chunked +//otherwise this could crash on big files +ssize_t CNFSFile::Write(const void* lpBuf, size_t uiBufSize) +{ + size_t numberOfBytesWritten = 0; + int writtenBytes = 0; + size_t leftBytes = uiBufSize; + //clamp max write chunksize to 32kb - fixme - this might be superfluous with future libnfs versions + size_t chunkSize = gNfsConnection.GetMaxWriteChunkSize() > 32768 ? 32768 : (size_t)gNfsConnection.GetMaxWriteChunkSize(); + + std::unique_lock<CCriticalSection> lock(gNfsConnection); + + if (m_pFileHandle == NULL || m_pNfsContext == NULL) return -1; + + //write as long as some bytes are left to be written + while( leftBytes ) + { + //the last chunk could be smalle than chunksize + if(leftBytes < chunkSize) + { + chunkSize = leftBytes;//write last chunk with correct size + } + //write chunk + //! @bug libnfs < 2.0.0 isn't const correct + writtenBytes = nfs_write(m_pNfsContext, + m_pFileHandle, + chunkSize, + const_cast<char*>((const char *)lpBuf) + numberOfBytesWritten); + //decrease left bytes + leftBytes-= writtenBytes; + //increase overall written bytes + numberOfBytesWritten += writtenBytes; + + //danger - something went wrong + if (writtenBytes < 0) + { + CLog::Log(LOGERROR, "Failed to pwrite({}) {}", m_url.GetFileName(), + nfs_get_error(m_pNfsContext)); + if (numberOfBytesWritten == 0) + return -1; + + break; + } + } + //return total number of written bytes + return numberOfBytesWritten; +} + +bool CNFSFile::Delete(const CURL& url) +{ + int ret = 0; + std::unique_lock<CCriticalSection> lock(gNfsConnection); + std::string filename; + + if(!gNfsConnection.Connect(url, filename)) + return false; + + + ret = nfs_unlink(gNfsConnection.GetNfsContext(), filename.c_str()); + + if(ret != 0) + { + CLog::Log(LOGERROR, "{} - Error( {} )", __FUNCTION__, + nfs_get_error(gNfsConnection.GetNfsContext())); + } + return (ret == 0); +} + +bool CNFSFile::Rename(const CURL& url, const CURL& urlnew) +{ + int ret = 0; + std::unique_lock<CCriticalSection> lock(gNfsConnection); + std::string strFile; + + if(!gNfsConnection.Connect(url,strFile)) + return false; + + std::string strFileNew; + std::string strDummy; + gNfsConnection.splitUrlIntoExportAndPath(urlnew, strDummy, strFileNew); + + ret = nfs_rename(gNfsConnection.GetNfsContext() , strFile.c_str(), strFileNew.c_str()); + + if(ret != 0) + { + CLog::Log(LOGERROR, "{} - Error( {} )", __FUNCTION__, + nfs_get_error(gNfsConnection.GetNfsContext())); + } + return (ret == 0); +} + +bool CNFSFile::OpenForWrite(const CURL& url, bool bOverWrite) +{ + int ret = 0; + // we can't open files like nfs://file.f or nfs://server/file.f + // if a file matches the if below return false, it can't exist on a nfs share. + if (!IsValidFile(url.GetFileName())) return false; + + Close(); + std::unique_lock<CCriticalSection> lock(gNfsConnection); + std::string filename; + + if(!gNfsConnection.Connect(url,filename)) + return false; + + m_pNfsContext = gNfsConnection.GetNfsContext(); + m_exportPath = gNfsConnection.GetContextMapId(); + + if (bOverWrite) + { + CLog::Log(LOGWARNING, "FileNFS::OpenForWrite() called with overwriting enabled! - {}", + filename); + //create file with proper permissions + ret = nfs_creat(m_pNfsContext, filename.c_str(), S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH, &m_pFileHandle); + //if file was created the file handle isn't valid ... so close it and open later + if(ret == 0) + { + nfs_close(m_pNfsContext,m_pFileHandle); + m_pFileHandle = NULL; + } + } + + ret = nfs_open(m_pNfsContext, filename.c_str(), O_RDWR, &m_pFileHandle); + + if (ret || m_pFileHandle == NULL) + { + // write error to logfile + CLog::Log(LOGERROR, "CNFSFile::Open: Unable to open file : '{}' error : '{}'", filename, + nfs_get_error(gNfsConnection.GetNfsContext())); + m_pNfsContext = NULL; + m_exportPath.clear(); + return false; + } + m_url=url; + + struct __stat64 tmpBuffer = {}; + + //only stat if file was not created + if(!bOverWrite) + { + if(Stat(&tmpBuffer)) + { + m_url.Reset(); + Close(); + return false; + } + m_fileSize = tmpBuffer.st_size;//cache filesize of this file + } + else//file was created - filesize is zero + { + m_fileSize = 0; + } + + // We've successfully opened the file! + return true; +} + +bool CNFSFile::IsValidFile(const std::string& strFileName) +{ + if (strFileName.find('/') == std::string::npos || /* doesn't have sharename */ + StringUtils::EndsWith(strFileName, "/.") || /* not current folder */ + StringUtils::EndsWith(strFileName, "/..")) /* not parent folder */ + return false; + return true; +} diff --git a/xbmc/filesystem/NFSFile.h b/xbmc/filesystem/NFSFile.h new file mode 100644 index 0000000..9e9dde9 --- /dev/null +++ b/xbmc/filesystem/NFSFile.h @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +// FileNFS.h: interface for the CNFSFile class. + +#include "IFile.h" +#include "URL.h" +#include "threads/CriticalSection.h" + +#include <chrono> +#include <list> +#include <map> + +struct nfs_stat_64; + +class CNfsConnection : public CCriticalSection +{ +public: + struct keepAliveStruct + { + std::string exportPath; + std::chrono::time_point<std::chrono::steady_clock> refreshTime; + }; + typedef std::map<struct nfsfh *, struct keepAliveStruct> tFileKeepAliveMap; + + struct contextTimeout + { + struct nfs_context *pContext; + std::chrono::time_point<std::chrono::steady_clock> lastAccessedTime; + }; + + typedef std::map<std::string, struct contextTimeout> tOpenContextMap; + + CNfsConnection(); + ~CNfsConnection(); + bool Connect(const CURL &url, std::string &relativePath); + struct nfs_context *GetNfsContext() {return m_pNfsContext;} + uint64_t GetMaxReadChunkSize() {return m_readChunkSize;} + uint64_t GetMaxWriteChunkSize() {return m_writeChunkSize;} + std::list<std::string> GetExportList(const CURL &url); + //this functions splits the url into the exportpath (feed to mount) and the rest of the path + //relative to the mounted export + bool splitUrlIntoExportAndPath(const CURL& url, std::string &exportPath, std::string &relativePath, std::list<std::string> &exportList); + bool splitUrlIntoExportAndPath(const CURL& url, std::string &exportPath, std::string &relativePath); + + //special stat which uses its own context + //needed for getting intervolume symlinks to work + int stat(const CURL& url, nfs_stat_64* statbuff); + + void AddActiveConnection(); + void AddIdleConnection(); + void CheckIfIdle(); + void Deinit(); + //adds the filehandle to the keep alive list or resets + //the timeout for this filehandle if already in list + void resetKeepAlive(const std::string& _exportPath, struct nfsfh* _pFileHandle); + //removes file handle from keep alive list + void removeFromKeepAliveList(struct nfsfh *_pFileHandle); + + const std::string& GetConnectedIp() const {return m_resolvedHostName;} + const std::string& GetConnectedExport() const {return m_exportPath;} + const std::string GetContextMapId() const {return m_hostName + m_exportPath;} + +private: + enum class ContextStatus + { + INVALID, + NEW, + CACHED + }; + + struct nfs_context *m_pNfsContext;//current nfs context + std::string m_exportPath;//current connected export path + std::string m_hostName;//current connected host + std::string m_resolvedHostName;//current connected host - as ip + uint64_t m_readChunkSize = 0;//current read chunksize of connected server + uint64_t m_writeChunkSize = 0;//current write chunksize of connected server + int m_OpenConnections = 0; //number of open connections + std::chrono::time_point<std::chrono::steady_clock> m_IdleTimeout; + tFileKeepAliveMap m_KeepAliveTimeouts;//mapping filehandles to its idle timeout + tOpenContextMap m_openContextMap;//unique map for tracking all open contexts + std::chrono::time_point<std::chrono::steady_clock> + m_lastAccessedTime; //last access time for m_pNfsContext + std::list<std::string> m_exportList;//list of exported paths of current connected servers + CCriticalSection keepAliveLock; + CCriticalSection openContextLock; + + void clearMembers(); + struct nfs_context *getContextFromMap(const std::string &exportname, bool forceCacheHit = false); + + // get context for given export and add to open contexts map - sets m_pNfsContext (may return an already mounted cached context) + ContextStatus getContextForExport(const std::string& exportname); + void destroyOpenContexts(); + void destroyContext(const std::string &exportName); + void resolveHost(const CURL &url);//resolve hostname by dnslookup + void keepAlive(const std::string& _exportPath, struct nfsfh* _pFileHandle); + static void setOptions(struct nfs_context* context); +}; + +extern CNfsConnection gNfsConnection; + +namespace XFILE +{ + class CNFSFile : public IFile + { + public: + CNFSFile(); + ~CNFSFile() override; + void Close() override; + int64_t Seek(int64_t iFilePosition, int iWhence = SEEK_SET) override; + ssize_t Read(void* lpBuf, size_t uiBufSize) override; + bool Open(const CURL& url) override; + bool Exists(const CURL& url) override; + int Stat(const CURL& url, struct __stat64* buffer) override; + int Stat(struct __stat64* buffer) override; + int64_t GetLength() override; + int64_t GetPosition() override; + ssize_t Write(const void* lpBuf, size_t uiBufSize) override; + int Truncate(int64_t iSize) override; + + //implement iocontrol for seek_possible for preventing the stat in File class for + //getting this info ... + int IoControl(EIoControl request, void* param) override + { + return request == IOCTRL_SEEK_POSSIBLE ? 1 : -1; + } + int GetChunkSize() override {return static_cast<int>(gNfsConnection.GetMaxReadChunkSize());} + + bool OpenForWrite(const CURL& url, bool bOverWrite = false) override; + bool Delete(const CURL& url) override; + bool Rename(const CURL& url, const CURL& urlnew) override; + protected: + CURL m_url; + bool IsValidFile(const std::string& strFileName); + int64_t m_fileSize = 0; + struct nfsfh *m_pFileHandle; + struct nfs_context *m_pNfsContext;//current nfs context + std::string m_exportPath; + }; +} + diff --git a/xbmc/filesystem/NptXbmcFile.cpp b/xbmc/filesystem/NptXbmcFile.cpp new file mode 100644 index 0000000..bcbdea1 --- /dev/null +++ b/xbmc/filesystem/NptXbmcFile.cpp @@ -0,0 +1,534 @@ +/* + * Neptune - Files :: XBMC Implementation + * + * Copyright (c) 2002-2008, Axiomatic Systems, LLC. + * All rights reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + * See LICENSES/README.md for more information. + */ + +/*---------------------------------------------------------------------- +| includes ++---------------------------------------------------------------------*/ +#include "File.h" +#include "FileFactory.h" +#include "PasswordManager.h" +#include "URL.h" +#include "utils/URIUtils.h" + +#include <limits> + +#include <Neptune/Source/Core/NptDebug.h> +#include <Neptune/Source/Core/NptFile.h> +#include <Neptune/Source/Core/NptStrings.h> +#include <Neptune/Source/Core/NptUtils.h> + +#ifdef TARGET_WINDOWS +#define S_IWUSR _S_IWRITE +#define S_ISDIR(m) ((m & _S_IFDIR) != 0) +#define S_ISREG(m) ((m & _S_IFREG) != 0) +#endif + +using namespace XFILE; + +typedef NPT_Reference<IFile> NPT_XbmcFileReference; + +/*---------------------------------------------------------------------- +| NPT_XbmcFileStream ++---------------------------------------------------------------------*/ +class NPT_XbmcFileStream +{ +public: + // constructors and destructor + explicit NPT_XbmcFileStream(const NPT_XbmcFileReference& file) : m_FileReference(file) {} + + // NPT_FileInterface methods + NPT_Result Seek(NPT_Position offset); + NPT_Result Tell(NPT_Position& offset); + NPT_Result Flush(); + +protected: + // constructors and destructors + virtual ~NPT_XbmcFileStream() = default; + + // members + NPT_XbmcFileReference m_FileReference; +}; + +/*---------------------------------------------------------------------- +| NPT_XbmcFileStream::Seek ++---------------------------------------------------------------------*/ +NPT_Result +NPT_XbmcFileStream::Seek(NPT_Position offset) +{ + int64_t result; + + result = m_FileReference->Seek(offset, SEEK_SET) ; + if (result >= 0) { + return NPT_SUCCESS; + } else { + return NPT_FAILURE; + } +} + +/*---------------------------------------------------------------------- +| NPT_XbmcFileStream::Tell ++---------------------------------------------------------------------*/ +NPT_Result +NPT_XbmcFileStream::Tell(NPT_Position& offset) +{ + int64_t result = m_FileReference->GetPosition(); + if (result >= 0) { + offset = (NPT_Position)result; + return NPT_SUCCESS; + } else { + return NPT_FAILURE; + } +} + +/*---------------------------------------------------------------------- +| NPT_XbmcFileStream::Flush ++---------------------------------------------------------------------*/ +NPT_Result +NPT_XbmcFileStream::Flush() +{ + m_FileReference->Flush(); + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_XbmcFileInputStream ++---------------------------------------------------------------------*/ +class NPT_XbmcFileInputStream : public NPT_InputStream, + private NPT_XbmcFileStream + +{ +public: + // constructors and destructor + explicit NPT_XbmcFileInputStream(NPT_XbmcFileReference& file) : + NPT_XbmcFileStream(file) {} + + // NPT_InputStream methods + NPT_Result Read(void* buffer, + NPT_Size bytes_to_read, + NPT_Size* bytes_read) override; + NPT_Result Seek(NPT_Position offset) override { + return NPT_XbmcFileStream::Seek(offset); + } + NPT_Result Tell(NPT_Position& offset) override { + return NPT_XbmcFileStream::Tell(offset); + } + NPT_Result GetSize(NPT_LargeSize& size) override; + NPT_Result GetAvailable(NPT_LargeSize& available) override; +}; + +/*---------------------------------------------------------------------- +| NPT_XbmcFileInputStream::Read ++---------------------------------------------------------------------*/ +NPT_Result +NPT_XbmcFileInputStream::Read(void* buffer, + NPT_Size bytes_to_read, + NPT_Size* bytes_read) +{ + unsigned int nb_read; + + // check the parameters + if (buffer == NULL) { + return NPT_ERROR_INVALID_PARAMETERS; + } + + // read from the file + nb_read = m_FileReference->Read(buffer, bytes_to_read); + if (nb_read > 0) { + if (bytes_read) *bytes_read = (NPT_Size)nb_read; + return NPT_SUCCESS; + } else { + if (bytes_read) *bytes_read = 0; + return NPT_ERROR_EOS; + //} else { // currently no way to indicate failure + // if (bytes_read) *bytes_read = 0; + // return NPT_ERROR_READ_FAILED; + } +} + +/*---------------------------------------------------------------------- +| NPT_XbmcFileInputStream::GetSize ++---------------------------------------------------------------------*/ +NPT_Result +NPT_XbmcFileInputStream::GetSize(NPT_LargeSize& size) +{ + size = m_FileReference->GetLength(); + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_XbmcFileInputStream::GetAvailable ++---------------------------------------------------------------------*/ +NPT_Result +NPT_XbmcFileInputStream::GetAvailable(NPT_LargeSize& available) +{ + int64_t offset = m_FileReference->GetPosition(); + NPT_LargeSize size = 0; + + if (NPT_SUCCEEDED(GetSize(size)) && offset >= 0 && (NPT_LargeSize)offset <= size) { + available = size - offset; + return NPT_SUCCESS; + } else { + available = 0; + return NPT_FAILURE; + } +} + +/*---------------------------------------------------------------------- +| NPT_XbmcFileOutputStream ++---------------------------------------------------------------------*/ +class NPT_XbmcFileOutputStream : public NPT_OutputStream, + private NPT_XbmcFileStream +{ +public: + // constructors and destructor + explicit NPT_XbmcFileOutputStream(NPT_XbmcFileReference& file) : + NPT_XbmcFileStream(file) {} + + // NPT_OutputStream methods + NPT_Result Write(const void* buffer, + NPT_Size bytes_to_write, + NPT_Size* bytes_written) override; + NPT_Result Seek(NPT_Position offset) override { + return NPT_XbmcFileStream::Seek(offset); + } + NPT_Result Tell(NPT_Position& offset) override { + return NPT_XbmcFileStream::Tell(offset); + } + NPT_Result Flush() override { + return NPT_XbmcFileStream::Flush(); + } +}; + +/*---------------------------------------------------------------------- +| NPT_XbmcFileOutputStream::Write ++---------------------------------------------------------------------*/ +NPT_Result +NPT_XbmcFileOutputStream::Write(const void* buffer, + NPT_Size bytes_to_write, + NPT_Size* bytes_written) +{ + int nb_written; + nb_written = m_FileReference->Write(buffer, bytes_to_write); + + if (nb_written > 0) { + if (bytes_written) *bytes_written = (NPT_Size)nb_written; + return NPT_SUCCESS; + } else { + if (bytes_written) *bytes_written = 0; + return NPT_ERROR_WRITE_FAILED; + } +} + +/*---------------------------------------------------------------------- +| NPT_XbmcFile ++---------------------------------------------------------------------*/ +class NPT_XbmcFile: public NPT_FileInterface +{ +public: + // constructors and destructor + explicit NPT_XbmcFile(NPT_File& delegator); + ~NPT_XbmcFile() override; + + // NPT_FileInterface methods + NPT_Result Open(OpenMode mode) override; + NPT_Result Close() override; + NPT_Result GetInputStream(NPT_InputStreamReference& stream) override; + NPT_Result GetOutputStream(NPT_OutputStreamReference& stream) override; + +private: + // members + NPT_File& m_Delegator; + OpenMode m_Mode; + NPT_XbmcFileReference m_FileReference; +}; + +/*---------------------------------------------------------------------- +| NPT_XbmcFile::NPT_XbmcFile ++---------------------------------------------------------------------*/ +NPT_XbmcFile::NPT_XbmcFile(NPT_File& delegator) : + m_Delegator(delegator), + m_Mode(0) +{ +} + +/*---------------------------------------------------------------------- +| NPT_XbmcFile::~NPT_XbmcFile ++---------------------------------------------------------------------*/ +NPT_XbmcFile::~NPT_XbmcFile() +{ + Close(); +} + +/*---------------------------------------------------------------------- +| NPT_XbmcFile::Open ++---------------------------------------------------------------------*/ +NPT_Result +NPT_XbmcFile::Open(NPT_File::OpenMode mode) +{ + NPT_XbmcFileReference file; + + // check if we're already open + if (!m_FileReference.IsNull()) { + return NPT_ERROR_FILE_ALREADY_OPEN; + } + + // store the mode + m_Mode = mode; + + // check for special names + const char* name = (const char*)m_Delegator.GetPath(); + if (NPT_StringsEqual(name, NPT_FILE_STANDARD_INPUT)) { + return NPT_ERROR_FILE_NOT_READABLE; + } else if (NPT_StringsEqual(name, NPT_FILE_STANDARD_OUTPUT)) { + return NPT_ERROR_FILE_NOT_WRITABLE; + } else if (NPT_StringsEqual(name, NPT_FILE_STANDARD_ERROR)) { + return NPT_ERROR_FILE_NOT_WRITABLE; + } else { + + file = CFileFactory::CreateLoader(name); + if (file.IsNull()) { + return NPT_ERROR_NO_SUCH_FILE; + } + + bool result; + CURL url(URIUtils::SubstitutePath(name)); + + if (CPasswordManager::GetInstance().IsURLSupported(url) && url.GetUserName().empty()) + CPasswordManager::GetInstance().AuthenticateURL(url); + + // compute mode + if (mode & NPT_FILE_OPEN_MODE_WRITE) + result = file->OpenForWrite(url, (mode & NPT_FILE_OPEN_MODE_TRUNCATE) ? true : false); + else + result = file->Open(url); + + if (!result) return NPT_ERROR_NO_SUCH_FILE; + } + + // store reference + m_FileReference = file; + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_XbmcFile::Close ++---------------------------------------------------------------------*/ +NPT_Result +NPT_XbmcFile::Close() +{ + // release the file reference + m_FileReference = NULL; + + // reset the mode + m_Mode = 0; + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_XbmcFile::GetInputStream ++---------------------------------------------------------------------*/ +NPT_Result +NPT_XbmcFile::GetInputStream(NPT_InputStreamReference& stream) +{ + // default value + stream = NULL; + + // check that the file is open + if (m_FileReference.IsNull()) return NPT_ERROR_FILE_NOT_OPEN; + + // check that the mode is compatible + if (!(m_Mode & NPT_FILE_OPEN_MODE_READ)) { + return NPT_ERROR_FILE_NOT_READABLE; + } + + // create a stream + stream = new NPT_XbmcFileInputStream(m_FileReference); + + return NPT_SUCCESS; +} + +/*---------------------------------------------------------------------- +| NPT_XbmcFile::GetOutputStream ++---------------------------------------------------------------------*/ +NPT_Result +NPT_XbmcFile::GetOutputStream(NPT_OutputStreamReference& stream) +{ + // default value + stream = NULL; + + // check that the file is open + if (m_FileReference.IsNull()) return NPT_ERROR_FILE_NOT_OPEN; + + // check that the mode is compatible + if (!(m_Mode & NPT_FILE_OPEN_MODE_WRITE)) { + return NPT_ERROR_FILE_NOT_WRITABLE; + } + + // create a stream + stream = new NPT_XbmcFileOutputStream(m_FileReference); + + return NPT_SUCCESS; +} + +static NPT_Result +MapErrno(int err) { + switch (err) { + case EACCES: return NPT_ERROR_PERMISSION_DENIED; + case EPERM: return NPT_ERROR_PERMISSION_DENIED; + case ENOENT: return NPT_ERROR_NO_SUCH_FILE; + case ENAMETOOLONG: return NPT_ERROR_INVALID_PARAMETERS; + case EBUSY: return NPT_ERROR_FILE_BUSY; + case EROFS: return NPT_ERROR_FILE_NOT_WRITABLE; + case ENOTDIR: return NPT_ERROR_FILE_NOT_DIRECTORY; + case EEXIST: return NPT_ERROR_FILE_ALREADY_EXISTS; + case ENOSPC: return NPT_ERROR_FILE_NOT_ENOUGH_SPACE; + case ENOTEMPTY: return NPT_ERROR_DIRECTORY_NOT_EMPTY; + default: return NPT_ERROR_ERRNO(err); + } +} +/*---------------------------------------------------------------------- +| NPT_FilePath::Separator ++---------------------------------------------------------------------*/ +const char* const NPT_FilePath::Separator = "/"; + +/*---------------------------------------------------------------------- +| NPT_File::NPT_File ++---------------------------------------------------------------------*/ +NPT_File::NPT_File(const char* path) : m_Path(path) +{ + m_Delegate = new NPT_XbmcFile(*this); +} + +/*---------------------------------------------------------------------- +| NPT_File::operator= ++---------------------------------------------------------------------*/ +NPT_File& +NPT_File::operator=(const NPT_File& file) +{ + if (this != &file) { + delete m_Delegate; + m_Path = file.m_Path; + m_Delegate = new NPT_XbmcFile(*this); + } + return *this; +} + +/*---------------------------------------------------------------------- +| NPT_File::GetRoots ++---------------------------------------------------------------------*/ +NPT_Result +NPT_File::GetRoots(NPT_List<NPT_String>& roots) +{ + return NPT_FAILURE; +} + +/*---------------------------------------------------------------------- +| NPT_File::CreateDir ++---------------------------------------------------------------------*/ +NPT_Result +NPT_File::CreateDir(const char* path) +{ + return NPT_ERROR_PERMISSION_DENIED; +} + +/*---------------------------------------------------------------------- +| NPT_File::RemoveFile ++---------------------------------------------------------------------*/ +NPT_Result +NPT_File::RemoveFile(const char* path) +{ + return NPT_ERROR_PERMISSION_DENIED; +} + +/*---------------------------------------------------------------------- +| NPT_File::RemoveDir ++---------------------------------------------------------------------*/ +NPT_Result +NPT_File::RemoveDir(const char* path) +{ + return NPT_ERROR_PERMISSION_DENIED; +} + +/*---------------------------------------------------------------------- +| NPT_File::Rename ++---------------------------------------------------------------------*/ +NPT_Result +NPT_File::Rename(const char* from_path, const char* to_path) +{ + return NPT_ERROR_PERMISSION_DENIED; +} + +/*---------------------------------------------------------------------- +| NPT_File::ListDir ++---------------------------------------------------------------------*/ +NPT_Result +NPT_File::ListDir(const char* path, + NPT_List<NPT_String>& entries, + NPT_Ordinal start /* = 0 */, + NPT_Cardinal max /* = 0 */) +{ + return NPT_FAILURE; +} + +/*---------------------------------------------------------------------- +| NPT_File::GetWorkingDir ++---------------------------------------------------------------------*/ +NPT_Result +NPT_File::GetWorkingDir(NPT_String& path) +{ + return NPT_FAILURE; +} + +/*---------------------------------------------------------------------- +| NPT_File::GetInfo ++---------------------------------------------------------------------*/ +NPT_Result +NPT_File::GetInfo(const char* path, NPT_FileInfo* info) +{ + struct __stat64 stat_buffer = {}; + int result; + + if (!info) + return NPT_FAILURE; + + *info = NPT_FileInfo(); + + result = CFile::Stat(path, &stat_buffer); + if (result != 0) + return MapErrno(errno); + if (info) + { + info->m_Size = stat_buffer.st_size; + if (S_ISREG(stat_buffer.st_mode)) + { + info->m_Type = NPT_FileInfo::FILE_TYPE_REGULAR; + } + else if (S_ISDIR(stat_buffer.st_mode)) + { + info->m_Type = NPT_FileInfo::FILE_TYPE_DIRECTORY; + } + else + { + info->m_Type = NPT_FileInfo::FILE_TYPE_OTHER; + } + info->m_AttributesMask &= NPT_FILE_ATTRIBUTE_READ_ONLY; + if ((stat_buffer.st_mode & S_IWUSR) == 0) + { + info->m_Attributes &= NPT_FILE_ATTRIBUTE_READ_ONLY; + } + info->m_CreationTime.SetSeconds(0); + info->m_ModificationTime.SetSeconds(stat_buffer.st_mtime); + } + + return NPT_SUCCESS; +} + diff --git a/xbmc/filesystem/OverrideDirectory.cpp b/xbmc/filesystem/OverrideDirectory.cpp new file mode 100644 index 0000000..0e24247 --- /dev/null +++ b/xbmc/filesystem/OverrideDirectory.cpp @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "OverrideDirectory.h" + +#include "URL.h" +#include "filesystem/Directory.h" + +using namespace XFILE; + + +COverrideDirectory::COverrideDirectory() = default; + + +COverrideDirectory::~COverrideDirectory() = default; + +bool COverrideDirectory::Create(const CURL& url) +{ + std::string translatedPath = TranslatePath(url); + + return CDirectory::Create(translatedPath.c_str()); +} + +bool COverrideDirectory::Remove(const CURL& url) +{ + std::string translatedPath = TranslatePath(url); + + return CDirectory::Remove(translatedPath.c_str()); +} + +bool COverrideDirectory::Exists(const CURL& url) +{ + std::string translatedPath = TranslatePath(url); + + return CDirectory::Exists(translatedPath.c_str()); +} diff --git a/xbmc/filesystem/OverrideDirectory.h b/xbmc/filesystem/OverrideDirectory.h new file mode 100644 index 0000000..0c6a7ab --- /dev/null +++ b/xbmc/filesystem/OverrideDirectory.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "filesystem/IDirectory.h" + +namespace XFILE +{ +class COverrideDirectory : public IDirectory +{ +public: + COverrideDirectory(); + ~COverrideDirectory() override; + + bool Create(const CURL& url) override; + bool Exists(const CURL& url) override; + bool Remove(const CURL& url) override; + +protected: + virtual std::string TranslatePath(const CURL &url) = 0; +}; +} diff --git a/xbmc/filesystem/OverrideFile.cpp b/xbmc/filesystem/OverrideFile.cpp new file mode 100644 index 0000000..73fb43e --- /dev/null +++ b/xbmc/filesystem/OverrideFile.cpp @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "OverrideFile.h" + +#include "URL.h" + +#include <sys/stat.h> + +using namespace XFILE; + + +COverrideFile::COverrideFile(bool writable) + : m_writable(writable) +{ } + + +COverrideFile::~COverrideFile() +{ + Close(); +} + +bool COverrideFile::Open(const CURL& url) +{ + std::string strFileName = TranslatePath(url); + + return m_file.Open(strFileName); +} + +bool COverrideFile::OpenForWrite(const CURL& url, bool bOverWrite /* = false */) +{ + if (!m_writable) + return false; + + std::string strFileName = TranslatePath(url); + + return m_file.OpenForWrite(strFileName, bOverWrite); +} + +bool COverrideFile::Delete(const CURL& url) +{ + if (!m_writable) + return false; + + std::string strFileName = TranslatePath(url); + + return m_file.Delete(strFileName); +} + +bool COverrideFile::Exists(const CURL& url) +{ + std::string strFileName = TranslatePath(url); + + return m_file.Exists(strFileName); +} + +int COverrideFile::Stat(const CURL& url, struct __stat64* buffer) +{ + std::string strFileName = TranslatePath(url); + + return m_file.Stat(strFileName, buffer); +} + +bool COverrideFile::Rename(const CURL& url, const CURL& urlnew) +{ + if (!m_writable) + return false; + + std::string strFileName = TranslatePath(url); + std::string strFileName2 = TranslatePath(urlnew); + + return m_file.Rename(strFileName, strFileName2); +} + +int COverrideFile::Stat(struct __stat64* buffer) +{ + return m_file.Stat(buffer); +} + +ssize_t COverrideFile::Read(void* lpBuf, size_t uiBufSize) +{ + return m_file.Read(lpBuf, uiBufSize); +} + +ssize_t COverrideFile::Write(const void* lpBuf, size_t uiBufSize) +{ + if (!m_writable) + return -1; + + return m_file.Write(lpBuf, uiBufSize); +} + +int64_t COverrideFile::Seek(int64_t iFilePosition, int iWhence /*=SEEK_SET*/) +{ + return m_file.Seek(iFilePosition, iWhence); +} + +void COverrideFile::Close() +{ + m_file.Close(); +} + +int64_t COverrideFile::GetPosition() +{ + return m_file.GetPosition(); +} + +int64_t COverrideFile::GetLength() +{ + return m_file.GetLength(); +} diff --git a/xbmc/filesystem/OverrideFile.h b/xbmc/filesystem/OverrideFile.h new file mode 100644 index 0000000..974b473 --- /dev/null +++ b/xbmc/filesystem/OverrideFile.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "filesystem/File.h" +#include "filesystem/IFile.h" + +namespace XFILE +{ +class COverrideFile : public IFile +{ +public: + explicit COverrideFile(bool writable); + ~COverrideFile() override; + + bool Open(const CURL& url) override; + bool Exists(const CURL& url) override; + int Stat(const CURL& url, struct __stat64* buffer) override; + int Stat(struct __stat64* buffer) override; + bool OpenForWrite(const CURL& url, bool bOverWrite = false) override; + bool Delete(const CURL& url) override; + bool Rename(const CURL& url, const CURL& urlnew) override; + + ssize_t Read(void* lpBuf, size_t uiBufSize) override; + ssize_t Write(const void* lpBuf, size_t uiBufSize) override; + int64_t Seek(int64_t iFilePosition, int iWhence = SEEK_SET) override; + void Close() override; + int64_t GetPosition() override; + int64_t GetLength() override; + +protected: + virtual std::string TranslatePath(const CURL &url) = 0; + + CFile m_file; + bool m_writable; +}; +} diff --git a/xbmc/filesystem/PVRDirectory.cpp b/xbmc/filesystem/PVRDirectory.cpp new file mode 100644 index 0000000..915d3f7 --- /dev/null +++ b/xbmc/filesystem/PVRDirectory.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRDirectory.h" + +#include "pvr/filesystem/PVRGUIDirectory.h" + +using namespace XFILE; +using namespace PVR; + +CPVRDirectory::CPVRDirectory() = default; + +CPVRDirectory::~CPVRDirectory() = default; + +bool CPVRDirectory::Exists(const CURL& url) +{ + const CPVRGUIDirectory dir(url); + return dir.Exists(); +} + +bool CPVRDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + const CPVRGUIDirectory dir(url); + return dir.GetDirectory(items); +} + +bool CPVRDirectory::SupportsWriteFileOperations(const std::string& strPath) +{ + const CPVRGUIDirectory dir(strPath); + return dir.SupportsWriteFileOperations(); +} + +bool CPVRDirectory::HasTVRecordings() +{ + return CPVRGUIDirectory::HasTVRecordings(); +} + +bool CPVRDirectory::HasDeletedTVRecordings() +{ + return CPVRGUIDirectory::HasDeletedTVRecordings(); +} + +bool CPVRDirectory::HasRadioRecordings() +{ + return CPVRGUIDirectory::HasRadioRecordings(); +} + +bool CPVRDirectory::HasDeletedRadioRecordings() +{ + return CPVRGUIDirectory::HasDeletedRadioRecordings(); +} diff --git a/xbmc/filesystem/PVRDirectory.h b/xbmc/filesystem/PVRDirectory.h new file mode 100644 index 0000000..8745fb1 --- /dev/null +++ b/xbmc/filesystem/PVRDirectory.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" + +namespace XFILE { + +class CPVRDirectory + : public IDirectory +{ +public: + CPVRDirectory(); + ~CPVRDirectory() override; + + bool GetDirectory(const CURL& url, CFileItemList &items) override; + bool AllowAll() const override { return true; } + DIR_CACHE_TYPE GetCacheType(const CURL& url) const override { return DIR_CACHE_NEVER; } + bool Exists(const CURL& url) override; + + static bool SupportsWriteFileOperations(const std::string& strPath); + + static bool HasTVRecordings(); + static bool HasDeletedTVRecordings(); + static bool HasRadioRecordings(); + static bool HasDeletedRadioRecordings(); +}; + +} diff --git a/xbmc/filesystem/PipeFile.cpp b/xbmc/filesystem/PipeFile.cpp new file mode 100644 index 0000000..f732843 --- /dev/null +++ b/xbmc/filesystem/PipeFile.cpp @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PipeFile.h" + +#include "PipesManager.h" +#include "URL.h" + +#include <mutex> + +using namespace XFILE; + +CPipeFile::CPipeFile() : m_pipe(NULL) +{ +} + +CPipeFile::~CPipeFile() +{ + Close(); +} + +int64_t CPipeFile::GetPosition() +{ + return m_pos; +} + +int64_t CPipeFile::GetLength() +{ + return m_length; +} + +void CPipeFile::SetLength(int64_t len) +{ + m_length = len; +} + +bool CPipeFile::Open(const CURL& url) +{ + std::string name = url.Get(); + m_pipe = PipesManager::GetInstance().OpenPipe(name); + if (m_pipe) + m_pipe->AddListener(this); + return (m_pipe != NULL); +} + +bool CPipeFile::Exists(const CURL& url) +{ + std::string name = url.Get(); + return PipesManager::GetInstance().Exists(name); +} + +int CPipeFile::Stat(const CURL& url, struct __stat64* buffer) +{ + return -1; +} + +int CPipeFile::Stat(struct __stat64* buffer) +{ + if (!buffer) + return -1; + + *buffer = {}; + buffer->st_size = m_length; + return 0; +} + +ssize_t CPipeFile::Read(void* lpBuf, size_t uiBufSize) +{ + if (!m_pipe) + return -1; + + if (uiBufSize > SSIZE_MAX) + uiBufSize = SSIZE_MAX; + + return m_pipe->Read((char *)lpBuf,(int)uiBufSize); +} + +ssize_t CPipeFile::Write(const void* lpBuf, size_t uiBufSize) +{ + if (!m_pipe) + return -1; + + // m_pipe->Write return bool. either all was written or not. + return m_pipe->Write((const char *)lpBuf,uiBufSize) ? uiBufSize : -1; +} + +void CPipeFile::SetEof() +{ + if (!m_pipe) + return ; + m_pipe->SetEof(); +} + +bool CPipeFile::IsEof() +{ + if (!m_pipe) + return true; + return m_pipe->IsEof(); +} + +bool CPipeFile::IsEmpty() +{ + if (!m_pipe) + return true; + return m_pipe->IsEmpty(); +} + +int64_t CPipeFile::Seek(int64_t iFilePosition, int iWhence) +{ + return -1; +} + +void CPipeFile::Close() +{ + if (m_pipe) + { + m_pipe->RemoveListener(this); + PipesManager::GetInstance().ClosePipe(m_pipe); + } + m_pipe = NULL; +} + +bool CPipeFile::IsClosed() +{ + return (m_pipe == NULL); +} + +void CPipeFile::Flush() +{ + if (m_pipe) + m_pipe->Flush(); +} + +bool CPipeFile::OpenForWrite(const CURL& url, bool bOverWrite) +{ + std::string name = url.Get(); + + m_pipe = PipesManager::GetInstance().CreatePipe(name); + if (m_pipe) + m_pipe->AddListener(this); + return (m_pipe != NULL); +} + +bool CPipeFile::Delete(const CURL& url) +{ + return false; +} + +bool CPipeFile::Rename(const CURL& url, const CURL& urlnew) +{ + return false; +} + +int CPipeFile::IoControl(EIoControl, void* param) +{ + return -1; +} + +std::string CPipeFile::GetName() const +{ + if (!m_pipe) + return ""; + return m_pipe->GetName(); +} + +void CPipeFile::OnPipeOverFlow() +{ + std::unique_lock<CCriticalSection> lock(m_lock); + for (size_t l=0; l<m_listeners.size(); l++) + m_listeners[l]->OnPipeOverFlow(); +} + +int64_t CPipeFile::GetAvailableRead() +{ + return m_pipe->GetAvailableRead(); +} + +void CPipeFile::OnPipeUnderFlow() +{ + for (size_t l=0; l<m_listeners.size(); l++) + m_listeners[l]->OnPipeUnderFlow(); +} + +void CPipeFile::AddListener(IPipeListener *l) +{ + std::unique_lock<CCriticalSection> lock(m_lock); + for (size_t i=0; i<m_listeners.size(); i++) + { + if (m_listeners[i] == l) + return; + } + m_listeners.push_back(l); +} + +void CPipeFile::RemoveListener(IPipeListener *l) +{ + std::unique_lock<CCriticalSection> lock(m_lock); + std::vector<XFILE::IPipeListener *>::iterator i = m_listeners.begin(); + while(i != m_listeners.end()) + { + if ( (*i) == l) + i = m_listeners.erase(i); + else + ++i; + } +} + +void CPipeFile::SetOpenThreshold(int threshold) +{ + m_pipe->SetOpenThreshold(threshold); +} + diff --git a/xbmc/filesystem/PipeFile.h b/xbmc/filesystem/PipeFile.h new file mode 100644 index 0000000..507006b --- /dev/null +++ b/xbmc/filesystem/PipeFile.h @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2002 Frodo + * Portions Copyright (c) by the authors of ffmpeg and xvid + * Copyright (C) 2002-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +// FilePipe.h: interface for the CPipeFile class. +// +////////////////////////////////////////////////////////////////////// + +#include "IFile.h" +#include "PipesManager.h" +#include "threads/CriticalSection.h" + +#include <string> +#include <vector> + +namespace XFILE +{ + +class CPipeFile : public IFile, public IPipeListener +{ +public: + CPipeFile(); + ~CPipeFile() override; + int64_t GetPosition() override; + int64_t GetLength() override; + virtual void SetLength(int64_t len); + bool Open(const CURL& url) override; + bool Exists(const CURL& url) override; + int Stat(const CURL& url, struct __stat64* buffer) override; + int Stat(struct __stat64* buffer) override; + ssize_t Read(void* lpBuf, size_t uiBufSize) override; + ssize_t Write(const void* lpBuf, size_t uiBufSize) override; + int64_t Seek(int64_t iFilePosition, int iWhence = SEEK_SET) override; + void Close() override; + void Flush() override; + virtual int64_t GetAvailableRead(); + + bool OpenForWrite(const CURL& url, bool bOverWrite = false) override; + + bool Delete(const CURL& url) override; + bool Rename(const CURL& url, const CURL& urlnew) override; + int IoControl(EIoControl request, void* param) override; + + std::string GetName() const; + + void OnPipeOverFlow() override; + void OnPipeUnderFlow() override; + + void AddListener(IPipeListener *l); + void RemoveListener(IPipeListener *l); + + void SetEof(); + bool IsEof(); + bool IsEmpty(); + bool IsClosed(); + + void SetOpenThreshold(int threshold); + +protected: + int64_t m_pos = 0; + int64_t m_length = -1; + + XFILE::Pipe *m_pipe; + + CCriticalSection m_lock; + std::vector<XFILE::IPipeListener *> m_listeners; +}; + +} diff --git a/xbmc/filesystem/PipesManager.cpp b/xbmc/filesystem/PipesManager.cpp new file mode 100644 index 0000000..58b9099 --- /dev/null +++ b/xbmc/filesystem/PipesManager.cpp @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PipesManager.h" + +#include "utils/StringUtils.h" + +#include <algorithm> +#include <mutex> + +using namespace XFILE; +using namespace std::chrono_literals; + +Pipe::Pipe(const std::string &name, int nMaxSize) +{ + m_buffer.Create(nMaxSize); + m_nRefCount = 1; + m_readEvent.Reset(); + m_writeEvent.Set(); + m_strPipeName = name; + m_bOpen = true; + m_bEof = false; + m_nOpenThreshold = PIPE_DEFAULT_MAX_SIZE / 2; + m_bReadyForRead = true; // open threshold disabled atm +} + +Pipe::~Pipe() = default; + +void Pipe::SetOpenThreshold(int threshold) +{ + m_nOpenThreshold = threshold; +} + +const std::string &Pipe::GetName() +{ + return m_strPipeName; +} + +void Pipe::AddRef() +{ + std::unique_lock<CCriticalSection> lock(m_lock); + m_nRefCount++; +} + +void Pipe::DecRef() +{ + std::unique_lock<CCriticalSection> lock(m_lock); + m_nRefCount--; +} + +int Pipe::RefCount() +{ + std::unique_lock<CCriticalSection> lock(m_lock); + return m_nRefCount; +} + +void Pipe::SetEof() +{ + m_bEof = true; +} + +bool Pipe::IsEof() +{ + return m_bEof; +} + +bool Pipe::IsEmpty() +{ + return (m_buffer.getMaxReadSize() == 0); +} + +void Pipe::Flush() +{ + std::unique_lock<CCriticalSection> lock(m_lock); + + if (!m_bOpen || !m_bReadyForRead || m_bEof) + { + return; + } + m_buffer.Clear(); + CheckStatus(); +} + +int Pipe::Read(char *buf, int nMaxSize, int nWaitMillis) +{ + std::unique_lock<CCriticalSection> lock(m_lock); + + if (!m_bOpen) + { + return -1; + } + + while (!m_bReadyForRead && !m_bEof) + m_readEvent.Wait(100ms); + + int nResult = 0; + if (!IsEmpty()) + { + int nToRead = std::min(static_cast<int>(m_buffer.getMaxReadSize()), nMaxSize); + m_buffer.ReadData(buf, nToRead); + nResult = nToRead; + } + else if (m_bEof) + { + nResult = 0; + } + else + { + // we're leaving the guard - add ref to make sure we are not getting erased. + // at the moment we leave m_listeners unprotected which might be a problem in future + // but as long as we only have 1 listener attaching at startup and detaching on close we're fine + AddRef(); + lock.unlock(); + + bool bHasData = false; + auto nMillisLeft = std::chrono::milliseconds(nWaitMillis); + if (nMillisLeft < 0ms) + nMillisLeft = 300000ms; // arbitrary. 5 min. + + do + { + for (size_t l=0; l<m_listeners.size(); l++) + m_listeners[l]->OnPipeUnderFlow(); + + bHasData = m_readEvent.Wait(std::min(200ms, nMillisLeft)); + nMillisLeft -= 200ms; + } while (!bHasData && nMillisLeft > 0ms && !m_bEof); + + lock.lock(); + DecRef(); + + if (!m_bOpen) + return -1; + + if (bHasData) + { + int nToRead = std::min(static_cast<int>(m_buffer.getMaxReadSize()), nMaxSize); + m_buffer.ReadData(buf, nToRead); + nResult = nToRead; + } + } + + CheckStatus(); + + return nResult; +} + +bool Pipe::Write(const char *buf, int nSize, int nWaitMillis) +{ + std::unique_lock<CCriticalSection> lock(m_lock); + if (!m_bOpen) + return false; + bool bOk = false; + int writeSize = m_buffer.getMaxWriteSize(); + if (writeSize > nSize) + { + m_buffer.WriteData(buf, nSize); + bOk = true; + } + else + { + while ( (int)m_buffer.getMaxWriteSize() < nSize && m_bOpen ) + { + lock.unlock(); + for (size_t l=0; l<m_listeners.size(); l++) + m_listeners[l]->OnPipeOverFlow(); + + bool bClear = nWaitMillis < 0 ? m_writeEvent.Wait() + : m_writeEvent.Wait(std::chrono::milliseconds(nWaitMillis)); + lock.lock(); + if (bClear && (int)m_buffer.getMaxWriteSize() >= nSize) + { + m_buffer.WriteData(buf, nSize); + bOk = true; + break; + } + + // FIXME: is this right? Shouldn't we see if the time limit has been reached? + if (nWaitMillis > 0) + break; + } + } + + CheckStatus(); + + return bOk && m_bOpen; +} + +void Pipe::CheckStatus() +{ + if (m_bEof) + { + m_writeEvent.Set(); + m_readEvent.Set(); + return; + } + + if (m_buffer.getMaxWriteSize() == 0) + m_writeEvent.Reset(); + else + m_writeEvent.Set(); + + if (m_buffer.getMaxReadSize() == 0) + m_readEvent.Reset(); + else + { + if (!m_bReadyForRead && (int)m_buffer.getMaxReadSize() >= m_nOpenThreshold) + m_bReadyForRead = true; + m_readEvent.Set(); + } +} + +void Pipe::Close() +{ + std::unique_lock<CCriticalSection> lock(m_lock); + m_bOpen = false; + m_readEvent.Set(); + m_writeEvent.Set(); +} + +void Pipe::AddListener(IPipeListener *l) +{ + std::unique_lock<CCriticalSection> lock(m_lock); + for (size_t i=0; i<m_listeners.size(); i++) + { + if (m_listeners[i] == l) + return; + } + m_listeners.push_back(l); +} + +void Pipe::RemoveListener(IPipeListener *l) +{ + std::unique_lock<CCriticalSection> lock(m_lock); + std::vector<XFILE::IPipeListener *>::iterator i = m_listeners.begin(); + while(i != m_listeners.end()) + { + if ( (*i) == l) + i = m_listeners.erase(i); + else + ++i; + } +} + +int Pipe::GetAvailableRead() +{ + std::unique_lock<CCriticalSection> lock(m_lock); + return m_buffer.getMaxReadSize(); +} + +PipesManager::~PipesManager() = default; + +PipesManager &PipesManager::GetInstance() +{ + static PipesManager instance; + return instance; +} + +std::string PipesManager::GetUniquePipeName() +{ + std::unique_lock<CCriticalSection> lock(m_lock); + return StringUtils::Format("pipe://{}/", m_nGenIdHelper++); +} + +XFILE::Pipe *PipesManager::CreatePipe(const std::string &name, int nMaxPipeSize) +{ + std::string pName = name; + if (pName.empty()) + pName = GetUniquePipeName(); + + std::unique_lock<CCriticalSection> lock(m_lock); + if (m_pipes.find(pName) != m_pipes.end()) + return NULL; + + XFILE::Pipe *p = new XFILE::Pipe(pName, nMaxPipeSize); + m_pipes[pName] = p; + return p; +} + +XFILE::Pipe *PipesManager::OpenPipe(const std::string &name) +{ + std::unique_lock<CCriticalSection> lock(m_lock); + if (m_pipes.find(name) == m_pipes.end()) + return NULL; + m_pipes[name]->AddRef(); + return m_pipes[name]; +} + +void PipesManager::ClosePipe(XFILE::Pipe *pipe) +{ + std::unique_lock<CCriticalSection> lock(m_lock); + if (!pipe) + return ; + + pipe->DecRef(); + if (pipe->RefCount() == 0) + { + pipe->Close(); + m_pipes.erase(pipe->GetName()); + delete pipe; + } +} + +bool PipesManager::Exists(const std::string &name) +{ + std::unique_lock<CCriticalSection> lock(m_lock); + return (m_pipes.find(name) != m_pipes.end()); +} + diff --git a/xbmc/filesystem/PipesManager.h b/xbmc/filesystem/PipesManager.h new file mode 100644 index 0000000..100f7d8 --- /dev/null +++ b/xbmc/filesystem/PipesManager.h @@ -0,0 +1,120 @@ +/* + * Many concepts and protocol are taken from + * the Boxee project. http://www.boxee.tv + * + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" +#include "threads/Event.h" +#include "utils/RingBuffer.h" + +#include <map> +#include <string> +#include <vector> + +#define PIPE_DEFAULT_MAX_SIZE (6 * 1024 * 1024) + +namespace XFILE +{ + +class IPipeListener +{ +public: + virtual ~IPipeListener() = default; + virtual void OnPipeOverFlow() = 0; + virtual void OnPipeUnderFlow() = 0; +}; + +class Pipe + { + public: + Pipe(const std::string &name, int nMaxSize = PIPE_DEFAULT_MAX_SIZE ); + virtual ~Pipe(); + const std::string &GetName(); + + void AddRef(); + void DecRef(); // a pipe does NOT delete itself with ref-count 0. + int RefCount(); + + bool IsEmpty(); + + /** + * Read into the buffer from the Pipe the num of bytes asked for + * blocking forever (which happens to be 5 minutes in this case). + * + * In the case where nWaitMillis is provided block for that number + * of milliseconds instead. + */ + int Read(char *buf, int nMaxSize, int nWaitMillis = -1); + + /** + * Write into the Pipe from the buffer the num of bytes asked for + * blocking forever. + * + * In the case where nWaitMillis is provided block for that number + * of milliseconds instead. + */ + bool Write(const char *buf, int nSize, int nWaitMillis = -1); + + void Flush(); + + void CheckStatus(); + void Close(); + + void AddListener(IPipeListener *l); + void RemoveListener(IPipeListener *l); + + void SetEof(); + bool IsEof(); + + int GetAvailableRead(); + void SetOpenThreshold(int threshold); + + protected: + + bool m_bOpen; + bool m_bReadyForRead; + + bool m_bEof; + CRingBuffer m_buffer; + std::string m_strPipeName; + int m_nRefCount; + int m_nOpenThreshold; + + CEvent m_readEvent; + CEvent m_writeEvent; + + std::vector<XFILE::IPipeListener *> m_listeners; + + CCriticalSection m_lock; + }; + + +class PipesManager +{ +public: + virtual ~PipesManager(); + static PipesManager &GetInstance(); + + std::string GetUniquePipeName(); + XFILE::Pipe *CreatePipe(const std::string &name="", int nMaxPipeSize = PIPE_DEFAULT_MAX_SIZE); + XFILE::Pipe *OpenPipe(const std::string &name); + void ClosePipe(XFILE::Pipe *pipe); + bool Exists(const std::string &name); + +protected: + int m_nGenIdHelper = 1; + std::map<std::string, XFILE::Pipe *> m_pipes; + + CCriticalSection m_lock; +}; + +} + diff --git a/xbmc/filesystem/PlaylistDirectory.cpp b/xbmc/filesystem/PlaylistDirectory.cpp new file mode 100644 index 0000000..8be88dc --- /dev/null +++ b/xbmc/filesystem/PlaylistDirectory.cpp @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PlaylistDirectory.h" + +#include "FileItem.h" +#include "PlayListPlayer.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "playlists/PlayList.h" + +using namespace XFILE; + +CPlaylistDirectory::CPlaylistDirectory() = default; + +CPlaylistDirectory::~CPlaylistDirectory() = default; + +bool CPlaylistDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + PLAYLIST::Id playlistId = PLAYLIST::TYPE_NONE; + if (url.IsProtocol("playlistmusic")) + playlistId = PLAYLIST::TYPE_MUSIC; + else if (url.IsProtocol("playlistvideo")) + playlistId = PLAYLIST::TYPE_VIDEO; + + if (playlistId == PLAYLIST::TYPE_NONE) + return false; + + const PLAYLIST::CPlayList& playlist = CServiceBroker::GetPlaylistPlayer().GetPlaylist(playlistId); + items.Reserve(playlist.size()); + + for (int i = 0; i < playlist.size(); ++i) + { + CFileItemPtr item = playlist[i]; + item->SetProperty("playlistposition", i); + item->SetProperty("playlisttype", playlistId); + //item->m_iprogramCount = i; // the programCount is set as items are added! + items.Add(item); + } + + return true; +} diff --git a/xbmc/filesystem/PlaylistDirectory.h b/xbmc/filesystem/PlaylistDirectory.h new file mode 100644 index 0000000..416c0ea --- /dev/null +++ b/xbmc/filesystem/PlaylistDirectory.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" + +namespace XFILE +{ + class CPlaylistDirectory : public IDirectory + { + public: + CPlaylistDirectory(void); + ~CPlaylistDirectory(void) override; + bool GetDirectory(const CURL& url, CFileItemList &items) override; + bool AllowAll() const override { return true; } + }; +} diff --git a/xbmc/filesystem/PlaylistFileDirectory.cpp b/xbmc/filesystem/PlaylistFileDirectory.cpp new file mode 100644 index 0000000..2bdb243 --- /dev/null +++ b/xbmc/filesystem/PlaylistFileDirectory.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PlaylistFileDirectory.h" + +#include "FileItem.h" +#include "URL.h" +#include "filesystem/File.h" +#include "playlists/PlayList.h" +#include "playlists/PlayListFactory.h" + +using namespace PLAYLIST; + +namespace XFILE +{ + CPlaylistFileDirectory::CPlaylistFileDirectory() = default; + + CPlaylistFileDirectory::~CPlaylistFileDirectory() = default; + + bool CPlaylistFileDirectory::GetDirectory(const CURL& url, CFileItemList& items) + { + const std::string pathToUrl = url.Get(); + std::unique_ptr<CPlayList> pPlayList (CPlayListFactory::Create(pathToUrl)); + if (nullptr != pPlayList) + { + // load it + if (!pPlayList->Load(pathToUrl)) + return false; //hmmm unable to load playlist? + + CPlayList playlist = *pPlayList; + // convert playlist items to songs + for (int i = 0; i < playlist.size(); ++i) + { + CFileItemPtr item = playlist[i]; + item->m_iprogramCount = i; // hack for playlist order + items.Add(item); + } + } + return true; + } + + bool CPlaylistFileDirectory::ContainsFiles(const CURL& url) + { + const std::string pathToUrl = url.Get(); + std::unique_ptr<CPlayList> pPlayList (CPlayListFactory::Create(pathToUrl)); + if (nullptr != pPlayList) + { + // load it + if (!pPlayList->Load(pathToUrl)) + return false; //hmmm unable to load playlist? + + return (pPlayList->size() > 1); + } + return false; + } + + bool CPlaylistFileDirectory::Remove(const CURL& url) + { + return XFILE::CFile::Delete(url); + } +} + diff --git a/xbmc/filesystem/PlaylistFileDirectory.h b/xbmc/filesystem/PlaylistFileDirectory.h new file mode 100644 index 0000000..9f956ce --- /dev/null +++ b/xbmc/filesystem/PlaylistFileDirectory.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IFileDirectory.h" + +namespace XFILE +{ + class CPlaylistFileDirectory : public IFileDirectory + { + public: + CPlaylistFileDirectory(); + ~CPlaylistFileDirectory() override; + bool GetDirectory(const CURL& url, CFileItemList& items) override; + bool ContainsFiles(const CURL& url) override; + bool Remove(const CURL& url) override; + bool AllowAll() const override { return true; } + }; +} diff --git a/xbmc/filesystem/PluginDirectory.cpp b/xbmc/filesystem/PluginDirectory.cpp new file mode 100644 index 0000000..8273886 --- /dev/null +++ b/xbmc/filesystem/PluginDirectory.cpp @@ -0,0 +1,548 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PluginDirectory.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "addons/AddonInstaller.h" +#include "addons/AddonManager.h" +#include "addons/IAddon.h" +#include "addons/PluginSource.h" +#include "addons/addoninfo/AddonType.h" +#include "interfaces/generic/RunningScriptObserver.h" +#include "messaging/ApplicationMessenger.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/URIUtils.h" +#include "utils/log.h" +#include "video/VideoInfoTag.h" + +#include <mutex> + +using namespace XFILE; +using namespace ADDON; +using namespace KODI::MESSAGING; + +namespace +{ +const unsigned int maxPluginResolutions = 5; + +/*! + \brief Get the plugin path from a CFileItem. + + \param item CFileItem where to get the path. + \return The plugin path if found otherwise an empty string. +*/ +std::string GetOriginalPluginPath(const CFileItem& item) +{ + std::string currentPath = item.GetPath(); + if (URIUtils::IsPlugin(currentPath)) + return currentPath; + + currentPath = item.GetDynPath(); + if (URIUtils::IsPlugin(currentPath)) + return currentPath; + + return std::string(); +} +} // unnamed namespace + +CPluginDirectory::CPluginDirectory() + : m_listItems(new CFileItemList), m_fileResult(new CFileItem), m_cancelled(false) + +{ +} + +CPluginDirectory::~CPluginDirectory(void) +{ +} + +bool CPluginDirectory::StartScript(const std::string& strPath, bool resume) +{ + CURL url(strPath); + + ADDON::AddonPtr addon; + // try the plugin type first, and if not found, try an unknown type + if (!CServiceBroker::GetAddonMgr().GetAddon(url.GetHostName(), addon, AddonType::PLUGIN, + OnlyEnabled::CHOICE_YES) && + !CServiceBroker::GetAddonMgr().GetAddon(url.GetHostName(), addon, AddonType::UNKNOWN, + OnlyEnabled::CHOICE_YES) && + !CAddonInstaller::GetInstance().InstallModal(url.GetHostName(), addon, + InstallModalPrompt::CHOICE_YES)) + { + CLog::Log(LOGERROR, "Unable to find plugin {}", url.GetHostName()); + return false; + } + + // clear out our status variables + m_fileResult->Reset(); + m_listItems->Clear(); + m_listItems->SetPath(strPath); + m_listItems->SetLabel(addon->Name()); + m_cancelled = false; + m_success = false; + m_totalItems = 0; + + // run the script + return RunScript(this, addon, strPath, resume); +} + +bool CPluginDirectory::GetResolvedPluginResult(CFileItem& resultItem) +{ + std::string lastResolvedPath; + if (resultItem.HasProperty("ForceResolvePlugin") && + resultItem.GetProperty("ForceResolvePlugin").asBoolean()) + { + // ensures that a plugin have the callback to resolve the paths in any case + // also when the same items in the playlist are played more times + lastResolvedPath = GetOriginalPluginPath(resultItem); + } + else + { + lastResolvedPath = resultItem.GetDynPath(); + } + + if (!lastResolvedPath.empty()) + { + // we try to resolve recursively up to n. (maxPluginResolutions) nested plugin paths + // to avoid deadlocks (plugin:// paths can resolve to plugin:// paths) + for (unsigned int i = 0; URIUtils::IsPlugin(lastResolvedPath) && i < maxPluginResolutions; ++i) + { + bool resume = resultItem.GetStartOffset() == STARTOFFSET_RESUME; + + // we modify the item so that it becomes a real URL + if (!XFILE::CPluginDirectory::GetPluginResult(lastResolvedPath, resultItem, resume) || + resultItem.GetDynPath() == + resultItem.GetPath()) // GetPluginResult resolved to an empty path + { + return false; + } + lastResolvedPath = resultItem.GetDynPath(); + } + // if after the maximum allowed resolution attempts the item is still a plugin just return, it isn't playable + if (URIUtils::IsPlugin(resultItem.GetDynPath())) + return false; + } + + return true; +} + +bool CPluginDirectory::GetPluginResult(const std::string& strPath, CFileItem &resultItem, bool resume) +{ + CURL url(strPath); + CPluginDirectory newDir; + + bool success = newDir.StartScript(strPath, resume); + + if (success) + { // update the play path and metadata, saving the old one as needed + if (!resultItem.HasProperty("original_listitem_url")) + resultItem.SetProperty("original_listitem_url", resultItem.GetPath()); + resultItem.SetDynPath(newDir.m_fileResult->GetPath()); + resultItem.SetMimeType(newDir.m_fileResult->GetMimeType()); + resultItem.SetContentLookup(newDir.m_fileResult->ContentLookup()); + + if (resultItem.HasProperty("OverrideInfotag") && + resultItem.GetProperty("OverrideInfotag").asBoolean()) + resultItem.UpdateInfo(*newDir.m_fileResult); + else + resultItem.MergeInfo(*newDir.m_fileResult); + + if (newDir.m_fileResult->HasVideoInfoTag() && newDir.m_fileResult->GetVideoInfoTag()->GetResumePoint().IsSet()) + resultItem.SetStartOffset( + STARTOFFSET_RESUME); // resume point set in the resume item, so force resume + } + + return success; +} + +bool CPluginDirectory::AddItem(int handle, const CFileItem *item, int totalItems) +{ + std::unique_lock<CCriticalSection> lock(GetScriptsLock()); + CPluginDirectory* dir = GetScriptFromHandle(handle); + if (!dir) + return false; + + CFileItemPtr pItem(new CFileItem(*item)); + dir->m_listItems->Add(pItem); + dir->m_totalItems = totalItems; + + return !dir->m_cancelled; +} + +bool CPluginDirectory::AddItems(int handle, const CFileItemList *items, int totalItems) +{ + std::unique_lock<CCriticalSection> lock(GetScriptsLock()); + CPluginDirectory* dir = GetScriptFromHandle(handle); + if (!dir) + return false; + + CFileItemList pItemList; + pItemList.Copy(*items); + dir->m_listItems->Append(pItemList); + dir->m_totalItems = totalItems; + + return !dir->m_cancelled; +} + +void CPluginDirectory::EndOfDirectory(int handle, bool success, bool replaceListing, bool cacheToDisc) +{ + std::unique_lock<CCriticalSection> lock(GetScriptsLock()); + CPluginDirectory* dir = GetScriptFromHandle(handle); + if (!dir) + return; + + // set cache to disc + dir->m_listItems->SetCacheToDisc(cacheToDisc ? CFileItemList::CACHE_IF_SLOW : CFileItemList::CACHE_NEVER); + + dir->m_success = success; + dir->m_listItems->SetReplaceListing(replaceListing); + + if (!dir->m_listItems->HasSortDetails()) + dir->m_listItems->AddSortMethod(SortByNone, 552, LABEL_MASKS("%L", "%D")); + + // set the event to mark that we're done + dir->SetDone(); +} + +void CPluginDirectory::AddSortMethod(int handle, SORT_METHOD sortMethod, const std::string &labelMask, const std::string &label2Mask) +{ + std::unique_lock<CCriticalSection> lock(GetScriptsLock()); + CPluginDirectory* dir = GetScriptFromHandle(handle); + if (!dir) + return; + + //! @todo Add all sort methods and fix which labels go on the right or left + switch(sortMethod) + { + case SORT_METHOD_LABEL: + case SORT_METHOD_LABEL_IGNORE_THE: + { + dir->m_listItems->AddSortMethod(SortByLabel, 551, LABEL_MASKS(labelMask, label2Mask, labelMask, label2Mask), CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone); + break; + } + case SORT_METHOD_TITLE: + case SORT_METHOD_TITLE_IGNORE_THE: + { + dir->m_listItems->AddSortMethod(SortByTitle, 556, LABEL_MASKS(labelMask, label2Mask, labelMask, label2Mask), CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone); + break; + } + case SORT_METHOD_ARTIST: + case SORT_METHOD_ARTIST_IGNORE_THE: + { + dir->m_listItems->AddSortMethod(SortByArtist, 557, LABEL_MASKS(labelMask, "%A", labelMask, "%A"), CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone); + break; + } + case SORT_METHOD_ALBUM: + case SORT_METHOD_ALBUM_IGNORE_THE: + { + dir->m_listItems->AddSortMethod(SortByAlbum, 558, LABEL_MASKS(labelMask, "%B", labelMask, "%B"), CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone); + break; + } + case SORT_METHOD_DATE: + { + dir->m_listItems->AddSortMethod(SortByDate, 552, LABEL_MASKS(labelMask, "%J", labelMask, "%J")); + break; + } + case SORT_METHOD_BITRATE: + { + dir->m_listItems->AddSortMethod(SortByBitrate, 623, LABEL_MASKS(labelMask, "%X", labelMask, "%X")); + break; + } + case SORT_METHOD_SIZE: + { + dir->m_listItems->AddSortMethod(SortBySize, 553, LABEL_MASKS(labelMask, "%I", labelMask, "%I")); + break; + } + case SORT_METHOD_FILE: + { + dir->m_listItems->AddSortMethod(SortByFile, 561, LABEL_MASKS(labelMask, label2Mask, labelMask, label2Mask)); + break; + } + case SORT_METHOD_TRACKNUM: + { + dir->m_listItems->AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS(labelMask, label2Mask, labelMask, label2Mask)); + break; + } + case SORT_METHOD_DURATION: + case SORT_METHOD_VIDEO_RUNTIME: + { + dir->m_listItems->AddSortMethod(SortByTime, 180, LABEL_MASKS(labelMask, "%D", labelMask, "%D")); + break; + } + case SORT_METHOD_VIDEO_RATING: + case SORT_METHOD_SONG_RATING: + { + dir->m_listItems->AddSortMethod(SortByRating, 563, LABEL_MASKS(labelMask, "%R", labelMask, "%R")); + break; + } + case SORT_METHOD_YEAR: + { + dir->m_listItems->AddSortMethod(SortByYear, 562, LABEL_MASKS(labelMask, "%Y", labelMask, "%Y")); + break; + } + case SORT_METHOD_GENRE: + { + dir->m_listItems->AddSortMethod(SortByGenre, 515, LABEL_MASKS(labelMask, "%G", labelMask, "%G")); + break; + } + case SORT_METHOD_COUNTRY: + { + dir->m_listItems->AddSortMethod(SortByCountry, 574, LABEL_MASKS(labelMask, "%G", labelMask, "%G")); + break; + } + case SORT_METHOD_VIDEO_TITLE: + { + dir->m_listItems->AddSortMethod(SortByTitle, 369, LABEL_MASKS(labelMask, "%M", labelMask, "%M")); + break; + } + case SORT_METHOD_VIDEO_SORT_TITLE: + case SORT_METHOD_VIDEO_SORT_TITLE_IGNORE_THE: + { + dir->m_listItems->AddSortMethod(SortBySortTitle, 556, LABEL_MASKS(labelMask, "%M", labelMask, "%M"), CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone); + break; + } + case SORT_METHOD_VIDEO_ORIGINAL_TITLE: + case SORT_METHOD_VIDEO_ORIGINAL_TITLE_IGNORE_THE: + { + dir->m_listItems->AddSortMethod( + SortByOriginalTitle, 20376, LABEL_MASKS(labelMask, "%M", labelMask, "%M"), + CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) + ? SortAttributeIgnoreArticle + : SortAttributeNone); + break; + } + case SORT_METHOD_MPAA_RATING: + { + dir->m_listItems->AddSortMethod(SortByMPAA, 20074, LABEL_MASKS(labelMask, "%O", labelMask, "%O")); + break; + } + case SORT_METHOD_STUDIO: + case SORT_METHOD_STUDIO_IGNORE_THE: + { + dir->m_listItems->AddSortMethod(SortByStudio, 572, LABEL_MASKS(labelMask, "%U", labelMask, "%U"), CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone); + break; + } + case SORT_METHOD_PROGRAM_COUNT: + { + dir->m_listItems->AddSortMethod(SortByProgramCount, 567, LABEL_MASKS(labelMask, "%C", labelMask, "%C")); + break; + } + case SORT_METHOD_UNSORTED: + { + dir->m_listItems->AddSortMethod(SortByNone, 571, LABEL_MASKS(labelMask, label2Mask, labelMask, label2Mask)); + break; + } + case SORT_METHOD_NONE: + { + dir->m_listItems->AddSortMethod(SortByNone, 552, LABEL_MASKS(labelMask, label2Mask, labelMask, label2Mask)); + break; + } + case SORT_METHOD_DRIVE_TYPE: + { + dir->m_listItems->AddSortMethod(SortByDriveType, 564, LABEL_MASKS()); // Preformatted + break; + } + case SORT_METHOD_PLAYLIST_ORDER: + { + std::string strTrack=CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT); + dir->m_listItems->AddSortMethod(SortByPlaylistOrder, 559, LABEL_MASKS(strTrack, "%D")); + break; + } + case SORT_METHOD_EPISODE: + { + dir->m_listItems->AddSortMethod(SortByEpisodeNumber, 20359, LABEL_MASKS(labelMask, "%R", labelMask, "%R")); + break; + } + case SORT_METHOD_PRODUCTIONCODE: + { + //dir->m_listItems.AddSortMethod(SORT_METHOD_PRODUCTIONCODE,20368,LABEL_MASKS("%E. %T","%P", "%E. %T","%P")); + dir->m_listItems->AddSortMethod(SortByProductionCode, 20368, LABEL_MASKS(labelMask, "%P", labelMask, "%P")); + break; + } + case SORT_METHOD_LISTENERS: + { + dir->m_listItems->AddSortMethod(SortByListeners, 20455, LABEL_MASKS(labelMask, "%W")); + break; + } + case SORT_METHOD_DATEADDED: + { + dir->m_listItems->AddSortMethod(SortByDateAdded, 570, LABEL_MASKS(labelMask, "%a")); + break; + } + case SORT_METHOD_FULLPATH: + { + dir->m_listItems->AddSortMethod(SortByPath, 573, LABEL_MASKS(labelMask, label2Mask, labelMask, label2Mask)); + break; + } + case SORT_METHOD_LABEL_IGNORE_FOLDERS: + { + dir->m_listItems->AddSortMethod(SortByLabel, SortAttributeIgnoreFolders, 551, LABEL_MASKS(labelMask, label2Mask, labelMask, label2Mask)); + break; + } + case SORT_METHOD_LASTPLAYED: + { + dir->m_listItems->AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS(labelMask, "%G")); + break; + } + case SORT_METHOD_PLAYCOUNT: + { + dir->m_listItems->AddSortMethod(SortByPlaycount, 567, LABEL_MASKS(labelMask, "%V", labelMask, "%V")); + break; + } + case SORT_METHOD_CHANNEL: + { + dir->m_listItems->AddSortMethod(SortByChannel, 19029, LABEL_MASKS(labelMask, label2Mask, labelMask, label2Mask)); + break; + } + + default: + break; + } +} + +bool CPluginDirectory::GetDirectory(const CURL& url, CFileItemList& items) +{ + const std::string pathToUrl(url.Get()); + bool success = StartScript(pathToUrl, false); + + // append the items to the list + items.Assign(*m_listItems, true); // true to keep the current items + m_listItems->Clear(); + return success; +} + +bool CPluginDirectory::RunScriptWithParams(const std::string& strPath, bool resume) +{ + CURL url(strPath); + if (url.GetHostName().empty()) // called with no script - should never happen + return false; + + AddonPtr addon; + if (!CServiceBroker::GetAddonMgr().GetAddon(url.GetHostName(), addon, AddonType::PLUGIN, + OnlyEnabled::CHOICE_YES) && + !CAddonInstaller::GetInstance().InstallModal(url.GetHostName(), addon, + InstallModalPrompt::CHOICE_YES)) + { + CLog::Log(LOGERROR, "Unable to find plugin {}", url.GetHostName()); + return false; + } + + return ExecuteScript(addon, strPath, resume) >= 0; +} + +void CPluginDirectory::SetResolvedUrl(int handle, bool success, const CFileItem *resultItem) +{ + std::unique_lock<CCriticalSection> lock(GetScriptsLock()); + CPluginDirectory* dir = GetScriptFromHandle(handle); + if (!dir) + return; + + dir->m_success = success; + *dir->m_fileResult = *resultItem; + + // set the event to mark that we're done + dir->SetDone(); +} + +std::string CPluginDirectory::GetSetting(int handle, const std::string &strID) +{ + std::unique_lock<CCriticalSection> lock(GetScriptsLock()); + CPluginDirectory* dir = GetScriptFromHandle(handle); + if (dir && dir->GetAddon()) + return dir->GetAddon()->GetSetting(strID); + else + return ""; +} + +void CPluginDirectory::SetSetting(int handle, const std::string &strID, const std::string &value) +{ + std::unique_lock<CCriticalSection> lock(GetScriptsLock()); + CPluginDirectory* dir = GetScriptFromHandle(handle); + if (dir && dir->GetAddon()) + dir->GetAddon()->UpdateSetting(strID, value); +} + +void CPluginDirectory::SetContent(int handle, const std::string &strContent) +{ + std::unique_lock<CCriticalSection> lock(GetScriptsLock()); + CPluginDirectory* dir = GetScriptFromHandle(handle); + if (dir) + dir->m_listItems->SetContent(strContent); +} + +void CPluginDirectory::SetProperty(int handle, const std::string &strProperty, const std::string &strValue) +{ + std::unique_lock<CCriticalSection> lock(GetScriptsLock()); + CPluginDirectory* dir = GetScriptFromHandle(handle); + if (!dir) + return; + if (strProperty == "fanart_image") + dir->m_listItems->SetArt("fanart", strValue); + else + dir->m_listItems->SetProperty(strProperty, strValue); +} + +void CPluginDirectory::CancelDirectory() +{ + m_cancelled = true; +} + +float CPluginDirectory::GetProgress() const +{ + if (m_totalItems > 0) + return (m_listItems->Size() * 100.0f) / m_totalItems; + return 0.0f; +} + +bool CPluginDirectory::IsMediaLibraryScanningAllowed(const std::string& content, const std::string& strPath) +{ + if (content.empty()) + return false; + + CURL url(strPath); + if (url.GetHostName().empty()) + return false; + AddonPtr addon; + if (!CServiceBroker::GetAddonMgr().GetAddon(url.GetHostName(), addon, AddonType::PLUGIN, + OnlyEnabled::CHOICE_YES)) + { + CLog::Log(LOGERROR, "Unable to find plugin {}", url.GetHostName()); + return false; + } + CPluginSource* plugin = dynamic_cast<CPluginSource*>(addon.get()); + if (!plugin) + return false; + + auto& paths = plugin->MediaLibraryScanPaths(); + if (paths.empty()) + return false; + auto it = paths.find(content); + if (it == paths.end()) + return false; + const std::string& path = url.GetFileName(); + for (const auto& p : it->second) + if (p.empty() || p == "/" || URIUtils::PathHasParent(path, p)) + return true; + return false; +} + +bool CPluginDirectory::CheckExists(const std::string& content, const std::string& strPath) +{ + if (!IsMediaLibraryScanningAllowed(content, strPath)) + return false; + // call the plugin at specified path with option "kodi_action=check_exists" + // url exists if the plugin returns any fileitem with setResolvedUrl + CURL url(strPath); + url.SetOption("kodi_action", "check_exists"); + CFileItem item; + return CPluginDirectory::GetPluginResult(url.Get(), item, false); +} diff --git a/xbmc/filesystem/PluginDirectory.h b/xbmc/filesystem/PluginDirectory.h new file mode 100644 index 0000000..ebcfd13 --- /dev/null +++ b/xbmc/filesystem/PluginDirectory.h @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" +#include "SortFileItem.h" +#include "interfaces/generic/RunningScriptsHandler.h" +#include "threads/Event.h" + +#include <atomic> +#include <memory> +#include <string> + +class CURL; +class CFileItem; +class CFileItemList; + +namespace XFILE +{ + +class CPluginDirectory : public IDirectory, public CRunningScriptsHandler<CPluginDirectory> +{ +public: + CPluginDirectory(); + ~CPluginDirectory(void) override; + bool GetDirectory(const CURL& url, CFileItemList& items) override; + bool AllowAll() const override { return true; } + bool Exists(const CURL& url) override { return true; } + float GetProgress() const override; + void CancelDirectory() override; + static bool RunScriptWithParams(const std::string& strPath, bool resume); + + /*! \brief Get a reproducible CFileItem by trying to recursively resolve the plugin paths + up to a maximum allowed limit. If no plugin paths exist it will be ignored. + \param resultItem the CFileItem with plugin paths to be resolved. + \return false if the plugin path cannot be resolved, true otherwise. + */ + static bool GetResolvedPluginResult(CFileItem& resultItem); + static bool GetPluginResult(const std::string& strPath, CFileItem &resultItem, bool resume); + + /*! \brief Check whether a plugin supports media library scanning. + \param content content type - movies, tvshows, musicvideos. + \param strPath full plugin url. + \return true if scanning at specified url is allowed, false otherwise. + */ + static bool IsMediaLibraryScanningAllowed(const std::string& content, const std::string& strPath); + + /*! \brief Check whether a plugin url exists by calling the plugin and checking result. + Applies only to plugins that support media library scanning. + \param content content type - movies, tvshows, musicvideos. + \param strPath full plugin url. + \return true if the plugin supports scanning and specified url exists, false otherwise. + */ + static bool CheckExists(const std::string& content, const std::string& strPath); + + // callbacks from python + static bool AddItem(int handle, const CFileItem *item, int totalItems); + static bool AddItems(int handle, const CFileItemList *items, int totalItems); + static void EndOfDirectory(int handle, bool success, bool replaceListing, bool cacheToDisc); + static void AddSortMethod(int handle, SORT_METHOD sortMethod, const std::string &labelMask, const std::string &label2Mask); + static std::string GetSetting(int handle, const std::string &key); + static void SetSetting(int handle, const std::string &key, const std::string &value); + static void SetContent(int handle, const std::string &strContent); + static void SetProperty(int handle, const std::string &strProperty, const std::string &strValue); + static void SetResolvedUrl(int handle, bool success, const CFileItem* resultItem); + static void SetLabel2(int handle, const std::string& ident); + +protected: + // implementations of CRunningScriptsHandler / CScriptRunner + bool IsSuccessful() const override { return m_success; } + bool IsCancelled() const override { return m_cancelled; } + +private: + bool StartScript(const std::string& strPath, bool resume); + + std::unique_ptr<CFileItemList> m_listItems; + std::unique_ptr<CFileItem> m_fileResult; + + std::atomic<bool> m_cancelled; + bool m_success = false; // set by script in EndOfDirectory + int m_totalItems = 0; // set by script in AddDirectoryItem +}; +} diff --git a/xbmc/filesystem/PluginFile.cpp b/xbmc/filesystem/PluginFile.cpp new file mode 100644 index 0000000..63bf827 --- /dev/null +++ b/xbmc/filesystem/PluginFile.cpp @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PluginFile.h" + +#include "URL.h" + +using namespace XFILE; + +CPluginFile::CPluginFile(void) : COverrideFile(false) +{ +} + +CPluginFile::~CPluginFile(void) = default; + +bool CPluginFile::Open(const CURL& url) +{ + return false; +} + +bool CPluginFile::Exists(const CURL& url) +{ + return true; +} + +int CPluginFile::Stat(const CURL& url, struct __stat64* buffer) +{ + return -1; +} + +int CPluginFile::Stat(struct __stat64* buffer) +{ + return -1; +} + +bool CPluginFile::OpenForWrite(const CURL& url, bool bOverWrite) +{ + return false; +} + +bool CPluginFile::Delete(const CURL& url) +{ + return false; +} + +bool CPluginFile::Rename(const CURL& url, const CURL& urlnew) +{ + return false; +} + +std::string CPluginFile::TranslatePath(const CURL& url) +{ + return url.Get(); +} diff --git a/xbmc/filesystem/PluginFile.h b/xbmc/filesystem/PluginFile.h new file mode 100644 index 0000000..2044fd9 --- /dev/null +++ b/xbmc/filesystem/PluginFile.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "filesystem/OverrideFile.h" + +namespace XFILE +{ +class CPluginFile : public COverrideFile +{ +public: + CPluginFile(void); + ~CPluginFile(void) override; + bool Open(const CURL& url) override; + bool Exists(const CURL& url) override; + int Stat(const CURL& url, struct __stat64* buffer) override; + int Stat(struct __stat64* buffer) override; + bool OpenForWrite(const CURL& url, bool bOverWrite = false) override; + bool Delete(const CURL& url) override; + bool Rename(const CURL& url, const CURL& urlnew) override; + +protected: + std::string TranslatePath(const CURL& url) override; +}; +} // namespace XFILE diff --git a/xbmc/filesystem/RSSDirectory.cpp b/xbmc/filesystem/RSSDirectory.cpp new file mode 100644 index 0000000..a176381 --- /dev/null +++ b/xbmc/filesystem/RSSDirectory.cpp @@ -0,0 +1,623 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "RSSDirectory.h" + +#include "CurlFile.h" +#include "FileItem.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/FileExtensionProvider.h" +#include "utils/HTMLUtil.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/XBMCTinyXML.h" +#include "utils/XMLUtils.h" +#include "utils/log.h" +#include "video/VideoInfoTag.h" + +#include <climits> +#include <mutex> +#include <utility> + +using namespace XFILE; +using namespace MUSIC_INFO; + +namespace { + + struct SResource + { + std::string tag; + std::string path; + std::string mime; + std::string lang; + int width = 0; + int height = 0; + int bitrate = 0; + int duration = 0; + int64_t size = 0; + }; + + typedef std::vector<SResource> SResources; + +} + +std::map<std::string,CDateTime> CRSSDirectory::m_cache; +CCriticalSection CRSSDirectory::m_section; + +CRSSDirectory::CRSSDirectory() = default; + +CRSSDirectory::~CRSSDirectory() = default; + +bool CRSSDirectory::ContainsFiles(const CURL& url) +{ + CFileItemList items; + if(!GetDirectory(url, items)) + return false; + + return items.Size() > 0; +} + +static bool IsPathToMedia(const std::string& strPath ) +{ + return URIUtils::HasExtension(strPath, + CServiceBroker::GetFileExtensionProvider().GetVideoExtensions() + '|' + + CServiceBroker::GetFileExtensionProvider().GetMusicExtensions() + '|' + + CServiceBroker::GetFileExtensionProvider().GetPictureExtensions()); +} + +static bool IsPathToThumbnail(const std::string& strPath ) +{ + // Currently just check if this is an image, maybe we will add some + // other checks later + return URIUtils::HasExtension(strPath, + CServiceBroker::GetFileExtensionProvider().GetPictureExtensions()); +} + +static time_t ParseDate(const std::string & strDate) +{ + struct tm pubDate = {}; + //! @todo Handle time zone + strptime(strDate.c_str(), "%a, %d %b %Y %H:%M:%S", &pubDate); + // Check the difference between the time of last check and time of the item + return mktime(&pubDate); +} +static void ParseItem(CFileItem* item, SResources& resources, TiXmlElement* root, const std::string& path); + +static std::string GetValue(TiXmlElement *element) +{ + if (element && !element->NoChildren()) + return element->FirstChild()->ValueStr(); + return ""; +} + +static void ParseItemMRSS(CFileItem* item, SResources& resources, TiXmlElement* item_child, const std::string& name, const std::string& xmlns, const std::string& path) +{ + CVideoInfoTag* vtag = item->GetVideoInfoTag(); + std::string text = GetValue(item_child); + + if(name == "content") + { + SResource res; + res.tag = "media:content"; + res.mime = XMLUtils::GetAttribute(item_child, "type"); + res.path = XMLUtils::GetAttribute(item_child, "url"); + item_child->Attribute("width", &res.width); + item_child->Attribute("height", &res.height); + item_child->Attribute("bitrate", &res.bitrate); + item_child->Attribute("duration", &res.duration); + if(item_child->Attribute("fileSize")) + res.size = std::atoll(item_child->Attribute("fileSize")); + + resources.push_back(res); + ParseItem(item, resources, item_child, path); + } + else if(name == "group") + { + ParseItem(item, resources, item_child, path); + } + else if(name == "thumbnail") + { + if(!item_child->NoChildren() && IsPathToThumbnail(item_child->FirstChild()->ValueStr())) + item->SetArt("thumb", item_child->FirstChild()->ValueStr()); + else + { + const char * url = item_child->Attribute("url"); + if(url && IsPathToThumbnail(url)) + item->SetArt("thumb", url); + } + } + else if (name == "title") + { + if(text.empty()) + return; + + if(text.length() > item->m_strTitle.length()) + item->m_strTitle = text; + } + else if(name == "description") + { + if(text.empty()) + return; + + std::string description = text; + if(XMLUtils::GetAttribute(item_child, "type") == "html") + HTML::CHTMLUtil::RemoveTags(description); + item->SetProperty("description", description); + } + else if(name == "category") + { + if(text.empty()) + return; + + std::string scheme = XMLUtils::GetAttribute(item_child, "scheme"); + + /* okey this is silly, boxee what did you think?? */ + if (scheme == "urn:boxee:genre") + vtag->m_genre.push_back(text); + else if(scheme == "urn:boxee:title-type") + { + if (text == "tv") + item->SetProperty("boxee:istvshow", true); + else if(text == "movie") + item->SetProperty("boxee:ismovie", true); + } + else if(scheme == "urn:boxee:episode") + vtag->m_iEpisode = atoi(text.c_str()); + else if(scheme == "urn:boxee:season") + vtag->m_iSeason = atoi(text.c_str()); + else if(scheme == "urn:boxee:show-title") + vtag->m_strShowTitle = text.c_str(); + else if(scheme == "urn:boxee:view-count") + vtag->SetPlayCount(atoi(text.c_str())); + else if(scheme == "urn:boxee:source") + item->SetProperty("boxee:provider_source", text); + else + vtag->m_genre = StringUtils::Split(text, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator); + } + else if(name == "rating") + { + std::string scheme = XMLUtils::GetAttribute(item_child, "scheme"); + if(scheme == "urn:user") + vtag->SetRating((float)atof(text.c_str())); + else + vtag->m_strMPAARating = text; + } + else if(name == "credit") + { + std::string role = XMLUtils::GetAttribute(item_child, "role"); + if (role == "director") + vtag->m_director.push_back(text); + else if(role == "author" + || role == "writer") + vtag->m_writingCredits.push_back(text); + else if(role == "actor") + { + SActorInfo actor; + actor.strName = text; + vtag->m_cast.push_back(actor); + } + } + else if(name == "copyright") + vtag->m_studio = StringUtils::Split(text, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator); + else if(name == "keywords") + item->SetProperty("keywords", text); + +} + +static void ParseItemItunes(CFileItem* item, SResources& resources, TiXmlElement* item_child, const std::string& name, const std::string& xmlns, const std::string& path) +{ + CVideoInfoTag* vtag = item->GetVideoInfoTag(); + std::string text = GetValue(item_child); + + if(name == "image") + { + const char * url = item_child->Attribute("href"); + if(url) + item->SetArt("thumb", url); + else + item->SetArt("thumb", text); + } + else if(name == "summary") + vtag->m_strPlot = text; + else if(name == "subtitle") + vtag->m_strPlotOutline = text; + else if(name == "author") + vtag->m_writingCredits.push_back(text); + else if(name == "duration") + vtag->SetDuration(StringUtils::TimeStringToSeconds(text)); + else if(name == "keywords") + item->SetProperty("keywords", text); +} + +static void ParseItemRSS(CFileItem* item, SResources& resources, TiXmlElement* item_child, const std::string& name, const std::string& xmlns, const std::string& path) +{ + std::string text = GetValue(item_child); + if (name == "title") + { + if(text.length() > item->m_strTitle.length()) + item->m_strTitle = text; + } + else if (name == "pubDate") + { + CDateTime pubDate(ParseDate(text)); + item->m_dateTime = pubDate; + } + else if (name == "link") + { + SResource res; + res.tag = "rss:link"; + res.path = text; + resources.push_back(res); + } + else if(name == "enclosure") + { + const char * len = item_child->Attribute("length"); + + SResource res; + res.tag = "rss:enclosure"; + res.path = XMLUtils::GetAttribute(item_child, "url"); + res.mime = XMLUtils::GetAttribute(item_child, "type"); + if(len) + res.size = std::atoll(len); + + resources.push_back(res); + } + else if(name == "description") + { + std::string description = text; + HTML::CHTMLUtil::RemoveTags(description); + item->SetProperty("description", description); + } + else if(name == "guid") + { + if(IsPathToMedia(text)) + { + SResource res; + res.tag = "rss:guid"; + res.path = text; + resources.push_back(res); + } + } +} + +static void ParseItemVoddler(CFileItem* item, SResources& resources, TiXmlElement* element, const std::string& name, const std::string& xmlns, const std::string& path) +{ + CVideoInfoTag* vtag = item->GetVideoInfoTag(); + std::string text = GetValue(element); + + if(name == "trailer") + { + vtag->m_strTrailer = text; + + SResource res; + res.tag = "voddler:trailer"; + res.mime = XMLUtils::GetAttribute(element, "type"); + res.path = text; + resources.push_back(res); + } + else if(name == "year") + vtag->SetYear(atoi(text.c_str())); + else if(name == "rating") + vtag->SetRating((float)atof(text.c_str())); + else if(name == "tagline") + vtag->m_strTagLine = text; + else if(name == "posterwall") + { + const char* url = element->Attribute("url"); + if(url) + item->SetArt("fanart", url); + else if(IsPathToThumbnail(text)) + item->SetArt("fanart", text); + } +} + +static void ParseItemBoxee(CFileItem* item, SResources& resources, TiXmlElement* element, const std::string& name, const std::string& xmlns, const std::string& path) +{ + CVideoInfoTag* vtag = item->GetVideoInfoTag(); + std::string text = GetValue(element); + + if (name == "image") + item->SetArt("thumb", text); + else if(name == "user_agent") + item->SetProperty("boxee:user_agent", text); + else if(name == "content_type") + item->SetMimeType(text); + else if(name == "runtime") + vtag->SetDuration(StringUtils::TimeStringToSeconds(text)); + else if(name == "episode") + vtag->m_iEpisode = atoi(text.c_str()); + else if(name == "season") + vtag->m_iSeason = atoi(text.c_str()); + else if(name == "view-count") + vtag->SetPlayCount(atoi(text.c_str())); + else if(name == "tv-show-title") + vtag->m_strShowTitle = text; + else if(name == "release-date") + item->SetProperty("boxee:releasedate", text); +} + +static void ParseItemZink(CFileItem* item, SResources& resources, TiXmlElement* element, const std::string& name, const std::string& xmlns, const std::string& path) +{ + CVideoInfoTag* vtag = item->GetVideoInfoTag(); + std::string text = GetValue(element); + if (name == "episode") + vtag->m_iEpisode = atoi(text.c_str()); + else if(name == "season") + vtag->m_iSeason = atoi(text.c_str()); + else if(name == "views") + vtag->SetPlayCount(atoi(text.c_str())); + else if(name == "airdate") + vtag->m_firstAired.SetFromDateString(text); + else if(name == "userrating") + vtag->SetRating((float)atof(text.c_str())); + else if(name == "duration") + vtag->SetDuration(atoi(text.c_str())); + else if(name == "durationstr") + vtag->SetDuration(StringUtils::TimeStringToSeconds(text)); +} + +static void ParseItemSVT(CFileItem* item, SResources& resources, TiXmlElement* element, const std::string& name, const std::string& xmlns, const std::string& path) +{ + std::string text = GetValue(element); + if (name == "xmllink") + { + SResource res; + res.tag = "svtplay:xmllink"; + res.path = text; + res.mime = "application/rss+xml"; + resources.push_back(res); + } + else if (name == "broadcasts") + { + CURL url(path); + if(StringUtils::StartsWith(url.GetFileName(), "v1/")) + { + SResource res; + res.tag = "svtplay:broadcasts"; + res.path = url.GetWithoutFilename() + "v1/video/list/" + text; + res.mime = "application/rss+xml"; + resources.push_back(res); + } + } +} + +static void ParseItem(CFileItem* item, SResources& resources, TiXmlElement* root, const std::string& path) +{ + for (TiXmlElement* child = root->FirstChildElement(); child; child = child->NextSiblingElement()) + { + std::string name = child->Value(); + std::string xmlns; + size_t pos = name.find(':'); + if(pos != std::string::npos) + { + xmlns = name.substr(0, pos); + name.erase(0, pos+1); + } + + if (xmlns == "media") + ParseItemMRSS (item, resources, child, name, xmlns, path); + else if (xmlns == "itunes") + ParseItemItunes (item, resources, child, name, xmlns, path); + else if (xmlns == "voddler") + ParseItemVoddler(item, resources, child, name, xmlns, path); + else if (xmlns == "boxee") + ParseItemBoxee (item, resources, child, name, xmlns, path); + else if (xmlns == "zn") + ParseItemZink (item, resources, child, name, xmlns, path); + else if (xmlns == "svtplay") + ParseItemSVT (item, resources, child, name, xmlns, path); + else + ParseItemRSS (item, resources, child, name, xmlns, path); + } +} + +static bool FindMime(const SResources& resources, const std::string& mime) +{ + for (const auto& it : resources) + { + if (StringUtils::StartsWithNoCase(it.mime, mime)) + return true; + } + return false; +} + +static void ParseItem(CFileItem* item, TiXmlElement* root, const std::string& path) +{ + SResources resources; + ParseItem(item, resources, root, path); + + const char* prio[] = { "media:content", "voddler:trailer", "rss:enclosure", "svtplay:broadcasts", "svtplay:xmllink", "rss:link", "rss:guid", NULL }; + + std::string mime; + if (FindMime(resources, "video/")) + mime = "video/"; + else if(FindMime(resources, "audio/")) + mime = "audio/"; + else if(FindMime(resources, "application/rss")) + mime = "application/rss"; + else if(FindMime(resources, "image/")) + mime = "image/"; + + int maxrate = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_NETWORK_BANDWIDTH); + if(maxrate == 0) + maxrate = INT_MAX; + + SResources::iterator best = resources.end(); + for(const char** type = prio; *type && best == resources.end(); type++) + { + for (SResources::iterator it = resources.begin(); it != resources.end(); ++it) + { + if(!StringUtils::StartsWith(it->mime, mime)) + continue; + + if(it->tag == *type) + { + if(best == resources.end()) + { + best = it; + continue; + } + + if(it->bitrate == best->bitrate) + { + if(it->width*it->height > best->width*best->height) + best = it; + continue; + } + + if(it->bitrate > maxrate) + { + if(it->bitrate < best->bitrate) + best = it; + continue; + } + + if(it->bitrate > best->bitrate) + { + best = it; + continue; + } + } + } + } + + if(best != resources.end()) + { + item->SetMimeType(best->mime); + item->SetPath(best->path); + item->m_dwSize = best->size; + + if(best->duration) + item->SetProperty("duration", StringUtils::SecondsToTimeString(best->duration)); + + /* handling of mimetypes fo directories are sub optimal at best */ + if(best->mime == "application/rss+xml" && StringUtils::StartsWithNoCase(item->GetPath(), "http://")) + item->SetPath("rss://" + item->GetPath().substr(7)); + + if(best->mime == "application/rss+xml" && StringUtils::StartsWithNoCase(item->GetPath(), "https://")) + item->SetPath("rsss://" + item->GetPath().substr(8)); + + if(StringUtils::StartsWithNoCase(item->GetPath(), "rss://") + || StringUtils::StartsWithNoCase(item->GetPath(), "rsss://")) + item->m_bIsFolder = true; + else + item->m_bIsFolder = false; + } + + if(!item->m_strTitle.empty()) + item->SetLabel(item->m_strTitle); + + if(item->HasVideoInfoTag()) + { + CVideoInfoTag* vtag = item->GetVideoInfoTag(); + + if(item->HasProperty("duration") && !vtag->GetDuration()) + vtag->SetDuration(StringUtils::TimeStringToSeconds(item->GetProperty("duration").asString())); + + if(item->HasProperty("description") && vtag->m_strPlot.empty()) + vtag->m_strPlot = item->GetProperty("description").asString(); + + if(vtag->m_strPlotOutline.empty() && !vtag->m_strPlot.empty()) + { + size_t pos = vtag->m_strPlot.find('\n'); + if(pos != std::string::npos) + vtag->m_strPlotOutline = vtag->m_strPlot.substr(0, pos); + else + vtag->m_strPlotOutline = vtag->m_strPlot; + } + + if(!vtag->GetDuration()) + item->SetLabel2(StringUtils::SecondsToTimeString(vtag->GetDuration())); + } +} + +bool CRSSDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + const std::string pathToUrl(url.Get()); + std::string strPath(pathToUrl); + URIUtils::RemoveSlashAtEnd(strPath); + std::map<std::string,CDateTime>::iterator it; + items.SetPath(strPath); + std::unique_lock<CCriticalSection> lock(m_section); + if ((it=m_cache.find(strPath)) != m_cache.end()) + { + if (it->second > CDateTime::GetCurrentDateTime() && + items.Load()) + return true; + m_cache.erase(it); + } + lock.unlock(); + + CXBMCTinyXML xmlDoc; + if (!xmlDoc.LoadFile(strPath)) + { + CLog::Log(LOGERROR, "failed to load xml from <{}>. error: <{}>", strPath, xmlDoc.ErrorId()); + return false; + } + if (xmlDoc.Error()) + { + CLog::Log(LOGERROR, "error parsing xml doc from <{}>. error: <{}>", strPath, xmlDoc.ErrorId()); + return false; + } + + TiXmlElement* rssXmlNode = xmlDoc.RootElement(); + + if (!rssXmlNode) + return false; + + TiXmlHandle docHandle( &xmlDoc ); + TiXmlElement* channelXmlNode = docHandle.FirstChild( "rss" ).FirstChild( "channel" ).Element(); + if (channelXmlNode) + ParseItem(&items, channelXmlNode, pathToUrl); + else + return false; + + TiXmlElement* child = NULL; + for (child = channelXmlNode->FirstChildElement("item"); child; child = child->NextSiblingElement()) + { + // Create new item, + CFileItemPtr item(new CFileItem()); + ParseItem(item.get(), child, pathToUrl); + + item->SetProperty("isrss", "1"); + // Use channel image if item doesn't have one + if (!item->HasArt("thumb") && items.HasArt("thumb")) + item->SetArt("thumb", items.GetArt("thumb")); + + if (!item->GetPath().empty()) + items.Add(item); + } + + items.AddSortMethod(SortByNone , 231, LABEL_MASKS("%L", "%D", "%L", "")); // FileName, Duration | Foldername, empty + items.AddSortMethod(SortByLabel , 551, LABEL_MASKS("%L", "%D", "%L", "")); // FileName, Duration | Foldername, empty + items.AddSortMethod(SortBySize , 553, LABEL_MASKS("%L", "%I", "%L", "%I")); // FileName, Size | Foldername, Size + items.AddSortMethod(SortByDate , 552, LABEL_MASKS("%L", "%J", "%L", "%J")); // FileName, Date | Foldername, Date + + CDateTime time = CDateTime::GetCurrentDateTime(); + int mins = 60; + TiXmlElement* ttl = docHandle.FirstChild("rss").FirstChild("ttl").Element(); + if (ttl) + mins = strtol(ttl->FirstChild()->Value(),NULL,10); + time += CDateTimeSpan(0,0,mins,0); + items.SetPath(strPath); + items.Save(); + std::unique_lock<CCriticalSection> lock2(m_section); + m_cache.insert(make_pair(strPath,time)); + + return true; +} + +bool CRSSDirectory::Exists(const CURL& url) +{ + CCurlFile rss; + return rss.Exists(url); +} diff --git a/xbmc/filesystem/RSSDirectory.h b/xbmc/filesystem/RSSDirectory.h new file mode 100644 index 0000000..b2e3d5f --- /dev/null +++ b/xbmc/filesystem/RSSDirectory.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IFileDirectory.h" +#include "XBDateTime.h" +#include "threads/CriticalSection.h" + +#include <map> +#include <string> + +class CFileItemList; + +namespace XFILE +{ + class CRSSDirectory : public IFileDirectory + { + public: + CRSSDirectory(); + ~CRSSDirectory() override; + bool GetDirectory(const CURL& url, CFileItemList &items) override; + bool Exists(const CURL& url) override; + bool AllowAll() const override { return true; } + bool ContainsFiles(const CURL& url) override; + DIR_CACHE_TYPE GetCacheType(const CURL& url) const override { return DIR_CACHE_ONCE; } + + protected: + // key is path, value is cache invalidation date + static std::map<std::string,CDateTime> m_cache; + static CCriticalSection m_section; + }; +} + diff --git a/xbmc/filesystem/ResourceDirectory.cpp b/xbmc/filesystem/ResourceDirectory.cpp new file mode 100644 index 0000000..c3623d0 --- /dev/null +++ b/xbmc/filesystem/ResourceDirectory.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "ResourceDirectory.h" + +#include "FileItem.h" +#include "URL.h" +#include "filesystem/Directory.h" +#include "filesystem/ResourceFile.h" +#include "utils/URIUtils.h" + +using namespace XFILE; + +CResourceDirectory::CResourceDirectory() = default; + +CResourceDirectory::~CResourceDirectory() = default; + +bool CResourceDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + const std::string pathToUrl(url.Get()); + std::string translatedPath; + if (!CResourceFile::TranslatePath(url, translatedPath)) + return false; + + if (CDirectory::GetDirectory(translatedPath, items, m_strFileMask, m_flags | DIR_FLAG_GET_HIDDEN)) + { // replace our paths as necessary + items.SetURL(url); + for (int i = 0; i < items.Size(); i++) + { + CFileItemPtr item = items[i]; + if (URIUtils::PathHasParent(item->GetPath(), translatedPath)) + item->SetPath(URIUtils::AddFileToFolder(pathToUrl, item->GetPath().substr(translatedPath.size()))); + } + + return true; + } + + return false; +} + +std::string CResourceDirectory::TranslatePath(const CURL &url) +{ + std::string translatedPath; + if (!CResourceFile::TranslatePath(url, translatedPath)) + return ""; + + return translatedPath; +} diff --git a/xbmc/filesystem/ResourceDirectory.h b/xbmc/filesystem/ResourceDirectory.h new file mode 100644 index 0000000..f487852 --- /dev/null +++ b/xbmc/filesystem/ResourceDirectory.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "filesystem/OverrideDirectory.h" + +namespace XFILE +{ + class CResourceDirectory : public COverrideDirectory + { + public: + CResourceDirectory(); + ~CResourceDirectory() override; + + bool GetDirectory(const CURL& url, CFileItemList &items) override; + + protected: + std::string TranslatePath(const CURL &url) override; + }; +} diff --git a/xbmc/filesystem/ResourceFile.cpp b/xbmc/filesystem/ResourceFile.cpp new file mode 100644 index 0000000..81a9982 --- /dev/null +++ b/xbmc/filesystem/ResourceFile.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "ResourceFile.h" + +#include "ServiceBroker.h" +#include "URL.h" +#include "Util.h" +#include "addons/AddonManager.h" +#include "addons/Resource.h" + +using namespace ADDON; +using namespace XFILE; + +CResourceFile::CResourceFile() + : COverrideFile(false) +{ } + +CResourceFile::~CResourceFile() = default; + +bool CResourceFile::TranslatePath(const std::string &path, std::string &translatedPath) +{ + return TranslatePath(CURL(path), translatedPath); +} + +bool CResourceFile::TranslatePath(const CURL &url, std::string &translatedPath) +{ + translatedPath = url.Get(); + + // only handle resource:// paths + if (!url.IsProtocol("resource")) + return false; + + // the share name represents an identifier that can be mapped to an addon ID + const std::string& addonId = url.GetShareName(); + std::string filePath; + if (url.GetFileName().length() > addonId.length()) + filePath = url.GetFileName().substr(addonId.size() + 1); + + if (addonId.empty()) + return false; + + AddonPtr addon; + if (!CServiceBroker::GetAddonMgr().GetAddon(addonId, addon, OnlyEnabled::CHOICE_YES) || + addon == NULL) + return false; + + std::shared_ptr<CResource> resource = std::dynamic_pointer_cast<ADDON::CResource>(addon); + if (resource == NULL) + return false; + + if (!resource->IsAllowed(filePath)) + return false; + + translatedPath = CUtil::ValidatePath(resource->GetFullPath(filePath)); + return true; +} + +std::string CResourceFile::TranslatePath(const CURL &url) +{ + std::string translatedPath; + if (!TranslatePath(url, translatedPath)) + return ""; + + return translatedPath; +} diff --git a/xbmc/filesystem/ResourceFile.h b/xbmc/filesystem/ResourceFile.h new file mode 100644 index 0000000..da721b5 --- /dev/null +++ b/xbmc/filesystem/ResourceFile.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "filesystem/OverrideFile.h" + +namespace XFILE +{ +class CResourceFile : public COverrideFile +{ +public: + CResourceFile(); + ~CResourceFile() override; + + static bool TranslatePath(const std::string &path, std::string &translatedPath); + static bool TranslatePath(const CURL &url, std::string &translatedPath); + +protected: + std::string TranslatePath(const CURL &url) override; +}; +} diff --git a/xbmc/filesystem/ShoutcastFile.cpp b/xbmc/filesystem/ShoutcastFile.cpp new file mode 100644 index 0000000..7488ee7 --- /dev/null +++ b/xbmc/filesystem/ShoutcastFile.cpp @@ -0,0 +1,362 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + + +// FileShoutcast.cpp: implementation of the CShoutcastFile class. +// +////////////////////////////////////////////////////////////////////// + +#include "ShoutcastFile.h" + +#include "FileCache.h" +#include "FileItem.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "filesystem/CurlFile.h" +#include "messaging/ApplicationMessenger.h" +#include "music/tags/MusicInfoTag.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "threads/SingleLock.h" +#include "utils/CharsetConverter.h" +#include "utils/HTMLUtil.h" +#include "utils/JSONVariantParser.h" +#include "utils/RegExp.h" +#include "utils/StringUtils.h" +#include "utils/UrlOptions.h" + +#include <climits> +#include <mutex> + +using namespace XFILE; +using namespace MUSIC_INFO; +using namespace std::chrono_literals; + +CShoutcastFile::CShoutcastFile() : + IFile(), CThread("ShoutcastFile") +{ + m_discarded = 0; + m_currint = 0; + m_buffer = NULL; + m_cacheReader = NULL; + m_metaint = 0; +} + +CShoutcastFile::~CShoutcastFile() +{ + Close(); +} + +int64_t CShoutcastFile::GetPosition() +{ + return m_file.GetPosition()-m_discarded; +} + +int64_t CShoutcastFile::GetLength() +{ + return 0; +} + +std::string CShoutcastFile::DecodeToUTF8(const std::string& str) +{ + std::string ret = str; + + if (m_fileCharset.empty()) + g_charsetConverter.unknownToUTF8(ret); + else + g_charsetConverter.ToUtf8(m_fileCharset, str, ret); + + std::wstring wBuffer, wConverted; + g_charsetConverter.utf8ToW(ret, wBuffer, false); + HTML::CHTMLUtil::ConvertHTMLToW(wBuffer, wConverted); + g_charsetConverter.wToUTF8(wConverted, ret); + + return ret; +} + +bool CShoutcastFile::Open(const CURL& url) +{ + CURL url2(url); + url2.SetProtocolOptions(url2.GetProtocolOptions()+"&noshout=true&Icy-MetaData=1"); + if (url.GetProtocol() == "shouts") + url2.SetProtocol("https"); + else if (url.GetProtocol() == "shout") + url2.SetProtocol("http"); + + std::string icyTitle; + std::string icyGenre; + + bool result = m_file.Open(url2); + if (result) + { + m_fileCharset = m_file.GetProperty(XFILE::FILE_PROPERTY_CONTENT_CHARSET); + + icyTitle = m_file.GetHttpHeader().GetValue("icy-name"); + if (icyTitle.empty()) + icyTitle = m_file.GetHttpHeader().GetValue("ice-name"); // icecast + if (icyTitle == "This is my server name") // Handle badly set up servers + icyTitle.clear(); + + icyTitle = DecodeToUTF8(icyTitle); + + icyGenre = m_file.GetHttpHeader().GetValue("icy-genre"); + if (icyGenre.empty()) + icyGenre = m_file.GetHttpHeader().GetValue("ice-genre"); // icecast + + icyGenre = DecodeToUTF8(icyGenre); + } + m_metaint = atoi(m_file.GetHttpHeader().GetValue("icy-metaint").c_str()); + if (!m_metaint) + m_metaint = -1; + + m_buffer = new char[16*255]; + + if (result) + { + std::unique_lock<CCriticalSection> lock(m_tagSection); + + m_masterTag.reset(new CMusicInfoTag()); + m_masterTag->SetStationName(icyTitle); + m_masterTag->SetGenre(icyGenre); + m_masterTag->SetLoaded(true); + + m_tags.push({1, m_masterTag}); + m_tagChange.Set(); + } + + return result; +} + +ssize_t CShoutcastFile::Read(void* lpBuf, size_t uiBufSize) +{ + if (uiBufSize > SSIZE_MAX) + uiBufSize = SSIZE_MAX; + + if (m_currint >= m_metaint && m_metaint > 0) + { + unsigned char header; + m_file.Read(&header,1); + ReadTruncated(m_buffer, header*16); + ExtractTagInfo(m_buffer); + m_discarded += header*16+1; + m_currint = 0; + } + + ssize_t toRead; + if (m_metaint > 0) + toRead = std::min<size_t>(uiBufSize,m_metaint-m_currint); + else + toRead = std::min<size_t>(uiBufSize,16*255); + toRead = m_file.Read(lpBuf,toRead); + if (toRead > 0) + m_currint += toRead; + return toRead; +} + +int64_t CShoutcastFile::Seek(int64_t iFilePosition, int iWhence) +{ + return -1; +} + +void CShoutcastFile::Close() +{ + StopThread(); + delete[] m_buffer; + m_buffer = NULL; + m_file.Close(); + m_title.clear(); + + { + std::unique_lock<CCriticalSection> lock(m_tagSection); + while (!m_tags.empty()) + m_tags.pop(); + m_masterTag.reset(); + m_tagChange.Set(); + } +} + +bool CShoutcastFile::ExtractTagInfo(const char* buf) +{ + std::string strBuffer = DecodeToUTF8(buf); + + bool result = false; + + CRegExp reTitle(true); + reTitle.RegComp("StreamTitle=\'(.*?)\';"); + + if (reTitle.RegFind(strBuffer.c_str()) != -1) + { + const std::string newtitle = reTitle.GetMatch(1); + + result = (m_title != newtitle); + if (result) // track has changed + { + m_title = newtitle; + + std::string title; + std::string artistInfo; + std::string coverURL; + + CRegExp reURL(true); + reURL.RegComp("StreamUrl=\'(.*?)\';"); + bool haveStreamUrlData = + (reURL.RegFind(strBuffer.c_str()) != -1) && !reURL.GetMatch(1).empty(); + + if (haveStreamUrlData) // track has changed and extra metadata might be available + { + const std::string streamUrlData = reURL.GetMatch(1); + if (StringUtils::StartsWithNoCase(streamUrlData, "http://") || + StringUtils::StartsWithNoCase(streamUrlData, "https://")) + { + // Bauer Media Radio listenapi null event to erase current data + if (!StringUtils::EndsWithNoCase(streamUrlData, "eventdata/-1")) + { + const CURL dataURL(streamUrlData); + XFILE::CCurlFile http; + std::string extData; + + if (http.Get(dataURL.Get(), extData)) + { + const std::string contentType = http.GetHttpHeader().GetMimeType(); + if (StringUtils::EqualsNoCase(contentType, "application/json")) + { + CVariant json; + if (CJSONVariantParser::Parse(extData, json)) + { + // Check for Bauer Media Radio listenapi meta data. + // Example: StreamUrl='https://listenapi.bauerradio.com/api9/eventdata/58431417' + artistInfo = json["eventSongArtist"].asString(); + title = json["eventSongTitle"].asString(); + coverURL = json["eventImageUrl"].asString(); + } + } + } + } + } + else if (StringUtils::StartsWithNoCase(streamUrlData, "&")) + { + // Check for SAM Cast meta data. + // Example: StreamUrl='&artist=RECLAM&title=BOLORDURAN%2017&album=&duration=17894&songtype=S&overlay=no&buycd=&website=&picture=' + + CUrlOptions urlOptions(streamUrlData); + const CUrlOptions::UrlOptions& options = urlOptions.GetOptions(); + + auto it = options.find("artist"); + if (it != options.end()) + artistInfo = (*it).second.asString(); + + it = options.find("title"); + if (it != options.end()) + title = (*it).second.asString(); + + it = options.find("picture"); + if (it != options.end()) + { + coverURL = (*it).second.asString(); + if (!coverURL.empty()) + { + // Check value being a URL (not just a file name) + const CURL url(coverURL); + if (url.GetProtocol().empty()) + coverURL.clear(); + } + } + } + } + + if (artistInfo.empty() || title.empty()) + { + // Most stations supply StreamTitle in format "artist - songtitle" + const std::vector<std::string> tokens = StringUtils::Split(newtitle, " - "); + if (tokens.size() == 2) + { + if (artistInfo.empty()) + artistInfo = tokens[0]; + + if (title.empty()) + title = tokens[1]; + } + else + { + if (title.empty()) + { + // Do not display Bauer Media Radio SteamTitle values to mark start/stop of ad breaks. + if (!StringUtils::StartsWithNoCase(newtitle, "START ADBREAK ") && + !StringUtils::StartsWithNoCase(newtitle, "STOP ADBREAK ")) + title = newtitle; + } + } + } + + if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bShoutcastArt) + coverURL.clear(); + + std::unique_lock<CCriticalSection> lock(m_tagSection); + + const std::shared_ptr<CMusicInfoTag> tag = std::make_shared<CMusicInfoTag>(*m_masterTag); + tag->SetArtist(artistInfo); + tag->SetTitle(title); + tag->SetStationArt(coverURL); + + m_tags.push({m_file.GetPosition(), tag}); + m_tagChange.Set(); + } + } + + return result; +} + +void CShoutcastFile::ReadTruncated(char* buf2, int size) +{ + char* buf = buf2; + while (size > 0) + { + int read = m_file.Read(buf,size); + size -= read; + buf += read; + } +} + +int CShoutcastFile::IoControl(EIoControl control, void* payload) +{ + if (control == IOCTRL_SET_CACHE && m_cacheReader == nullptr) + { + std::unique_lock<CCriticalSection> lock(m_tagSection); + m_cacheReader = static_cast<CFileCache*>(payload); + Create(); + } + + return IFile::IoControl(control, payload); +} + +void CShoutcastFile::Process() +{ + while (!m_bStop) + { + if (m_tagChange.Wait(500ms)) + { + std::unique_lock<CCriticalSection> lock(m_tagSection); + while (!m_bStop && !m_tags.empty()) + { + const TagInfo& front = m_tags.front(); + if (m_cacheReader->GetPosition() < front.first) // tagpos + { + CSingleExit ex(m_tagSection); + CThread::Sleep(20ms); + } + else + { + CFileItem* item = new CFileItem(*front.second); // will be deleted by msg receiver + m_tags.pop(); + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_UPDATE_CURRENT_ITEM, 1, -1, + static_cast<void*>(item)); + } + } + } + } +} diff --git a/xbmc/filesystem/ShoutcastFile.h b/xbmc/filesystem/ShoutcastFile.h new file mode 100644 index 0000000..6b34361 --- /dev/null +++ b/xbmc/filesystem/ShoutcastFile.h @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +// FileShoutcast.h: interface for the CShoutcastFile class. +// +////////////////////////////////////////////////////////////////////// + +#include "CurlFile.h" +#include "IFile.h" +#include "threads/Thread.h" + +#include <memory> +#include <queue> +#include <utility> + +namespace MUSIC_INFO +{ +class CMusicInfoTag; +} + +namespace XFILE +{ + +class CFileCache; + +class CShoutcastFile : public IFile, public CThread +{ +public: + CShoutcastFile(); + ~CShoutcastFile() override; + int64_t GetPosition() override; + int64_t GetLength() override; + bool Open(const CURL& url) override; + bool Exists(const CURL& url) override { return true; } + int Stat(const CURL& url, struct __stat64* buffer) override + { + errno = ENOENT; + return -1; + } + ssize_t Read(void* lpBuf, size_t uiBufSize) override; + int64_t Seek(int64_t iFilePosition, int iWhence = SEEK_SET) override; + void Close() override; + int IoControl(EIoControl request, void* param) override; + + void Process() override; +protected: + bool ExtractTagInfo(const char* buf); + void ReadTruncated(char* buf2, int size); + +private: + std::string DecodeToUTF8(const std::string& str); + + CCurlFile m_file; + std::string m_fileCharset; + int m_metaint; + int m_discarded; // data used for tags + int m_currint; + char* m_buffer; // buffer used for tags + std::string m_title; + + CFileCache* m_cacheReader; + CEvent m_tagChange; + CCriticalSection m_tagSection; + using TagInfo = std::pair<int64_t, std::shared_ptr<MUSIC_INFO::CMusicInfoTag>>; + std::queue<TagInfo> m_tags; // tagpos, tag + std::shared_ptr<MUSIC_INFO::CMusicInfoTag> m_masterTag; +}; +} + diff --git a/xbmc/filesystem/SmartPlaylistDirectory.cpp b/xbmc/filesystem/SmartPlaylistDirectory.cpp new file mode 100644 index 0000000..9e05599 --- /dev/null +++ b/xbmc/filesystem/SmartPlaylistDirectory.cpp @@ -0,0 +1,359 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "SmartPlaylistDirectory.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "filesystem/Directory.h" +#include "filesystem/File.h" +#include "filesystem/FileDirectoryFactory.h" +#include "music/MusicDatabase.h" +#include "music/MusicDbUrl.h" +#include "playlists/PlayListTypes.h" +#include "playlists/SmartPlayList.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/SortUtils.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "video/VideoDatabase.h" +#include "video/VideoDbUrl.h" + +#include <math.h> + +#define PROPERTY_PATH_DB "path.db" +#define PROPERTY_SORT_ORDER "sort.order" +#define PROPERTY_SORT_ASCENDING "sort.ascending" +#define PROPERTY_GROUP_BY "group.by" +#define PROPERTY_GROUP_MIXED "group.mixed" + +namespace XFILE +{ + CSmartPlaylistDirectory::CSmartPlaylistDirectory() = default; + + CSmartPlaylistDirectory::~CSmartPlaylistDirectory() = default; + + bool CSmartPlaylistDirectory::GetDirectory(const CURL& url, CFileItemList& items) + { + // Load in the SmartPlaylist and get the WHERE query + CSmartPlaylist playlist; + if (!playlist.Load(url)) + return false; + bool result = GetDirectory(playlist, items); + if (result) + items.SetProperty("library.smartplaylist", true); + + return result; + } + + bool CSmartPlaylistDirectory::GetDirectory(const CSmartPlaylist &playlist, CFileItemList& items, const std::string &strBaseDir /* = "" */, bool filter /* = false */) + { + bool success = false, success2 = false; + std::vector<std::string> virtualFolders; + + SortDescription sorting; + if (playlist.GetLimit() > 0) + sorting.limitEnd = playlist.GetLimit(); + sorting.sortBy = playlist.GetOrder(); + sorting.sortOrder = playlist.GetOrderAscending() ? SortOrderAscending : SortOrderDescending; + sorting.sortAttributes = playlist.GetOrderAttributes(); + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING)) + sorting.sortAttributes = (SortAttribute)(sorting.sortAttributes | SortAttributeIgnoreArticle); + if (playlist.IsMusicType() && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME)) + sorting.sortAttributes = + static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName); + items.SetSortIgnoreFolders((sorting.sortAttributes & SortAttributeIgnoreFolders) == + SortAttributeIgnoreFolders); + + std::string option = !filter ? "xsp" : "filter"; + std::string group = playlist.GetGroup(); + bool isGrouped = !group.empty() && !StringUtils::EqualsNoCase(group, "none") && !playlist.IsGroupMixed(); + // Hint for playlist files like STRM + PLAYLIST::Id playlistTypeHint = PLAYLIST::TYPE_NONE; + + // get all virtual folders and add them to the item list + playlist.GetVirtualFolders(virtualFolders); + for (const std::string& virtualFolder : virtualFolders) + { + CFileItemPtr pItem = CFileItemPtr(new CFileItem(virtualFolder, true)); + IFileDirectory *dir = CFileDirectoryFactory::Create(pItem->GetURL(), pItem.get()); + + if (dir != NULL) + { + pItem->SetSpecialSort(SortSpecialOnTop); + items.Add(pItem); + delete dir; + } + } + + if (playlist.GetType() == "movies" || + playlist.GetType() == "tvshows" || + playlist.GetType() == "episodes") + { + playlistTypeHint = PLAYLIST::TYPE_VIDEO; + CVideoDatabase db; + if (db.Open()) + { + MediaType mediaType = CMediaTypes::FromString(playlist.GetType()); + + std::string baseDir = strBaseDir; + if (strBaseDir.empty()) + { + if (mediaType == MediaTypeTvShow || mediaType == MediaTypeEpisode) + baseDir = "videodb://tvshows/"; + else if (mediaType == MediaTypeMovie) + baseDir = "videodb://movies/"; + else + return false; + + if (!isGrouped) + baseDir += "titles"; + else + baseDir += group; + URIUtils::AddSlashAtEnd(baseDir); + + if (mediaType == MediaTypeEpisode) + baseDir += "-1/-1/"; + } + + CVideoDbUrl videoUrl; + if (!videoUrl.FromString(baseDir)) + return false; + + // store the smartplaylist as JSON in the URL as well + std::string xsp; + if (!playlist.IsEmpty(filter)) + { + if (!playlist.SaveAsJson(xsp, !filter)) + return false; + } + + if (!xsp.empty()) + videoUrl.AddOption(option, xsp); + else + videoUrl.RemoveOption(option); + + CDatabase::Filter dbfilter; + success = db.GetItems(videoUrl.ToString(), items, dbfilter, sorting); + db.Close(); + + // if we retrieve a list of episodes and we didn't receive + // a pre-defined base path, we need to fix it + if (strBaseDir.empty() && mediaType == MediaTypeEpisode && !isGrouped) + videoUrl.AppendPath("-1/-1/"); + items.SetProperty(PROPERTY_PATH_DB, videoUrl.ToString()); + } + } + else if (playlist.IsMusicType() || playlist.GetType().empty()) + { + playlistTypeHint = PLAYLIST::TYPE_MUSIC; + CMusicDatabase db; + if (db.Open()) + { + CSmartPlaylist plist(playlist); + if (playlist.GetType() == "mixed" || playlist.GetType().empty()) + plist.SetType("songs"); + + MediaType mediaType = CMediaTypes::FromString(plist.GetType()); + + std::string baseDir = strBaseDir; + if (strBaseDir.empty()) + { + baseDir = "musicdb://"; + if (!isGrouped) + { + if (mediaType == MediaTypeArtist) + baseDir += "artists"; + else if (mediaType == MediaTypeAlbum) + baseDir += "albums"; + else if (mediaType == MediaTypeSong) + baseDir += "songs"; + else + return false; + } + else + baseDir += group; + + URIUtils::AddSlashAtEnd(baseDir); + } + + CMusicDbUrl musicUrl; + if (!musicUrl.FromString(baseDir)) + return false; + + // store the smartplaylist as JSON in the URL as well + std::string xsp; + if (!plist.IsEmpty(filter)) + { + if (!plist.SaveAsJson(xsp, !filter)) + return false; + } + + if (!xsp.empty()) + musicUrl.AddOption(option, xsp); + else + musicUrl.RemoveOption(option); + + CDatabase::Filter dbfilter; + success = db.GetItems(musicUrl.ToString(), items, dbfilter, sorting); + db.Close(); + + items.SetProperty(PROPERTY_PATH_DB, musicUrl.ToString()); + } + } + + if (playlist.GetType() == "musicvideos" || playlist.GetType() == "mixed") + { + playlistTypeHint = PLAYLIST::TYPE_VIDEO; + CVideoDatabase db; + if (db.Open()) + { + CSmartPlaylist mvidPlaylist(playlist); + if (playlist.GetType() == "mixed") + mvidPlaylist.SetType("musicvideos"); + + std::string baseDir = strBaseDir; + if (baseDir.empty()) + { + baseDir = "videodb://musicvideos/"; + + if (!isGrouped) + baseDir += "titles"; + else + baseDir += group; + URIUtils::AddSlashAtEnd(baseDir); + } + + CVideoDbUrl videoUrl; + if (!videoUrl.FromString(baseDir)) + return false; + + // adjust the group in case we're retrieving a grouped playlist + // based on artists. This is needed because the video library + // is using the actorslink table for artists. + if (isGrouped && group == "artists") + { + group = "actors"; + mvidPlaylist.SetGroup(group); + } + + // store the smartplaylist as JSON in the URL as well + std::string xsp; + if (!mvidPlaylist.IsEmpty(filter)) + { + if (!mvidPlaylist.SaveAsJson(xsp, !filter)) + return false; + } + + if (!xsp.empty()) + videoUrl.AddOption(option, xsp); + else + videoUrl.RemoveOption(option); + + CFileItemList items2; + CDatabase::Filter dbfilter; + success2 = db.GetItems(videoUrl.ToString(), items2, dbfilter, sorting); + + db.Close(); + if (items.Size() <= 0) + items.SetPath(videoUrl.ToString()); + + items.Append(items2); + if (items2.Size()) + { + if (items.Size() > items2.Size()) + items.SetContent("mixed"); + else + items.SetContent("musicvideos"); + } + items.SetProperty(PROPERTY_PATH_DB, videoUrl.ToString()); + } + } + + items.SetLabel(playlist.GetName()); + if (isGrouped) + items.SetContent(group); + else + items.SetContent(playlist.GetType()); + + items.SetProperty(PROPERTY_SORT_ORDER, (int)playlist.GetOrder()); + items.SetProperty(PROPERTY_SORT_ASCENDING, playlist.GetOrderDirection() == SortOrderAscending); + if (!group.empty()) + { + items.SetProperty(PROPERTY_GROUP_BY, group); + items.SetProperty(PROPERTY_GROUP_MIXED, playlist.IsGroupMixed()); + } + + // sort grouped list by label + if (items.Size() > 1 && !group.empty()) + items.Sort(SortByLabel, SortOrderAscending, CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone); + + // go through and set the playlist order + for (int i = 0; i < items.Size(); i++) + { + CFileItemPtr item = items[i]; + item->m_iprogramCount = i; // hack for playlist order + item->SetProperty("playlist_type_hint", playlistTypeHint); + } + + if (playlist.GetType() == "mixed") + return success || success2; + else if (playlist.GetType() == "musicvideos") + return success2; + else + return success; + } + + bool CSmartPlaylistDirectory::ContainsFiles(const CURL& url) + { + // smart playlists always have files?? + return true; + } + + std::string CSmartPlaylistDirectory::GetPlaylistByName(const std::string& name, const std::string& playlistType) + { + CFileItemList list; + bool filesExist = false; + if (CSmartPlaylist::IsMusicType(playlistType)) + filesExist = CDirectory::GetDirectory("special://musicplaylists/", list, ".xsp", DIR_FLAG_DEFAULTS); + else // all others are video + filesExist = CDirectory::GetDirectory("special://videoplaylists/", list, ".xsp", DIR_FLAG_DEFAULTS); + if (filesExist) + { + for (int i = 0; i < list.Size(); i++) + { + CFileItemPtr item = list[i]; + CSmartPlaylist playlist; + if (playlist.OpenAndReadName(item->GetURL())) + { + if (StringUtils::EqualsNoCase(playlist.GetName(), name)) + return item->GetPath(); + } + } + for (int i = 0; i < list.Size(); i++) + { // check based on filename + CFileItemPtr item = list[i]; + if (URIUtils::GetFileName(item->GetPath()) == name) + { // found :) + return item->GetPath(); + } + } + } + return ""; + } + + bool CSmartPlaylistDirectory::Remove(const CURL& url) + { + return XFILE::CFile::Delete(url); + } +} + + + diff --git a/xbmc/filesystem/SmartPlaylistDirectory.h b/xbmc/filesystem/SmartPlaylistDirectory.h new file mode 100644 index 0000000..145c262 --- /dev/null +++ b/xbmc/filesystem/SmartPlaylistDirectory.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IFileDirectory.h" + +#include <string> + +class CSmartPlaylist; + +namespace XFILE +{ + class CSmartPlaylistDirectory : public IFileDirectory + { + public: + CSmartPlaylistDirectory(); + ~CSmartPlaylistDirectory() override; + bool GetDirectory(const CURL& url, CFileItemList& items) override; + bool AllowAll() const override { return true; } + bool ContainsFiles(const CURL& url) override; + bool Remove(const CURL& url) override; + + static bool GetDirectory(const CSmartPlaylist &playlist, CFileItemList& items, const std::string &strBaseDir = "", bool filter = false); + + static std::string GetPlaylistByName(const std::string& name, const std::string& playlistType); + }; +} diff --git a/xbmc/filesystem/SourcesDirectory.cpp b/xbmc/filesystem/SourcesDirectory.cpp new file mode 100644 index 0000000..b83ee2e --- /dev/null +++ b/xbmc/filesystem/SourcesDirectory.cpp @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2005-2020 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "SourcesDirectory.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "Util.h" +#include "guilib/TextureManager.h" +#include "media/MediaLockState.h" +#include "profiles/ProfileManager.h" +#include "settings/MediaSourceSettings.h" +#include "storage/MediaManager.h" +#include "utils/FileUtils.h" +#include "utils/URIUtils.h" + +using namespace XFILE; + +CSourcesDirectory::CSourcesDirectory(void) = default; + +CSourcesDirectory::~CSourcesDirectory(void) = default; + +bool CSourcesDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + // break up our path + // format is: sources://<type>/ + std::string type(url.GetFileName()); + URIUtils::RemoveSlashAtEnd(type); + + VECSOURCES sources; + VECSOURCES *sourcesFromType = CMediaSourceSettings::GetInstance().GetSources(type); + if (!sourcesFromType) + return false; + + sources = *sourcesFromType; + CServiceBroker::GetMediaManager().GetRemovableDrives(sources); + + return GetDirectory(sources, items); +} + +bool CSourcesDirectory::GetDirectory(const VECSOURCES &sources, CFileItemList &items) +{ + for (unsigned int i = 0; i < sources.size(); ++i) + { + const CMediaSource& share = sources[i]; + CFileItemPtr pItem(new CFileItem(share)); + if (URIUtils::IsProtocol(pItem->GetPath(), "musicsearch")) + pItem->SetCanQueue(false); + + std::string strIcon; + // We have the real DVD-ROM, set icon on disktype + if (share.m_iDriveType == CMediaSource::SOURCE_TYPE_DVD && share.m_strThumbnailImage.empty()) + { + CUtil::GetDVDDriveIcon( pItem->GetPath(), strIcon ); + // CDetectDVDMedia::SetNewDVDShareUrl() caches disc thumb as special://temp/dvdicon.tbn + std::string strThumb = "special://temp/dvdicon.tbn"; + if (CFileUtils::Exists(strThumb)) + pItem->SetArt("thumb", strThumb); + } + else if (URIUtils::IsProtocol(pItem->GetPath(), "addons")) + strIcon = "DefaultHardDisk.png"; + else if ( pItem->IsPath("special://musicplaylists/") + || pItem->IsPath("special://videoplaylists/")) + strIcon = "DefaultPlaylist.png"; + else if ( pItem->IsVideoDb() + || pItem->IsMusicDb() + || pItem->IsPlugin() + || pItem->IsPath("musicsearch://")) + strIcon = "DefaultFolder.png"; + else if (pItem->IsRemote()) + strIcon = "DefaultNetwork.png"; + else if (pItem->IsISO9660()) + strIcon = "DefaultDVDRom.png"; + else if (pItem->IsDVD()) + strIcon = "DefaultDVDFull.png"; + else if (pItem->IsBluray()) + strIcon = "DefaultBluray.png"; + else if (pItem->IsCDDA()) + strIcon = "DefaultCDDA.png"; + else if (pItem->IsRemovable() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture("DefaultRemovableDisk.png")) + strIcon = "DefaultRemovableDisk.png"; + else + strIcon = "DefaultHardDisk.png"; + + pItem->SetArt("icon", strIcon); + if (share.m_iHasLock == LOCK_STATE_LOCKED && + m_profileManager->GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE) + pItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_LOCKED); + else + pItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_NONE); + + items.Add(pItem); + } + return true; +} + +bool CSourcesDirectory::Exists(const CURL& url) +{ + return true; +} diff --git a/xbmc/filesystem/SourcesDirectory.h b/xbmc/filesystem/SourcesDirectory.h new file mode 100644 index 0000000..84df8f0 --- /dev/null +++ b/xbmc/filesystem/SourcesDirectory.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" + +#include <vector> + +class CMediaSource; +typedef std::vector<CMediaSource> VECSOURCES; + +namespace XFILE +{ + class CSourcesDirectory : public IDirectory + { + public: + CSourcesDirectory(void); + ~CSourcesDirectory(void) override; + bool GetDirectory(const CURL& url, CFileItemList &items) override; + bool GetDirectory(const VECSOURCES &sources, CFileItemList &items); + bool Exists(const CURL& url) override; + bool AllowAll() const override { return true; } + }; +} diff --git a/xbmc/filesystem/SpecialProtocol.cpp b/xbmc/filesystem/SpecialProtocol.cpp new file mode 100644 index 0000000..1828773 --- /dev/null +++ b/xbmc/filesystem/SpecialProtocol.cpp @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "SpecialProtocol.h" + +#include "ServiceBroker.h" +#include "URL.h" +#include "Util.h" +#include "profiles/ProfileManager.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/URIUtils.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" + +#include <cassert> + +#include "PlatformDefs.h" +#ifdef TARGET_POSIX +#include <dirent.h> +#include "utils/StringUtils.h" +#endif + +const CProfileManager *CSpecialProtocol::m_profileManager = nullptr; + +void CSpecialProtocol::RegisterProfileManager(const CProfileManager &profileManager) +{ + m_profileManager = &profileManager; +} + +void CSpecialProtocol::UnregisterProfileManager() +{ + m_profileManager = nullptr; +} + +void CSpecialProtocol::SetProfilePath(const std::string &dir) +{ + SetPath("profile", dir); + CLog::Log(LOGINFO, "special://profile/ is mapped to: {}", GetPath("profile")); +} + +void CSpecialProtocol::SetXBMCPath(const std::string &dir) +{ + SetPath("xbmc", dir); +} + +void CSpecialProtocol::SetXBMCBinPath(const std::string &dir) +{ + SetPath("xbmcbin", dir); +} + +void CSpecialProtocol::SetXBMCBinAddonPath(const std::string &dir) +{ + SetPath("xbmcbinaddons", dir); +} + +void CSpecialProtocol::SetXBMCAltBinAddonPath(const std::string &dir) +{ + SetPath("xbmcaltbinaddons", dir); +} + +void CSpecialProtocol::SetXBMCFrameworksPath(const std::string &dir) +{ + SetPath("frameworks", dir); +} + +void CSpecialProtocol::SetHomePath(const std::string &dir) +{ + SetPath("home", dir); +} + +void CSpecialProtocol::SetUserHomePath(const std::string &dir) +{ + SetPath("userhome", dir); +} + +void CSpecialProtocol::SetEnvHomePath(const std::string &dir) +{ + SetPath("envhome", dir); +} + +void CSpecialProtocol::SetMasterProfilePath(const std::string &dir) +{ + SetPath("masterprofile", dir); +} + +void CSpecialProtocol::SetTempPath(const std::string &dir) +{ + SetPath("temp", dir); +} + +void CSpecialProtocol::SetLogPath(const std::string &dir) +{ + SetPath("logpath", dir); +} + +bool CSpecialProtocol::ComparePath(const std::string &path1, const std::string &path2) +{ + return TranslatePath(path1) == TranslatePath(path2); +} + +std::string CSpecialProtocol::TranslatePath(const std::string &path) +{ + CURL url(path); + // check for special-protocol, if not, return + if (!url.IsProtocol("special")) + { + return path; + } + return TranslatePath(url); +} + +std::string CSpecialProtocol::TranslatePath(const CURL &url) +{ + // check for special-protocol, if not, return + if (!url.IsProtocol("special")) + { +#if defined(TARGET_POSIX) && defined(_DEBUG) + std::string path(url.Get()); + if (path.length() >= 2 && path[1] == ':') + { + CLog::Log(LOGWARNING, "Trying to access old style dir: {}", path); + // printf("Trying to access old style dir: %s\n", path.c_str()); + } +#endif + + return url.Get(); + } + + const std::string& FullFileName = url.GetFileName(); + + std::string translatedPath; + std::string FileName; + std::string RootDir; + + // Split up into the special://root and the rest of the filename + size_t pos = FullFileName.find('/'); + if (pos != std::string::npos && pos > 1) + { + RootDir = FullFileName.substr(0, pos); + + if (pos < FullFileName.size()) + FileName = FullFileName.substr(pos + 1); + } + else + RootDir = FullFileName; + + if (RootDir == "subtitles") + translatedPath = URIUtils::AddFileToFolder(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SUBTITLES_CUSTOMPATH), FileName); + else if (RootDir == "userdata" && m_profileManager) + translatedPath = URIUtils::AddFileToFolder(m_profileManager->GetUserDataFolder(), FileName); + else if (RootDir == "database" && m_profileManager) + translatedPath = URIUtils::AddFileToFolder(m_profileManager->GetDatabaseFolder(), FileName); + else if (RootDir == "thumbnails" && m_profileManager) + translatedPath = URIUtils::AddFileToFolder(m_profileManager->GetThumbnailsFolder(), FileName); + else if (RootDir == "recordings" || RootDir == "cdrips") + translatedPath = URIUtils::AddFileToFolder(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_AUDIOCDS_RECORDINGPATH), FileName); + else if (RootDir == "screenshots") + translatedPath = URIUtils::AddFileToFolder(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_DEBUG_SCREENSHOTPATH), FileName); + else if (RootDir == "musicartistsinfo") + translatedPath = URIUtils::AddFileToFolder(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER), FileName); + else if (RootDir == "musicplaylists") + translatedPath = URIUtils::AddFileToFolder(CUtil::MusicPlaylistsLocation(), FileName); + else if (RootDir == "videoplaylists") + translatedPath = URIUtils::AddFileToFolder(CUtil::VideoPlaylistsLocation(), FileName); + else if (RootDir == "skin") + { + auto winSystem = CServiceBroker::GetWinSystem(); + // windowing may not have been initialized yet + if (winSystem) + translatedPath = URIUtils::AddFileToFolder(winSystem->GetGfxContext().GetMediaDir(), FileName); + } + // from here on, we have our "real" special paths + else if (RootDir == "xbmc" || + RootDir == "xbmcbin" || + RootDir == "xbmcbinaddons" || + RootDir == "xbmcaltbinaddons" || + RootDir == "home" || + RootDir == "envhome" || + RootDir == "userhome" || + RootDir == "temp" || + RootDir == "profile" || + RootDir == "masterprofile" || + RootDir == "frameworks" || + RootDir == "logpath") + { + std::string basePath = GetPath(RootDir); + if (!basePath.empty()) + translatedPath = URIUtils::AddFileToFolder(basePath, FileName); + else + translatedPath.clear(); + } + + // check if we need to recurse in + if (URIUtils::IsSpecial(translatedPath)) + { // we need to recurse in, as there may be multiple translations required + return TranslatePath(translatedPath); + } + + // Validate the final path, just in case + return CUtil::ValidatePath(translatedPath); +} + +std::string CSpecialProtocol::TranslatePathConvertCase(const std::string& path) +{ + std::string translatedPath = TranslatePath(path); + +#ifdef TARGET_POSIX + if (translatedPath.find("://") != std::string::npos) + return translatedPath; + + // If the file exists with the requested name, simply return it + struct stat stat_buf; + if (stat(translatedPath.c_str(), &stat_buf) == 0) + return translatedPath; + + std::string result; + std::vector<std::string> tokens; + StringUtils::Tokenize(translatedPath, tokens, "/"); + std::string file; + DIR* dir; + struct dirent* de; + + for (unsigned int i = 0; i < tokens.size(); i++) + { + file = result + "/"; + file += tokens[i]; + if (stat(file.c_str(), &stat_buf) == 0) + { + result += "/" + tokens[i]; + } + else + { + dir = opendir(result.c_str()); + if (dir) + { + while ((de = readdir(dir)) != NULL) + { + // check if there's a file with same name but different case + if (StringUtils::CompareNoCase(de->d_name, tokens[i]) == 0) + { + result += "/"; + result += de->d_name; + break; + } + } + + // if we did not find any file that somewhat matches, just + // fallback but we know it's not gonna be a good ending + if (de == NULL) + result += "/" + tokens[i]; + + closedir(dir); + } + else + { // this is just fallback, we won't succeed anyway... + result += "/" + tokens[i]; + } + } + } + + return result; +#else + return translatedPath; +#endif +} + +void CSpecialProtocol::LogPaths() +{ + CLog::Log(LOGINFO, "special://xbmc/ is mapped to: {}", GetPath("xbmc")); + CLog::Log(LOGINFO, "special://xbmcbin/ is mapped to: {}", GetPath("xbmcbin")); + CLog::Log(LOGINFO, "special://xbmcbinaddons/ is mapped to: {}", GetPath("xbmcbinaddons")); + CLog::Log(LOGINFO, "special://masterprofile/ is mapped to: {}", GetPath("masterprofile")); +#if defined(TARGET_POSIX) + CLog::Log(LOGINFO, "special://envhome/ is mapped to: {}", GetPath("envhome")); +#endif + CLog::Log(LOGINFO, "special://home/ is mapped to: {}", GetPath("home")); + CLog::Log(LOGINFO, "special://temp/ is mapped to: {}", GetPath("temp")); + CLog::Log(LOGINFO, "special://logpath/ is mapped to: {}", GetPath("logpath")); + //CLog::Log(LOGINFO, "special://userhome/ is mapped to: {}", GetPath("userhome")); + if (!CUtil::GetFrameworksPath().empty()) + CLog::Log(LOGINFO, "special://frameworks/ is mapped to: {}", GetPath("frameworks")); +} + +// private routines, to ensure we only set/get an appropriate path +void CSpecialProtocol::SetPath(const std::string &key, const std::string &path) +{ + m_pathMap[key] = path; +} + +std::string CSpecialProtocol::GetPath(const std::string &key) +{ + std::map<std::string, std::string>::iterator it = m_pathMap.find(key); + if (it != m_pathMap.end()) + return it->second; + assert(false); + return ""; +} diff --git a/xbmc/filesystem/SpecialProtocol.h b/xbmc/filesystem/SpecialProtocol.h new file mode 100644 index 0000000..2d0cec8 --- /dev/null +++ b/xbmc/filesystem/SpecialProtocol.h @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include <map> +#include <string> + +class CProfileManager; + +// static class for path translation from our special:// URLs. + +/* paths are as follows: + + special://xbmc/ - the main XBMC folder (i.e. where the app resides). + special://home/ - a writeable version of the main XBMC folder + Linux: ~/.kodi/ + OS X: ~/Library/Application Support/Kodi/ + Win32: ~/Application Data/XBMC/ + special://envhome/ - on posix systems this will be equal to the $HOME + special://userhome/ - a writable version of the user home directory + Linux, OS X: ~/.kodi + Win32: home directory of user + special://masterprofile/ - the master users userdata folder - usually special://home/userdata + Linux: ~/.kodi/userdata/ + OS X: ~/Library/Application Support/Kodi/UserData/ + Win32: ~/Application Data/XBMC/UserData/ + special://profile/ - the current users userdata folder - usually special://masterprofile/profiles/<current_profile> + Linux: ~/.kodi/userdata/profiles/<current_profile> + OS X: ~/Library/Application Support/Kodi/UserData/profiles/<current_profile> + Win32: ~/Application Data/XBMC/UserData/profiles/<current_profile> + + special://temp/ - the temporary directory. + Linux: ~/.kodi/temp + OS X: ~/ + Win32: ~/Application Data/XBMC/cache +*/ +class CURL; +class CSpecialProtocol +{ +public: + static void RegisterProfileManager(const CProfileManager &profileManager); + static void UnregisterProfileManager(); + + static void SetProfilePath(const std::string &path); + static void SetXBMCPath(const std::string &path); + static void SetXBMCBinPath(const std::string &path); + static void SetXBMCBinAddonPath(const std::string &path); + static void SetXBMCAltBinAddonPath(const std::string &path); + static void SetXBMCFrameworksPath(const std::string &path); + static void SetHomePath(const std::string &path); + static void SetUserHomePath(const std::string &path); + static void SetEnvHomePath(const std::string &path); + static void SetMasterProfilePath(const std::string &path); + static void SetTempPath(const std::string &path); + static void SetLogPath(const std::string &dir); + + static bool ComparePath(const std::string &path1, const std::string &path2); + static void LogPaths(); + + static std::string TranslatePath(const std::string &path); + static std::string TranslatePath(const CURL &url); + static std::string TranslatePathConvertCase(const std::string& path); + +private: + static const CProfileManager *m_profileManager; + + static void SetPath(const std::string &key, const std::string &path); + static std::string GetPath(const std::string &key); + + static std::map<std::string, std::string> m_pathMap; +}; + +#ifdef TARGET_WINDOWS +#define PATH_SEPARATOR_CHAR '\\' +#define PATH_SEPARATOR_STRING "\\" +#else +#define PATH_SEPARATOR_CHAR '/' +#define PATH_SEPARATOR_STRING "/" +#endif diff --git a/xbmc/filesystem/SpecialProtocolDirectory.cpp b/xbmc/filesystem/SpecialProtocolDirectory.cpp new file mode 100644 index 0000000..e4a970e --- /dev/null +++ b/xbmc/filesystem/SpecialProtocolDirectory.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "SpecialProtocolDirectory.h" + +#include "Directory.h" +#include "FileItem.h" +#include "SpecialProtocol.h" +#include "URL.h" +#include "utils/URIUtils.h" + +using namespace XFILE; + +CSpecialProtocolDirectory::CSpecialProtocolDirectory(void) = default; + +CSpecialProtocolDirectory::~CSpecialProtocolDirectory(void) = default; + +bool CSpecialProtocolDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + const std::string pathToUrl(url.Get()); + std::string translatedPath = CSpecialProtocol::TranslatePath(url); + if (CDirectory::GetDirectory(translatedPath, items, m_strFileMask, m_flags | DIR_FLAG_GET_HIDDEN)) + { // replace our paths as necessary + items.SetURL(url); + for (int i = 0; i < items.Size(); i++) + { + CFileItemPtr item = items[i]; + if (URIUtils::PathHasParent(item->GetPath(), translatedPath)) + item->SetPath(URIUtils::AddFileToFolder(pathToUrl, item->GetPath().substr(translatedPath.size()))); + } + return true; + } + return false; +} + +std::string CSpecialProtocolDirectory::TranslatePath(const CURL &url) +{ + return CSpecialProtocol::TranslatePath(url); +} diff --git a/xbmc/filesystem/SpecialProtocolDirectory.h b/xbmc/filesystem/SpecialProtocolDirectory.h new file mode 100644 index 0000000..97b3613 --- /dev/null +++ b/xbmc/filesystem/SpecialProtocolDirectory.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "filesystem/OverrideDirectory.h" + +namespace XFILE +{ + class CSpecialProtocolDirectory : public COverrideDirectory + { + public: + CSpecialProtocolDirectory(void); + ~CSpecialProtocolDirectory(void) override; + bool GetDirectory(const CURL& url, CFileItemList &items) override; + + protected: + std::string TranslatePath(const CURL &url) override; + }; +} diff --git a/xbmc/filesystem/SpecialProtocolFile.cpp b/xbmc/filesystem/SpecialProtocolFile.cpp new file mode 100644 index 0000000..9ef367d --- /dev/null +++ b/xbmc/filesystem/SpecialProtocolFile.cpp @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "SpecialProtocolFile.h" + +#include "URL.h" +#include "filesystem/SpecialProtocol.h" + +using namespace XFILE; + +CSpecialProtocolFile::CSpecialProtocolFile(void) + : COverrideFile(true) +{ } + +CSpecialProtocolFile::~CSpecialProtocolFile(void) = default; + +std::string CSpecialProtocolFile::TranslatePath(const CURL& url) +{ + return CSpecialProtocol::TranslatePath(url); +} diff --git a/xbmc/filesystem/SpecialProtocolFile.h b/xbmc/filesystem/SpecialProtocolFile.h new file mode 100644 index 0000000..58fd649 --- /dev/null +++ b/xbmc/filesystem/SpecialProtocolFile.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "filesystem/OverrideFile.h" + +namespace XFILE +{ +class CSpecialProtocolFile : public COverrideFile +{ +public: + CSpecialProtocolFile(void); + ~CSpecialProtocolFile(void) override; + +protected: + std::string TranslatePath(const CURL& url) override; +}; +} diff --git a/xbmc/filesystem/StackDirectory.cpp b/xbmc/filesystem/StackDirectory.cpp new file mode 100644 index 0000000..a752a94 --- /dev/null +++ b/xbmc/filesystem/StackDirectory.cpp @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "StackDirectory.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +#include <stdlib.h> + +namespace XFILE +{ + CStackDirectory::CStackDirectory() = default; + + CStackDirectory::~CStackDirectory() = default; + + bool CStackDirectory::GetDirectory(const CURL& url, CFileItemList& items) + { + items.Clear(); + std::vector<std::string> files; + const std::string pathToUrl(url.Get()); + if (!GetPaths(pathToUrl, files)) + return false; // error in path + + for (const std::string& i : files) + { + CFileItemPtr item(new CFileItem(i)); + item->SetPath(i); + item->m_bIsFolder = false; + items.Add(item); + } + return true; + } + + std::string CStackDirectory::GetStackedTitlePath(const std::string &strPath) + { + // Load up our REs + VECCREGEXP RegExps; + CRegExp tempRE(true, CRegExp::autoUtf8); + const std::vector<std::string>& strRegExps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoStackRegExps; + std::vector<std::string>::const_iterator itRegExp = strRegExps.begin(); + while (itRegExp != strRegExps.end()) + { + (void)tempRE.RegComp(*itRegExp); + if (tempRE.GetCaptureTotal() == 4) + RegExps.push_back(tempRE); + else + CLog::Log(LOGERROR, "Invalid video stack RE ({}). Must have exactly 4 captures.", + itRegExp->c_str()); + ++itRegExp; + } + return GetStackedTitlePath(strPath, RegExps); + } + + std::string CStackDirectory::GetStackedTitlePath(const std::string &strPath, VECCREGEXP& RegExps) + { + CStackDirectory stack; + CFileItemList files; + std::string strStackTitlePath, + strCommonDir = URIUtils::GetParentPath(strPath); + + const CURL pathToUrl(strPath); + stack.GetDirectory(pathToUrl, files); + + if (files.Size() > 1) + { + std::string strStackTitle; + + std::string File1 = URIUtils::GetFileName(files[0]->GetPath()); + std::string File2 = URIUtils::GetFileName(files[1]->GetPath()); + // Check if source path uses URL encoding + if (URIUtils::HasEncodedFilename(CURL(strCommonDir))) + { + File1 = CURL::Decode(File1); + File2 = CURL::Decode(File2); + } + + std::vector<CRegExp>::iterator itRegExp = RegExps.begin(); + int offset = 0; + + while (itRegExp != RegExps.end()) + { + if (itRegExp->RegFind(File1, offset) != -1) + { + std::string Title1 = itRegExp->GetMatch(1), + Volume1 = itRegExp->GetMatch(2), + Ignore1 = itRegExp->GetMatch(3), + Extension1 = itRegExp->GetMatch(4); + if (offset) + Title1 = File1.substr(0, itRegExp->GetSubStart(2)); + if (itRegExp->RegFind(File2, offset) != -1) + { + std::string Title2 = itRegExp->GetMatch(1), + Volume2 = itRegExp->GetMatch(2), + Ignore2 = itRegExp->GetMatch(3), + Extension2 = itRegExp->GetMatch(4); + if (offset) + Title2 = File2.substr(0, itRegExp->GetSubStart(2)); + if (StringUtils::EqualsNoCase(Title1, Title2)) + { + if (!StringUtils::EqualsNoCase(Volume1, Volume2)) + { + if (StringUtils::EqualsNoCase(Ignore1, Ignore2) && + StringUtils::EqualsNoCase(Extension1, Extension2)) + { + // got it + strStackTitle = Title1 + Ignore1 + Extension1; + // Check if source path uses URL encoding + if (URIUtils::HasEncodedFilename(CURL(strCommonDir))) + strStackTitle = CURL::Encode(strStackTitle); + + itRegExp = RegExps.end(); + break; + } + else // Invalid stack + break; + } + else // Early match, retry with offset + { + offset = itRegExp->GetSubStart(3); + continue; + } + } + } + } + offset = 0; + ++itRegExp; + } + if (!strCommonDir.empty() && !strStackTitle.empty()) + strStackTitlePath = strCommonDir + strStackTitle; + } + + return strStackTitlePath; + } + + std::string CStackDirectory::GetFirstStackedFile(const std::string &strPath) + { + // the stacked files are always in volume order, so just get up to the first filename + // occurrence of " , " + std::string file, folder; + size_t pos = strPath.find(" , "); + if (pos != std::string::npos) + URIUtils::Split(strPath.substr(0, pos), folder, file); + else + URIUtils::Split(strPath, folder, file); // single filed stacks - should really not happen + + // remove "stack://" from the folder + folder = folder.substr(8); + StringUtils::Replace(file, ",,", ","); + + return URIUtils::AddFileToFolder(folder, file); + } + + bool CStackDirectory::GetPaths(const std::string& strPath, std::vector<std::string>& vecPaths) + { + // format is: + // stack://file1 , file2 , file3 , file4 + // filenames with commas are double escaped (ie replaced with ,,), thus the " , " separator used. + std::string path = strPath; + // remove stack:// from the beginning + path = path.substr(8); + + vecPaths = StringUtils::Split(path, " , "); + if (vecPaths.empty()) + return false; + + // because " , " is used as a separator any "," in the real paths are double escaped + for (std::string& itPath : vecPaths) + StringUtils::Replace(itPath, ",,", ","); + + return true; + } + + std::string CStackDirectory::ConstructStackPath(const CFileItemList &items, const std::vector<int> &stack) + { + // no checks on the range of stack here. + // we replace all instances of comma's with double comma's, then separate + // the files using " , ". + std::string stackedPath = "stack://"; + std::string folder, file; + URIUtils::Split(items[stack[0]]->GetPath(), folder, file); + stackedPath += folder; + // double escape any occurrence of commas + StringUtils::Replace(file, ",", ",,"); + stackedPath += file; + for (unsigned int i = 1; i < stack.size(); ++i) + { + stackedPath += " , "; + file = items[stack[i]]->GetPath(); + + // double escape any occurrence of commas + StringUtils::Replace(file, ",", ",,"); + stackedPath += file; + } + return stackedPath; + } + + bool CStackDirectory::ConstructStackPath(const std::vector<std::string> &paths, std::string& stackedPath) + { + if (paths.size() < 2) + return false; + stackedPath = "stack://"; + std::string folder, file; + URIUtils::Split(paths[0], folder, file); + stackedPath += folder; + // double escape any occurrence of commas + StringUtils::Replace(file, ",", ",,"); + stackedPath += file; + for (unsigned int i = 1; i < paths.size(); ++i) + { + stackedPath += " , "; + file = paths[i]; + + // double escape any occurrence of commas + StringUtils::Replace(file, ",", ",,"); + stackedPath += file; + } + return true; + } +} + diff --git a/xbmc/filesystem/StackDirectory.h b/xbmc/filesystem/StackDirectory.h new file mode 100644 index 0000000..5f61fe3 --- /dev/null +++ b/xbmc/filesystem/StackDirectory.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" +#include "utils/RegExp.h" + +#include <string> +#include <vector> + +namespace XFILE +{ + class CStackDirectory : public IDirectory + { + public: + CStackDirectory(); + ~CStackDirectory() override; + bool GetDirectory(const CURL& url, CFileItemList& items) override; + bool AllowAll() const override { return true; } + static std::string GetStackedTitlePath(const std::string &strPath); + static std::string GetStackedTitlePath(const std::string &strPath, VECCREGEXP& RegExps); + static std::string GetFirstStackedFile(const std::string &strPath); + static bool GetPaths(const std::string& strPath, std::vector<std::string>& vecPaths); + static std::string ConstructStackPath(const CFileItemList& items, const std::vector<int> &stack); + static bool ConstructStackPath(const std::vector<std::string> &paths, std::string &stackedPath); + }; +} diff --git a/xbmc/filesystem/UDFBlockInput.cpp b/xbmc/filesystem/UDFBlockInput.cpp new file mode 100644 index 0000000..b4eb265 --- /dev/null +++ b/xbmc/filesystem/UDFBlockInput.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "UDFBlockInput.h" + +#include "filesystem/File.h" + +#include <mutex> + +#include <udfread/udfread.h> + +int CUDFBlockInput::Close(udfread_block_input* bi) +{ + auto m_bi = reinterpret_cast<UDF_BI*>(bi); + + m_bi->fp->Close(); + + return 0; +} + +uint32_t CUDFBlockInput::Size(udfread_block_input* bi) +{ + auto m_bi = reinterpret_cast<UDF_BI*>(bi); + + return static_cast<uint32_t>(m_bi->fp->GetLength() / UDF_BLOCK_SIZE); +} + +int CUDFBlockInput::Read( + udfread_block_input* bi, uint32_t lba, void* buf, uint32_t blocks, int flags) +{ + auto m_bi = reinterpret_cast<UDF_BI*>(bi); + std::unique_lock<CCriticalSection> lock(m_bi->lock); + + int64_t pos = static_cast<int64_t>(lba) * UDF_BLOCK_SIZE; + + if (m_bi->fp->Seek(pos, SEEK_SET) != pos) + return -1; + + ssize_t size = blocks * UDF_BLOCK_SIZE; + ssize_t read = m_bi->fp->Read(buf, size); + if (read > 0) + return static_cast<int>(read / UDF_BLOCK_SIZE); + + return static_cast<int>(read); +} + +udfread_block_input* CUDFBlockInput::GetBlockInput(const std::string& file) +{ + auto fp = std::make_shared<XFILE::CFile>(); + + if (fp->Open(file)) + { + m_bi = std::make_unique<UDF_BI>(); + if (m_bi) + { + m_bi->fp = fp; + m_bi->bi.close = CUDFBlockInput::Close; + m_bi->bi.read = CUDFBlockInput::Read; + m_bi->bi.size = CUDFBlockInput::Size; + + return &m_bi->bi; + } + + fp->Close(); + } + + return nullptr; +} diff --git a/xbmc/filesystem/UDFBlockInput.h b/xbmc/filesystem/UDFBlockInput.h new file mode 100644 index 0000000..e7cfc88 --- /dev/null +++ b/xbmc/filesystem/UDFBlockInput.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" + +#include <memory> + +#include <udfread/blockinput.h> + +namespace XFILE +{ +class CFile; +} + +class CUDFBlockInput +{ +public: + CUDFBlockInput() = default; + ~CUDFBlockInput() = default; + + udfread_block_input* GetBlockInput(const std::string& file); + +private: + static int Close(udfread_block_input* bi); + static uint32_t Size(udfread_block_input* bi); + static int Read(udfread_block_input* bi, uint32_t lba, void* buf, uint32_t nblocks, int flags); + + struct UDF_BI + { + struct udfread_block_input bi; + std::shared_ptr<XFILE::CFile> fp{nullptr}; + CCriticalSection lock; + }; + + std::unique_ptr<UDF_BI> m_bi{nullptr}; +}; diff --git a/xbmc/filesystem/UDFDirectory.cpp b/xbmc/filesystem/UDFDirectory.cpp new file mode 100644 index 0000000..236b281 --- /dev/null +++ b/xbmc/filesystem/UDFDirectory.cpp @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2010 Team Boxee + * http://www.boxee.tv + * + * Copyright (C) 2010-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "UDFDirectory.h" + +#include "FileItem.h" +#include "URL.h" +#include "Util.h" +#include "filesystem/UDFBlockInput.h" +#include "utils/URIUtils.h" + +#include <udfread/udfread.h> + +using namespace XFILE; + +bool CUDFDirectory::GetDirectory(const CURL& url, CFileItemList& items) +{ + CURL url2(url); + if (!url2.IsProtocol("udf")) + { + url2.Reset(); + url2.SetProtocol("udf"); + url2.SetHostName(url.Get()); + } + + std::string strRoot(url2.Get()); + std::string strSub(url2.GetFileName()); + + URIUtils::AddSlashAtEnd(strRoot); + URIUtils::AddSlashAtEnd(strSub); + + auto udf = udfread_init(); + + if (!udf) + return false; + + CUDFBlockInput udfbi; + + auto bi = udfbi.GetBlockInput(url2.GetHostName()); + + if (udfread_open_input(udf, bi) < 0) + { + udfread_close(udf); + return false; + } + + auto path = udfread_opendir(udf, strSub.c_str()); + if (!path) + { + udfread_close(udf); + return false; + } + + struct udfread_dirent dirent; + + while (udfread_readdir(path, &dirent)) + { + if (dirent.d_type == UDF_DT_DIR) + { + std::string filename = dirent.d_name; + if (filename != "." && filename != "..") + { + CFileItemPtr pItem(new CFileItem(filename)); + std::string strDir(strRoot + filename); + URIUtils::AddSlashAtEnd(strDir); + pItem->SetPath(strDir); + pItem->m_bIsFolder = true; + + items.Add(pItem); + } + } + else + { + std::string filename = dirent.d_name; + std::string filenameWithPath{strSub + filename}; + auto file = udfread_file_open(udf, filenameWithPath.c_str()); + if (!file) + continue; + + CFileItemPtr pItem(new CFileItem(filename)); + pItem->SetPath(strRoot + filename); + pItem->m_bIsFolder = false; + pItem->m_dwSize = udfread_file_size(file); + items.Add(pItem); + + udfread_file_close(file); + } + } + + udfread_closedir(path); + udfread_close(udf); + + return true; +} + +bool CUDFDirectory::Exists(const CURL& url) +{ + CFileItemList items; + return GetDirectory(url, items); +} diff --git a/xbmc/filesystem/UDFDirectory.h b/xbmc/filesystem/UDFDirectory.h new file mode 100644 index 0000000..ea4487a --- /dev/null +++ b/xbmc/filesystem/UDFDirectory.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2010 Team Boxee + * http://www.boxee.tv + * + * Copyright (C) 2010-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IFileDirectory.h" + +namespace XFILE +{ + +class CUDFDirectory : public IFileDirectory +{ +public: + CUDFDirectory() = default; + ~CUDFDirectory() = default; + bool GetDirectory(const CURL& url, CFileItemList& items) override; + bool Exists(const CURL& url) override; + bool ContainsFiles(const CURL& url) override { return true; } +}; +} + diff --git a/xbmc/filesystem/UDFFile.cpp b/xbmc/filesystem/UDFFile.cpp new file mode 100644 index 0000000..f4c5d5b --- /dev/null +++ b/xbmc/filesystem/UDFFile.cpp @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "UDFFile.h" + +#include "URL.h" + +#include <udfread/udfread.h> + +using namespace XFILE; + +CUDFFile::CUDFFile() : m_bi{std::make_unique<CUDFBlockInput>()} +{ +} + +bool CUDFFile::Open(const CURL& url) +{ + if (m_udf && m_file) + return true; + + m_udf = udfread_init(); + + if (!m_udf) + return false; + + auto bi = m_bi->GetBlockInput(url.GetHostName()); + + if (!bi) + { + udfread_close(m_udf); + return false; + } + + if (udfread_open_input(m_udf, bi) < 0) + { + bi->close(bi); + udfread_close(m_udf); + return false; + } + + m_file = udfread_file_open(m_udf, url.GetFileName().c_str()); + if (!m_file) + { + Close(); + return false; + } + + return true; +} + +void CUDFFile::Close() +{ + if (m_file) + { + udfread_file_close(m_file); + m_file = nullptr; + } + + if (m_udf) + { + udfread_close(m_udf); + m_udf = nullptr; + } +} + +int CUDFFile::Stat(const CURL& url, struct __stat64* buffer) +{ + if (!m_udf || !m_file || !buffer) + return -1; + + *buffer = {}; + buffer->st_size = GetLength(); + + return 0; +} + +ssize_t CUDFFile::Read(void* buffer, size_t size) +{ + return udfread_file_read(m_file, buffer, size); +} + +int64_t CUDFFile::Seek(int64_t filePosition, int whence) +{ + return udfread_file_seek(m_file, filePosition, whence); +} + +int64_t CUDFFile::GetLength() +{ + return udfread_file_size(m_file); +} + +int64_t CUDFFile::GetPosition() +{ + return udfread_file_tell(m_file); +} + +bool CUDFFile::Exists(const CURL& url) +{ + return Open(url); +} diff --git a/xbmc/filesystem/UDFFile.h b/xbmc/filesystem/UDFFile.h new file mode 100644 index 0000000..1617a1f --- /dev/null +++ b/xbmc/filesystem/UDFFile.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IFile.h" +#include "filesystem/UDFBlockInput.h" + +#include <memory> + +class udfread; +typedef struct udfread_file UDFFILE; + +namespace XFILE +{ + +class CUDFFile : public IFile +{ +public: + CUDFFile(); + ~CUDFFile() override = default; + + bool Open(const CURL& url) override; + void Close() override; + + int Stat(const CURL& url, struct __stat64* buffer) override; + + ssize_t Read(void* buffer, size_t size) override; + int64_t Seek(int64_t filePosition, int whence) override; + + int64_t GetLength() override; + int64_t GetPosition() override; + + bool Exists(const CURL& url) override; + +private: + std::unique_ptr<CUDFBlockInput> m_bi{nullptr}; + + udfread* m_udf{nullptr}; + UDFFILE* m_file{nullptr}; +}; + +} // namespace XFILE diff --git a/xbmc/filesystem/UPnPDirectory.cpp b/xbmc/filesystem/UPnPDirectory.cpp new file mode 100644 index 0000000..b93b6bf --- /dev/null +++ b/xbmc/filesystem/UPnPDirectory.cpp @@ -0,0 +1,358 @@ +/* + * UPnP Support for XBMC + * Copyright (c) 2006 c0diq (Sylvain Rebaud) + * Portions Copyright (c) by the authors of libPlatinum + * http://www.plutinosoft.com/blog/category/platinum/ + * + * Copyright (C) 2010-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ +#include "UPnPDirectory.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "network/upnp/UPnP.h" +#include "network/upnp/UPnPInternal.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +#include <Platinum/Source/Devices/MediaServer/PltSyncMediaBrowser.h> +#include <Platinum/Source/Platinum/Platinum.h> + +using namespace MUSIC_INFO; +using namespace XFILE; +using namespace UPNP; + +namespace XFILE +{ + +static std::string GetContentMapping(NPT_String& objectClass) +{ + struct SClassMapping + { + const char* ObjectClass; + const char* Content; + }; + static const SClassMapping mapping[] = { + { "object.item.videoItem.videoBroadcast" , "episodes" } + , { "object.item.videoItem.musicVideoClip" , "musicvideos" } + , { "object.item.videoItem" , "movies" } + , { "object.item.audioItem.musicTrack" , "songs" } + , { "object.item.audioItem" , "songs" } + , { "object.item.imageItem.photo" , "photos" } + , { "object.item.imageItem" , "photos" } + , { "object.container.album.videoAlbum.videoBroadcastShow" , "tvshows" } + , { "object.container.album.videoAlbum.videoBroadcastSeason", "seasons" } + , { "object.container.album.musicAlbum" , "albums" } + , { "object.container.album.photoAlbum" , "photos" } + , { "object.container.album" , "albums" } + , { "object.container.person" , "artists" } + , { NULL , NULL } + }; + for(const SClassMapping* map = mapping; map->ObjectClass; map++) + { + if(objectClass.StartsWith(map->ObjectClass, true)) + { + return map->Content; + break; + } + } + return "unknown"; +} + +static bool FindDeviceWait(CUPnP* upnp, const char* uuid, PLT_DeviceDataReference& device) +{ + bool client_started = upnp->IsClientStarted(); + upnp->StartClient(); + + // look for device in our list + // (and wait for it to respond for 5 secs if we're just starting upnp client) + NPT_TimeStamp watchdog; + NPT_System::GetCurrentTimeStamp(watchdog); + watchdog += 5.0; + + for (;;) { + if (NPT_SUCCEEDED(upnp->m_MediaBrowser->FindServer(uuid, device)) && !device.IsNull()) + break; + + // fail right away if device not found and upnp client was already running + if (client_started) + return false; + + // otherwise check if we've waited long enough without success + NPT_TimeStamp now; + NPT_System::GetCurrentTimeStamp(now); + if (now > watchdog) + return false; + + // sleep a bit and try again + NPT_System::Sleep(NPT_TimeInterval((double)1)); + } + + return !device.IsNull(); +} + +/*---------------------------------------------------------------------- +| CUPnPDirectory::GetFriendlyName ++---------------------------------------------------------------------*/ +std::string CUPnPDirectory::GetFriendlyName(const CURL& url) +{ + NPT_String path = url.Get().c_str(); + if (!path.EndsWith("/")) path += "/"; + + if (path.Left(7).Compare("upnp://", true) != 0) { + return {}; + } else if (path.Compare("upnp://", true) == 0) { + return "UPnP Media Servers (Auto-Discover)"; + } + + // look for nextslash + int next_slash = path.Find('/', 7); + if (next_slash == -1) + return {}; + + NPT_String uuid = path.SubString(7, next_slash-7); + NPT_String object_id = path.SubString(next_slash+1, path.GetLength()-next_slash-2); + + // look for device + PLT_DeviceDataReference device; + if(!FindDeviceWait(CUPnP::GetInstance(), uuid, device)) + return {}; + + return device->GetFriendlyName().GetChars(); +} + +/*---------------------------------------------------------------------- +| CUPnPDirectory::GetDirectory ++---------------------------------------------------------------------*/ +bool CUPnPDirectory::GetResource(const CURL& path, CFileItem &item) +{ + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SERVICES_UPNP)) + return false; + + if(!path.IsProtocol("upnp")) + return false; + + CUPnP* upnp = CUPnP::GetInstance(); + if(!upnp) + return false; + + const std::string& uuid = path.GetHostName(); + std::string object = path.GetFileName(); + StringUtils::TrimRight(object, "/"); + object = CURL::Decode(object); + + PLT_DeviceDataReference device; + if(!FindDeviceWait(upnp, uuid.c_str(), device)) { + CLog::Log(LOGERROR, "CUPnPDirectory::GetResource - unable to find uuid {}", uuid); + return false; + } + + PLT_MediaObjectListReference list; + if (NPT_FAILED(upnp->m_MediaBrowser->BrowseSync(device, object.c_str(), list, true))) { + CLog::Log(LOGERROR, "CUPnPDirectory::GetResource - unable to find object {}", object); + return false; + } + + if (list.IsNull() || !list->GetItemCount()) { + CLog::Log(LOGERROR, "CUPnPDirectory::GetResource - no items returned for object {}", object); + return false; + } + + PLT_MediaObjectList::Iterator entry = list->GetFirstItem(); + if (entry == 0) + return false; + + return UPNP::GetResource(*entry, item); +} + + +/*---------------------------------------------------------------------- +| CUPnPDirectory::GetDirectory ++---------------------------------------------------------------------*/ +bool +CUPnPDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SERVICES_UPNP)) + return false; + + CUPnP* upnp = CUPnP::GetInstance(); + + /* upnp should never be cached, it has internal cache */ + items.SetCacheToDisc(CFileItemList::CACHE_NEVER); + + // We accept upnp://devuuid/[item_id/] + NPT_String path = url.Get().c_str(); + if (!path.StartsWith("upnp://", true)) { + return false; + } + + if (path.Compare("upnp://", true) == 0) { + upnp->StartClient(); + + // root -> get list of devices + const NPT_Lock<PLT_DeviceDataReferenceList>& devices = upnp->m_MediaBrowser->GetMediaServers(); + NPT_List<PLT_DeviceDataReference>::Iterator device = devices.GetFirstItem(); + while (device) { + NPT_String name = (*device)->GetFriendlyName(); + NPT_String uuid = (*device)->GetUUID(); + + CFileItemPtr pItem(new CFileItem((const char*)name)); + pItem->SetPath(std::string((const char*) "upnp://" + uuid + "/")); + pItem->m_bIsFolder = true; + pItem->SetArt("thumb", (const char*)(*device)->GetIconUrl("image/png")); + + items.Add(pItem); + + ++device; + } + } else { + if (!path.EndsWith("/")) path += "/"; + + // look for nextslash + int next_slash = path.Find('/', 7); + + NPT_String uuid = (next_slash==-1)?path.SubString(7):path.SubString(7, next_slash-7); + NPT_String object_id = (next_slash == -1) ? NPT_String("") : path.SubString(next_slash + 1); + object_id.TrimRight("/"); + if (object_id.GetLength()) { + object_id = CURL::Decode((char*)object_id).c_str(); + } + + // try to find the device with wait on startup + PLT_DeviceDataReference device; + if (!FindDeviceWait(upnp, uuid, device)) + goto failure; + + // issue a browse request with object_id + // if object_id is empty use "0" for root + object_id = object_id.IsEmpty() ? NPT_String("0") : object_id; + + // remember a count of object classes + std::map<NPT_String, int> classes; + + // just a guess as to what types of files we want + bool video = true; + bool audio = true; + bool image = true; + StringUtils::TrimLeft(m_strFileMask, "/"); + if (!m_strFileMask.empty()) { + video = m_strFileMask.find(".wmv") != std::string::npos; + audio = m_strFileMask.find(".wma") != std::string::npos; + image = m_strFileMask.find(".jpg") != std::string::npos; + } + + // special case for Windows Media Connect and WMP11 when looking for root + // We can target which root subfolder we want based on directory mask + if (object_id == "0" && ((device->GetFriendlyName().Find("Windows Media Connect", 0, true) >= 0) || + (device->m_ModelName == "Windows Media Player Sharing"))) { + + // look for a specific type to differentiate which folder we want + if (audio && !video && !image) { + // music + object_id = "1"; + } else if (!audio && video && !image) { + // video + object_id = "2"; + } else if (!audio && !video && image) { + // pictures + object_id = "3"; + } + } + +#ifdef DISABLE_SPECIALCASE + // same thing but special case for XBMC + if (object_id == "0" && ((device->m_ModelName.Find("XBMC", 0, true) >= 0) || + (device->m_ModelName.Find("Xbox Media Center", 0, true) >= 0))) { + // look for a specific type to differentiate which folder we want + if (audio && !video && !image) { + // music + object_id = "virtualpath://upnpmusic"; + } else if (!audio && video && !image) { + // video + object_id = "virtualpath://upnpvideo"; + } else if (!audio && !video && image) { + // pictures + object_id = "virtualpath://upnppictures"; + } + } +#endif + + // if error, return now, the device could have gone away + // this will make us go back to the sources list + PLT_MediaObjectListReference list; + NPT_Result res = upnp->m_MediaBrowser->BrowseSync(device, object_id, list); + if (NPT_FAILED(res)) goto failure; + + // empty list is ok + if (list.IsNull()) goto cleanup; + + PLT_MediaObjectList::Iterator entry = list->GetFirstItem(); + while (entry) { + // disregard items with wrong class/type + if( (!video && (*entry)->m_ObjectClass.type.CompareN("object.item.videoitem", 21,true) == 0) + || (!audio && (*entry)->m_ObjectClass.type.CompareN("object.item.audioitem", 21,true) == 0) + || (!image && (*entry)->m_ObjectClass.type.CompareN("object.item.imageitem", 21,true) == 0) ) + { + ++entry; + continue; + } + + // keep count of classes + classes[(*entry)->m_ObjectClass.type]++; + CFileItemPtr pItem = BuildObject(*entry, UPnPClient); + if(!pItem) { + ++entry; + continue; + } + + std::string id; + if ((*entry)->m_ReferenceID.IsEmpty()) + id = (const char*) (*entry)->m_ObjectID; + else + id = (const char*) (*entry)->m_ReferenceID; + + id = CURL::Encode(id); + URIUtils::AddSlashAtEnd(id); + pItem->SetPath(std::string((const char*) "upnp://" + uuid + "/" + id.c_str())); + + items.Add(pItem); + + ++entry; + } + + NPT_String max_string = ""; + int max_count = 0; + for (auto& it : classes) + { + if (it.second > max_count) + { + max_string = it.first; + max_count = it.second; + } + } + std::string content = GetContentMapping(max_string); + items.SetContent(content); + if (content == "unknown") + { + items.AddSortMethod(SortByNone, 571, LABEL_MASKS("%L", "%I", "%L", "")); + items.AddSortMethod(SortByLabel, SortAttributeIgnoreFolders, 551, LABEL_MASKS("%L", "%I", "%L", "")); + items.AddSortMethod(SortBySize, 553, LABEL_MASKS("%L", "%I", "%L", "%I")); + items.AddSortMethod(SortByDate, 552, LABEL_MASKS("%L", "%J", "%L", "%J")); + } + } + +cleanup: + return true; + +failure: + return false; +} +} diff --git a/xbmc/filesystem/UPnPDirectory.h b/xbmc/filesystem/UPnPDirectory.h new file mode 100644 index 0000000..e49407e --- /dev/null +++ b/xbmc/filesystem/UPnPDirectory.h @@ -0,0 +1,37 @@ +/* + * UPnP Support for XBMC + * Copyright (c) 2006 c0diq (Sylvain Rebaud) + * Portions Copyright (c) by the authors of libPlatinum + * http://www.plutinosoft.com/blog/category/platinum/ + * + * Copyright (C) 2010-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" + +class CFileItem; +class CURL; + +namespace XFILE +{ +class CUPnPDirectory : public IDirectory +{ +public: + CUPnPDirectory(void) = default; + ~CUPnPDirectory(void) override = default; + + // IDirectory methods + bool GetDirectory(const CURL& url, CFileItemList &items) override; + bool AllowAll() const override { return true; } + + // class methods + static std::string GetFriendlyName(const CURL& url); + static bool GetResource(const CURL &path, CFileItem& item); +}; +} diff --git a/xbmc/filesystem/UPnPFile.cpp b/xbmc/filesystem/UPnPFile.cpp new file mode 100644 index 0000000..5605170 --- /dev/null +++ b/xbmc/filesystem/UPnPFile.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "UPnPFile.h" + +#include "FileFactory.h" +#include "FileItem.h" +#include "UPnPDirectory.h" +#include "URL.h" + +using namespace XFILE; + +CUPnPFile::CUPnPFile() = default; + +CUPnPFile::~CUPnPFile() = default; + +bool CUPnPFile::Open(const CURL& url) +{ + CFileItem item_new; + if (CUPnPDirectory::GetResource(url, item_new)) + { + //CLog::Log(LOGDEBUG,"FileUPnP - file redirect to {}.", item_new.GetPath()); + IFile *pNewImp = CFileFactory::CreateLoader(item_new.GetPath()); + CURL *pNewUrl = new CURL(item_new.GetPath()); + if (pNewImp) + { + throw new CRedirectException(pNewImp, pNewUrl); + } + delete pNewUrl; + } + return false; +} + +int CUPnPFile::Stat(const CURL& url, struct __stat64* buffer) +{ + CFileItem item_new; + if (CUPnPDirectory::GetResource(url, item_new)) + { + //CLog::Log(LOGDEBUG,"FileUPnP - file redirect to {}.", item_new.GetPath()); + IFile *pNewImp = CFileFactory::CreateLoader(item_new.GetPath()); + CURL *pNewUrl = new CURL(item_new.GetPath()); + if (pNewImp) + { + throw new CRedirectException(pNewImp, pNewUrl); + } + delete pNewUrl; + } + return -1; +} + +bool CUPnPFile::Exists(const CURL& url) +{ + CFileItem item_new; + if (CUPnPDirectory::GetResource(url, item_new)) + { + //CLog::Log(LOGDEBUG,"FileUPnP - file redirect to {}.", item_new.GetPath()); + IFile *pNewImp = CFileFactory::CreateLoader(item_new.GetPath()); + CURL *pNewUrl = new CURL(item_new.GetPath()); + if (pNewImp) + { + throw new CRedirectException(pNewImp, pNewUrl); + } + delete pNewUrl; + } + return false; +} diff --git a/xbmc/filesystem/UPnPFile.h b/xbmc/filesystem/UPnPFile.h new file mode 100644 index 0000000..b56dcb1 --- /dev/null +++ b/xbmc/filesystem/UPnPFile.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IFile.h" + +namespace XFILE +{ + class CUPnPFile : public IFile + { + public: + CUPnPFile(); + ~CUPnPFile() override; + bool Open(const CURL& url) override; + bool Exists(const CURL& url) override; + int Stat(const CURL& url, struct __stat64* buffer) override; + + ssize_t Read(void* lpBuf, size_t uiBufSize) override {return -1;} + int64_t Seek(int64_t iFilePosition, int iWhence = SEEK_SET) override {return -1;} + void Close() override{} + int64_t GetPosition() override {return -1;} + int64_t GetLength() override {return -1;} + }; +} diff --git a/xbmc/filesystem/VideoDatabaseDirectory.cpp b/xbmc/filesystem/VideoDatabaseDirectory.cpp new file mode 100644 index 0000000..6a2cb6a --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory.cpp @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "VideoDatabaseDirectory.h" + +#include "File.h" +#include "FileItem.h" +#include "ServiceBroker.h" +#include "VideoDatabaseDirectory/QueryParams.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/TextureManager.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/Crc32.h" +#include "utils/LegacyPathTranslation.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "video/VideoDatabase.h" + +using namespace XFILE; +using namespace VIDEODATABASEDIRECTORY; + +CVideoDatabaseDirectory::CVideoDatabaseDirectory(void) = default; + +CVideoDatabaseDirectory::~CVideoDatabaseDirectory(void) = default; + +namespace +{ +std::string GetChildContentType(const std::unique_ptr<CDirectoryNode>& node) +{ + switch (node->GetChildType()) + { + case NODE_TYPE_EPISODES: + case NODE_TYPE_RECENTLY_ADDED_EPISODES: + return "episodes"; + case NODE_TYPE_SEASONS: + return "seasons"; + case NODE_TYPE_TITLE_MOVIES: + case NODE_TYPE_RECENTLY_ADDED_MOVIES: + return "movies"; + case NODE_TYPE_TITLE_TVSHOWS: + case NODE_TYPE_INPROGRESS_TVSHOWS: + return "tvshows"; + case NODE_TYPE_TITLE_MUSICVIDEOS: + case NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS: + return "musicvideos"; + case NODE_TYPE_GENRE: + return "genres"; + case NODE_TYPE_COUNTRY: + return "countries"; + case NODE_TYPE_ACTOR: + { + CQueryParams params; + node->CollectQueryParams(params); + if (static_cast<VideoDbContentType>(params.GetContentType()) == + VideoDbContentType::MUSICVIDEOS) + return "artists"; + + return "actors"; + } + case NODE_TYPE_DIRECTOR: + return "directors"; + case NODE_TYPE_STUDIO: + return "studios"; + case NODE_TYPE_YEAR: + return "years"; + case NODE_TYPE_MUSICVIDEOS_ALBUM: + return "albums"; + case NODE_TYPE_SETS: + return "sets"; + case NODE_TYPE_TAGS: + return "tags"; + default: + break; + } + return {}; +} + +} // unnamed namespace + +bool CVideoDatabaseDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + std::string path = CLegacyPathTranslation::TranslateVideoDbPath(url); + items.SetPath(path); + items.m_dwSize = -1; // No size + std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path)); + + if (!pNode) + return false; + + bool bResult = pNode->GetChilds(items); + for (int i=0;i<items.Size();++i) + { + CFileItemPtr item = items[i]; + if (item->m_bIsFolder && !item->HasArt("icon") && !item->HasArt("thumb")) + { + std::string strImage = GetIcon(item->GetPath()); + if (!strImage.empty() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture(strImage)) + item->SetArt("icon", strImage); + } + if (item->GetVideoInfoTag()) + { + item->SetDynPath(item->GetVideoInfoTag()->GetPath()); + } + } + if (items.HasProperty("customtitle")) + items.SetLabel(items.GetProperty("customtitle").asString()); + else + items.SetLabel(pNode->GetLocalizedName()); + + items.SetContent(GetChildContentType(pNode)); + + return bResult; +} + +NODE_TYPE CVideoDatabaseDirectory::GetDirectoryChildType(const std::string& strPath) +{ + std::string path = CLegacyPathTranslation::TranslateVideoDbPath(strPath); + std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path)); + + if (!pNode) + return NODE_TYPE_NONE; + + return pNode->GetChildType(); +} + +NODE_TYPE CVideoDatabaseDirectory::GetDirectoryType(const std::string& strPath) +{ + std::string path = CLegacyPathTranslation::TranslateVideoDbPath(strPath); + std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path)); + + if (!pNode) + return NODE_TYPE_NONE; + + return pNode->GetType(); +} + +NODE_TYPE CVideoDatabaseDirectory::GetDirectoryParentType(const std::string& strPath) +{ + std::string path = CLegacyPathTranslation::TranslateVideoDbPath(strPath); + std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path)); + + if (!pNode) + return NODE_TYPE_NONE; + + CDirectoryNode* pParentNode=pNode->GetParent(); + + if (!pParentNode) + return NODE_TYPE_NONE; + + return pParentNode->GetChildType(); +} + +bool CVideoDatabaseDirectory::GetQueryParams(const std::string& strPath, CQueryParams& params) +{ + std::string path = CLegacyPathTranslation::TranslateVideoDbPath(strPath); + std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path)); + + if (!pNode) + return false; + + CDirectoryNode::GetDatabaseInfo(strPath,params); + return true; +} + +void CVideoDatabaseDirectory::ClearDirectoryCache(const std::string& strDirectory) +{ + std::string path = CLegacyPathTranslation::TranslateVideoDbPath(strDirectory); + URIUtils::RemoveSlashAtEnd(path); + + uint32_t crc = Crc32::ComputeFromLowerCase(path); + + std::string strFileName = StringUtils::Format("special://temp/archive_cache/{:08x}.fi", crc); + CFile::Delete(strFileName); +} + +bool CVideoDatabaseDirectory::IsAllItem(const std::string& strDirectory) +{ + if (StringUtils::EndsWith(strDirectory, "/-1/")) + return true; + return false; +} + +bool CVideoDatabaseDirectory::GetLabel(const std::string& strDirectory, std::string& strLabel) +{ + strLabel = ""; + + std::string path = CLegacyPathTranslation::TranslateVideoDbPath(strDirectory); + std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path)); + if (!pNode || path.empty()) + return false; + + // first see if there's any filter criteria + CQueryParams params; + CDirectoryNode::GetDatabaseInfo(path, params); + + CVideoDatabase videodatabase; + if (!videodatabase.Open()) + return false; + + // get genre + if (params.GetGenreId() != -1) + strLabel += videodatabase.GetGenreById(params.GetGenreId()); + + // get country + if (params.GetCountryId() != -1) + strLabel += videodatabase.GetCountryById(params.GetCountryId()); + + // get set + if (params.GetSetId() != -1) + strLabel += videodatabase.GetSetById(params.GetSetId()); + + // get tag + if (params.GetTagId() != -1) + strLabel += videodatabase.GetTagById(params.GetTagId()); + + // get year + if (params.GetYear() != -1) + { + std::string strTemp = std::to_string(params.GetYear()); + if (!strLabel.empty()) + strLabel += " / "; + strLabel += strTemp; + } + + if (strLabel.empty()) + { + switch (pNode->GetChildType()) + { + case NODE_TYPE_TITLE_MOVIES: + case NODE_TYPE_TITLE_TVSHOWS: + case NODE_TYPE_TITLE_MUSICVIDEOS: + strLabel = g_localizeStrings.Get(369); break; + case NODE_TYPE_ACTOR: // Actor + strLabel = g_localizeStrings.Get(344); break; + case NODE_TYPE_GENRE: // Genres + strLabel = g_localizeStrings.Get(135); break; + case NODE_TYPE_COUNTRY: // Countries + strLabel = g_localizeStrings.Get(20451); break; + case NODE_TYPE_YEAR: // Year + strLabel = g_localizeStrings.Get(562); break; + case NODE_TYPE_DIRECTOR: // Director + strLabel = g_localizeStrings.Get(20348); break; + case NODE_TYPE_SETS: // Sets + strLabel = g_localizeStrings.Get(20434); break; + case NODE_TYPE_TAGS: // Tags + strLabel = g_localizeStrings.Get(20459); break; + case NODE_TYPE_MOVIES_OVERVIEW: // Movies + strLabel = g_localizeStrings.Get(342); break; + case NODE_TYPE_TVSHOWS_OVERVIEW: // TV Shows + strLabel = g_localizeStrings.Get(20343); break; + case NODE_TYPE_RECENTLY_ADDED_MOVIES: // Recently Added Movies + strLabel = g_localizeStrings.Get(20386); break; + case NODE_TYPE_RECENTLY_ADDED_EPISODES: // Recently Added Episodes + strLabel = g_localizeStrings.Get(20387); break; + case NODE_TYPE_STUDIO: // Studios + strLabel = g_localizeStrings.Get(20388); break; + case NODE_TYPE_MUSICVIDEOS_OVERVIEW: // Music Videos + strLabel = g_localizeStrings.Get(20389); break; + case NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS: // Recently Added Music Videos + strLabel = g_localizeStrings.Get(20390); break; + case NODE_TYPE_SEASONS: // Seasons + strLabel = g_localizeStrings.Get(33054); break; + case NODE_TYPE_EPISODES: // Episodes + strLabel = g_localizeStrings.Get(20360); break; + case NODE_TYPE_INPROGRESS_TVSHOWS: // InProgress TvShows + strLabel = g_localizeStrings.Get(626); break; + default: + return false; + } + } + + return true; +} + +std::string CVideoDatabaseDirectory::GetIcon(const std::string &strDirectory) +{ + std::string path = CLegacyPathTranslation::TranslateVideoDbPath(strDirectory); + switch (GetDirectoryChildType(path)) + { + case NODE_TYPE_TITLE_MOVIES: + if (URIUtils::PathEquals(path, "videodb://movies/titles/")) + { + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_FLATTEN)) + return "DefaultMovies.png"; + return "DefaultMovieTitle.png"; + } + return ""; + case NODE_TYPE_TITLE_TVSHOWS: + if (URIUtils::PathEquals(path, "videodb://tvshows/titles/")) + { + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_FLATTEN)) + return "DefaultTVShows.png"; + return "DefaultTVShowTitle.png"; + } + return ""; + case NODE_TYPE_TITLE_MUSICVIDEOS: + if (URIUtils::PathEquals(path, "videodb://musicvideos/titles/")) + { + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_FLATTEN)) + return "DefaultMusicVideos.png"; + return "DefaultMusicVideoTitle.png"; + } + return ""; + case NODE_TYPE_ACTOR: // Actor + return "DefaultActor.png"; + case NODE_TYPE_GENRE: // Genres + return "DefaultGenre.png"; + case NODE_TYPE_COUNTRY: // Countries + return "DefaultCountry.png"; + case NODE_TYPE_SETS: // Sets + return "DefaultSets.png"; + case NODE_TYPE_TAGS: // Tags + return "DefaultTags.png"; + case NODE_TYPE_YEAR: // Year + return "DefaultYear.png"; + case NODE_TYPE_DIRECTOR: // Director + return "DefaultDirector.png"; + case NODE_TYPE_MOVIES_OVERVIEW: // Movies + return "DefaultMovies.png"; + case NODE_TYPE_TVSHOWS_OVERVIEW: // TV Shows + return "DefaultTVShows.png"; + case NODE_TYPE_RECENTLY_ADDED_MOVIES: // Recently Added Movies + return "DefaultRecentlyAddedMovies.png"; + case NODE_TYPE_RECENTLY_ADDED_EPISODES: // Recently Added Episodes + return "DefaultRecentlyAddedEpisodes.png"; + case NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS: // Recently Added Episodes + return "DefaultRecentlyAddedMusicVideos.png"; + case NODE_TYPE_INPROGRESS_TVSHOWS: // InProgress TvShows + return "DefaultInProgressShows.png"; + case NODE_TYPE_STUDIO: // Studios + return "DefaultStudios.png"; + case NODE_TYPE_MUSICVIDEOS_OVERVIEW: // Music Videos + return "DefaultMusicVideos.png"; + case NODE_TYPE_MUSICVIDEOS_ALBUM: // Music Videos - Albums + return "DefaultMusicAlbums.png"; + default: + break; + } + + return ""; +} + +bool CVideoDatabaseDirectory::ContainsMovies(const std::string &path) +{ + VIDEODATABASEDIRECTORY::NODE_TYPE type = GetDirectoryChildType(path); + if (type == VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_MOVIES || type == VIDEODATABASEDIRECTORY::NODE_TYPE_EPISODES || type == VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_MUSICVIDEOS) return true; + return false; +} + +bool CVideoDatabaseDirectory::Exists(const CURL& url) +{ + std::string path = CLegacyPathTranslation::TranslateVideoDbPath(url); + std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path)); + + if (!pNode) + return false; + + if (pNode->GetChildType() == VIDEODATABASEDIRECTORY::NODE_TYPE_NONE) + return false; + + return true; +} + +bool CVideoDatabaseDirectory::CanCache(const std::string& strPath) +{ + std::string path = CLegacyPathTranslation::TranslateVideoDbPath(strPath); + std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path)); + if (!pNode) + return false; + return pNode->CanCache(); +} diff --git a/xbmc/filesystem/VideoDatabaseDirectory.h b/xbmc/filesystem/VideoDatabaseDirectory.h new file mode 100644 index 0000000..9603b7d --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" +#include "VideoDatabaseDirectory/DirectoryNode.h" +#include "VideoDatabaseDirectory/QueryParams.h" + +namespace XFILE +{ + class CVideoDatabaseDirectory : public IDirectory + { + public: + CVideoDatabaseDirectory(void); + ~CVideoDatabaseDirectory(void) override; + bool GetDirectory(const CURL& url, CFileItemList &items) override; + bool Exists(const CURL& url) override; + bool AllowAll() const override { return true; } + static VIDEODATABASEDIRECTORY::NODE_TYPE GetDirectoryChildType(const std::string& strPath); + static VIDEODATABASEDIRECTORY::NODE_TYPE GetDirectoryType(const std::string& strPath); + static VIDEODATABASEDIRECTORY::NODE_TYPE GetDirectoryParentType(const std::string& strPath); + static bool GetQueryParams(const std::string& strPath, VIDEODATABASEDIRECTORY::CQueryParams& params); + void ClearDirectoryCache(const std::string& strDirectory); + static bool IsAllItem(const std::string& strDirectory); + static bool GetLabel(const std::string& strDirectory, std::string& strLabel); + static std::string GetIcon(const std::string& strDirectory); + bool ContainsMovies(const std::string &path); + static bool CanCache(const std::string &path); + }; +} diff --git a/xbmc/filesystem/VideoDatabaseDirectory/CMakeLists.txt b/xbmc/filesystem/VideoDatabaseDirectory/CMakeLists.txt new file mode 100644 index 0000000..149fa5c --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/CMakeLists.txt @@ -0,0 +1,37 @@ +set(SOURCES DirectoryNode.cpp + DirectoryNodeEpisodes.cpp + DirectoryNodeGrouped.cpp + DirectoryNodeInProgressTvShows.cpp + DirectoryNodeMoviesOverview.cpp + DirectoryNodeMusicVideosOverview.cpp + DirectoryNodeOverview.cpp + DirectoryNodeRecentlyAddedEpisodes.cpp + DirectoryNodeRecentlyAddedMovies.cpp + DirectoryNodeRecentlyAddedMusicVideos.cpp + DirectoryNodeRoot.cpp + DirectoryNodeSeasons.cpp + DirectoryNodeTitleMovies.cpp + DirectoryNodeTitleMusicVideos.cpp + DirectoryNodeTitleTvShows.cpp + DirectoryNodeTvShowsOverview.cpp + QueryParams.cpp) + +set(HEADERS DirectoryNode.h + DirectoryNodeEpisodes.h + DirectoryNodeGrouped.h + DirectoryNodeInProgressTvShows.h + DirectoryNodeMoviesOverview.h + DirectoryNodeMusicVideosOverview.h + DirectoryNodeOverview.h + DirectoryNodeRecentlyAddedEpisodes.h + DirectoryNodeRecentlyAddedMovies.h + DirectoryNodeRecentlyAddedMusicVideos.h + DirectoryNodeRoot.h + DirectoryNodeSeasons.h + DirectoryNodeTitleMovies.h + DirectoryNodeTitleMusicVideos.h + DirectoryNodeTitleTvShows.h + DirectoryNodeTvShowsOverview.h + QueryParams.h) + +core_add_library(videodatabasedirectory) diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNode.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNode.cpp new file mode 100644 index 0000000..7f83cff --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNode.cpp @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNode.h" + +#include "DirectoryNodeEpisodes.h" +#include "DirectoryNodeGrouped.h" +#include "DirectoryNodeInProgressTvShows.h" +#include "DirectoryNodeMoviesOverview.h" +#include "DirectoryNodeMusicVideosOverview.h" +#include "DirectoryNodeOverview.h" +#include "DirectoryNodeRecentlyAddedEpisodes.h" +#include "DirectoryNodeRecentlyAddedMovies.h" +#include "DirectoryNodeRecentlyAddedMusicVideos.h" +#include "DirectoryNodeRoot.h" +#include "DirectoryNodeSeasons.h" +#include "DirectoryNodeTitleMovies.h" +#include "DirectoryNodeTitleMusicVideos.h" +#include "DirectoryNodeTitleTvShows.h" +#include "DirectoryNodeTvShowsOverview.h" +#include "FileItem.h" +#include "QueryParams.h" +#include "URL.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +using namespace XFILE::VIDEODATABASEDIRECTORY; + +// Constructor is protected use ParseURL() +CDirectoryNode::CDirectoryNode(NODE_TYPE Type, const std::string& strName, CDirectoryNode* pParent) +{ + m_Type = Type; + m_strName = strName; + m_pParent = pParent; +} + +CDirectoryNode::~CDirectoryNode() +{ + delete m_pParent, m_pParent = nullptr; +} + +// Parses a given path and returns the current node of the path +CDirectoryNode* CDirectoryNode::ParseURL(const std::string& strPath) +{ + CURL url(strPath); + + std::string strDirectory = url.GetFileName(); + URIUtils::RemoveSlashAtEnd(strDirectory); + + std::vector<std::string> Path = StringUtils::Tokenize(strDirectory, '/'); + // we always have a root node, it is special and has a path of "" + Path.insert(Path.begin(), ""); + + CDirectoryNode *pNode = nullptr; + CDirectoryNode *pParent = nullptr; + NODE_TYPE NodeType = NODE_TYPE_ROOT; + // loop down the dir path, creating a node with a parent. + // if we hit a child type of NODE_TYPE_NONE, then we are done. + for (size_t i = 0; i < Path.size() && NodeType != NODE_TYPE_NONE; ++i) + { + pNode = CDirectoryNode::CreateNode(NodeType, Path[i], pParent); + NodeType = pNode ? pNode->GetChildType() : NODE_TYPE_NONE; + pParent = pNode; + } + + // Add all the additional URL options to the last node + if (pNode) + pNode->AddOptions(url.GetOptions()); + + return pNode; +} + +// returns the database ids of the path, +void CDirectoryNode::GetDatabaseInfo(const std::string& strPath, CQueryParams& params) +{ + std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(strPath)); + + if (!pNode) + return; + + pNode->CollectQueryParams(params); +} + +// Create a node object +CDirectoryNode* CDirectoryNode::CreateNode(NODE_TYPE Type, const std::string& strName, CDirectoryNode* pParent) +{ + switch (Type) + { + case NODE_TYPE_ROOT: + return new CDirectoryNodeRoot(strName, pParent); + case NODE_TYPE_OVERVIEW: + return new CDirectoryNodeOverview(strName, pParent); + case NODE_TYPE_GENRE: + case NODE_TYPE_COUNTRY: + case NODE_TYPE_SETS: + case NODE_TYPE_TAGS: + case NODE_TYPE_YEAR: + case NODE_TYPE_ACTOR: + case NODE_TYPE_DIRECTOR: + case NODE_TYPE_STUDIO: + case NODE_TYPE_MUSICVIDEOS_ALBUM: + return new CDirectoryNodeGrouped(Type, strName, pParent); + case NODE_TYPE_TITLE_MOVIES: + return new CDirectoryNodeTitleMovies(strName, pParent); + case NODE_TYPE_TITLE_TVSHOWS: + return new CDirectoryNodeTitleTvShows(strName, pParent); + case NODE_TYPE_MOVIES_OVERVIEW: + return new CDirectoryNodeMoviesOverview(strName, pParent); + case NODE_TYPE_TVSHOWS_OVERVIEW: + return new CDirectoryNodeTvShowsOverview(strName, pParent); + case NODE_TYPE_SEASONS: + return new CDirectoryNodeSeasons(strName, pParent); + case NODE_TYPE_EPISODES: + return new CDirectoryNodeEpisodes(strName, pParent); + case NODE_TYPE_RECENTLY_ADDED_MOVIES: + return new CDirectoryNodeRecentlyAddedMovies(strName,pParent); + case NODE_TYPE_RECENTLY_ADDED_EPISODES: + return new CDirectoryNodeRecentlyAddedEpisodes(strName,pParent); + case NODE_TYPE_MUSICVIDEOS_OVERVIEW: + return new CDirectoryNodeMusicVideosOverview(strName,pParent); + case NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS: + return new CDirectoryNodeRecentlyAddedMusicVideos(strName,pParent); + case NODE_TYPE_INPROGRESS_TVSHOWS: + return new CDirectoryNodeInProgressTvShows(strName,pParent); + case NODE_TYPE_TITLE_MUSICVIDEOS: + return new CDirectoryNodeTitleMusicVideos(strName,pParent); + default: + break; + } + + return nullptr; +} + +// Current node name +const std::string& CDirectoryNode::GetName() const +{ + return m_strName; +} + +int CDirectoryNode::GetID() const +{ + return atoi(m_strName.c_str()); +} + +std::string CDirectoryNode::GetLocalizedName() const +{ + return ""; +} + +// Current node type +NODE_TYPE CDirectoryNode::GetType() const +{ + return m_Type; +} + +// Return the parent directory node or NULL, if there is no +CDirectoryNode* CDirectoryNode::GetParent() const +{ + return m_pParent; +} + +void CDirectoryNode::RemoveParent() +{ + m_pParent = nullptr; +} + +// should be overloaded by a derived class +// to get the content of a node. Will be called +// by GetChilds() of a parent node +bool CDirectoryNode::GetContent(CFileItemList& items) const +{ + return false; +} + +// Creates a videodb url +std::string CDirectoryNode::BuildPath() const +{ + std::vector<std::string> array; + + if (!m_strName.empty()) + array.insert(array.begin(), m_strName); + + CDirectoryNode* pParent=m_pParent; + while (pParent != nullptr) + { + const std::string& strNodeName=pParent->GetName(); + if (!strNodeName.empty()) + array.insert(array.begin(), strNodeName); + + pParent = pParent->GetParent(); + } + + std::string strPath="videodb://"; + for (int i = 0; i < static_cast<int>(array.size()); ++i) + strPath += array[i]+"/"; + + std::string options = m_options.GetOptionsString(); + if (!options.empty()) + strPath += "?" + options; + + return strPath; +} + +void CDirectoryNode::AddOptions(const std::string &options) +{ + if (options.empty()) + return; + + m_options.AddOptions(options); +} + +// Collects Query params from this and all parent nodes. If a NODE_TYPE can +// be used as a database parameter, it will be added to the +// params object. +void CDirectoryNode::CollectQueryParams(CQueryParams& params) const +{ + params.SetQueryParam(m_Type, m_strName); + + CDirectoryNode* pParent=m_pParent; + while (pParent != nullptr) + { + params.SetQueryParam(pParent->GetType(), pParent->GetName()); + pParent = pParent->GetParent(); + } +} + +// Should be overloaded by a derived class. +// Returns the NODE_TYPE of the child nodes. +NODE_TYPE CDirectoryNode::GetChildType() const +{ + return NODE_TYPE_NONE; +} + +// Get the child fileitems of this node +bool CDirectoryNode::GetChilds(CFileItemList& items) +{ + if (CanCache() && items.Load()) + return true; + + std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::CreateNode(GetChildType(), "", this)); + + bool bSuccess=false; + if (pNode) + { + pNode->m_options = m_options; + bSuccess = pNode->GetContent(items); + if (bSuccess) + { + if (CanCache()) + items.SetCacheToDisc(CFileItemList::CACHE_ALWAYS); + } + else + items.Clear(); + + pNode->RemoveParent(); + } + + return bSuccess; +} + +bool CDirectoryNode::CanCache() const +{ + // no caching is required - the list is cached in CGUIMediaWindow::GetDirectory + // if it was slow to fetch anyway. + return false; +} diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNode.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNode.h new file mode 100644 index 0000000..7ab27c5 --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNode.h @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "utils/UrlOptions.h" + +#include <string> + +class CFileItemList; + +namespace XFILE +{ + namespace VIDEODATABASEDIRECTORY + { + class CQueryParams; + + typedef enum _NODE_TYPE + { + NODE_TYPE_NONE=0, + NODE_TYPE_MOVIES_OVERVIEW, + NODE_TYPE_TVSHOWS_OVERVIEW, + NODE_TYPE_GENRE, + NODE_TYPE_ACTOR, + NODE_TYPE_ROOT, + NODE_TYPE_OVERVIEW, + NODE_TYPE_TITLE_MOVIES, + NODE_TYPE_YEAR, + NODE_TYPE_DIRECTOR, + NODE_TYPE_TITLE_TVSHOWS, + NODE_TYPE_SEASONS, + NODE_TYPE_EPISODES, + NODE_TYPE_RECENTLY_ADDED_MOVIES, + NODE_TYPE_RECENTLY_ADDED_EPISODES, + NODE_TYPE_STUDIO, + NODE_TYPE_MUSICVIDEOS_OVERVIEW, + NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS, + NODE_TYPE_TITLE_MUSICVIDEOS, + NODE_TYPE_MUSICVIDEOS_ALBUM, + NODE_TYPE_SETS, + NODE_TYPE_COUNTRY, + NODE_TYPE_TAGS, + NODE_TYPE_INPROGRESS_TVSHOWS + } NODE_TYPE; + + typedef struct { + NODE_TYPE node; + std::string id; + int label; + } Node; + + class CDirectoryNode + { + public: + static CDirectoryNode* ParseURL(const std::string& strPath); + static void GetDatabaseInfo(const std::string& strPath, CQueryParams& params); + virtual ~CDirectoryNode(); + + NODE_TYPE GetType() const; + + bool GetChilds(CFileItemList& items); + virtual NODE_TYPE GetChildType() const; + virtual std::string GetLocalizedName() const; + void CollectQueryParams(CQueryParams& params) const; + + CDirectoryNode* GetParent() const; + + std::string BuildPath() const; + + virtual bool CanCache() const; + protected: + CDirectoryNode(NODE_TYPE Type, const std::string& strName, CDirectoryNode* pParent); + static CDirectoryNode* CreateNode(NODE_TYPE Type, const std::string& strName, CDirectoryNode* pParent); + + void AddOptions(const std::string& options); + + const std::string& GetName() const; + int GetID() const; + void RemoveParent(); + + virtual bool GetContent(CFileItemList& items) const; + + + private: + NODE_TYPE m_Type; + std::string m_strName; + CDirectoryNode* m_pParent; + CUrlOptions m_options; + }; + } +} + + + diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeEpisodes.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeEpisodes.cpp new file mode 100644 index 0000000..9f629ba --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeEpisodes.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeEpisodes.h" + +#include "FileItem.h" +#include "QueryParams.h" +#include "video/VideoDatabase.h" + +using namespace XFILE::VIDEODATABASEDIRECTORY; + +CDirectoryNodeEpisodes::CDirectoryNodeEpisodes(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_EPISODES, strName, pParent) +{ + +} + +bool CDirectoryNodeEpisodes::GetContent(CFileItemList& items) const +{ + CVideoDatabase videodatabase; + if (!videodatabase.Open()) + return false; + + CQueryParams params; + CollectQueryParams(params); + + int season = (int)params.GetSeason(); + if (season == -2) + season = -1; + + int details = items.HasProperty("set_videodb_details") + ? items.GetProperty("set_videodb_details").asInteger32() + : VideoDbDetailsNone; + + bool bSuccess = videodatabase.GetEpisodesNav( + BuildPath(), items, params.GetGenreId(), params.GetYear(), params.GetActorId(), + params.GetDirectorId(), params.GetTvShowId(), season, SortDescription(), details); + + videodatabase.Close(); + + return bSuccess; +} + +NODE_TYPE CDirectoryNodeEpisodes::GetChildType() const +{ + return NODE_TYPE_EPISODES; +} diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeEpisodes.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeEpisodes.h new file mode 100644 index 0000000..e5a640a --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeEpisodes.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace VIDEODATABASEDIRECTORY + { + class CDirectoryNodeEpisodes : public CDirectoryNode + { + public: + CDirectoryNodeEpisodes(const std::string& strEntryName, CDirectoryNode* pParent); + protected: + bool GetContent(CFileItemList& items) const override; + NODE_TYPE GetChildType() const override; + }; + } +} diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeGrouped.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeGrouped.cpp new file mode 100644 index 0000000..73ec115 --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeGrouped.cpp @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeGrouped.h" + +#include "QueryParams.h" +#include "video/VideoDatabase.h" +#include "video/VideoDbUrl.h" + +using namespace XFILE::VIDEODATABASEDIRECTORY; + +CDirectoryNodeGrouped::CDirectoryNodeGrouped(NODE_TYPE type, const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(type, strName, pParent) +{ } + +NODE_TYPE CDirectoryNodeGrouped::GetChildType() const +{ + CQueryParams params; + CollectQueryParams(params); + + VideoDbContentType type = static_cast<VideoDbContentType>(params.GetContentType()); + if (type == VideoDbContentType::MOVIES) + return NODE_TYPE_TITLE_MOVIES; + if (type == VideoDbContentType::MUSICVIDEOS) + { + if (GetType() == NODE_TYPE_ACTOR) + return NODE_TYPE_MUSICVIDEOS_ALBUM; + else + return NODE_TYPE_TITLE_MUSICVIDEOS; + } + + return NODE_TYPE_TITLE_TVSHOWS; +} + +std::string CDirectoryNodeGrouped::GetLocalizedName() const +{ + CVideoDatabase db; + if (db.Open()) + return db.GetItemById(GetContentType(), GetID()); + + return ""; +} + +bool CDirectoryNodeGrouped::GetContent(CFileItemList& items) const +{ + CVideoDatabase videodatabase; + if (!videodatabase.Open()) + return false; + + CQueryParams params; + CollectQueryParams(params); + + std::string itemType = GetContentType(params); + if (itemType.empty()) + return false; + + // make sure to translate all IDs in the path into URL parameters + CVideoDbUrl videoUrl; + if (!videoUrl.FromString(BuildPath())) + return false; + + return videodatabase.GetItems(videoUrl.ToString(), + static_cast<VideoDbContentType>(params.GetContentType()), itemType, + items); +} + +std::string CDirectoryNodeGrouped::GetContentType() const +{ + CQueryParams params; + CollectQueryParams(params); + + return GetContentType(params); +} + +std::string CDirectoryNodeGrouped::GetContentType(const CQueryParams ¶ms) const +{ + switch (GetType()) + { + case NODE_TYPE_GENRE: + return "genres"; + case NODE_TYPE_COUNTRY: + return "countries"; + case NODE_TYPE_SETS: + return "sets"; + case NODE_TYPE_TAGS: + return "tags"; + case NODE_TYPE_YEAR: + return "years"; + case NODE_TYPE_ACTOR: + if (static_cast<VideoDbContentType>(params.GetContentType()) == + VideoDbContentType::MUSICVIDEOS) + return "artists"; + else + return "actors"; + case NODE_TYPE_DIRECTOR: + return "directors"; + case NODE_TYPE_STUDIO: + return "studios"; + case NODE_TYPE_MUSICVIDEOS_ALBUM: + return "albums"; + + case NODE_TYPE_EPISODES: + case NODE_TYPE_MOVIES_OVERVIEW: + case NODE_TYPE_MUSICVIDEOS_OVERVIEW: + case NODE_TYPE_NONE: + case NODE_TYPE_OVERVIEW: + case NODE_TYPE_RECENTLY_ADDED_EPISODES: + case NODE_TYPE_RECENTLY_ADDED_MOVIES: + case NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS: + case NODE_TYPE_INPROGRESS_TVSHOWS: + case NODE_TYPE_ROOT: + case NODE_TYPE_SEASONS: + case NODE_TYPE_TITLE_MOVIES: + case NODE_TYPE_TITLE_MUSICVIDEOS: + case NODE_TYPE_TITLE_TVSHOWS: + case NODE_TYPE_TVSHOWS_OVERVIEW: + default: + break; + } + + return ""; +} diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeGrouped.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeGrouped.h new file mode 100644 index 0000000..63a5929 --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeGrouped.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace VIDEODATABASEDIRECTORY + { + class CDirectoryNodeGrouped : public CDirectoryNode + { + public: + CDirectoryNodeGrouped(NODE_TYPE type, const std::string& strName, CDirectoryNode* pParent); + protected: + NODE_TYPE GetChildType() const override; + bool GetContent(CFileItemList& items) const override; + std::string GetLocalizedName() const override; + + private: + std::string GetContentType() const; + std::string GetContentType(const CQueryParams ¶ms) const; + }; + } +} + + diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeInProgressTvShows.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeInProgressTvShows.cpp new file mode 100644 index 0000000..044f8b3 --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeInProgressTvShows.cpp @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeInProgressTvShows.h" + +#include "FileItem.h" +#include "video/VideoDatabase.h" + +using namespace XFILE::VIDEODATABASEDIRECTORY; + +CDirectoryNodeInProgressTvShows::CDirectoryNodeInProgressTvShows(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_INPROGRESS_TVSHOWS, strName, pParent) +{ + +} + +NODE_TYPE CDirectoryNodeInProgressTvShows::GetChildType() const +{ + return NODE_TYPE_SEASONS; +} + +std::string CDirectoryNodeInProgressTvShows::GetLocalizedName() const +{ + CVideoDatabase db; + if (db.Open()) + return db.GetTvShowTitleById(GetID()); + return ""; +} + +bool CDirectoryNodeInProgressTvShows::GetContent(CFileItemList& items) const +{ + CVideoDatabase videodatabase; + if (!videodatabase.Open()) + return false; + + int details = items.HasProperty("set_videodb_details") + ? items.GetProperty("set_videodb_details").asInteger32() + : VideoDbDetailsNone; + bool bSuccess = videodatabase.GetInProgressTvShowsNav(BuildPath(), items, 0, details); + + videodatabase.Close(); + + return bSuccess; +} diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeInProgressTvShows.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeInProgressTvShows.h new file mode 100644 index 0000000..987a166 --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeInProgressTvShows.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace VIDEODATABASEDIRECTORY + { + class CDirectoryNodeInProgressTvShows : public CDirectoryNode + { + public: + CDirectoryNodeInProgressTvShows(const std::string& strEntryName, CDirectoryNode* pParent); + protected: + NODE_TYPE GetChildType() const override; + bool GetContent(CFileItemList& items) const override; + std::string GetLocalizedName() const override; + }; + } +} + + diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMoviesOverview.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMoviesOverview.cpp new file mode 100644 index 0000000..20ef46e --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMoviesOverview.cpp @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeMoviesOverview.h" + +#include "FileItem.h" +#include "guilib/LocalizeStrings.h" +#include "utils/StringUtils.h" +#include "video/VideoDatabase.h" +#include "video/VideoDbUrl.h" + +using namespace XFILE::VIDEODATABASEDIRECTORY; + +Node MovieChildren[] = { + { NODE_TYPE_GENRE, "genres", 135 }, + { NODE_TYPE_TITLE_MOVIES, "titles", 10024 }, + { NODE_TYPE_YEAR, "years", 652 }, + { NODE_TYPE_ACTOR, "actors", 344 }, + { NODE_TYPE_DIRECTOR, "directors", 20348 }, + { NODE_TYPE_STUDIO, "studios", 20388 }, + { NODE_TYPE_SETS, "sets", 20434 }, + { NODE_TYPE_COUNTRY, "countries", 20451 }, + { NODE_TYPE_TAGS, "tags", 20459 } + }; + +CDirectoryNodeMoviesOverview::CDirectoryNodeMoviesOverview(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_MOVIES_OVERVIEW, strName, pParent) +{ + +} + +NODE_TYPE CDirectoryNodeMoviesOverview::GetChildType() const +{ + for (const Node& node : MovieChildren) + if (GetName() == node.id) + return node.node; + + return NODE_TYPE_NONE; +} + +std::string CDirectoryNodeMoviesOverview::GetLocalizedName() const +{ + for (const Node& node : MovieChildren) + if (GetName() == node.id) + return g_localizeStrings.Get(node.label); + return ""; +} + +bool CDirectoryNodeMoviesOverview::GetContent(CFileItemList& items) const +{ + CVideoDbUrl videoUrl; + if (!videoUrl.FromString(BuildPath())) + return false; + + for (unsigned int i = 0; i < sizeof(MovieChildren) / sizeof(Node); ++i) + { + if (i == 6) + { + CVideoDatabase db; + if (db.Open() && !db.HasSets()) + continue; + } + + CVideoDbUrl itemUrl = videoUrl; + std::string strDir = StringUtils::Format("{}/", MovieChildren[i].id); + itemUrl.AppendPath(strDir); + + CFileItemPtr pItem(new CFileItem(itemUrl.ToString(), true)); + pItem->SetLabel(g_localizeStrings.Get(MovieChildren[i].label)); + pItem->SetCanQueue(false); + items.Add(pItem); + } + + return true; +} diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMoviesOverview.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMoviesOverview.h new file mode 100644 index 0000000..0f32e28 --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMoviesOverview.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace VIDEODATABASEDIRECTORY + { + class CDirectoryNodeMoviesOverview : public CDirectoryNode + { + public: + CDirectoryNodeMoviesOverview(const std::string& strName, CDirectoryNode* pParent); + protected: + NODE_TYPE GetChildType() const override; + bool GetContent(CFileItemList& items) const override; + std::string GetLocalizedName() const override; + }; + } +} + + diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMusicVideosOverview.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMusicVideosOverview.cpp new file mode 100644 index 0000000..b71d8f1 --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMusicVideosOverview.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeMusicVideosOverview.h" + +#include "FileItem.h" +#include "guilib/LocalizeStrings.h" +#include "utils/StringUtils.h" +#include "video/VideoDbUrl.h" + +using namespace XFILE::VIDEODATABASEDIRECTORY; + +Node MusicVideoChildren[] = { + { NODE_TYPE_GENRE, "genres", 135 }, + { NODE_TYPE_TITLE_MUSICVIDEOS, "titles", 10024 }, + { NODE_TYPE_YEAR, "years", 652 }, + { NODE_TYPE_ACTOR, "artists", 133 }, + { NODE_TYPE_MUSICVIDEOS_ALBUM, "albums", 132 }, + { NODE_TYPE_DIRECTOR, "directors", 20348 }, + { NODE_TYPE_STUDIO, "studios", 20388 }, + { NODE_TYPE_TAGS, "tags", 20459 } + }; + +CDirectoryNodeMusicVideosOverview::CDirectoryNodeMusicVideosOverview(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_MUSICVIDEOS_OVERVIEW, strName, pParent) +{ + +} + +NODE_TYPE CDirectoryNodeMusicVideosOverview::GetChildType() const +{ + for (const Node& node : MusicVideoChildren) + if (GetName() == node.id) + return node.node; + + return NODE_TYPE_NONE; +} + +std::string CDirectoryNodeMusicVideosOverview::GetLocalizedName() const +{ + for (const Node& node : MusicVideoChildren) + if (GetName() == node.id) + return g_localizeStrings.Get(node.label); + return ""; +} + +bool CDirectoryNodeMusicVideosOverview::GetContent(CFileItemList& items) const +{ + CVideoDbUrl videoUrl; + if (!videoUrl.FromString(BuildPath())) + return false; + + for (const Node& node : MusicVideoChildren) + { + CFileItemPtr pItem(new CFileItem(g_localizeStrings.Get(node.label))); + + CVideoDbUrl itemUrl = videoUrl; + std::string strDir = StringUtils::Format("{}/", node.id); + itemUrl.AppendPath(strDir); + pItem->SetPath(itemUrl.ToString()); + + pItem->m_bIsFolder = true; + pItem->SetCanQueue(false); + pItem->SetSpecialSort(SortSpecialOnTop); + items.Add(pItem); + } + + return true; +} + diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMusicVideosOverview.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMusicVideosOverview.h new file mode 100644 index 0000000..4a79592 --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMusicVideosOverview.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace VIDEODATABASEDIRECTORY + { + class CDirectoryNodeMusicVideosOverview : public CDirectoryNode + { + public: + CDirectoryNodeMusicVideosOverview(const std::string& strName, CDirectoryNode* pParent); + protected: + NODE_TYPE GetChildType() const override; + bool GetContent(CFileItemList& items) const override; + std::string GetLocalizedName() const override; + }; + } +} + + diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeOverview.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeOverview.cpp new file mode 100644 index 0000000..b52ae24 --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeOverview.cpp @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeOverview.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "guilib/LocalizeStrings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "video/VideoDatabase.h" + +#include <utility> + +using namespace XFILE::VIDEODATABASEDIRECTORY; + +Node OverviewChildren[] = { + { NODE_TYPE_MOVIES_OVERVIEW, "movies", 342 }, + { NODE_TYPE_TVSHOWS_OVERVIEW, "tvshows", 20343 }, + { NODE_TYPE_MUSICVIDEOS_OVERVIEW, "musicvideos", 20389 }, + { NODE_TYPE_RECENTLY_ADDED_MOVIES, "recentlyaddedmovies", 20386 }, + { NODE_TYPE_RECENTLY_ADDED_EPISODES, "recentlyaddedepisodes", 20387 }, + { NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS, "recentlyaddedmusicvideos", 20390 }, + { NODE_TYPE_INPROGRESS_TVSHOWS, "inprogresstvshows", 626 }, + }; + +CDirectoryNodeOverview::CDirectoryNodeOverview(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_OVERVIEW, strName, pParent) +{ + +} + +NODE_TYPE CDirectoryNodeOverview::GetChildType() const +{ + for (const Node& node : OverviewChildren) + if (GetName() == node.id) + return node.node; + + return NODE_TYPE_NONE; +} + +std::string CDirectoryNodeOverview::GetLocalizedName() const +{ + for (const Node& node : OverviewChildren) + if (GetName() == node.id) + return g_localizeStrings.Get(node.label); + return ""; +} + +bool CDirectoryNodeOverview::GetContent(CFileItemList& items) const +{ + CVideoDatabase database; + database.Open(); + bool hasMovies = database.HasContent(VideoDbContentType::MOVIES); + bool hasTvShows = database.HasContent(VideoDbContentType::TVSHOWS); + bool hasMusicVideos = database.HasContent(VideoDbContentType::MUSICVIDEOS); + std::vector<std::pair<const char*, int> > vec; + if (hasMovies) + { + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_FLATTEN)) + vec.emplace_back("movies/titles", 342); + else + vec.emplace_back("movies", 342); // Movies + } + if (hasTvShows) + { + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_FLATTEN)) + vec.emplace_back("tvshows/titles", 20343); + else + vec.emplace_back("tvshows", 20343); // TV Shows + } + if (hasMusicVideos) + { + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_FLATTEN)) + vec.emplace_back("musicvideos/titles", 20389); + else + vec.emplace_back("musicvideos", 20389); // Music Videos + } + { + if (hasMovies) + vec.emplace_back("recentlyaddedmovies", 20386); // Recently Added Movies + if (hasTvShows) + { + vec.emplace_back("recentlyaddedepisodes", 20387); // Recently Added Episodes + vec.emplace_back("inprogresstvshows", 626); // InProgress TvShows + } + if (hasMusicVideos) + vec.emplace_back("recentlyaddedmusicvideos", 20390); // Recently Added Music Videos + } + std::string path = BuildPath(); + for (unsigned int i = 0; i < vec.size(); ++i) + { + CFileItemPtr pItem(new CFileItem(path + vec[i].first + "/", true)); + pItem->SetLabel(g_localizeStrings.Get(vec[i].second)); + pItem->SetLabelPreformatted(true); + pItem->SetCanQueue(false); + items.Add(pItem); + } + + return true; +} diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeOverview.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeOverview.h new file mode 100644 index 0000000..5ad498b --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeOverview.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace VIDEODATABASEDIRECTORY + { + class CDirectoryNodeOverview : public CDirectoryNode + { + public: + CDirectoryNodeOverview(const std::string& strName, CDirectoryNode* pParent); + protected: + NODE_TYPE GetChildType() const override; + bool GetContent(CFileItemList& items) const override; + std::string GetLocalizedName() const override; + }; + } +} + + diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedEpisodes.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedEpisodes.cpp new file mode 100644 index 0000000..fe50dad --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedEpisodes.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeRecentlyAddedEpisodes.h" + +#include "FileItem.h" +#include "video/VideoDatabase.h" + +using namespace XFILE::VIDEODATABASEDIRECTORY; + +CDirectoryNodeRecentlyAddedEpisodes::CDirectoryNodeRecentlyAddedEpisodes(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_RECENTLY_ADDED_EPISODES, strName, pParent) +{ + +} + +bool CDirectoryNodeRecentlyAddedEpisodes::GetContent(CFileItemList& items) const +{ + CVideoDatabase videodatabase; + if (!videodatabase.Open()) + return false; + + int details = items.HasProperty("set_videodb_details") + ? items.GetProperty("set_videodb_details").asInteger32() + : VideoDbDetailsNone; + bool bSuccess = videodatabase.GetRecentlyAddedEpisodesNav(BuildPath(), items, 0, details); + + videodatabase.Close(); + + return bSuccess; +} diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedEpisodes.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedEpisodes.h new file mode 100644 index 0000000..2cb1e6c --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedEpisodes.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace VIDEODATABASEDIRECTORY + { + class CDirectoryNodeRecentlyAddedEpisodes : public CDirectoryNode + { + public: + CDirectoryNodeRecentlyAddedEpisodes(const std::string& strEntryName, CDirectoryNode* pParent); + protected: + bool GetContent(CFileItemList& items) const override; + }; + } +} + + diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMovies.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMovies.cpp new file mode 100644 index 0000000..202d6f7 --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMovies.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeRecentlyAddedMovies.h" + +#include "FileItem.h" +#include "video/VideoDatabase.h" + +using namespace XFILE::VIDEODATABASEDIRECTORY; + +CDirectoryNodeRecentlyAddedMovies::CDirectoryNodeRecentlyAddedMovies(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_RECENTLY_ADDED_MOVIES, strName, pParent) +{ + +} + +bool CDirectoryNodeRecentlyAddedMovies::GetContent(CFileItemList& items) const +{ + CVideoDatabase videodatabase; + if (!videodatabase.Open()) + return false; + + int details = items.HasProperty("set_videodb_details") + ? items.GetProperty("set_videodb_details").asInteger32() + : VideoDbDetailsNone; + bool bSuccess = videodatabase.GetRecentlyAddedMoviesNav(BuildPath(), items, 0, details); + + videodatabase.Close(); + + return bSuccess; +} diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMovies.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMovies.h new file mode 100644 index 0000000..318e0fc --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMovies.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace VIDEODATABASEDIRECTORY + { + class CDirectoryNodeRecentlyAddedMovies : public CDirectoryNode + { + public: + CDirectoryNodeRecentlyAddedMovies(const std::string& strEntryName, CDirectoryNode* pParent); + protected: + bool GetContent(CFileItemList& items) const override; + }; + } +} + + diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMusicVideos.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMusicVideos.cpp new file mode 100644 index 0000000..d8ba5ef --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMusicVideos.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeRecentlyAddedMusicVideos.h" + +#include "FileItem.h" +#include "video/VideoDatabase.h" + +using namespace XFILE::VIDEODATABASEDIRECTORY; + +CDirectoryNodeRecentlyAddedMusicVideos::CDirectoryNodeRecentlyAddedMusicVideos(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS, strName, pParent) +{ + +} + +bool CDirectoryNodeRecentlyAddedMusicVideos::GetContent(CFileItemList& items) const +{ + CVideoDatabase videodatabase; + if (!videodatabase.Open()) + return false; + + int details = items.HasProperty("set_videodb_details") + ? items.GetProperty("set_videodb_details").asInteger32() + : VideoDbDetailsNone; + bool bSuccess = videodatabase.GetRecentlyAddedMusicVideosNav(BuildPath(), items, 0, details); + + videodatabase.Close(); + + return bSuccess; +} + diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMusicVideos.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMusicVideos.h new file mode 100644 index 0000000..bbc9711 --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMusicVideos.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace VIDEODATABASEDIRECTORY + { + class CDirectoryNodeRecentlyAddedMusicVideos : public CDirectoryNode + { + public: + CDirectoryNodeRecentlyAddedMusicVideos(const std::string& strEntryName, CDirectoryNode* pParent); + protected: + bool GetContent(CFileItemList& items) const override; + }; + } +} + + diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRoot.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRoot.cpp new file mode 100644 index 0000000..6e1b69c --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRoot.cpp @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeRoot.h" + +using namespace XFILE::VIDEODATABASEDIRECTORY; + +CDirectoryNodeRoot::CDirectoryNodeRoot(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_ROOT, strName, pParent) +{ + +} + +NODE_TYPE CDirectoryNodeRoot::GetChildType() const +{ + return NODE_TYPE_OVERVIEW; +} diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRoot.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRoot.h new file mode 100644 index 0000000..99f8e6e --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRoot.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace VIDEODATABASEDIRECTORY + { + class CDirectoryNodeRoot : public CDirectoryNode + { + public: + CDirectoryNodeRoot(const std::string& strName, CDirectoryNode* pParent); + protected: + NODE_TYPE GetChildType() const override; + }; + } +} + + diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeSeasons.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeSeasons.cpp new file mode 100644 index 0000000..25b8b09 --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeSeasons.cpp @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeSeasons.h" + +#include "FileItem.h" +#include "QueryParams.h" +#include "guilib/LocalizeStrings.h" +#include "utils/StringUtils.h" +#include "video/VideoDatabase.h" + +using namespace XFILE::VIDEODATABASEDIRECTORY; + +CDirectoryNodeSeasons::CDirectoryNodeSeasons(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_SEASONS, strName, pParent) +{ + +} + +NODE_TYPE CDirectoryNodeSeasons::GetChildType() const +{ + return NODE_TYPE_EPISODES; +} + +std::string CDirectoryNodeSeasons::GetLocalizedName() const +{ + switch (GetID()) + { + case 0: + return g_localizeStrings.Get(20381); // Specials + case -1: + return g_localizeStrings.Get(20366); // All Seasons + case -2: + { + CDirectoryNode* pParent = GetParent(); + if (pParent) + return pParent->GetLocalizedName(); + return ""; + } + default: + return GetSeasonTitle(); + } +} + +std::string CDirectoryNodeSeasons::GetSeasonTitle() const +{ + std::string season; + CVideoDatabase db; + if (db.Open()) + { + CQueryParams params; + CollectQueryParams(params); + + season = db.GetTvShowNamedSeasonById(params.GetTvShowId(), params.GetSeason()); + } + if (season.empty()) + season = StringUtils::Format(g_localizeStrings.Get(20358), GetID()); // Season <n> + + return season; +} + +bool CDirectoryNodeSeasons::GetContent(CFileItemList& items) const +{ + CVideoDatabase videodatabase; + if (!videodatabase.Open()) + return false; + + CQueryParams params; + CollectQueryParams(params); + + bool bSuccess=videodatabase.GetSeasonsNav(BuildPath(), items, params.GetActorId(), params.GetDirectorId(), params.GetGenreId(), params.GetYear(), params.GetTvShowId()); + + videodatabase.Close(); + + return bSuccess; +} diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeSeasons.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeSeasons.h new file mode 100644 index 0000000..d5dd3a1 --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeSeasons.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace VIDEODATABASEDIRECTORY + { + class CDirectoryNodeSeasons : public CDirectoryNode + { + public: + CDirectoryNodeSeasons(const std::string& strName, CDirectoryNode* pParent); + protected: + NODE_TYPE GetChildType() const override; + bool GetContent(CFileItemList& items) const override; + std::string GetLocalizedName() const override; + + private: + /*! + * \brief Get the title of choosen season. + * \return The season title. + */ + std::string GetSeasonTitle() const; + }; + } +} + + diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMovies.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMovies.cpp new file mode 100644 index 0000000..45a83ce --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMovies.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeTitleMovies.h" + +#include "FileItem.h" +#include "QueryParams.h" +#include "video/VideoDatabase.h" + +using namespace XFILE::VIDEODATABASEDIRECTORY; + +CDirectoryNodeTitleMovies::CDirectoryNodeTitleMovies(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_TITLE_MOVIES, strName, pParent) +{ + +} + +bool CDirectoryNodeTitleMovies::GetContent(CFileItemList& items) const +{ + CVideoDatabase videodatabase; + if (!videodatabase.Open()) + return false; + + CQueryParams params; + CollectQueryParams(params); + + int details = items.HasProperty("set_videodb_details") + ? items.GetProperty("set_videodb_details").asInteger32() + : VideoDbDetailsNone; + + bool bSuccess = videodatabase.GetMoviesNav( + BuildPath(), items, params.GetGenreId(), params.GetYear(), params.GetActorId(), + params.GetDirectorId(), params.GetStudioId(), params.GetCountryId(), params.GetSetId(), + params.GetTagId(), SortDescription(), details); + + videodatabase.Close(); + + return bSuccess; +} diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMovies.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMovies.h new file mode 100644 index 0000000..7166ac8 --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMovies.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace VIDEODATABASEDIRECTORY + { + class CDirectoryNodeTitleMovies : public CDirectoryNode + { + public: + CDirectoryNodeTitleMovies(const std::string& strEntryName, CDirectoryNode* pParent); + protected: + bool GetContent(CFileItemList& items) const override; + }; + } +} diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMusicVideos.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMusicVideos.cpp new file mode 100644 index 0000000..95fe300 --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMusicVideos.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeTitleMusicVideos.h" + +#include "FileItem.h" +#include "QueryParams.h" +#include "video/VideoDatabase.h" + +using namespace XFILE::VIDEODATABASEDIRECTORY; + +CDirectoryNodeTitleMusicVideos::CDirectoryNodeTitleMusicVideos(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_TITLE_MUSICVIDEOS, strName, pParent) +{ + +} + +bool CDirectoryNodeTitleMusicVideos::GetContent(CFileItemList& items) const +{ + CVideoDatabase videodatabase; + if (!videodatabase.Open()) + return false; + + CQueryParams params; + CollectQueryParams(params); + + int details = items.HasProperty("set_videodb_details") + ? items.GetProperty("set_videodb_details").asInteger32() + : VideoDbDetailsNone; + bool bSuccess = videodatabase.GetMusicVideosNav( + BuildPath(), items, params.GetGenreId(), params.GetYear(), params.GetActorId(), + params.GetDirectorId(), params.GetStudioId(), params.GetAlbumId(), params.GetTagId(), + SortDescription(), details); + + videodatabase.Close(); + + return bSuccess; +} diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMusicVideos.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMusicVideos.h new file mode 100644 index 0000000..88c54fe --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMusicVideos.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace VIDEODATABASEDIRECTORY + { + class CDirectoryNodeTitleMusicVideos : public CDirectoryNode + { + public: + CDirectoryNodeTitleMusicVideos(const std::string& strEntryName, CDirectoryNode* pParent); + protected: + bool GetContent(CFileItemList& item) const override; + }; + } +} diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleTvShows.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleTvShows.cpp new file mode 100644 index 0000000..b1a247c --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleTvShows.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeTitleTvShows.h" + +#include "FileItem.h" +#include "QueryParams.h" +#include "video/VideoDatabase.h" + +using namespace XFILE::VIDEODATABASEDIRECTORY; + +CDirectoryNodeTitleTvShows::CDirectoryNodeTitleTvShows(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_TITLE_TVSHOWS, strName, pParent) +{ + +} + +NODE_TYPE CDirectoryNodeTitleTvShows::GetChildType() const +{ + return NODE_TYPE_SEASONS; +} + +std::string CDirectoryNodeTitleTvShows::GetLocalizedName() const +{ + CVideoDatabase db; + if (db.Open()) + return db.GetTvShowTitleById(GetID()); + return ""; +} + +bool CDirectoryNodeTitleTvShows::GetContent(CFileItemList& items) const +{ + CVideoDatabase videodatabase; + if (!videodatabase.Open()) + return false; + + CQueryParams params; + CollectQueryParams(params); + + int details = items.HasProperty("set_videodb_details") + ? items.GetProperty("set_videodb_details").asInteger32() + : VideoDbDetailsNone; + + bool bSuccess = videodatabase.GetTvShowsNav( + BuildPath(), items, params.GetGenreId(), params.GetYear(), params.GetActorId(), + params.GetDirectorId(), params.GetStudioId(), params.GetTagId(), SortDescription(), details); + + videodatabase.Close(); + + return bSuccess; +} diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleTvShows.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleTvShows.h new file mode 100644 index 0000000..697d572 --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleTvShows.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace VIDEODATABASEDIRECTORY + { + class CDirectoryNodeTitleTvShows : public CDirectoryNode + { + public: + CDirectoryNodeTitleTvShows(const std::string& strName, CDirectoryNode* pParent); + protected: + NODE_TYPE GetChildType() const override; + bool GetContent(CFileItemList& items) const override; + std::string GetLocalizedName() const override; + }; + } +} + + diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTvShowsOverview.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTvShowsOverview.cpp new file mode 100644 index 0000000..07e5ff4 --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTvShowsOverview.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DirectoryNodeTvShowsOverview.h" + +#include "FileItem.h" +#include "guilib/LocalizeStrings.h" +#include "utils/StringUtils.h" +#include "video/VideoDbUrl.h" + +using namespace XFILE::VIDEODATABASEDIRECTORY; + +Node TvShowChildren[] = { + { NODE_TYPE_GENRE, "genres", 135 }, + { NODE_TYPE_TITLE_TVSHOWS, "titles", 10024 }, + { NODE_TYPE_YEAR, "years", 652 }, + { NODE_TYPE_ACTOR, "actors", 344 }, + { NODE_TYPE_STUDIO, "studios", 20388 }, + { NODE_TYPE_TAGS, "tags", 20459 } + }; + +CDirectoryNodeTvShowsOverview::CDirectoryNodeTvShowsOverview(const std::string& strName, CDirectoryNode* pParent) + : CDirectoryNode(NODE_TYPE_TVSHOWS_OVERVIEW, strName, pParent) +{ + +} + +NODE_TYPE CDirectoryNodeTvShowsOverview::GetChildType() const +{ + if (GetName()=="0") + return NODE_TYPE_EPISODES; + + for (const Node& node : TvShowChildren) + if (GetName() == node.id) + return node.node; + + return NODE_TYPE_NONE; +} + +std::string CDirectoryNodeTvShowsOverview::GetLocalizedName() const +{ + for (const Node& node : TvShowChildren) + if (GetName() == node.id) + return g_localizeStrings.Get(node.label); + return ""; +} + +bool CDirectoryNodeTvShowsOverview::GetContent(CFileItemList& items) const +{ + CVideoDbUrl videoUrl; + if (!videoUrl.FromString(BuildPath())) + return false; + + for (const Node& node : TvShowChildren) + { + CFileItemPtr pItem(new CFileItem(g_localizeStrings.Get(node.label))); + + CVideoDbUrl itemUrl = videoUrl; + std::string strDir = StringUtils::Format("{}/", node.id); + itemUrl.AppendPath(strDir); + pItem->SetPath(itemUrl.ToString()); + + pItem->m_bIsFolder = true; + pItem->SetCanQueue(false); + items.Add(pItem); + } + + return true; +} diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTvShowsOverview.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTvShowsOverview.h new file mode 100644 index 0000000..4394e1f --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTvShowsOverview.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace VIDEODATABASEDIRECTORY + { + class CDirectoryNodeTvShowsOverview : public CDirectoryNode + { + public: + CDirectoryNodeTvShowsOverview(const std::string& strName, CDirectoryNode* pParent); + protected: + NODE_TYPE GetChildType() const override; + bool GetContent(CFileItemList& items) const override; + std::string GetLocalizedName() const override; + }; + } +} + + diff --git a/xbmc/filesystem/VideoDatabaseDirectory/QueryParams.cpp b/xbmc/filesystem/VideoDatabaseDirectory/QueryParams.cpp new file mode 100644 index 0000000..d4c78ff --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/QueryParams.cpp @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "QueryParams.h" + +#include "video/VideoDatabase.h" + +using namespace XFILE::VIDEODATABASEDIRECTORY; + +CQueryParams::CQueryParams() +{ + m_idMovie = -1; + m_idGenre = -1; + m_idCountry = -1; + m_idYear = -1; + m_idActor = -1; + m_idDirector = -1; + m_idContent = static_cast<long>(VideoDbContentType::UNKNOWN); + m_idShow = -1; + m_idSeason = -1; + m_idEpisode = -1; + m_idStudio = -1; + m_idMVideo = -1; + m_idAlbum = -1; + m_idSet = -1; + m_idTag = -1; +} + +void CQueryParams::SetQueryParam(NODE_TYPE NodeType, const std::string& strNodeName) +{ + long idDb=atol(strNodeName.c_str()); + + switch (NodeType) + { + case NODE_TYPE_OVERVIEW: + if (strNodeName == "tvshows") + m_idContent = static_cast<long>(VideoDbContentType::TVSHOWS); + else if (strNodeName == "musicvideos") + m_idContent = static_cast<long>(VideoDbContentType::MUSICVIDEOS); + else + m_idContent = static_cast<long>(VideoDbContentType::MOVIES); + break; + case NODE_TYPE_GENRE: + m_idGenre = idDb; + break; + case NODE_TYPE_COUNTRY: + m_idCountry = idDb; + break; + case NODE_TYPE_YEAR: + m_idYear = idDb; + break; + case NODE_TYPE_ACTOR: + m_idActor = idDb; + break; + case NODE_TYPE_DIRECTOR: + m_idDirector = idDb; + break; + case NODE_TYPE_TITLE_MOVIES: + case NODE_TYPE_RECENTLY_ADDED_MOVIES: + m_idMovie = idDb; + break; + case NODE_TYPE_TITLE_TVSHOWS: + case NODE_TYPE_INPROGRESS_TVSHOWS: + m_idShow = idDb; + break; + case NODE_TYPE_SEASONS: + m_idSeason = idDb; + break; + case NODE_TYPE_EPISODES: + case NODE_TYPE_RECENTLY_ADDED_EPISODES: + m_idEpisode = idDb; + break; + case NODE_TYPE_STUDIO: + m_idStudio = idDb; + break; + case NODE_TYPE_TITLE_MUSICVIDEOS: + case NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS: + m_idMVideo = idDb; + break; + case NODE_TYPE_MUSICVIDEOS_ALBUM: + m_idAlbum = idDb; + break; + case NODE_TYPE_SETS: + m_idSet = idDb; + break; + case NODE_TYPE_TAGS: + m_idTag = idDb; + break; + default: + break; + } +} diff --git a/xbmc/filesystem/VideoDatabaseDirectory/QueryParams.h b/xbmc/filesystem/VideoDatabaseDirectory/QueryParams.h new file mode 100644 index 0000000..f9d7e45 --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseDirectory/QueryParams.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DirectoryNode.h" + +namespace XFILE +{ + namespace VIDEODATABASEDIRECTORY + { + class CQueryParams + { + public: + CQueryParams(); + long GetContentType() const { return m_idContent; } + long GetMovieId() const { return m_idMovie; } + long GetYear() const { return m_idYear; } + long GetGenreId() const { return m_idGenre; } + long GetCountryId() const { return m_idCountry; } + long GetActorId() const { return m_idActor; } + long GetAlbumId() const { return m_idAlbum; } + long GetDirectorId() const { return m_idDirector; } + long GetTvShowId() const { return m_idShow; } + long GetSeason() const { return m_idSeason; } + long GetEpisodeId() const { return m_idEpisode; } + long GetStudioId() const { return m_idStudio; } + long GetMVideoId() const { return m_idMVideo; } + long GetSetId() const { return m_idSet; } + long GetTagId() const { return m_idTag; } + + protected: + void SetQueryParam(NODE_TYPE NodeType, const std::string& strNodeName); + + friend class CDirectoryNode; + private: + long m_idContent; + long m_idMovie; + long m_idGenre; + long m_idCountry; + long m_idYear; + long m_idActor; + long m_idDirector; + long m_idShow; + long m_idSeason; + long m_idEpisode; + long m_idStudio; + long m_idMVideo; + long m_idAlbum; + long m_idSet; + long m_idTag; + }; + } +} diff --git a/xbmc/filesystem/VideoDatabaseFile.cpp b/xbmc/filesystem/VideoDatabaseFile.cpp new file mode 100644 index 0000000..036eeba --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseFile.cpp @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "VideoDatabaseFile.h" + +#include "URL.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "video/VideoDatabase.h" +#include "video/VideoInfoTag.h" + +using namespace XFILE; + +CVideoDatabaseFile::CVideoDatabaseFile(void) + : COverrideFile(true) +{ } + +CVideoDatabaseFile::~CVideoDatabaseFile(void) = default; + +CVideoInfoTag CVideoDatabaseFile::GetVideoTag(const CURL& url) +{ + CVideoInfoTag tag; + + std::string strFileName = URIUtils::GetFileName(url.Get()); + if (strFileName.empty()) + return tag; + + URIUtils::RemoveExtension(strFileName); + if (!StringUtils::IsNaturalNumber(strFileName)) + return tag; + long idDb = atol(strFileName.c_str()); + + VideoDbContentType type = GetType(url); + if (type == VideoDbContentType::UNKNOWN) + return tag; + + CVideoDatabase videoDatabase; + if (!videoDatabase.Open()) + return tag; + + tag = videoDatabase.GetDetailsByTypeAndId(type, idDb); + + return tag; +} + +VideoDbContentType CVideoDatabaseFile::GetType(const CURL& url) +{ + std::string strPath = URIUtils::GetDirectory(url.Get()); + if (strPath.empty()) + return VideoDbContentType::UNKNOWN; + + std::vector<std::string> pathElem = StringUtils::Split(strPath, "/"); + if (pathElem.size() == 0) + return VideoDbContentType::UNKNOWN; + + std::string itemType = pathElem.at(2); + VideoDbContentType type; + if (itemType == "movies" || itemType == "recentlyaddedmovies") + type = VideoDbContentType::MOVIES; + else if (itemType == "episodes" || itemType == "recentlyaddedepisodes" || itemType == "inprogresstvshows" || itemType == "tvshows") + type = VideoDbContentType::EPISODES; + else if (itemType == "musicvideos" || itemType == "recentlyaddedmusicvideos") + type = VideoDbContentType::MUSICVIDEOS; + else + type = VideoDbContentType::UNKNOWN; + + return type; +} + + +std::string CVideoDatabaseFile::TranslatePath(const CURL& url) +{ + std::string strFileName = URIUtils::GetFileName(url.Get()); + if (strFileName.empty()) + return ""; + + URIUtils::RemoveExtension(strFileName); + if (!StringUtils::IsNaturalNumber(strFileName)) + return ""; + long idDb = atol(strFileName.c_str()); + + VideoDbContentType type = GetType(url); + if (type == VideoDbContentType::UNKNOWN) + return ""; + + CVideoDatabase videoDatabase; + if (!videoDatabase.Open()) + return ""; + + std::string realFilename; + videoDatabase.GetFilePathById(idDb, realFilename, type); + + return realFilename; +} diff --git a/xbmc/filesystem/VideoDatabaseFile.h b/xbmc/filesystem/VideoDatabaseFile.h new file mode 100644 index 0000000..d5488f9 --- /dev/null +++ b/xbmc/filesystem/VideoDatabaseFile.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "filesystem/OverrideFile.h" + +enum class VideoDbContentType; +class CVideoInfoTag; +class CURL; + +namespace XFILE +{ +class CVideoDatabaseFile : public COverrideFile +{ +public: + CVideoDatabaseFile(void); + ~CVideoDatabaseFile(void) override; + + static CVideoInfoTag GetVideoTag(const CURL& url); + +protected: + std::string TranslatePath(const CURL& url) override; + static VideoDbContentType GetType(const CURL& url); +}; +} diff --git a/xbmc/filesystem/VirtualDirectory.cpp b/xbmc/filesystem/VirtualDirectory.cpp new file mode 100644 index 0000000..7134eba --- /dev/null +++ b/xbmc/filesystem/VirtualDirectory.cpp @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "VirtualDirectory.h" + +#include "Directory.h" +#include "DirectoryFactory.h" +#include "FileItem.h" +#include "ServiceBroker.h" +#include "SourcesDirectory.h" +#include "URL.h" +#include "Util.h" +#include "storage/MediaManager.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +using namespace XFILE; + +namespace XFILE +{ + +CVirtualDirectory::CVirtualDirectory(void) +{ + m_flags = DIR_FLAG_ALLOW_PROMPT; + m_allowNonLocalSources = true; +} + +CVirtualDirectory::~CVirtualDirectory(void) = default; + +/*! + \brief Add shares to the virtual directory + \param VECSOURCES Shares to add + \sa CMediaSource, VECSOURCES + */ +void CVirtualDirectory::SetSources(const VECSOURCES& vecSources) +{ + m_vecSources = vecSources; +} + +/*! + \brief Retrieve the shares or the content of a directory. + \param strPath Specifies the path of the directory to retrieve or pass an empty string to get the shares. + \param items Content of the directory. + \return Returns \e true, if directory access is successful. + \note If \e strPath is an empty string, the share \e items have thumbnails and icons set, else the thumbnails + and icons have to be set manually. + */ + +bool CVirtualDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + return GetDirectory(url, items, true, false); +} + +bool CVirtualDirectory::GetDirectory(const CURL& url, CFileItemList &items, bool bUseFileDirectories, bool keepImpl) +{ + std::string strPath = url.Get(); + int flags = m_flags; + if (!bUseFileDirectories) + flags |= DIR_FLAG_NO_FILE_DIRS; + if (!strPath.empty() && strPath != "files://") + { + CURL realURL = URIUtils::SubstitutePath(url); + if (!m_pDir) + m_pDir.reset(CDirectoryFactory::Create(realURL)); + bool ret = CDirectory::GetDirectory(strPath, m_pDir, items, m_strFileMask, flags); + if (!keepImpl) + m_pDir.reset(); + return ret; + } + + // if strPath is blank, clear the list (to avoid parent items showing up) + if (strPath.empty()) + items.Clear(); + + // return the root listing + items.SetPath(strPath); + + // grab our shares + VECSOURCES shares; + GetSources(shares); + CSourcesDirectory dir; + return dir.GetDirectory(shares, items); +} + +void CVirtualDirectory::CancelDirectory() +{ + if (m_pDir) + m_pDir->CancelDirectory(); +} + +/*! + \brief Is the share \e strPath in the virtual directory. + \param strPath Share to test + \return Returns \e true, if share is in the virtual directory. + \note The parameter \e strPath can not be a share with directory. Eg. "iso9660://dir" will return \e false. + It must be "iso9660://". + */ +bool CVirtualDirectory::IsSource(const std::string& strPath, VECSOURCES *sources, std::string *name) const +{ + std::string strPathCpy = strPath; + StringUtils::TrimRight(strPathCpy, "/\\"); + + // just to make sure there's no mixed slashing in share/default defines + // ie. f:/video and f:\video was not be recognised as the same directory, + // resulting in navigation to a lower directory then the share. + if(URIUtils::IsDOSPath(strPathCpy)) + StringUtils::Replace(strPathCpy, '/', '\\'); + + VECSOURCES shares; + if (sources) + shares = *sources; + else + GetSources(shares); + for (int i = 0; i < (int)shares.size(); ++i) + { + const CMediaSource& share = shares.at(i); + std::string strShare = share.strPath; + StringUtils::TrimRight(strShare, "/\\"); + if(URIUtils::IsDOSPath(strShare)) + StringUtils::Replace(strShare, '/', '\\'); + if (strShare == strPathCpy) + { + if (name) + *name = share.strName; + return true; + } + } + return false; +} + +/*! + \brief Is the share \e path in the virtual directory. + \param path Share to test + \return Returns \e true, if share is in the virtual directory. + \note The parameter \e path CAN be a share with directory. Eg. "iso9660://dir" will + return the same as "iso9660://". + */ +bool CVirtualDirectory::IsInSource(const std::string &path) const +{ + bool isSourceName; + VECSOURCES shares; + GetSources(shares); + int iShare = CUtil::GetMatchingSource(path, shares, isSourceName); + if (URIUtils::IsOnDVD(path)) + { // check to see if our share path is still available and of the same type, as it changes during autodetect + // and GetMatchingSource() is too naive at it's matching + for (unsigned int i = 0; i < shares.size(); i++) + { + CMediaSource &share = shares[i]; + if (URIUtils::IsOnDVD(share.strPath) && + URIUtils::PathHasParent(path, share.strPath)) + return true; + } + return false; + } + //! @todo May need to handle other special cases that GetMatchingSource() fails on + return (iShare > -1); +} + +void CVirtualDirectory::GetSources(VECSOURCES &shares) const +{ + shares = m_vecSources; + // add our plug n play shares + + if (m_allowNonLocalSources) + CServiceBroker::GetMediaManager().GetRemovableDrives(shares); + +#ifdef HAS_DVD_DRIVE + // and update our dvd share + for (unsigned int i = 0; i < shares.size(); ++i) + { + CMediaSource& share = shares[i]; + if (share.m_iDriveType == CMediaSource::SOURCE_TYPE_DVD) + { + if (CServiceBroker::GetMediaManager().IsAudio(share.strPath)) + { + share.strStatus = "Audio-CD"; + share.strPath = "cdda://local/"; + share.strDiskUniqueId = ""; + } + else + { + share.strStatus = CServiceBroker::GetMediaManager().GetDiskLabel(share.strPath); + share.strDiskUniqueId = CServiceBroker::GetMediaManager().GetDiskUniqueId(share.strPath); + if (!share.strPath.length()) // unmounted CD + { + if (CServiceBroker::GetMediaManager().GetDiscPath() == "iso9660://") + share.strPath = "iso9660://"; + else + // share is unmounted and not iso9660, discard it + shares.erase(shares.begin() + i--); + } + } + } + } +#endif +} +} + diff --git a/xbmc/filesystem/VirtualDirectory.h b/xbmc/filesystem/VirtualDirectory.h new file mode 100644 index 0000000..5a46353 --- /dev/null +++ b/xbmc/filesystem/VirtualDirectory.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" +#include "MediaSource.h" + +#include <memory> +#include <string> + +namespace XFILE +{ + + /*! + \ingroup windows + \brief Get access to shares and it's directories. + */ + class CVirtualDirectory : public IDirectory + { + public: + CVirtualDirectory(void); + ~CVirtualDirectory(void) override; + bool GetDirectory(const CURL& url, CFileItemList &items) override; + void CancelDirectory() override; + bool GetDirectory(const CURL& url, CFileItemList &items, bool bUseFileDirectories, bool keepImpl); + void SetSources(const VECSOURCES& vecSources); + inline unsigned int GetNumberOfSources() + { + return static_cast<uint32_t>(m_vecSources.size()); + } + + bool IsSource(const std::string& strPath, VECSOURCES *sources = NULL, std::string *name = NULL) const; + bool IsInSource(const std::string& strPath) const; + + inline const CMediaSource& operator [](const int index) const + { + return m_vecSources[index]; + } + + inline CMediaSource& operator[](const int index) + { + return m_vecSources[index]; + } + + void GetSources(VECSOURCES &sources) const; + + void AllowNonLocalSources(bool allow) { m_allowNonLocalSources = allow; } + + std::shared_ptr<IDirectory> GetDirImpl() { return m_pDir; } + void ReleaseDirImpl() { m_pDir.reset(); } + + protected: + void CacheThumbs(CFileItemList &items); + + VECSOURCES m_vecSources; + bool m_allowNonLocalSources; + std::shared_ptr<IDirectory> m_pDir; + }; +} diff --git a/xbmc/filesystem/XbtDirectory.cpp b/xbmc/filesystem/XbtDirectory.cpp new file mode 100644 index 0000000..5c7d395 --- /dev/null +++ b/xbmc/filesystem/XbtDirectory.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "XbtDirectory.h" + +#include "FileItem.h" +#include "URL.h" +#include "filesystem/Directorization.h" +#include "filesystem/XbtManager.h" +#include "guilib/XBTF.h" + +#include <stdio.h> + +namespace XFILE +{ + +static CFileItemPtr XBTFFileToFileItem(const CXBTFFile& entry, const std::string& label, const std::string& path, bool isFolder) +{ + CFileItemPtr item(new CFileItem(label)); + if (!isFolder) + item->m_dwSize = static_cast<int64_t>(entry.GetUnpackedSize()); + + return item; +} + +CXbtDirectory::CXbtDirectory() = default; + +CXbtDirectory::~CXbtDirectory() = default; + +bool CXbtDirectory::GetDirectory(const CURL& urlOrig, CFileItemList& items) +{ + CURL urlXbt(urlOrig); + + // if this isn't a proper xbt:// path, assume it's the path to a xbt file + if (!urlOrig.IsProtocol("xbt")) + urlXbt = URIUtils::CreateArchivePath("xbt", urlOrig); + + CURL url(urlXbt); + url.SetOptions(""); // delete options to have a clean path to add stuff too + url.SetFileName(""); // delete filename too as our names later will contain it + + std::vector<CXBTFFile> files; + if (!CXbtManager::GetInstance().GetFiles(url, files)) + return false; + + // prepare the files for directorization + DirectorizeEntries<CXBTFFile> entries; + entries.reserve(files.size()); + for (const auto& file : files) + entries.push_back(DirectorizeEntry<CXBTFFile>(file.GetPath(), file)); + + Directorize(urlXbt, entries, XBTFFileToFileItem, items); + + return true; +} + +bool CXbtDirectory::ContainsFiles(const CURL& url) +{ + return CXbtManager::GetInstance().HasFiles(url); +} + +} diff --git a/xbmc/filesystem/XbtDirectory.h b/xbmc/filesystem/XbtDirectory.h new file mode 100644 index 0000000..feb4540 --- /dev/null +++ b/xbmc/filesystem/XbtDirectory.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IFileDirectory.h" + +#include <map> +#include <string> + +class CXBTFFile; + +namespace XFILE +{ +class CXbtDirectory : public IFileDirectory +{ +public: + CXbtDirectory(); + ~CXbtDirectory() override; + + // specialization of IDirectory + DIR_CACHE_TYPE GetCacheType(const CURL& url) const override { return DIR_CACHE_ALWAYS; } + bool GetDirectory(const CURL& url, CFileItemList& items) override; + + // specialization of IFileDirectory + bool ContainsFiles(const CURL& url) override; +}; +} diff --git a/xbmc/filesystem/XbtFile.cpp b/xbmc/filesystem/XbtFile.cpp new file mode 100644 index 0000000..92586b7 --- /dev/null +++ b/xbmc/filesystem/XbtFile.cpp @@ -0,0 +1,368 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "XbtFile.h" + +#include "URL.h" +#include "filesystem/File.h" +#include "filesystem/XbtManager.h" +#include "guilib/TextureBundleXBT.h" +#include "guilib/XBTFReader.h" +#include "utils/StringUtils.h" + +#include <algorithm> +#include <climits> +#include <cstring> +#include <memory> +#include <string> +#include <utility> + +namespace XFILE +{ + +CXbtFile::CXbtFile() + : m_url(), + m_xbtfReader(nullptr), + m_xbtfFile(), + m_frameStartPositions(), + m_unpackedFrames() +{ } + +CXbtFile::~CXbtFile() +{ + Close(); +} + +bool CXbtFile::Open(const CURL& url) +{ + if (m_open) + return false; + + CURL xbtUrl(url); + xbtUrl.SetOptions(""); + + if (!GetReaderAndFile(url, m_xbtfReader, m_xbtfFile)) + return false; + + m_url = url; + m_open = true; + + uint64_t frameStartPosition = 0; + const auto& frames = m_xbtfFile.GetFrames(); + for (const auto& frame : frames) + { + m_frameStartPositions.push_back(frameStartPosition); + + frameStartPosition += frame.GetUnpackedSize(); + } + + m_frameIndex = 0; + m_positionWithinFrame = 0; + m_positionTotal = 0; + + m_unpackedFrames.resize(frames.size()); + + return true; +} + +void CXbtFile::Close() +{ + m_unpackedFrames.clear(); + m_frameIndex = 0; + m_positionWithinFrame = 0; + m_positionTotal = 0; + m_frameStartPositions.clear(); + m_open = false; +} + +bool CXbtFile::Exists(const CURL& url) +{ + CXBTFFile dummy; + return GetFile(url, dummy); +} + +int64_t CXbtFile::GetPosition() +{ + if (!m_open) + return -1; + + return m_positionTotal; +} + +int64_t CXbtFile::GetLength() +{ + if (!m_open) + return -1; + + return static_cast<int>(m_xbtfFile.GetUnpackedSize()); +} + +int CXbtFile::Stat(struct __stat64 *buffer) +{ + if (!m_open) + return -1; + + return Stat(m_url, buffer); +} + +int CXbtFile::Stat(const CURL& url, struct __stat64* buffer) +{ + if (!buffer) + return -1; + + *buffer = {}; + + // check if the file exists + CXBTFReaderPtr reader; + CXBTFFile file; + if (!GetReaderAndFile(url, reader, file)) + { + // check if the URL points to the XBT file itself + if (!url.GetFileName().empty() || !CFile::Exists(url.GetHostName())) + return -1; + + // stat the XBT file itself + if (XFILE::CFile::Stat(url.GetHostName(), buffer) != 0) + return -1; + + buffer->st_mode = _S_IFDIR; + return 0; + } + + // stat the XBT file itself + if (XFILE::CFile::Stat(url.GetHostName(), buffer) != 0) + return -1; + + buffer->st_size = file.GetUnpackedSize(); + + return 0; +} + +ssize_t CXbtFile::Read(void* lpBuf, size_t uiBufSize) +{ + if (lpBuf == nullptr || !m_open) + return -1; + + // nothing to read + if (m_xbtfFile.GetFrames().empty() || m_positionTotal >= GetLength()) + return 0; + + // we can't read more than is left + if (static_cast<int64_t>(uiBufSize) > GetLength() - m_positionTotal) + uiBufSize = static_cast<ssize_t>(GetLength() - m_positionTotal); + + // we can't read more than we can signal with the return value + if (uiBufSize > SSIZE_MAX) + uiBufSize = SSIZE_MAX; + + const auto& frames = m_xbtfFile.GetFrames(); + + size_t remaining = uiBufSize; + while (remaining > 0) + { + const CXBTFFrame& frame = frames[m_frameIndex]; + + // check if we have already unpacked the current frame + if (m_unpackedFrames[m_frameIndex].empty()) + { + // unpack the data from the current frame + std::vector<uint8_t> unpackedFrame = + CTextureBundleXBT::UnpackFrame(*m_xbtfReader.get(), frame); + if (unpackedFrame.empty()) + { + Close(); + return -1; + } + + m_unpackedFrames[m_frameIndex] = std::move(unpackedFrame); + } + + // determine how many bytes we need to copy from the current frame + uint64_t remainingBytesInFrame = frame.GetUnpackedSize() - m_positionWithinFrame; + size_t bytesToCopy = remaining; + if (remainingBytesInFrame <= SIZE_MAX) + bytesToCopy = std::min(remaining, static_cast<size_t>(remainingBytesInFrame)); + + // copy the data + memcpy(lpBuf, m_unpackedFrames[m_frameIndex].data() + m_positionWithinFrame, bytesToCopy); + m_positionWithinFrame += bytesToCopy; + m_positionTotal += bytesToCopy; + remaining -= bytesToCopy; + + // check if we need to go to the next frame and there is a next frame + if (m_positionWithinFrame >= frame.GetUnpackedSize() && m_frameIndex < frames.size() - 1) + { + m_positionWithinFrame = 0; + m_frameIndex += 1; + } + } + + return uiBufSize; +} + +int64_t CXbtFile::Seek(int64_t iFilePosition, int iWhence) +{ + if (!m_open) + return -1; + + int64_t newPosition = m_positionTotal; + switch (iWhence) + { + case SEEK_SET: + newPosition = iFilePosition; + break; + + case SEEK_CUR: + newPosition += iFilePosition; + break; + + case SEEK_END: + newPosition = GetLength() + iFilePosition; + break; + + // unsupported seek mode + default: + return -1; + } + + // can't seek before the beginning or after the end of the file + if (newPosition < 0 || newPosition >= GetLength()) + return -1; + + // seeking backwards doesn't require additional work + if (newPosition <= m_positionTotal) + { + m_positionTotal = newPosition; + return m_positionTotal; + } + + // when seeking forward we need to unpack all frames we seek past + const auto& frames = m_xbtfFile.GetFrames(); + while (m_positionTotal < newPosition) + { + const CXBTFFrame& frame = frames[m_frameIndex]; + + // check if we have already unpacked the current frame + if (m_unpackedFrames[m_frameIndex].empty()) + { + // unpack the data from the current frame + std::vector<uint8_t> unpackedFrame = + CTextureBundleXBT::UnpackFrame(*m_xbtfReader.get(), frame); + if (unpackedFrame.empty()) + { + Close(); + return -1; + } + + m_unpackedFrames[m_frameIndex] = std::move(unpackedFrame); + } + + int64_t remainingBytesToSeek = newPosition - m_positionTotal; + // check if the new position is within the current frame + uint64_t remainingBytesInFrame = frame.GetUnpackedSize() - m_positionWithinFrame; + if (static_cast<uint64_t>(remainingBytesToSeek) < remainingBytesInFrame) + { + m_positionWithinFrame += remainingBytesToSeek; + break; + } + + // otherwise move to the end of the frame + m_positionTotal += remainingBytesInFrame; + m_positionWithinFrame += remainingBytesInFrame; + + // and go to the next frame if there is a next frame + if (m_frameIndex < frames.size() - 1) + { + m_positionWithinFrame = 0; + m_frameIndex += 1; + } + } + + m_positionTotal = newPosition; + return m_positionTotal; +} + +uint32_t CXbtFile::GetImageWidth() const +{ + CXBTFFrame frame; + if (!GetFirstFrame(frame)) + return false; + + return frame.GetWidth(); +} + +uint32_t CXbtFile::GetImageHeight() const +{ + CXBTFFrame frame; + if (!GetFirstFrame(frame)) + return false; + + return frame.GetHeight(); +} + +uint32_t CXbtFile::GetImageFormat() const +{ + CXBTFFrame frame; + if (!GetFirstFrame(frame)) + return false; + + return frame.GetFormat(); +} + +bool CXbtFile::HasImageAlpha() const +{ + CXBTFFrame frame; + if (!GetFirstFrame(frame)) + return false; + + return frame.HasAlpha(); +} + +bool CXbtFile::GetFirstFrame(CXBTFFrame& frame) const +{ + if (!m_open) + return false; + + const auto& frames = m_xbtfFile.GetFrames(); + if (frames.empty()) + return false; + + frame = frames.at(0); + return true; +} + +bool CXbtFile::GetReader(const CURL& url, CXBTFReaderPtr& reader) +{ + CURL xbtUrl(url); + xbtUrl.SetOptions(""); + + return CXbtManager::GetInstance().GetReader(xbtUrl, reader); +} + +bool CXbtFile::GetReaderAndFile(const CURL& url, CXBTFReaderPtr& reader, CXBTFFile& file) +{ + if (!GetReader(url, reader)) + return false; + + CURL xbtUrl(url); + xbtUrl.SetOptions(""); + + // CXBTFReader stores all filenames in lower case + std::string fileName = xbtUrl.GetFileName(); + StringUtils::ToLower(fileName); + + return reader->Get(fileName, file); +} + +bool CXbtFile::GetFile(const CURL& url, CXBTFFile& file) +{ + CXBTFReaderPtr reader; + return GetReaderAndFile(url, reader, file); +} + +} diff --git a/xbmc/filesystem/XbtFile.h b/xbmc/filesystem/XbtFile.h new file mode 100644 index 0000000..fad28a5 --- /dev/null +++ b/xbmc/filesystem/XbtFile.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IFile.h" +#include "URL.h" +#include "guilib/XBTF.h" +#include "guilib/XBTFReader.h" + +#include <cstdint> +#include <cstdio> +#include <vector> + +#include "PlatformDefs.h" + +namespace XFILE +{ +class CXbtFile : public IFile +{ +public: + CXbtFile(); + ~CXbtFile() override; + + bool Open(const CURL& url) override; + void Close() override; + bool Exists(const CURL& url) override; + + int64_t GetPosition() override; + int64_t GetLength() override; + + int Stat(struct __stat64* buffer) override; + int Stat(const CURL& url, struct __stat64* buffer) override; + + ssize_t Read(void* lpBuf, size_t uiBufSize) override; + int64_t Seek(int64_t iFilePosition, int iWhence = SEEK_SET) override; + + uint32_t GetImageWidth() const; + uint32_t GetImageHeight() const; + uint32_t GetImageFormat() const; + bool HasImageAlpha() const; + +private: + bool GetFirstFrame(CXBTFFrame& frame) const; + + static bool GetReader(const CURL& url, CXBTFReaderPtr& reader); + static bool GetReaderAndFile(const CURL& url, CXBTFReaderPtr& reader, CXBTFFile& file); + static bool GetFile(const CURL& url, CXBTFFile& file); + + CURL m_url; + bool m_open = false; + CXBTFReaderPtr m_xbtfReader; + CXBTFFile m_xbtfFile; + + std::vector<uint64_t> m_frameStartPositions; + size_t m_frameIndex = 0; + uint64_t m_positionWithinFrame = 0; + int64_t m_positionTotal = 0; + + std::vector<std::vector<uint8_t>> m_unpackedFrames; +}; +} diff --git a/xbmc/filesystem/XbtManager.cpp b/xbmc/filesystem/XbtManager.cpp new file mode 100644 index 0000000..bb091f1 --- /dev/null +++ b/xbmc/filesystem/XbtManager.cpp @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "XbtManager.h" + +#include "URL.h" +#include "guilib/XBTF.h" +#include "guilib/XBTFReader.h" + +#include <utility> + +namespace XFILE +{ + +CXbtManager::CXbtManager() = default; + +CXbtManager::~CXbtManager() = default; + +CXbtManager& CXbtManager::GetInstance() +{ + static CXbtManager xbtManager; + return xbtManager; +} + +bool CXbtManager::HasFiles(const CURL& path) const +{ + return ProcessFile(path) != m_readers.end(); +} + +bool CXbtManager::GetFiles(const CURL& path, std::vector<CXBTFFile>& files) const +{ + const auto& reader = ProcessFile(path); + if (reader == m_readers.end()) + return false; + + files = reader->second.reader->GetFiles(); + return true; +} + +bool CXbtManager::GetReader(const CURL& path, CXBTFReaderPtr& reader) const +{ + const auto& it = ProcessFile(path); + if (it == m_readers.end()) + return false; + + reader = it->second.reader; + return true; +} + +void CXbtManager::Release(const CURL& path) +{ + const auto& it = GetReader(path); + if (it == m_readers.end()) + return; + + RemoveReader(it); +} + +CXbtManager::XBTFReaders::iterator CXbtManager::GetReader(const CURL& path) const +{ + return GetReader(NormalizePath(path)); +} + +CXbtManager::XBTFReaders::iterator CXbtManager::GetReader(const std::string& path) const +{ + if (path.empty()) + return m_readers.end(); + + return m_readers.find(path); +} + +void CXbtManager::RemoveReader(XBTFReaders::iterator readerIterator) const +{ + if (readerIterator == m_readers.end()) + return; + + // close the reader + readerIterator->second.reader->Close(); + + // and remove it from the map + m_readers.erase(readerIterator); +} + +CXbtManager::XBTFReaders::const_iterator CXbtManager::ProcessFile(const CURL& path) const +{ + std::string filePath = NormalizePath(path); + + // check if the file is known + auto it = GetReader(filePath); + if (it != m_readers.end()) + { + // check if the XBT file has been modified + if (it->second.reader->GetLastModificationTimestamp() <= it->second.lastModification) + return it; + + // the XBT file has been modified so close and remove it from the cache + // it will be re-opened by the following logic + RemoveReader(it); + } + + // try to read the file + CXBTFReaderPtr reader(new CXBTFReader()); + if (!reader->Open(filePath)) + return m_readers.end(); + + XBTFReader xbtfReader = { + reader, + reader->GetLastModificationTimestamp() + }; + std::pair<XBTFReaders::iterator, bool> result = m_readers.insert(std::make_pair(filePath, xbtfReader)); + return result.first; +} + +std::string CXbtManager::NormalizePath(const CURL& path) +{ + if (path.IsProtocol("xbt")) + return path.GetHostName(); + + return path.Get(); +} + +} diff --git a/xbmc/filesystem/XbtManager.h b/xbmc/filesystem/XbtManager.h new file mode 100644 index 0000000..1b46eba --- /dev/null +++ b/xbmc/filesystem/XbtManager.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "guilib/XBTFReader.h" + +#include <map> +#include <memory> +#include <string> +#include <vector> + +class CURL; +class CXBTFFile; + +namespace XFILE +{ +class CXbtManager +{ +public: + ~CXbtManager(); + + static CXbtManager& GetInstance(); + + bool HasFiles(const CURL& path) const; + bool GetFiles(const CURL& path, std::vector<CXBTFFile>& files) const; + + bool GetReader(const CURL& path, CXBTFReaderPtr& reader) const; + + void Release(const CURL& path); + +private: + CXbtManager(); + CXbtManager(const CXbtManager&) = delete; + CXbtManager& operator=(const CXbtManager&) = delete; + + struct XBTFReader + { + CXBTFReaderPtr reader; + time_t lastModification; + }; + using XBTFReaders = std::map<std::string, XBTFReader>; + + XBTFReaders::iterator GetReader(const CURL& path) const; + XBTFReaders::iterator GetReader(const std::string& path) const; + void RemoveReader(XBTFReaders::iterator readerIterator) const; + XBTFReaders::const_iterator ProcessFile(const CURL& path) const; + + static std::string NormalizePath(const CURL& path); + + mutable XBTFReaders m_readers; +}; +} diff --git a/xbmc/filesystem/ZeroconfDirectory.cpp b/xbmc/filesystem/ZeroconfDirectory.cpp new file mode 100644 index 0000000..f34b6b2 --- /dev/null +++ b/xbmc/filesystem/ZeroconfDirectory.cpp @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "ZeroconfDirectory.h" + +#include "Directory.h" +#include "FileItem.h" +#include "URL.h" +#include "network/ZeroconfBrowser.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +#include <cassert> +#include <stdexcept> + +using namespace XFILE; + +CZeroconfDirectory::CZeroconfDirectory() +{ + CZeroconfBrowser::GetInstance()->Start(); +} + +CZeroconfDirectory::~CZeroconfDirectory() = default; + +namespace +{ + std::string GetHumanReadableProtocol(std::string const& fcr_service_type) + { + if(fcr_service_type == "_smb._tcp.") + return "SAMBA"; + else if(fcr_service_type == "_ftp._tcp.") + return "FTP"; + else if(fcr_service_type == "_webdav._tcp.") + return "WebDAV"; + else if(fcr_service_type == "_nfs._tcp.") + return "NFS"; + else if(fcr_service_type == "_sftp-ssh._tcp.") + return "SFTP"; + //fallback, just return the received type + return fcr_service_type; + } + bool GetXBMCProtocol(std::string const& fcr_service_type, std::string& fr_protocol) + { + if(fcr_service_type == "_smb._tcp.") + fr_protocol = "smb"; + else if(fcr_service_type == "_ftp._tcp.") + fr_protocol = "ftp"; + else if(fcr_service_type == "_webdav._tcp.") + fr_protocol = "dav"; + else if(fcr_service_type == "_nfs._tcp.") + fr_protocol = "nfs"; + else if(fcr_service_type == "_sftp-ssh._tcp.") + fr_protocol = "sftp"; + else + return false; + return true; + } +} + +bool GetDirectoryFromTxtRecords(const CZeroconfBrowser::ZeroconfService& zeroconf_service, CURL& url, CFileItemList &items) +{ + bool ret = false; + + //get the txt-records from this service + CZeroconfBrowser::ZeroconfService::tTxtRecordMap txtRecords=zeroconf_service.GetTxtRecords(); + + //if we have some records + if(!txtRecords.empty()) + { + std::string path; + std::string username; + std::string password; + + //search for a path key entry + CZeroconfBrowser::ZeroconfService::tTxtRecordMap::iterator it = txtRecords.find(TXT_RECORD_PATH_KEY); + + //if we found the key - be sure there is a value there + if( it != txtRecords.end() && !it->second.empty() ) + { + //from now on we treat the value as a path - everything else would mean + //a misconfigured zeroconf server. + path=it->second; + } + + //search for a username key entry + it = txtRecords.find(TXT_RECORD_USERNAME_KEY); + + //if we found the key - be sure there is a value there + if( it != txtRecords.end() && !it->second.empty() ) + { + username=it->second; + url.SetUserName(username); + } + + //search for a password key entry + it = txtRecords.find(TXT_RECORD_PASSWORD_KEY); + + //if we found the key - be sure there is a value there + if( it != txtRecords.end() && !it->second.empty() ) + { + password=it->second; + url.SetPassword(password); + } + + //if we got a path - add a item - else at least we maybe have set username and password to theurl + if( !path.empty()) + { + CFileItemPtr item(new CFileItem("", true)); + std::string urlStr(url.Get()); + //if path has a leading slash (sure it should have one) + if( path.at(0) == '/' ) + { + URIUtils::RemoveSlashAtEnd(urlStr);//we don't need the slash at and of url then + } + else//path doesn't start with slash - + {//this is some kind of misconfiguration - we fix it by adding a slash to the url + URIUtils::AddSlashAtEnd(urlStr); + } + + //add slash at end of path since it has to be a folder + URIUtils::AddSlashAtEnd(path); + //this is the full path including remote stuff (e.x. nfs://ip/path + item->SetPath(urlStr + path); + //remove the slash at the end of the path or GetFileName will not give the last dir + URIUtils::RemoveSlashAtEnd(path); + //set the label to the last directory in path + if( !URIUtils::GetFileName(path).empty() ) + item->SetLabel(URIUtils::GetFileName(path)); + else + item->SetLabel("/"); + + item->SetLabelPreformatted(true); + //just set the default folder icon + item->FillInDefaultIcon(); + item->m_bIsShareOrDrive=true; + items.Add(item); + ret = true; + } + } + return ret; +} + +bool CZeroconfDirectory::GetDirectory(const CURL& url, CFileItemList &items) +{ + assert(url.IsProtocol("zeroconf")); + std::string strPath = url.Get(); + std::string path = strPath.substr(11, strPath.length()); + URIUtils::RemoveSlashAtEnd(path); + if(path.empty()) + { + std::vector<CZeroconfBrowser::ZeroconfService> found_services = CZeroconfBrowser::GetInstance()->GetFoundServices(); + for (auto& it : found_services) + { + //only use discovered services we can connect to through directory + std::string tmp; + if (GetXBMCProtocol(it.GetType(), tmp)) + { + CFileItemPtr item(new CFileItem("", true)); + CURL url; + url.SetProtocol("zeroconf"); + std::string service_path(CURL::Encode(CZeroconfBrowser::ZeroconfService::toPath(it))); + url.SetFileName(service_path); + item->SetPath(url.Get()); + + //now do the formatting + std::string protocol = GetHumanReadableProtocol(it.GetType()); + item->SetLabel(it.GetName() + " (" + protocol + ")"); + item->SetLabelPreformatted(true); + //just set the default folder icon + item->FillInDefaultIcon(); + items.Add(item); + } + } + return true; + } + else + { + //decode the path first + std::string decoded(CURL::Decode(path)); + try + { + CZeroconfBrowser::ZeroconfService zeroconf_service = CZeroconfBrowser::ZeroconfService::fromPath(decoded); + + if(!CZeroconfBrowser::GetInstance()->ResolveService(zeroconf_service)) + { + CLog::Log(LOGINFO, + "CZeroconfDirectory::GetDirectory service ( {} ) could not be resolved in time", + zeroconf_service.GetName()); + return false; + } + else + { + assert(!zeroconf_service.GetIP().empty()); + CURL service; + service.SetPort(zeroconf_service.GetPort()); + service.SetHostName(zeroconf_service.GetIP()); + //do protocol conversion (_smb._tcp -> smb) + //! @todo try automatic conversion -> remove leading '_' and '._tcp'? + std::string protocol; + if(!GetXBMCProtocol(zeroconf_service.GetType(), protocol)) + { + CLog::Log(LOGERROR, + "CZeroconfDirectory::GetDirectory Unknown service type ({}), skipping; ", + zeroconf_service.GetType()); + return false; + } + + service.SetProtocol(protocol); + + //first try to show the txt-record defined path if any + if(GetDirectoryFromTxtRecords(zeroconf_service, service, items)) + { + return true; + } + else//no txt record path - so let the CDirectory handler show the folders + { + return CDirectory::GetDirectory(service.Get(), items, "", DIR_FLAG_ALLOW_PROMPT); + } + } + } catch (std::runtime_error& e) { + CLog::Log(LOGERROR, + "CZeroconfDirectory::GetDirectory failed getting directory: '{}'. Error: '{}'", + decoded, e.what()); + return false; + } + } +} diff --git a/xbmc/filesystem/ZeroconfDirectory.h b/xbmc/filesystem/ZeroconfDirectory.h new file mode 100644 index 0000000..fddc2ee --- /dev/null +++ b/xbmc/filesystem/ZeroconfDirectory.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IDirectory.h" +//txt-records as of http://www.dns-sd.org/ServiceTypes.html +#define TXT_RECORD_PATH_KEY "path" +#define TXT_RECORD_USERNAME_KEY "u" +#define TXT_RECORD_PASSWORD_KEY "p" + +namespace XFILE +{ + class CZeroconfDirectory : public IDirectory + { + public: + CZeroconfDirectory(void); + ~CZeroconfDirectory(void) override; + bool GetDirectory(const CURL& url, CFileItemList &items) override; + DIR_CACHE_TYPE GetCacheType(const CURL& url) const override { return DIR_CACHE_NEVER; } + }; +} + diff --git a/xbmc/filesystem/ZipDirectory.cpp b/xbmc/filesystem/ZipDirectory.cpp new file mode 100644 index 0000000..bfe43fb --- /dev/null +++ b/xbmc/filesystem/ZipDirectory.cpp @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "ZipDirectory.h" + +#include "FileItem.h" +#include "URL.h" +#include "ZipManager.h" +#include "filesystem/Directorization.h" +#include "utils/CharsetConverter.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +#include <vector> + +namespace XFILE +{ + + static CFileItemPtr ZipEntryToFileItem(const SZipEntry& entry, const std::string& label, const std::string& path, bool isFolder) + { + CFileItemPtr item(new CFileItem(label)); + if (!isFolder) + { + item->m_dwSize = entry.usize; + item->m_idepth = entry.method; + } + + return item; + } + + CZipDirectory::CZipDirectory() = default; + + CZipDirectory::~CZipDirectory() = default; + + bool CZipDirectory::GetDirectory(const CURL& urlOrig, CFileItemList& items) + { + CURL urlZip(urlOrig); + + /* if this isn't a proper archive path, assume it's the path to a archive file */ + if (!urlOrig.IsProtocol("zip")) + urlZip = URIUtils::CreateArchivePath("zip", urlOrig); + + std::vector<SZipEntry> zipEntries; + if (!g_ZipManager.GetZipList(urlZip, zipEntries)) + return false; + + // prepare the ZIP entries for directorization + DirectorizeEntries<SZipEntry> entries; + entries.reserve(zipEntries.size()); + for (const auto& zipEntry : zipEntries) + entries.push_back(DirectorizeEntry<SZipEntry>(zipEntry.name, zipEntry)); + + // directorize the ZIP entries into files and directories + Directorize(urlZip, entries, ZipEntryToFileItem, items); + + return true; + } + + bool CZipDirectory::ContainsFiles(const CURL& url) + { + std::vector<SZipEntry> items; + g_ZipManager.GetZipList(url, items); + if (items.size()) + { + if (items.size() > 1) + return true; + + return false; + } + + return false; + } +} + diff --git a/xbmc/filesystem/ZipDirectory.h b/xbmc/filesystem/ZipDirectory.h new file mode 100644 index 0000000..bd09c34 --- /dev/null +++ b/xbmc/filesystem/ZipDirectory.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IFileDirectory.h" + +namespace XFILE +{ + class CZipDirectory : public IFileDirectory + { + public: + CZipDirectory(); + ~CZipDirectory() override; + bool GetDirectory(const CURL& url, CFileItemList& items) override; + bool ContainsFiles(const CURL& url) override; + DIR_CACHE_TYPE GetCacheType(const CURL& url) const override { return DIR_CACHE_ALWAYS; } + }; +} diff --git a/xbmc/filesystem/ZipFile.cpp b/xbmc/filesystem/ZipFile.cpp new file mode 100644 index 0000000..eb3d953 --- /dev/null +++ b/xbmc/filesystem/ZipFile.cpp @@ -0,0 +1,531 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "ZipFile.h" + +#include "URL.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +#include <sys/stat.h> + +#define ZIP_CACHE_LIMIT 4*1024*1024 + +using namespace XFILE; + +CZipFile::CZipFile() +{ + m_szStringBuffer = NULL; + m_szStartOfStringBuffer = NULL; + m_iDataInStringBuffer = 0; + m_bCached = false; + m_iRead = -1; +} + +CZipFile::~CZipFile() +{ + delete[] m_szStringBuffer; + Close(); +} + +bool CZipFile::Open(const CURL&url) +{ + const std::string& strOpts = url.GetOptions(); + CURL url2(url); + url2.SetOptions(""); + if (!g_ZipManager.GetZipEntry(url2,mZipItem)) + return false; + + if ((mZipItem.flags & 64) == 64) + { + CLog::Log(LOGERROR,"FileZip: encrypted file, not supported!"); + return false; + } + + if ((mZipItem.method != 8) && (mZipItem.method != 0)) + { + CLog::Log(LOGERROR,"FileZip: unsupported compression method!"); + return false; + } + + if (mZipItem.method != 0 && mZipItem.usize > ZIP_CACHE_LIMIT && strOpts != "?cache=no") + { + if (!CFile::Exists("special://temp/" + URIUtils::GetFileName(url2))) + { + url2.SetOptions("?cache=no"); + const CURL pathToUrl("special://temp/" + URIUtils::GetFileName(url2)); + if (!CFile::Copy(url2, pathToUrl)) + return false; + } + m_bCached = true; + return mFile.Open("special://temp/" + URIUtils::GetFileName(url2)); + } + + if (!mFile.Open(url.GetHostName())) // this is the zip-file, always open binary + { + CLog::Log(LOGERROR, "FileZip: unable to open zip file {}!", url.GetHostName()); + return false; + } + mFile.Seek(mZipItem.offset,SEEK_SET); + return InitDecompress(); +} + +bool CZipFile::InitDecompress() +{ + m_iRead = 1; + m_iFilePos = 0; + m_iZipFilePos = 0; + m_iAvailBuffer = 0; + m_bFlush = false; + m_ZStream.zalloc = Z_NULL; + m_ZStream.zfree = Z_NULL; + m_ZStream.opaque = Z_NULL; + if( mZipItem.method != 0 ) + { + if (inflateInit2(&m_ZStream,-MAX_WBITS) != Z_OK) + { + CLog::Log(LOGERROR,"FileZip: error initializing zlib!"); + return false; + } + } + m_ZStream.next_in = (Bytef*)m_szBuffer; + m_ZStream.avail_in = 0; + m_ZStream.total_out = 0; + + return true; +} + +int64_t CZipFile::GetLength() +{ + return mZipItem.usize; +} + +int64_t CZipFile::GetPosition() +{ + if (m_bCached) + return mFile.GetPosition(); + + return m_iFilePos; +} + +int64_t CZipFile::Seek(int64_t iFilePosition, int iWhence) +{ + if (m_bCached) + return mFile.Seek(iFilePosition,iWhence); + if (mZipItem.method == 0) // this is easy + { + int64_t iResult; + switch (iWhence) + { + case SEEK_SET: + if (iFilePosition > mZipItem.usize) + return -1; + m_iFilePos = iFilePosition; + m_iZipFilePos = m_iFilePos; + iResult = mFile.Seek(iFilePosition+mZipItem.offset,SEEK_SET)-mZipItem.offset; + return iResult; + break; + + case SEEK_CUR: + if (m_iFilePos+iFilePosition > mZipItem.usize) + return -1; + m_iFilePos += iFilePosition; + m_iZipFilePos = m_iFilePos; + iResult = mFile.Seek(iFilePosition,SEEK_CUR)-mZipItem.offset; + return iResult; + break; + + case SEEK_END: + if (iFilePosition > mZipItem.usize) + return -1; + m_iFilePos = mZipItem.usize+iFilePosition; + m_iZipFilePos = m_iFilePos; + iResult = mFile.Seek(mZipItem.offset+mZipItem.usize+iFilePosition,SEEK_SET)-mZipItem.offset; + return iResult; + break; + default: + return -1; + + } + } + // here goes the stupid part.. + if (mZipItem.method == 8) + { + static const int blockSize = 128 * 1024; + std::vector<char> buf(blockSize); + switch (iWhence) + { + case SEEK_SET: + if (iFilePosition == m_iFilePos) + return m_iFilePos; // mp3reader does this lots-of-times + if (iFilePosition > mZipItem.usize || iFilePosition < 0) + return -1; + // read until position in 128k blocks.. only way to do it due to format. + // can't start in the middle of data since then we'd have no clue where + // we are in uncompressed data.. + if (iFilePosition < m_iFilePos) + { + m_iFilePos = 0; + m_iZipFilePos = 0; + inflateEnd(&m_ZStream); + inflateInit2(&m_ZStream,-MAX_WBITS); // simply restart zlib + mFile.Seek(mZipItem.offset,SEEK_SET); + m_ZStream.next_in = (Bytef*)m_szBuffer; + m_ZStream.avail_in = 0; + m_ZStream.total_out = 0; + while (m_iFilePos < iFilePosition) + { + ssize_t iToRead = (iFilePosition - m_iFilePos) > blockSize ? blockSize : iFilePosition - m_iFilePos; + if (Read(buf.data(), iToRead) != iToRead) + return -1; + } + return m_iFilePos; + } + else // seek forward + return Seek(iFilePosition-m_iFilePos,SEEK_CUR); + break; + + case SEEK_CUR: + if (iFilePosition < 0) + return Seek(m_iFilePos+iFilePosition,SEEK_SET); // can't rewind stream + // read until requested position, drop data + if (m_iFilePos+iFilePosition > mZipItem.usize) + return -1; + iFilePosition += m_iFilePos; + while (m_iFilePos < iFilePosition) + { + ssize_t iToRead = (iFilePosition - m_iFilePos)>blockSize ? blockSize : iFilePosition - m_iFilePos; + if (Read(buf.data(), iToRead) != iToRead) + return -1; + } + return m_iFilePos; + break; + + case SEEK_END: + // now this is a nasty bastard, possibly takes lotsoftime + // uncompress, minding m_ZStream.total_out + + while(static_cast<ssize_t>(m_ZStream.total_out) < mZipItem.usize+iFilePosition) + { + ssize_t iToRead = (mZipItem.usize + iFilePosition - m_ZStream.total_out > blockSize) ? blockSize : mZipItem.usize + iFilePosition - m_ZStream.total_out; + if (Read(buf.data(), iToRead) != iToRead) + return -1; + } + return m_iFilePos; + break; + default: + return -1; + } + } + return -1; +} + +bool CZipFile::Exists(const CURL& url) +{ + SZipEntry item; + if (g_ZipManager.GetZipEntry(url,item)) + return true; + return false; +} + +int CZipFile::Stat(struct __stat64 *buffer) +{ + int ret; + struct tm tm = {}; + + ret = mFile.Stat(buffer); + tm.tm_sec = (mZipItem.mod_time & 0x1F) << 1; + tm.tm_min = (mZipItem.mod_time & 0x7E0) >> 5; + tm.tm_hour = (mZipItem.mod_time & 0xF800) >> 11; + tm.tm_mday = (mZipItem.mod_date & 0x1F); + tm.tm_mon = (mZipItem.mod_date & 0x1E0) >> 5; + tm.tm_year = (mZipItem.mod_date & 0xFE00) >> 9; + buffer->st_atime = buffer->st_ctime = buffer->st_mtime = mktime(&tm); + + buffer->st_size = mZipItem.usize; + buffer->st_dev = (buffer->st_dev << 16) ^ (buffer->st_ino << 16); + buffer->st_ino ^= mZipItem.crc32; + return ret; +} + +int CZipFile::Stat(const CURL& url, struct __stat64* buffer) +{ + if (!buffer) + return -1; + + if (!g_ZipManager.GetZipEntry(url, mZipItem)) + { + if (url.GetFileName().empty() && CFile::Exists(url.GetHostName())) + { // when accessing the zip "root" recognize it as a directory + *buffer = {}; + buffer->st_mode = _S_IFDIR; + return 0; + } + else + return -1; + } + + *buffer = {}; + buffer->st_gid = 0; + buffer->st_atime = buffer->st_ctime = mZipItem.mod_time; + buffer->st_size = mZipItem.usize; + return 0; +} + +ssize_t CZipFile::Read(void* lpBuf, size_t uiBufSize) +{ + if (uiBufSize > SSIZE_MAX) + uiBufSize = SSIZE_MAX; + + if (m_bCached) + return mFile.Read(lpBuf,uiBufSize); + + // flush what might be left in the string buffer + if (m_iDataInStringBuffer > 0) + { + size_t iMax = uiBufSize>m_iDataInStringBuffer?m_iDataInStringBuffer:uiBufSize; + memcpy(lpBuf,m_szStartOfStringBuffer,iMax); + uiBufSize -= iMax; + m_iDataInStringBuffer -= iMax; + } + if (mZipItem.method == 8) // deflated + { + uLong iDecompressed = 0; + uLong prevOut = m_ZStream.total_out; + while ((iDecompressed < uiBufSize) && ((m_iZipFilePos < mZipItem.csize) || (m_bFlush))) + { + m_ZStream.next_out = (Bytef*)(lpBuf)+iDecompressed; + m_ZStream.avail_out = static_cast<uInt>(uiBufSize-iDecompressed); + if (m_bFlush) // need to flush buffer ! + { + int iMessage = inflate(&m_ZStream,Z_SYNC_FLUSH); + m_bFlush = ((iMessage == Z_OK) && (m_ZStream.avail_out == 0))?true:false; + if (!m_ZStream.avail_out) // flush filled buffer, get out of here + { + iDecompressed = m_ZStream.total_out-prevOut; + break; + } + } + + if (!m_ZStream.avail_in) + { + if (!FillBuffer()) // eof! + { + iDecompressed = m_ZStream.total_out-prevOut; + break; + } + } + + int iMessage = inflate(&m_ZStream,Z_SYNC_FLUSH); + if (iMessage < 0) + { + Close(); + return -1; // READ ERROR + } + + m_bFlush = ((iMessage == Z_OK) && (m_ZStream.avail_out == 0))?true:false; // more info in input buffer + + iDecompressed = m_ZStream.total_out-prevOut; + } + m_iFilePos += iDecompressed; + return static_cast<unsigned int>(iDecompressed); + } + else if (mZipItem.method == 0) // uncompressed. just read from file, but mind our boundaries. + { + if (uiBufSize+m_iFilePos > mZipItem.csize) + uiBufSize = mZipItem.csize-m_iFilePos; + + if (uiBufSize == 0) + return 0; // we are past eof, this shouldn't happen but test anyway + + ssize_t iResult = mFile.Read(lpBuf,uiBufSize); + if (iResult < 0) + return -1; + m_iZipFilePos += iResult; + m_iFilePos += iResult; + return iResult; + } + else + return -1; // shouldn't happen. compression method checked in open +} + +void CZipFile::Close() +{ + if (mZipItem.method == 8 && !m_bCached && m_iRead != -1) + inflateEnd(&m_ZStream); + + mFile.Close(); +} + +bool CZipFile::FillBuffer() +{ + ssize_t sToRead = 65535; + if (m_iZipFilePos+65535 > mZipItem.csize) + sToRead = mZipItem.csize-m_iZipFilePos; + + if (sToRead <= 0) + return false; // eof! + + if (mFile.Read(m_szBuffer,sToRead) != sToRead) + return false; + m_ZStream.avail_in = static_cast<unsigned int>(sToRead); + m_ZStream.next_in = reinterpret_cast<Byte*>(m_szBuffer); + m_iZipFilePos += sToRead; + return true; +} + +void CZipFile::DestroyBuffer(void* lpBuffer, int iBufSize) +{ + if (!m_bFlush) + return; + int iMessage = Z_OK; + while ((iMessage == Z_OK) && (m_ZStream.avail_out == 0)) + { + m_ZStream.next_out = (Bytef*)lpBuffer; + m_ZStream.avail_out = iBufSize; + iMessage = inflate(&m_ZStream,Z_SYNC_FLUSH); + } + m_bFlush = false; +} + +int CZipFile::UnpackFromMemory(std::string& strDest, const std::string& strInput, bool isGZ) +{ + unsigned int iPos=0; + int iResult=0; + while( iPos+LHDR_SIZE < strInput.size() || isGZ) + { + if (!isGZ) + { + CZipManager::readHeader(strInput.data()+iPos,mZipItem); + if (mZipItem.header == ZIP_DATA_RECORD_HEADER) + { + // this header concerns a file we already processed, so we can just skip it + iPos += DREC_SIZE; + continue; + } + if (mZipItem.header != ZIP_LOCAL_HEADER) + return iResult; + if( (mZipItem.flags & 8) == 8 ) + { + // if an extended local header (=data record header) is present, + // the following fields are 0 in the local header and we need to read + // them from the extended local header + + // search for the extended local header + unsigned int i = iPos + LHDR_SIZE + mZipItem.flength + mZipItem.elength; + while (1) + { + if (i + DREC_SIZE > strInput.size()) + { + CLog::Log(LOGERROR, "FileZip: extended local header expected, but not present!"); + return iResult; + } + if ((strInput[i] == 0x50) && (strInput[i + 1] == 0x4b) && + (strInput[i + 2] == 0x07) && (strInput[i + 3] == 0x08)) + break; // header found + i++; + } + // ZIP is little endian: + mZipItem.crc32 = static_cast<uint8_t>(strInput[i + 4]) | + static_cast<uint8_t>(strInput[i + 5]) << 8 | + static_cast<uint8_t>(strInput[i + 6]) << 16 | + static_cast<uint8_t>(strInput[i + 7]) << 24; + mZipItem.csize = static_cast<uint8_t>(strInput[i + 8]) | + static_cast<uint8_t>(strInput[i + 9]) << 8 | + static_cast<uint8_t>(strInput[i + 10]) << 16 | + static_cast<uint8_t>(strInput[i + 11]) << 24; + mZipItem.usize = static_cast<uint8_t>(strInput[i + 12]) | + static_cast<uint8_t>(strInput[i + 13]) << 8 | + static_cast<uint8_t>(strInput[i + 14]) << 16 | + static_cast<uint8_t>(strInput[i + 15]) << 24; + } + } + if (!InitDecompress()) + return iResult; + // we have a file - fill the buffer + char* temp; + ssize_t toRead=0; + if (isGZ) + { + m_ZStream.avail_in = static_cast<unsigned int>(strInput.size()); + m_ZStream.next_in = const_cast<Bytef*>((const Bytef*)strInput.data()); + temp = new char[8192]; + toRead = 8191; + } + else + { + m_ZStream.avail_in = mZipItem.csize; + m_ZStream.next_in = const_cast<Bytef*>((const Bytef*)strInput.data())+iPos+LHDR_SIZE+mZipItem.flength+mZipItem.elength; + // init m_zipitem + strDest.reserve(mZipItem.usize); + temp = new char[mZipItem.usize+1]; + toRead = mZipItem.usize; + } + int iCurrResult; + while((iCurrResult = static_cast<int>(Read(temp, toRead))) > 0) + { + strDest.append(temp,temp+iCurrResult); + iResult += iCurrResult; + } + Close(); + delete[] temp; + iPos += LHDR_SIZE+mZipItem.flength+mZipItem.elength+mZipItem.csize; + if (isGZ) + break; + } + + return iResult; +} + +bool CZipFile::DecompressGzip(const std::string& in, std::string& out) +{ + const int windowBits = MAX_WBITS + 16; + + z_stream strm; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + + int err = inflateInit2(&strm, windowBits); + if (err != Z_OK) + { + CLog::Log(LOGERROR, "FileZip: zlib error {}", err); + return false; + } + + const int bufferSize = 16384; + unsigned char buffer[bufferSize]; + + strm.avail_in = static_cast<unsigned int>(in.size()); + strm.next_in = reinterpret_cast<unsigned char*>(const_cast<char*>(in.c_str())); + + do + { + strm.avail_out = bufferSize; + strm.next_out = buffer; + int err = inflate(&strm, Z_NO_FLUSH); + switch (err) + { + case Z_NEED_DICT: + err = Z_DATA_ERROR; + [[fallthrough]]; + case Z_DATA_ERROR: + case Z_MEM_ERROR: + case Z_STREAM_ERROR: + CLog::Log(LOGERROR, "FileZip: failed to decompress. zlib error {}", err); + inflateEnd(&strm); + return false; + } + int read = bufferSize - strm.avail_out; + out.append((char*)buffer, read); + } + while (strm.avail_out == 0); + + inflateEnd(&strm); + return true; +} diff --git a/xbmc/filesystem/ZipFile.h b/xbmc/filesystem/ZipFile.h new file mode 100644 index 0000000..1b09e5c --- /dev/null +++ b/xbmc/filesystem/ZipFile.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "File.h" +#include "IFile.h" +#include "ZipManager.h" + +#include <zlib.h> + +namespace XFILE +{ + class CZipFile : public IFile + { + public: + CZipFile(); + ~CZipFile() override; + + int64_t GetPosition() override; + int64_t GetLength() override; + bool Open(const CURL& url) override; + bool Exists(const CURL& url) override; + int Stat(struct __stat64* buffer) override; + int Stat(const CURL& url, struct __stat64* buffer) override; + ssize_t Read(void* lpBuf, size_t uiBufSize) override; + //virtual bool ReadString(char *szLine, int iLineLength); + int64_t Seek(int64_t iFilePosition, int iWhence = SEEK_SET) override; + void Close() override; + + //NOTE: gzip doesn't work. use DecompressGzip() instead + int UnpackFromMemory(std::string& strDest, const std::string& strInput, bool isGZ=false); + + /*! Decompress gzip encoded buffer in-memory */ + static bool DecompressGzip(const std::string& in, std::string& out); + + private: + bool InitDecompress(); + bool FillBuffer(); + void DestroyBuffer(void* lpBuffer, int iBufSize); + CFile mFile; + SZipEntry mZipItem; + int64_t m_iFilePos = 0; // position in _uncompressed_ data read + int64_t m_iZipFilePos = 0; // position in _compressed_ data + int m_iAvailBuffer = 0; + z_stream m_ZStream; + char m_szBuffer[65535]; // 64k buffer for compressed data + char* m_szStringBuffer; + char* m_szStartOfStringBuffer; // never allocated! + size_t m_iDataInStringBuffer; + int m_iRead; + bool m_bFlush = false; + bool m_bCached; + }; +} + diff --git a/xbmc/filesystem/ZipManager.cpp b/xbmc/filesystem/ZipManager.cpp new file mode 100644 index 0000000..710cfbc --- /dev/null +++ b/xbmc/filesystem/ZipManager.cpp @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "ZipManager.h" + +#include <algorithm> +#include <utility> + +#include "File.h" +#include "URL.h" +#if defined(TARGET_POSIX) +#include "PlatformDefs.h" +#endif +#include "utils/CharsetConverter.h" +#include "utils/EndianSwap.h" +#include "utils/log.h" +#include "utils/RegExp.h" +#include "utils/URIUtils.h" + +using namespace XFILE; + +static const size_t ZC_FLAG_EFS = 1 << 11; // general purpose bit 11 - zip holds utf-8 filenames + +CZipManager::CZipManager() = default; + +CZipManager::~CZipManager() = default; + +bool CZipManager::GetZipList(const CURL& url, std::vector<SZipEntry>& items) +{ + struct __stat64 m_StatData = {}; + + std::string strFile = url.GetHostName(); + + if (CFile::Stat(strFile,&m_StatData)) + { + CLog::Log(LOGDEBUG, "CZipManager::GetZipList: failed to stat file {}", url.GetRedacted()); + return false; + } + + std::map<std::string, std::vector<SZipEntry> >::iterator it = mZipMap.find(strFile); + if (it != mZipMap.end()) // already listed, just return it if not changed, else release and reread + { + std::map<std::string,int64_t>::iterator it2=mZipDate.find(strFile); + + if (m_StatData.st_mtime == it2->second) + { + items = it->second; + return true; + } + mZipMap.erase(it); + mZipDate.erase(it2); + } + + CFile mFile; + if (!mFile.Open(strFile)) + { + CLog::Log(LOGDEBUG, "ZipManager: unable to open file {}!", strFile); + return false; + } + + unsigned int hdr; + if (mFile.Read(&hdr, 4)!=4 || (Endian_SwapLE32(hdr) != ZIP_LOCAL_HEADER && + Endian_SwapLE32(hdr) != ZIP_DATA_RECORD_HEADER && + Endian_SwapLE32(hdr) != ZIP_SPLIT_ARCHIVE_HEADER)) + { + CLog::Log(LOGDEBUG,"ZipManager: not a zip file!"); + mFile.Close(); + return false; + } + + if (Endian_SwapLE32(hdr) == ZIP_SPLIT_ARCHIVE_HEADER) + CLog::LogF(LOGWARNING, "ZIP split archive header found. Trying to process as a single archive.."); + + // push date for update detection + mZipDate.insert(make_pair(strFile,m_StatData.st_mtime)); + + + // Look for end of central directory record + // Zipfile comment may be up to 65535 bytes + // End of central directory record is 22 bytes (ECDREC_SIZE) + // -> need to check the last 65557 bytes + int64_t fileSize = mFile.GetLength(); + // Don't need to look in the last 18 bytes (ECDREC_SIZE-4) + // But as we need to do overlapping between blocks (3 bytes), + // we start the search at ECDREC_SIZE-1 from the end of file + if (fileSize < ECDREC_SIZE - 1) + { + CLog::Log(LOGERROR, "ZipManager: Invalid zip file length: {}", fileSize); + return false; + } + int searchSize = (int) std::min(static_cast<int64_t>(65557), fileSize-ECDREC_SIZE+1); + int blockSize = (int) std::min(1024, searchSize); + int nbBlock = searchSize / blockSize; + int extraBlockSize = searchSize % blockSize; + // Signature is on 4 bytes + // It could be between 2 blocks, so we need to read 3 extra bytes + std::vector<char> buffer(blockSize + 3); + bool found = false; + + // Loop through blocks starting at the end of the file (minus ECDREC_SIZE-1) + for (int nb=1; !found && (nb <= nbBlock); nb++) + { + mFile.Seek(fileSize-ECDREC_SIZE+1-(blockSize*nb),SEEK_SET); + if (mFile.Read(buffer.data(), blockSize + 3) != blockSize + 3) + return false; + for (int i=blockSize-1; !found && (i >= 0); i--) + { + if (Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer.data() + i)) == ZIP_END_CENTRAL_HEADER) + { + // Set current position to start of end of central directory + mFile.Seek(fileSize-ECDREC_SIZE+1-(blockSize*nb)+i,SEEK_SET); + found = true; + } + } + } + + // If not found, look in the last block left... + if ( !found && (extraBlockSize > 0) ) + { + mFile.Seek(fileSize-ECDREC_SIZE+1-searchSize,SEEK_SET); + if (mFile.Read(buffer.data(), extraBlockSize + 3) != extraBlockSize + 3) + return false; + for (int i=extraBlockSize-1; !found && (i >= 0); i--) + { + if (Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer.data() + i)) == ZIP_END_CENTRAL_HEADER) + { + // Set current position to start of end of central directory + mFile.Seek(fileSize-ECDREC_SIZE+1-searchSize+i,SEEK_SET); + found = true; + } + } + } + + buffer.clear(); + + if ( !found ) + { + CLog::Log(LOGDEBUG, "ZipManager: broken file {}!", strFile); + mFile.Close(); + return false; + } + + unsigned int cdirOffset, cdirSize; + // Get size of the central directory + mFile.Seek(12,SEEK_CUR); + if (mFile.Read(&cdirSize, 4) != 4) + return false; + cdirSize = Endian_SwapLE32(cdirSize); + // Get Offset of start of central directory with respect to the starting disk number + if (mFile.Read(&cdirOffset, 4) != 4) + return false; + cdirOffset = Endian_SwapLE32(cdirOffset); + + // Go to the start of central directory + mFile.Seek(cdirOffset,SEEK_SET); + + CRegExp pathTraversal; + pathTraversal.RegComp(PATH_TRAVERSAL); + + char temp[CHDR_SIZE]; + while (mFile.GetPosition() < cdirOffset + cdirSize) + { + SZipEntry ze; + if (mFile.Read(temp, CHDR_SIZE) != CHDR_SIZE) + return false; + readCHeader(temp, ze); + if (ze.header != ZIP_CENTRAL_HEADER) + { + CLog::Log(LOGDEBUG, "ZipManager: broken file {}!", strFile); + mFile.Close(); + return false; + } + + // Get the filename just after the central file header + std::vector<char> bufName(ze.flength); + if (mFile.Read(bufName.data(), ze.flength) != ze.flength) + return false; + std::string strName(bufName.data(), bufName.size()); + bufName.clear(); + if ((ze.flags & ZC_FLAG_EFS) == 0) + { + std::string tmp(strName); + g_charsetConverter.ToUtf8("CP437", tmp, strName); + } + memset(ze.name, 0, 255); + strncpy(ze.name, strName.c_str(), strName.size() > 254 ? 254 : strName.size()); + + // Jump after central file header extra field and file comment + mFile.Seek(ze.eclength + ze.clength,SEEK_CUR); + + if (pathTraversal.RegFind(strName) < 0) + items.push_back(ze); + } + + /* go through list and figure out file header lengths */ + for (auto& ze : items) + { + // Go to the local file header to get the extra field length + // !! local header extra field length != central file header extra field length !! + mFile.Seek(ze.lhdrOffset+28,SEEK_SET); + if (mFile.Read(&(ze.elength), 2) != 2) + return false; + ze.elength = Endian_SwapLE16(ze.elength); + + // Compressed data offset = local header offset + size of local header + filename length + local file header extra field length + ze.offset = ze.lhdrOffset + LHDR_SIZE + ze.flength + ze.elength; + + } + + mZipMap.insert(make_pair(strFile,items)); + mFile.Close(); + return true; +} + +bool CZipManager::GetZipEntry(const CURL& url, SZipEntry& item) +{ + const std::string& strFile = url.GetHostName(); + + std::map<std::string, std::vector<SZipEntry> >::iterator it = mZipMap.find(strFile); + std::vector<SZipEntry> items; + if (it == mZipMap.end()) // we need to list the zip + { + GetZipList(url,items); + } + else + { + items = it->second; + } + + const std::string& strFileName = url.GetFileName(); + for (const auto& it2 : items) + { + if (std::string(it2.name) == strFileName) + { + item = it2; + return true; + } + } + return false; +} + +bool CZipManager::ExtractArchive(const std::string& strArchive, const std::string& strPath) +{ + const CURL pathToUrl(strArchive); + return ExtractArchive(pathToUrl, strPath); +} + +bool CZipManager::ExtractArchive(const CURL& archive, const std::string& strPath) +{ + std::vector<SZipEntry> entry; + CURL url = URIUtils::CreateArchivePath("zip", archive); + GetZipList(url, entry); + for (const auto& it : entry) + { + if (it.name[strlen(it.name) - 1] == '/') // skip dirs + continue; + std::string strFilePath(it.name); + + CURL zipPath = URIUtils::CreateArchivePath("zip", archive, strFilePath); + const CURL pathToUrl(strPath + strFilePath); + if (!CFile::Copy(zipPath, pathToUrl)) + return false; + } + return true; +} + +// Read local file header +void CZipManager::readHeader(const char* buffer, SZipEntry& info) +{ + info.header = Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer)); + info.version = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 4)); + info.flags = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 6)); + info.method = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 8)); + info.mod_time = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 10)); + info.mod_date = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 12)); + info.crc32 = Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer + 14)); + info.csize = Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer + 18)); + info.usize = Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer + 22)); + info.flength = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 26)); + info.elength = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 28)); +} + +// Read central file header (from central directory) +void CZipManager::readCHeader(const char* buffer, SZipEntry& info) +{ + info.header = Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer)); + // Skip version made by + info.version = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 6)); + info.flags = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 8)); + info.method = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 10)); + info.mod_time = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 12)); + info.mod_date = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 14)); + info.crc32 = Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer + 16)); + info.csize = Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer + 20)); + info.usize = Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer + 24)); + info.flength = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 28)); + info.eclength = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 30)); + info.clength = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 32)); + // Skip disk number start, internal/external file attributes + info.lhdrOffset = Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer + 42)); +} + +void CZipManager::release(const std::string& strPath) +{ + CURL url(strPath); + std::map<std::string, std::vector<SZipEntry> >::iterator it= mZipMap.find(url.GetHostName()); + if (it != mZipMap.end()) + { + std::map<std::string,int64_t>::iterator it2=mZipDate.find(url.GetHostName()); + mZipMap.erase(it); + mZipDate.erase(it2); + } +} + + diff --git a/xbmc/filesystem/ZipManager.h b/xbmc/filesystem/ZipManager.h new file mode 100644 index 0000000..3fba27f --- /dev/null +++ b/xbmc/filesystem/ZipManager.h @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +// See http://www.pkware.com/documents/casestudies/APPNOTE.TXT +#define ZIP_LOCAL_HEADER 0x04034b50 +#define ZIP_DATA_RECORD_HEADER 0x08074b50 +#define ZIP_CENTRAL_HEADER 0x02014b50 +#define ZIP_END_CENTRAL_HEADER 0x06054b50 +#define ZIP_SPLIT_ARCHIVE_HEADER 0x30304b50 +#define LHDR_SIZE 30 +#define DREC_SIZE 16 +#define CHDR_SIZE 46 +#define ECDREC_SIZE 22 + +#include <cstring> +#include <map> +#include <string> +#include <vector> + +class CURL; + +static const std::string PATH_TRAVERSAL(R"_((^|\/|\\)\.{2}($|\/|\\))_"); + +struct SZipEntry { + unsigned int header = 0; + unsigned short version = 0; + unsigned short flags = 0; + unsigned short method = 0; + unsigned short mod_time = 0; + unsigned short mod_date = 0; + unsigned int crc32 = 0; + unsigned int csize = 0; // compressed size + unsigned int usize = 0; // uncompressed size + unsigned short flength = 0; // filename length + unsigned short elength = 0; // extra field length (local file header) + unsigned short eclength = 0; // extra field length (central file header) + unsigned short clength = 0; // file comment length (central file header) + unsigned int lhdrOffset = 0; // Relative offset of local header + int64_t offset = 0; // offset in file to compressed data + char name[255]; + + SZipEntry() + { + name[0] = '\0'; + } +}; + +class CZipManager +{ +public: + CZipManager(); + ~CZipManager(); + + bool GetZipList(const CURL& url, std::vector<SZipEntry>& items); + bool GetZipEntry(const CURL& url, SZipEntry& item); + bool ExtractArchive(const std::string& strArchive, const std::string& strPath); + bool ExtractArchive(const CURL& archive, const std::string& strPath); + void release(const std::string& strPath); // release resources used by list zip + static void readHeader(const char* buffer, SZipEntry& info); + static void readCHeader(const char* buffer, SZipEntry& info); +private: + std::map<std::string,std::vector<SZipEntry> > mZipMap; + std::map<std::string,int64_t> mZipDate; + + template<typename T> + static T ReadUnaligned(const void* mem) + { + T var; + std::memcpy(&var, mem, sizeof(T)); + return var; + } +}; + +extern CZipManager g_ZipManager; + diff --git a/xbmc/filesystem/test/CMakeLists.txt b/xbmc/filesystem/test/CMakeLists.txt new file mode 100644 index 0000000..9572459 --- /dev/null +++ b/xbmc/filesystem/test/CMakeLists.txt @@ -0,0 +1,15 @@ +set(SOURCES TestDirectory.cpp + TestFile.cpp + TestFileFactory.cpp + TestZipFile.cpp + TestZipManager.cpp) + +if(MICROHTTPD_FOUND) + list(APPEND SOURCES TestHTTPDirectory.cpp) +endif() + +if(NFS_FOUND) + list(APPEND SOURCES TestNfsFile.cpp) +endif() + +core_add_test_library(filesystem_test) diff --git a/xbmc/filesystem/test/TestDirectory.cpp b/xbmc/filesystem/test/TestDirectory.cpp new file mode 100644 index 0000000..e99408c --- /dev/null +++ b/xbmc/filesystem/test/TestDirectory.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "FileItem.h" +#include "filesystem/Directory.h" +#include "filesystem/IDirectory.h" +#include "filesystem/SpecialProtocol.h" +#include "test/TestUtils.h" +#include "utils/URIUtils.h" + +#include <gtest/gtest.h> + +TEST(TestDirectory, General) +{ + std::string tmppath1, tmppath2, tmppath3; + CFileItemList items; + CFileItemPtr itemptr; + tmppath1 = CSpecialProtocol::TranslatePath("special://temp/"); + tmppath1 = URIUtils::AddFileToFolder(tmppath1, "TestDirectory"); + tmppath2 = tmppath1; + tmppath2 = URIUtils::AddFileToFolder(tmppath2, "subdir"); + EXPECT_TRUE(XFILE::CDirectory::Create(tmppath1)); + EXPECT_TRUE(XFILE::CDirectory::Exists(tmppath1)); + EXPECT_FALSE(XFILE::CDirectory::Exists(tmppath2)); + EXPECT_TRUE(XFILE::CDirectory::Create(tmppath2)); + EXPECT_TRUE(XFILE::CDirectory::Exists(tmppath2)); + EXPECT_TRUE(XFILE::CDirectory::GetDirectory(tmppath1, items, "", XFILE::DIR_FLAG_DEFAULTS)); + XFILE::CDirectory::FilterFileDirectories(items, ""); + tmppath3 = tmppath2; + URIUtils::AddSlashAtEnd(tmppath3); + itemptr = items[0]; + EXPECT_STREQ(tmppath3.c_str(), itemptr->GetPath().c_str()); + EXPECT_TRUE(XFILE::CDirectory::Remove(tmppath2)); + EXPECT_FALSE(XFILE::CDirectory::Exists(tmppath2)); + EXPECT_TRUE(XFILE::CDirectory::Exists(tmppath1)); + EXPECT_TRUE(XFILE::CDirectory::Remove(tmppath1)); + EXPECT_FALSE(XFILE::CDirectory::Exists(tmppath1)); +} + +TEST(TestDirectory, CreateRecursive) +{ + auto path1 = URIUtils::AddFileToFolder( + CSpecialProtocol::TranslatePath("special://temp/"), + "level1"); + auto path2 = URIUtils::AddFileToFolder(path1, + "level2", + "level3"); + + EXPECT_TRUE(XFILE::CDirectory::Create(path2)); + EXPECT_TRUE(XFILE::CDirectory::RemoveRecursive(path1)); +} diff --git a/xbmc/filesystem/test/TestFile.cpp b/xbmc/filesystem/test/TestFile.cpp new file mode 100644 index 0000000..abefe46 --- /dev/null +++ b/xbmc/filesystem/test/TestFile.cpp @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "filesystem/File.h" +#include "test/TestUtils.h" + +#include <errno.h> +#include <string> + +#include <gtest/gtest.h> + +TEST(TestFile, Read) +{ + const std::string newLine = CXBMCTestUtils::Instance().getNewLineCharacters(); + const size_t size = 1616; + const size_t lines = 25; + size_t addPerLine = newLine.length() - 1; + size_t realSize = size + lines * addPerLine; + + const std::string firstBuf = "About" + newLine + "-----" + newLine + "XBMC is "; + const std::string secondBuf = "an award-winning fre"; + const std::string thirdBuf = "ent hub for digital "; + const std::string fourthBuf = "rs, XBMC is a non-pr"; + const std::string fifthBuf = "multimedia jukebox." + newLine; + + XFILE::CFile file; + char buf[23] = {}; + + size_t currentPos; + ASSERT_TRUE(file.Open( + XBMC_REF_FILE_PATH("/xbmc/filesystem/test/reffile.txt"))); + EXPECT_EQ(0, file.GetPosition()); + EXPECT_EQ(realSize, file.GetLength()); + EXPECT_EQ(firstBuf.length(), static_cast<size_t>(file.Read(buf, firstBuf.length()))); + file.Flush(); + currentPos = firstBuf.length(); + EXPECT_EQ(currentPos, file.GetPosition()); + EXPECT_EQ(0, memcmp(firstBuf.c_str(), buf, firstBuf.length())); + EXPECT_EQ(secondBuf.length(), static_cast<size_t>(file.Read(buf, secondBuf.length()))); + currentPos += secondBuf.length(); + EXPECT_EQ(currentPos, file.GetPosition()); + EXPECT_EQ(0, memcmp(secondBuf.c_str(), buf, secondBuf.length())); + currentPos = 100 + addPerLine * 3; + EXPECT_EQ(currentPos, file.Seek(currentPos)); + EXPECT_EQ(currentPos, file.GetPosition()); + EXPECT_EQ(thirdBuf.length(), static_cast<size_t>(file.Read(buf, thirdBuf.length()))); + file.Flush(); + currentPos += thirdBuf.length(); + EXPECT_EQ(currentPos, file.GetPosition()); + EXPECT_EQ(0, memcmp(thirdBuf.c_str(), buf, thirdBuf.length())); + currentPos += 100 + addPerLine * 1; + EXPECT_EQ(currentPos, file.Seek(100 + addPerLine * 1, SEEK_CUR)); + EXPECT_EQ(currentPos, file.GetPosition()); + EXPECT_EQ(fourthBuf.length(), static_cast<size_t>(file.Read(buf, fourthBuf.length()))); + file.Flush(); + currentPos += fourthBuf.length(); + EXPECT_EQ(currentPos, file.GetPosition()); + EXPECT_EQ(0, memcmp(fourthBuf.c_str(), buf, fourthBuf.length())); + currentPos = realSize - fifthBuf.length(); + EXPECT_EQ(currentPos, file.Seek(-(int64_t)fifthBuf.length(), SEEK_END)); + EXPECT_EQ(currentPos, file.GetPosition()); + EXPECT_EQ(fifthBuf.length(), static_cast<size_t>(file.Read(buf, fifthBuf.length()))); + file.Flush(); + currentPos += fifthBuf.length(); + EXPECT_EQ(currentPos, file.GetPosition()); + EXPECT_EQ(0, memcmp(fifthBuf.c_str(), buf, fifthBuf.length())); + currentPos += 100; + EXPECT_EQ(currentPos, file.Seek(100, SEEK_CUR)); + EXPECT_EQ(currentPos, file.GetPosition()); + currentPos = 0; + EXPECT_EQ(currentPos, file.Seek(currentPos, SEEK_SET)); + EXPECT_EQ(firstBuf.length(), static_cast<size_t>(file.Read(buf, firstBuf.length()))); + file.Flush(); + currentPos += firstBuf.length(); + EXPECT_EQ(currentPos, file.GetPosition()); + EXPECT_EQ(0, memcmp(firstBuf.c_str(), buf, firstBuf.length())); + EXPECT_EQ(0, file.Seek(0, SEEK_SET)); + EXPECT_EQ(-1, file.Seek(-100, SEEK_SET)); + file.Close(); +} + +TEST(TestFile, Write) +{ + XFILE::CFile *file; + const char str[] = "TestFile.Write test string\n"; + char buf[30] = {}; + + ASSERT_NE(nullptr, file = XBMC_CREATETEMPFILE("")); + file->Close(); + ASSERT_TRUE(file->OpenForWrite(XBMC_TEMPFILEPATH(file), true)); + EXPECT_EQ((int)sizeof(str), file->Write(str, sizeof(str))); + file->Flush(); + EXPECT_EQ((int64_t)sizeof(str), file->GetPosition()); + file->Close(); + ASSERT_TRUE(file->Open(XBMC_TEMPFILEPATH(file))); + EXPECT_EQ(0, file->GetPosition()); + EXPECT_EQ((int64_t)sizeof(str), file->Seek(0, SEEK_END)); + EXPECT_EQ(0, file->Seek(0, SEEK_SET)); + EXPECT_EQ((int64_t)sizeof(str), file->GetLength()); + EXPECT_EQ(sizeof(str), static_cast<size_t>(file->Read(buf, sizeof(buf)))); + file->Flush(); + EXPECT_EQ((int64_t)sizeof(str), file->GetPosition()); + EXPECT_EQ(0, memcmp(str, buf, sizeof(str))); + file->Close(); + EXPECT_TRUE(XBMC_DELETETEMPFILE(file)); +} + +TEST(TestFile, Exists) +{ + XFILE::CFile *file; + + ASSERT_NE(nullptr, file = XBMC_CREATETEMPFILE("")); + file->Close(); + EXPECT_TRUE(XFILE::CFile::Exists(XBMC_TEMPFILEPATH(file))); + EXPECT_FALSE(XFILE::CFile::Exists("")); + EXPECT_TRUE(XBMC_DELETETEMPFILE(file)); +} + +TEST(TestFile, Stat) +{ + XFILE::CFile *file; + struct __stat64 buffer; + + ASSERT_NE(nullptr, file = XBMC_CREATETEMPFILE("")); + EXPECT_EQ(0, file->Stat(&buffer)); + file->Close(); + EXPECT_NE(0U, buffer.st_mode | _S_IFREG); + EXPECT_EQ(-1, XFILE::CFile::Stat("", &buffer)); + EXPECT_EQ(ENOENT, errno); + EXPECT_TRUE(XBMC_DELETETEMPFILE(file)); +} + +TEST(TestFile, Delete) +{ + XFILE::CFile *file; + std::string path; + + ASSERT_NE(nullptr, file = XBMC_CREATETEMPFILE("")); + file->Close(); + path = XBMC_TEMPFILEPATH(file); + EXPECT_TRUE(XFILE::CFile::Exists(path)); + EXPECT_TRUE(XFILE::CFile::Delete(path)); + EXPECT_FALSE(XFILE::CFile::Exists(path)); + EXPECT_FALSE(XBMC_DELETETEMPFILE(file)); +} + +TEST(TestFile, Rename) +{ + XFILE::CFile *file1, *file2; + std::string path1, path2; + + ASSERT_NE(nullptr, file1 = XBMC_CREATETEMPFILE("")); + file1->Close(); + path1 = XBMC_TEMPFILEPATH(file1); + ASSERT_NE(nullptr, file2 = XBMC_CREATETEMPFILE("")); + file2->Close(); + path2 = XBMC_TEMPFILEPATH(file2); + EXPECT_TRUE(XFILE::CFile::Delete(path1)); + EXPECT_FALSE(XFILE::CFile::Exists(path1)); + EXPECT_TRUE(XFILE::CFile::Exists(path2)); + EXPECT_TRUE(XFILE::CFile::Rename(path2, path1)); + EXPECT_TRUE(XFILE::CFile::Exists(path1)); + EXPECT_FALSE(XFILE::CFile::Exists(path2)); + EXPECT_TRUE(XFILE::CFile::Delete(path1)); + EXPECT_FALSE(XBMC_DELETETEMPFILE(file1)); + EXPECT_FALSE(XBMC_DELETETEMPFILE(file2)); +} + +TEST(TestFile, Copy) +{ + XFILE::CFile *file1, *file2; + std::string path1, path2; + + ASSERT_NE(nullptr, file1 = XBMC_CREATETEMPFILE("")); + file1->Close(); + path1 = XBMC_TEMPFILEPATH(file1); + ASSERT_NE(nullptr, file2 = XBMC_CREATETEMPFILE("")); + file2->Close(); + path2 = XBMC_TEMPFILEPATH(file2); + EXPECT_TRUE(XFILE::CFile::Delete(path1)); + EXPECT_FALSE(XFILE::CFile::Exists(path1)); + EXPECT_TRUE(XFILE::CFile::Exists(path2)); + EXPECT_TRUE(XFILE::CFile::Copy(path2, path1)); + EXPECT_TRUE(XFILE::CFile::Exists(path1)); + EXPECT_TRUE(XFILE::CFile::Exists(path2)); + EXPECT_TRUE(XFILE::CFile::Delete(path1)); + EXPECT_TRUE(XFILE::CFile::Delete(path2)); + EXPECT_FALSE(XBMC_DELETETEMPFILE(file1)); + EXPECT_FALSE(XBMC_DELETETEMPFILE(file2)); +} + +TEST(TestFile, SetHidden) +{ + XFILE::CFile *file; + + ASSERT_NE(nullptr, file = XBMC_CREATETEMPFILE("")); + file->Close(); + EXPECT_TRUE(XFILE::CFile::Exists(XBMC_TEMPFILEPATH(file))); + bool result = XFILE::CFile::SetHidden(XBMC_TEMPFILEPATH(file), true); +#ifdef TARGET_WINDOWS + EXPECT_TRUE(result); +#else + EXPECT_FALSE(result); +#endif + EXPECT_TRUE(XFILE::CFile::Exists(XBMC_TEMPFILEPATH(file))); + EXPECT_TRUE(XBMC_DELETETEMPFILE(file)); +} diff --git a/xbmc/filesystem/test/TestFileFactory.cpp b/xbmc/filesystem/test/TestFileFactory.cpp new file mode 100644 index 0000000..6129592 --- /dev/null +++ b/xbmc/filesystem/test/TestFileFactory.cpp @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "ServiceBroker.h" +#include "filesystem/File.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "test/TestUtils.h" +#include "utils/StringUtils.h" + +#include <gtest/gtest.h> + +class TestFileFactory : public testing::Test +{ +protected: + TestFileFactory() + { + std::vector<std::string> advancedsettings = + CXBMCTestUtils::Instance().getAdvancedSettingsFiles(); + std::vector<std::string> guisettings = + CXBMCTestUtils::Instance().getGUISettingsFiles(); + + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + for (const auto& it : guisettings) + settings->Load(it); + + const std::shared_ptr<CAdvancedSettings> advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings(); + for (const auto& it : advancedsettings) + advancedSettings->ParseSettingsFile(it); + + settings->SetLoaded(); + } + + ~TestFileFactory() override + { + CServiceBroker::GetSettingsComponent()->GetSettings()->Unload(); + } +}; + +/* The tests for XFILE::CFileFactory are tested indirectly through + * XFILE::CFile. Since most parts of the VFS require some form of + * network connection, the settings and VFS URLs must be given as + * arguments in the main testsuite program. + */ +TEST_F(TestFileFactory, Read) +{ + XFILE::CFile file; + std::string str; + ssize_t size, i; + unsigned char buf[16]; + int64_t count = 0; + + std::vector<std::string> urls = + CXBMCTestUtils::Instance().getTestFileFactoryReadUrls(); + + for (const auto& url : urls) + { + std::cout << "Testing URL: " << url << std::endl; + ASSERT_TRUE(file.Open(url)); + std::cout << "file.GetLength(): " << + testing::PrintToString(file.GetLength()) << std::endl; + std::cout << "file.Seek(file.GetLength() / 2, SEEK_CUR) return value: " << + testing::PrintToString(file.Seek(file.GetLength() / 2, SEEK_CUR)) << std::endl; + std::cout << "file.Seek(0, SEEK_END) return value: " << + testing::PrintToString(file.Seek(0, SEEK_END)) << std::endl; + std::cout << "file.Seek(0, SEEK_SET) return value: " << + testing::PrintToString(file.Seek(0, SEEK_SET)) << std::endl; + std::cout << "File contents:" << std::endl; + while ((size = file.Read(buf, sizeof(buf))) > 0) + { + str = StringUtils::Format(" {:08X}", count); + std::cout << str << " "; + count += size; + for (i = 0; i < size; i++) + { + str = StringUtils::Format("{:02X} ", buf[i]); + std::cout << str; + } + while (i++ < static_cast<ssize_t> (sizeof(buf))) + std::cout << " "; + std::cout << " ["; + for (i = 0; i < size; i++) + { + if (buf[i] >= ' ' && buf[i] <= '~') + std::cout << buf[i]; + else + std::cout << "."; + } + std::cout << "]" << std::endl; + } + file.Close(); + } +} + +TEST_F(TestFileFactory, Write) +{ + XFILE::CFile file, inputfile; + std::string str; + size_t size, i; + unsigned char buf[16]; + int64_t count = 0; + + str = CXBMCTestUtils::Instance().getTestFileFactoryWriteInputFile(); + ASSERT_TRUE(inputfile.Open(str)); + + std::vector<std::string> urls = + CXBMCTestUtils::Instance().getTestFileFactoryWriteUrls(); + + for (const auto& url : urls) + { + std::cout << "Testing URL: " << url << std::endl; + std::cout << "Writing..."; + ASSERT_TRUE(file.OpenForWrite(url, true)); + while ((size = inputfile.Read(buf, sizeof(buf))) > 0) + { + EXPECT_GE(file.Write(buf, size), 0); + } + file.Close(); + std::cout << "done." << std::endl; + std::cout << "Reading..." << std::endl; + ASSERT_TRUE(file.Open(url)); + EXPECT_EQ(inputfile.GetLength(), file.GetLength()); + std::cout << "file.Seek(file.GetLength() / 2, SEEK_CUR) return value: " << + testing::PrintToString(file.Seek(file.GetLength() / 2, SEEK_CUR)) << std::endl; + std::cout << "file.Seek(0, SEEK_END) return value: " << + testing::PrintToString(file.Seek(0, SEEK_END)) << std::endl; + std::cout << "file.Seek(0, SEEK_SET) return value: " << + testing::PrintToString(file.Seek(0, SEEK_SET)) << std::endl; + std::cout << "File contents:\n"; + while ((size = file.Read(buf, sizeof(buf))) > 0) + { + str = StringUtils::Format(" {:08X}", count); + std::cout << str << " "; + count += size; + for (i = 0; i < size; i++) + { + str = StringUtils::Format("{:02X} ", buf[i]); + std::cout << str; + } + while (i++ < sizeof(buf)) + std::cout << " "; + std::cout << " ["; + for (i = 0; i < size; i++) + { + if (buf[i] >= ' ' && buf[i] <= '~') + std::cout << buf[i]; + else + std::cout << "."; + } + std::cout << "]" << std::endl; + } + file.Close(); + } + inputfile.Close(); +} diff --git a/xbmc/filesystem/test/TestHTTPDirectory.cpp b/xbmc/filesystem/test/TestHTTPDirectory.cpp new file mode 100644 index 0000000..7736307 --- /dev/null +++ b/xbmc/filesystem/test/TestHTTPDirectory.cpp @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2015-2020 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "FileItem.h" +#include "URL.h" +#include "filesystem/CurlFile.h" +#include "filesystem/HTTPDirectory.h" +#include "network/WebServer.h" +#include "network/httprequesthandler/HTTPVfsHandler.h" +#include "settings/MediaSourceSettings.h" +#include "test/TestUtils.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/XTimeUtils.h" + +#include <random> +#include <stdlib.h> + +#include <gtest/gtest.h> + +using namespace XFILE; + +#define WEBSERVER_HOST "localhost" + +#define SOURCE_PATH "xbmc/filesystem/test/data/httpdirectory/" + +#define TEST_FILE_APACHE_DEFAULT "apache-default.html" +#define TEST_FILE_APACHE_FANCY "apache-fancy.html" +#define TEST_FILE_APACHE_HTML "apache-html.html" +#define TEST_FILE_BASIC "basic.html" +#define TEST_FILE_BASIC_MULTILINE "basic-multiline.html" +#define TEST_FILE_LIGHTTP_DEFAULT "lighttp-default.html" +#define TEST_FILE_NGINX_DEFAULT "nginx-default.html" +#define TEST_FILE_NGINX_FANCYINDEX "nginx-fancyindex.html" + +#define SAMPLE_ITEM_COUNT 6 + +#define SAMPLE_ITEM_1_LABEL "folder1" +#define SAMPLE_ITEM_2_LABEL "folder2" +#define SAMPLE_ITEM_3_LABEL "sample3: the sampling.mpg" +#define SAMPLE_ITEM_4_LABEL "sample & samplability 4.mpg" +#define SAMPLE_ITEM_5_LABEL "sample5.mpg" +#define SAMPLE_ITEM_6_LABEL "sample6.mpg" + +#define SAMPLE_ITEM_1_SIZE 0 +#define SAMPLE_ITEM_2_SIZE 0 +#define SAMPLE_ITEM_3_SIZE 123 +#define SAMPLE_ITEM_4_SIZE 125952 // 123K +#define SAMPLE_ITEM_5_SIZE 128974848 // 123M +#define SAMPLE_ITEM_6_SIZE 132070244352 // 123G + +// HTTPDirectory ignores the seconds component of parsed date/times +#define SAMPLE_ITEM_1_DATETIME "2019-01-01 01:01:00" +#define SAMPLE_ITEM_2_DATETIME "2019-02-02 02:02:00" +#define SAMPLE_ITEM_3_DATETIME "2019-03-03 03:03:00" +#define SAMPLE_ITEM_4_DATETIME "2019-04-04 04:04:00" +#define SAMPLE_ITEM_5_DATETIME "2019-05-05 05:05:00" +#define SAMPLE_ITEM_6_DATETIME "2019-06-06 06:06:00" + +class TestHTTPDirectory : public testing::Test +{ +protected: + TestHTTPDirectory() : m_sourcePath(XBMC_REF_FILE_PATH(SOURCE_PATH)) + { + std::random_device rd; + std::mt19937 mt(rd()); + std::uniform_int_distribution<uint16_t> dist(49152, 65535); + m_webServerPort = dist(mt); + + m_baseUrl = StringUtils::Format("http://" WEBSERVER_HOST ":{}", m_webServerPort); + } + + ~TestHTTPDirectory() override = default; + +protected: + void SetUp() override + { + SetupMediaSources(); + + m_webServer.Start(m_webServerPort, "", ""); + m_webServer.RegisterRequestHandler(&m_vfsHandler); + } + + void TearDown() override + { + if (m_webServer.IsStarted()) + m_webServer.Stop(); + + m_webServer.UnregisterRequestHandler(&m_vfsHandler); + + TearDownMediaSources(); + } + + void SetupMediaSources() + { + CMediaSource source; + source.strName = "WebServer Share"; + source.strPath = m_sourcePath; + source.vecPaths.push_back(m_sourcePath); + source.m_allowSharing = true; + source.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL; + source.m_iLockMode = LOCK_MODE_EVERYONE; + source.m_ignore = true; + + CMediaSourceSettings::GetInstance().AddShare("videos", source); + } + + void TearDownMediaSources() { CMediaSourceSettings::GetInstance().Clear(); } + + std::string GetUrl(const std::string& path) + { + if (path.empty()) + return m_baseUrl; + + return URIUtils::AddFileToFolder(m_baseUrl, path); + } + + std::string GetUrlOfTestFile(const std::string& testFile) + { + if (testFile.empty()) + return ""; + + std::string path = URIUtils::AddFileToFolder(m_sourcePath, testFile); + path = CURL::Encode(path); + path = URIUtils::AddFileToFolder("vfs", path); + + return GetUrl(path); + } + + void CheckFileItemTypes(CFileItemList const& items) + { + ASSERT_EQ(items.GetObjectCount(), SAMPLE_ITEM_COUNT); + + // folders + ASSERT_TRUE(items[0]->m_bIsFolder); + ASSERT_TRUE(items[1]->m_bIsFolder); + + // files + ASSERT_FALSE(items[2]->m_bIsFolder); + ASSERT_FALSE(items[3]->m_bIsFolder); + ASSERT_FALSE(items[4]->m_bIsFolder); + ASSERT_FALSE(items[5]->m_bIsFolder); + } + + void CheckFileItemLabels(CFileItemList const& items) + { + ASSERT_EQ(items.GetObjectCount(), SAMPLE_ITEM_COUNT); + + ASSERT_STREQ(items[0]->GetLabel().c_str(), SAMPLE_ITEM_1_LABEL); + ASSERT_STREQ(items[1]->GetLabel().c_str(), SAMPLE_ITEM_2_LABEL); + ASSERT_STREQ(items[2]->GetLabel().c_str(), SAMPLE_ITEM_3_LABEL); + ASSERT_STREQ(items[3]->GetLabel().c_str(), SAMPLE_ITEM_4_LABEL); + ASSERT_STREQ(items[4]->GetLabel().c_str(), SAMPLE_ITEM_5_LABEL); + ASSERT_STREQ(items[5]->GetLabel().c_str(), SAMPLE_ITEM_6_LABEL); + } + + void CheckFileItemDateTimes(CFileItemList const& items) + { + ASSERT_EQ(items.GetObjectCount(), SAMPLE_ITEM_COUNT); + + ASSERT_STREQ(items[0]->m_dateTime.GetAsDBDateTime().c_str(), SAMPLE_ITEM_1_DATETIME); + ASSERT_STREQ(items[1]->m_dateTime.GetAsDBDateTime().c_str(), SAMPLE_ITEM_2_DATETIME); + ASSERT_STREQ(items[2]->m_dateTime.GetAsDBDateTime().c_str(), SAMPLE_ITEM_3_DATETIME); + ASSERT_STREQ(items[3]->m_dateTime.GetAsDBDateTime().c_str(), SAMPLE_ITEM_4_DATETIME); + ASSERT_STREQ(items[4]->m_dateTime.GetAsDBDateTime().c_str(), SAMPLE_ITEM_5_DATETIME); + ASSERT_STREQ(items[5]->m_dateTime.GetAsDBDateTime().c_str(), SAMPLE_ITEM_6_DATETIME); + } + + void CheckFileItemSizes(CFileItemList const& items) + { + ASSERT_EQ(items.GetObjectCount(), SAMPLE_ITEM_COUNT); + + // folders + ASSERT_EQ(items[0]->m_dwSize, SAMPLE_ITEM_1_SIZE); + ASSERT_EQ(items[1]->m_dwSize, SAMPLE_ITEM_2_SIZE); + + // files - due to K/M/G conversions provided by some formats, allow for + // non-zero values that are less than or equal to the expected file size + ASSERT_NE(items[2]->m_dwSize, 0); + ASSERT_LE(items[2]->m_dwSize, SAMPLE_ITEM_3_SIZE); + ASSERT_NE(items[3]->m_dwSize, 0); + ASSERT_LE(items[3]->m_dwSize, SAMPLE_ITEM_4_SIZE); + ASSERT_NE(items[4]->m_dwSize, 0); + ASSERT_LE(items[4]->m_dwSize, SAMPLE_ITEM_5_SIZE); + ASSERT_NE(items[5]->m_dwSize, 0); + ASSERT_LE(items[5]->m_dwSize, SAMPLE_ITEM_6_SIZE); + } + + void CheckFileItems(CFileItemList const& items) + { + CheckFileItemTypes(items); + CheckFileItemLabels(items); + } + + void CheckFileItemsAndMetadata(CFileItemList const& items) + { + CheckFileItems(items); + CheckFileItemDateTimes(items); + CheckFileItemSizes(items); + } + + CWebServer m_webServer; + uint16_t m_webServerPort; + std::string m_baseUrl; + std::string const m_sourcePath; + CHTTPVfsHandler m_vfsHandler; + CHTTPDirectory m_httpDirectory; +}; + +TEST_F(TestHTTPDirectory, IsStarted) +{ + ASSERT_TRUE(m_webServer.IsStarted()); +} + +TEST_F(TestHTTPDirectory, ApacheDefaultIndex) +{ + CFileItemList items; + + ASSERT_TRUE(m_httpDirectory.Exists(CURL(GetUrlOfTestFile(TEST_FILE_APACHE_DEFAULT)))); + ASSERT_TRUE( + m_httpDirectory.GetDirectory(CURL(GetUrlOfTestFile(TEST_FILE_APACHE_DEFAULT)), items)); + + CheckFileItems(items); +} + +TEST_F(TestHTTPDirectory, ApacheFancyIndex) +{ + CFileItemList items; + + ASSERT_TRUE(m_httpDirectory.Exists(CURL(GetUrlOfTestFile(TEST_FILE_APACHE_FANCY)))); + ASSERT_TRUE(m_httpDirectory.GetDirectory(CURL(GetUrlOfTestFile(TEST_FILE_APACHE_FANCY)), items)); + + CheckFileItemsAndMetadata(items); +} + +TEST_F(TestHTTPDirectory, ApacheHtmlIndex) +{ + CFileItemList items; + + ASSERT_TRUE(m_httpDirectory.Exists(CURL(GetUrlOfTestFile(TEST_FILE_APACHE_HTML)))); + ASSERT_TRUE(m_httpDirectory.GetDirectory(CURL(GetUrlOfTestFile(TEST_FILE_APACHE_HTML)), items)); + + CheckFileItemsAndMetadata(items); +} + +TEST_F(TestHTTPDirectory, BasicIndex) +{ + CFileItemList items; + + ASSERT_TRUE(m_httpDirectory.Exists(CURL(GetUrlOfTestFile(TEST_FILE_BASIC)))); + ASSERT_TRUE(m_httpDirectory.GetDirectory(CURL(GetUrlOfTestFile(TEST_FILE_BASIC)), items)); + + CheckFileItems(items); +} + +TEST_F(TestHTTPDirectory, BasicMultilineIndex) +{ + CFileItemList items; + + ASSERT_TRUE(m_httpDirectory.Exists(CURL(GetUrlOfTestFile(TEST_FILE_BASIC_MULTILINE)))); + ASSERT_TRUE( + m_httpDirectory.GetDirectory(CURL(GetUrlOfTestFile(TEST_FILE_BASIC_MULTILINE)), items)); + + CheckFileItems(items); +} + +TEST_F(TestHTTPDirectory, LighttpDefaultIndex) +{ + CFileItemList items; + + ASSERT_TRUE(m_httpDirectory.Exists(CURL(GetUrlOfTestFile(TEST_FILE_LIGHTTP_DEFAULT)))); + ASSERT_TRUE( + m_httpDirectory.GetDirectory(CURL(GetUrlOfTestFile(TEST_FILE_LIGHTTP_DEFAULT)), items)); + + CheckFileItemsAndMetadata(items); +} + +TEST_F(TestHTTPDirectory, NginxDefaultIndex) +{ + CFileItemList items; + + ASSERT_TRUE(m_httpDirectory.Exists(CURL(GetUrlOfTestFile(TEST_FILE_NGINX_DEFAULT)))); + ASSERT_TRUE(m_httpDirectory.GetDirectory(CURL(GetUrlOfTestFile(TEST_FILE_NGINX_DEFAULT)), items)); + + CheckFileItemsAndMetadata(items); +} + +TEST_F(TestHTTPDirectory, NginxFancyIndex) +{ + CFileItemList items; + + ASSERT_TRUE(m_httpDirectory.Exists(CURL(GetUrlOfTestFile(TEST_FILE_NGINX_FANCYINDEX)))); + ASSERT_TRUE(m_httpDirectory.GetDirectory(CURL(GetUrlOfTestFile(TEST_FILE_NGINX_FANCYINDEX)), items)); + + CheckFileItemsAndMetadata(items); +} diff --git a/xbmc/filesystem/test/TestNfsFile.cpp b/xbmc/filesystem/test/TestNfsFile.cpp new file mode 100644 index 0000000..69b1203 --- /dev/null +++ b/xbmc/filesystem/test/TestNfsFile.cpp @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "URL.h" +#include "filesystem/NFSFile.h" +#include "test/TestUtils.h" + +#include <errno.h> +#include <string> + +#include <gtest/gtest.h> + +using ::testing::Test; +using ::testing::WithParamInterface; +using ::testing::ValuesIn; + +struct SplitPath +{ + std::string url; + std::string exportPath; + std::string relativePath; + bool expectedResultExport; + bool expectedResultPath; +} g_TestData[] = { + {"nfs://192.168.0.1:2049/srv/test/tvmedia/foo.txt", "/srv/test", "//tvmedia/foo.txt", true, true}, + {"nfs://192.168.0.1/srv/test/tv/media/foo.txt", "/srv/test/tv", "//media/foo.txt", true, true}, + {"nfs://192.168.0.1:2049/srv/test/tvmedia", "/srv/test", "//tvmedia", true, true}, + {"nfs://192.168.0.1:2049/srv/test/tvmedia/", "/srv/test", "//tvmedia/", true, true}, + {"nfs://192.168.0.1:2049/srv/test/tv/media", "/srv/test/tv", "//media", true, true}, + {"nfs://192.168.0.1:2049/srv/test/tv/media/", "/srv/test/tv", "//media/", true, true}, + {"nfs://192.168.0.1:2049/srv/test/tv", "/srv/test/tv", "//", true, true}, + {"nfs://192.168.0.1:2049/srv/test/", "/srv/test", "//", true, true}, + {"nfs://192.168.0.1:2049/", "/", "//", true, true}, + {"nfs://192.168.0.1:2049/notexported/foo.txt", "/", "//notexported/foo.txt", true, true}, + + {"nfs://192.168.0.1:2049/notexported/foo.txt", "/notexported", "//foo.txt", false, false}, + }; + +class TestNfs : public Test, + public WithParamInterface<SplitPath> +{ +}; + +class ExportList +{ + public: + std::list<std::string> data; + + ExportList() + { + data.emplace_back("/srv/test"); + data.emplace_back("/srv/test/tv"); + data.emplace_back("/"); + data.sort(); + data.reverse(); + } +}; + +static ExportList exportList; + +TEST_P(TestNfs, splitUrlIntoExportAndPath) +{ + CURL url(GetParam().url); + std::string exportPath; + std::string relativePath; + gNfsConnection.splitUrlIntoExportAndPath(url, exportPath, relativePath, exportList.data); + + if (GetParam().expectedResultExport) + EXPECT_STREQ(GetParam().exportPath.c_str(), exportPath.c_str()); + else + EXPECT_STRNE(GetParam().exportPath.c_str(), exportPath.c_str()); + + if (GetParam().expectedResultPath) + EXPECT_STREQ(GetParam().relativePath.c_str(), relativePath.c_str()); + else + EXPECT_STRNE(GetParam().relativePath.c_str(), relativePath.c_str()); +} + +INSTANTIATE_TEST_SUITE_P(NfsFile, TestNfs, ValuesIn(g_TestData)); diff --git a/xbmc/filesystem/test/TestZipFile.cpp b/xbmc/filesystem/test/TestZipFile.cpp new file mode 100644 index 0000000..3ff518b --- /dev/null +++ b/xbmc/filesystem/test/TestZipFile.cpp @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "filesystem/Directory.h" +#include "filesystem/File.h" +#include "filesystem/ZipFile.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "test/TestUtils.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +#include <errno.h> + +#include <gtest/gtest.h> + +class TestZipFile : public testing::Test +{ +protected: + TestZipFile() = default; + + ~TestZipFile() override + { + CServiceBroker::GetSettingsComponent()->GetSettings()->Unload(); + } +}; + +TEST_F(TestZipFile, Read) +{ + XFILE::CFile file; + char buf[20] = {}; + std::string reffile, strpathinzip; + CFileItemList itemlist; + + reffile = XBMC_REF_FILE_PATH("xbmc/filesystem/test/reffile.txt.zip"); + CURL zipUrl = URIUtils::CreateArchivePath("zip", CURL(reffile), ""); + ASSERT_TRUE(XFILE::CDirectory::GetDirectory(zipUrl, itemlist, "", + XFILE::DIR_FLAG_NO_FILE_DIRS)); + EXPECT_GT(itemlist.Size(), 0); + EXPECT_FALSE(itemlist[0]->GetPath().empty()); + strpathinzip = itemlist[0]->GetPath(); + ASSERT_TRUE(file.Open(strpathinzip)); + EXPECT_EQ(0, file.GetPosition()); + EXPECT_EQ(1616, file.GetLength()); + EXPECT_EQ(sizeof(buf), static_cast<size_t>(file.Read(buf, sizeof(buf)))); + file.Flush(); + EXPECT_EQ(20, file.GetPosition()); + EXPECT_TRUE(!memcmp("About\n-----\nXBMC is ", buf, sizeof(buf) - 1)); + EXPECT_TRUE(file.ReadString(buf, sizeof(buf))); + EXPECT_EQ(39, file.GetPosition()); + EXPECT_STREQ("an award-winning fr", buf); + EXPECT_EQ(100, file.Seek(100)); + EXPECT_EQ(100, file.GetPosition()); + EXPECT_EQ(sizeof(buf), static_cast<size_t>(file.Read(buf, sizeof(buf)))); + file.Flush(); + EXPECT_EQ(120, file.GetPosition()); + EXPECT_TRUE(!memcmp("ent hub for digital ", buf, sizeof(buf) - 1)); + EXPECT_EQ(220, file.Seek(100, SEEK_CUR)); + EXPECT_EQ(220, file.GetPosition()); + EXPECT_EQ(sizeof(buf), static_cast<size_t>(file.Read(buf, sizeof(buf)))); + file.Flush(); + EXPECT_EQ(240, file.GetPosition()); + EXPECT_TRUE(!memcmp("rs, XBMC is a non-pr", buf, sizeof(buf) - 1)); + EXPECT_EQ(1596, file.Seek(-(int64_t)sizeof(buf), SEEK_END)); + EXPECT_EQ(1596, file.GetPosition()); + EXPECT_EQ(sizeof(buf), static_cast<size_t>(file.Read(buf, sizeof(buf)))); + file.Flush(); + EXPECT_EQ(1616, file.GetPosition()); + EXPECT_TRUE(!memcmp("multimedia jukebox.\n", buf, sizeof(buf) - 1)); + EXPECT_EQ(-1, file.Seek(100, SEEK_CUR)); + EXPECT_EQ(1616, file.GetPosition()); + EXPECT_EQ(0, file.Seek(0, SEEK_SET)); + EXPECT_EQ(sizeof(buf), static_cast<size_t>(file.Read(buf, sizeof(buf)))); + file.Flush(); + EXPECT_EQ(20, file.GetPosition()); + EXPECT_TRUE(!memcmp("About\n-----\nXBMC is ", buf, sizeof(buf) - 1)); + EXPECT_EQ(0, file.Seek(0, SEEK_SET)); + EXPECT_EQ(-1, file.Seek(-100, SEEK_SET)); + file.Close(); +} + +TEST_F(TestZipFile, Exists) +{ + std::string reffile, strpathinzip; + CFileItemList itemlist; + + reffile = XBMC_REF_FILE_PATH("xbmc/filesystem/test/reffile.txt.zip"); + CURL zipUrl = URIUtils::CreateArchivePath("zip", CURL(reffile), ""); + ASSERT_TRUE(XFILE::CDirectory::GetDirectory(zipUrl, itemlist, "", + XFILE::DIR_FLAG_NO_FILE_DIRS)); + strpathinzip = itemlist[0]->GetPath(); + + EXPECT_TRUE(XFILE::CFile::Exists(strpathinzip)); +} + +TEST_F(TestZipFile, Stat) +{ + struct __stat64 buffer; + std::string reffile, strpathinzip; + CFileItemList itemlist; + + reffile = XBMC_REF_FILE_PATH("xbmc/filesystem/test/reffile.txt.zip"); + CURL zipUrl = URIUtils::CreateArchivePath("zip", CURL(reffile), ""); + ASSERT_TRUE(XFILE::CDirectory::GetDirectory(zipUrl, itemlist, "", + XFILE::DIR_FLAG_NO_FILE_DIRS)); + strpathinzip = itemlist[0]->GetPath(); + + EXPECT_EQ(0, XFILE::CFile::Stat(strpathinzip, &buffer)); + EXPECT_TRUE(buffer.st_mode | _S_IFREG); +} + +/* Test case to test for graceful handling of corrupted input. + * NOTE: The test case is considered a "success" as long as the corrupted + * file was successfully generated and the test case runs without a segfault. + */ +TEST_F(TestZipFile, CorruptedFile) +{ + XFILE::CFile *file; + char buf[16] = {}; + std::string reffilepath, strpathinzip, str; + CFileItemList itemlist; + ssize_t size, i; + int64_t count = 0; + + reffilepath = XBMC_REF_FILE_PATH("xbmc/filesystem/test/reffile.txt.zip"); + ASSERT_TRUE((file = XBMC_CREATECORRUPTEDFILE(reffilepath, ".zip")) != NULL); + std::cout << "Reference file generated at '" << XBMC_TEMPFILEPATH(file) << "'" << std::endl; + + CURL zipUrl = URIUtils::CreateArchivePath("zip", CURL(reffilepath), ""); + if (!XFILE::CDirectory::GetDirectory(zipUrl, itemlist, "", + XFILE::DIR_FLAG_NO_FILE_DIRS)) + { + XBMC_DELETETEMPFILE(file); + SUCCEED(); + return; + } + if (itemlist.IsEmpty()) + { + XBMC_DELETETEMPFILE(file); + SUCCEED(); + return; + } + strpathinzip = itemlist[0]->GetPath(); + + if (!file->Open(strpathinzip)) + { + XBMC_DELETETEMPFILE(file); + SUCCEED(); + return; + } + std::cout << "file->GetLength(): " << + testing::PrintToString(file->GetLength()) << std::endl; + std::cout << "file->Seek(file->GetLength() / 2, SEEK_CUR) return value: " << + testing::PrintToString(file->Seek(file->GetLength() / 2, SEEK_CUR)) << std::endl; + std::cout << "file->Seek(0, SEEK_END) return value: " << + testing::PrintToString(file->Seek(0, SEEK_END)) << std::endl; + std::cout << "file->Seek(0, SEEK_SET) return value: " << + testing::PrintToString(file->Seek(0, SEEK_SET)) << std::endl; + std::cout << "File contents:" << std::endl; + while ((size = file->Read(buf, sizeof(buf))) > 0) + { + str = StringUtils::Format(" {:08X}", count); + std::cout << str << " "; + count += size; + for (i = 0; i < size; i++) + { + str = StringUtils::Format("{:02X} ", buf[i]); + std::cout << str; + } + while (i++ < static_cast<ssize_t> (sizeof(buf))) + std::cout << " "; + std::cout << " ["; + for (i = 0; i < size; i++) + { + if (buf[i] >= ' ' && buf[i] <= '~') + std::cout << buf[i]; + else + std::cout << "."; + } + std::cout << "]" << std::endl; + } + file->Close(); + XBMC_DELETETEMPFILE(file); +} + +TEST_F(TestZipFile, ExtendedLocalHeader) +{ + XFILE::CFile file; + ssize_t readlen; + char zipdata[20000]; // size of zip file is 15352 Bytes + + ASSERT_TRUE(file.Open(XBMC_REF_FILE_PATH("xbmc/filesystem/test/extendedlocalheader.zip"))); + readlen = file.Read(zipdata, sizeof(zipdata)); + EXPECT_TRUE(readlen); + + XFILE::CZipFile zipfile; + std::string strBuffer; + + int iSize = zipfile.UnpackFromMemory(strBuffer, std::string(zipdata, readlen), false); + EXPECT_EQ(152774, iSize); // sum of uncompressed size of all files in zip + EXPECT_TRUE(strBuffer.substr(0, 6) == "<Data>"); + file.Close(); +} diff --git a/xbmc/filesystem/test/TestZipManager.cpp b/xbmc/filesystem/test/TestZipManager.cpp new file mode 100644 index 0000000..ca669c5 --- /dev/null +++ b/xbmc/filesystem/test/TestZipManager.cpp @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "filesystem/ZipManager.h" +#include "utils/RegExp.h" + +#include <gtest/gtest.h> + +TEST(TestZipManager, PathTraversal) +{ + CRegExp pathTraversal; + pathTraversal.RegComp(PATH_TRAVERSAL); + + ASSERT_TRUE(pathTraversal.RegFind("..") >= 0); + ASSERT_TRUE(pathTraversal.RegFind("../test.txt") >= 0); + ASSERT_TRUE(pathTraversal.RegFind("..\\test.txt") >= 0); + ASSERT_TRUE(pathTraversal.RegFind("test/../test.txt") >= 0); + ASSERT_TRUE(pathTraversal.RegFind("test\\../test.txt") >= 0); + ASSERT_TRUE(pathTraversal.RegFind("test\\..\\test.txt") >= 0); + + ASSERT_FALSE(pathTraversal.RegFind("...") >= 0); + ASSERT_FALSE(pathTraversal.RegFind("..test.txt") >= 0); + ASSERT_FALSE(pathTraversal.RegFind("test.txt..") >= 0); + ASSERT_FALSE(pathTraversal.RegFind("test..test.txt") >= 0); +} diff --git a/xbmc/filesystem/test/data/httpdirectory/apache-default.html b/xbmc/filesystem/test/data/httpdirectory/apache-default.html new file mode 100644 index 0000000..e29ff27 --- /dev/null +++ b/xbmc/filesystem/test/data/httpdirectory/apache-default.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> +<html> + <head> + <title>Index of /</title> + </head> + <body> +<h1>Index of /</h1> +<ul><li><a href="folder1/"> folder1/</a></li> +<li><a href="folder2/"> folder2/</a></li> +<li><a href="./sample3:%20the%20sampling.mpg"> sample3: the sampling.mpg</a></li> +<li><a href="sample%20&%20samplability%204.mpg"> sample & samplability 4.mpg</a></li> +<li><a href="sample5.mpg"> sample5.mpg</a></li> +<li><a href="sample6.mpg"> sample6.mpg</a></li> +</ul> +</body></html>
\ No newline at end of file diff --git a/xbmc/filesystem/test/data/httpdirectory/apache-fancy.html b/xbmc/filesystem/test/data/httpdirectory/apache-fancy.html new file mode 100644 index 0000000..a45d52a --- /dev/null +++ b/xbmc/filesystem/test/data/httpdirectory/apache-fancy.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> +<html> + <head> + <title>Index of /</title> + </head> + <body> +<h1>Index of /</h1> +<pre> <a href="?C=N;O=D;F=1">Name</a> <a href="?C=M;O=A;F=1">Last modified</a> <a href="?C=S;O=A;F=1">Size</a> <a href="?C=D;O=A;F=1">Description</a><hr> <a href="folder1/">folder1/</a> 2019-01-01 01:01 - + <a href="folder2/">folder2/</a> 2019-02-02 02:02 - + <a href="./sample3:%20the%20sampling.mpg">sample3: the sampling.mpg</a> 2019-03-03 03:03 123 + <a href="sample%20&%20samplability%204.mpg">sample & samplability 4.mpg</a> 2019-04-04 04:04 123K + <a href="sample5.mpg">sample5.mpg</a> 2019-05-05 05:05 123M + <a href="sample6.mpg">sample6.mpg</a> 2019-06-06 06:06 123G +<hr></pre> +</body></html> diff --git a/xbmc/filesystem/test/data/httpdirectory/apache-html.html b/xbmc/filesystem/test/data/httpdirectory/apache-html.html new file mode 100644 index 0000000..8e69ab4 --- /dev/null +++ b/xbmc/filesystem/test/data/httpdirectory/apache-html.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> +<html> + <head> + <title>Index of /</title> + </head> + <body> +<h1>Index of /</h1> + <table> + <tr><th valign="top"> </th><th><a href="?C=N;O=D;F=2">Name</a></th><th><a href="?C=M;O=A;F=2">Last modified</a></th><th><a href="?C=S;O=A;F=2">Size</a></th><th><a href="?C=D;O=A;F=2">Description</a></th></tr> + <tr><th colspan="5"><hr></th></tr> +<tr><td valign="top"> </td><td><a href="folder1/">folder1/</a> </td><td align="right">2019-01-01 01:01 </td><td align="right"> - </td><td> </td></tr> +<tr><td valign="top"> </td><td><a href="folder2/">folder2/</a> </td><td align="right">2019-02-02 02:02 </td><td align="right"> - </td><td> </td></tr> +<tr><td valign="top"> </td><td><a href="./sample3:%20the%20sampling.mpg">sample3: the sampling.mpg</a> </td><td align="right">2019-03-03 03:03 </td><td align="right">123 </td><td> </td></tr> +<tr><td valign="top"> </td><td><a href="sample%20&%20samplability%204.mpg">sample & samplability 4.mpg</a> </td><td align="right">2019-04-04 04:04 </td><td align="right">123K</td><td> </td></tr> +<tr><td valign="top"> </td><td><a href="sample5.mpg">sample5.mpg</a> </td><td align="right">2019-05-05 05:05 </td><td align="right">123M</td><td> </td></tr> +<tr><td valign="top"> </td><td><a href="sample6.mpg">sample6.mpg</a> </td><td align="right">2019-06-06 06:06 </td><td align="right">123G</td><td> </td></tr> + <tr><th colspan="5"><hr></th></tr> +</table> +</body></html> diff --git a/xbmc/filesystem/test/data/httpdirectory/basic-multiline.html b/xbmc/filesystem/test/data/httpdirectory/basic-multiline.html new file mode 100644 index 0000000..707a1f0 --- /dev/null +++ b/xbmc/filesystem/test/data/httpdirectory/basic-multiline.html @@ -0,0 +1,16 @@ +<html> + <head> + <title>Directory Listing</title> + </head> + <body> + <a href="folder1/">folder1/</a> + <a href="folder2/"> + folder2/ + </a> + <a href="./sample3:%20the%20sampling.mpg">sample3: the sampling.mpg</a> + <a href="sample%20&%20samplability%204.mpg">sample & samplability 4.mpg</a> <a href="sample5.mpg">sample5.mpg</a> + <a href="sample6.mpg"> + sample6.mpg + </a> + </body> +</html>
\ No newline at end of file diff --git a/xbmc/filesystem/test/data/httpdirectory/basic.html b/xbmc/filesystem/test/data/httpdirectory/basic.html new file mode 100644 index 0000000..ce98a10 --- /dev/null +++ b/xbmc/filesystem/test/data/httpdirectory/basic.html @@ -0,0 +1,13 @@ +<html> + <head> + <title>Directory Listing</title> + </head> + <body> + <a href="folder1/">folder1/</a> + <a href="folder2/">folder2/</a> + <a href="./sample3:%20the%20sampling.mpg">sample3: the sampling.mpg</a> + <a href="sample%20&%20samplability%204.mpg">sample & samplability 4.mpg</a> + <a href="sample5.mpg">sample5.mpg</a> + <a href="sample6.mpg">sample6.mpg</a> + </body> +</html>
\ No newline at end of file diff --git a/xbmc/filesystem/test/data/httpdirectory/lighttp-default.html b/xbmc/filesystem/test/data/httpdirectory/lighttp-default.html new file mode 100644 index 0000000..505f477 --- /dev/null +++ b/xbmc/filesystem/test/data/httpdirectory/lighttp-default.html @@ -0,0 +1,211 @@ +<!DOCTYPE html> +<html> +<head> +<title>Index of /</title> +<style type="text/css"> +a, a:active {text-decoration: none; color: blue;} +a:visited {color: #48468F;} +a:hover, a:focus {text-decoration: underline; color: red;} +body {background-color: #F5F5F5;} +h2 {margin-bottom: 12px;} +table {margin-left: 12px;} +th, td { font: 90% monospace; text-align: left;} +th { font-weight: bold; padding-right: 14px; padding-bottom: 3px;} +td {padding-right: 14px;} +td.s, th.s {text-align: right;} +div.list { background-color: white; border-top: 1px solid #646464; border-bottom: 1px solid #646464; padding-top: 10px; padding-bottom: 14px;} +div.foot { font: 90% monospace; color: #787878; padding-top: 4px;} +</style> +</head> +<body> +<h2>Index of /</h2> +<div class="list"> +<table summary="Directory Listing" cellpadding="0" cellspacing="0"> +<thead><tr><th class="n">Name</th><th class="m">Last Modified</th><th class="s">Size</th><th class="t">Type</th></tr></thead> +<tbody> +<tr class="d"><td class="n"><a href="folder1/">folder1</a>/</td><td class="m">2019-Jan-01 01:01:01</td><td class="s">- </td><td class="t">Directory</td></tr> +<tr class="d"><td class="n"><a href="folder2/">folder2</a>/</td><td class="m">2019-Feb-02 02:02:02</td><td class="s">- </td><td class="t">Directory</td></tr> +<tr><td class="n"><a href="sample3%3a%20the%20sampling.mpg">sample3: the sampling.mpg</a></td><td class="m">2019-Mar-03 03:03:03</td><td class="s">0.1K</td><td class="t">video/mpeg</td></tr> +<tr><td class="n"><a href="sample%20%26%20samplability%204.mpg">sample & samplability 4.mpg</a></td><td class="m">2019-Apr-04 04:04:04</td><td class="s">123.0K</td><td class="t">video/mpeg</td></tr> +<tr><td class="n"><a href="sample5.mpg">sample5.mpg</a></td><td class="m">2019-May-05 05:05:05</td><td class="s">123.0M</td><td class="t">video/mpeg</td></tr> +<tr><td class="n"><a href="sample6.mpg">sample6.mpg</a></td><td class="m">2019-Jun-06 06:06:06</td><td class="s">123.0G</td><td class="t">video/mpeg</td></tr> +</tbody> +</table> +</div> +<div class="foot">lighttpd/1.4.49</div> + +<script type="text/javascript"> +// <!-- + +var click_column; +var name_column = 0; +var date_column = 1; +var size_column = 2; +var type_column = 3; +var prev_span = null; + +if (typeof(String.prototype.localeCompare) === 'undefined') { + String.prototype.localeCompare = function(str, locale, options) { + return ((this == str) ? 0 : ((this > str) ? 1 : -1)); + }; +} + +if (typeof(String.prototype.toLocaleUpperCase) === 'undefined') { + String.prototype.toLocaleUpperCase = function() { + return this.toUpperCase(); + }; +} + +function get_inner_text(el) { + if((typeof el == 'string')||(typeof el == 'undefined')) + return el; + if(el.innerText) + return el.innerText; + else { + var str = ""; + var cs = el.childNodes; + var l = cs.length; + for (i=0;i<l;i++) { + if (cs[i].nodeType==1) str += get_inner_text(cs[i]); + else if (cs[i].nodeType==3) str += cs[i].nodeValue; + } + } + return str; +} + +function isdigit(c) { + return (c >= '0' && c <= '9'); +} + +function unit_multiplier(unit) { + return (unit=='K') ? 1000 + : (unit=='M') ? 1000000 + : (unit=='G') ? 1000000000 + : (unit=='T') ? 1000000000000 + : (unit=='P') ? 1000000000000000 + : (unit=='E') ? 1000000000000000000 : 1; +} + +var li_date_regex=/(\d{4})-(\w{3})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/; + +var li_mon = ['Jan','Feb','Mar','Apr','May','Jun', + 'Jul','Aug','Sep','Oct','Nov','Dec']; + +function li_mon_num(mon) { + var i; for (i = 0; i < 12 && mon != li_mon[i]; ++i); return i; +} + +function li_date_cmp(s1, s2) { + var dp1 = li_date_regex.exec(s1) + var dp2 = li_date_regex.exec(s2) + for (var i = 1; i < 7; ++i) { + var cmp = (2 != i) + ? parseInt(dp1[i]) - parseInt(dp2[i]) + : li_mon_num(dp1[2]) - li_mon_num(dp2[2]); + if (0 != cmp) return cmp; + } + return 0; +} + +function sortfn_then_by_name(a,b,sort_column) { + if (sort_column == name_column || sort_column == type_column) { + var ad = (a.cells[type_column].innerHTML === 'Directory'); + var bd = (b.cells[type_column].innerHTML === 'Directory'); + if (ad != bd) return (ad ? -1 : 1); + } + var at = get_inner_text(a.cells[sort_column]); + var bt = get_inner_text(b.cells[sort_column]); + var cmp; + if (sort_column == name_column) { + if (at == '..') return -1; + if (bt == '..') return 1; + } + if (a.cells[sort_column].className == 'int') { + cmp = parseInt(at)-parseInt(bt); + } else if (sort_column == date_column) { + var ad = isdigit(at.substr(0,1)); + var bd = isdigit(bt.substr(0,1)); + if (ad != bd) return (!ad ? -1 : 1); + cmp = li_date_cmp(at,bt); + } else if (sort_column == size_column) { + var ai = parseInt(at, 10) * unit_multiplier(at.substr(-1,1)); + var bi = parseInt(bt, 10) * unit_multiplier(bt.substr(-1,1)); + if (at.substr(0,1) == '-') ai = -1; + if (bt.substr(0,1) == '-') bi = -1; + cmp = ai - bi; + } else { + cmp = at.toLocaleUpperCase().localeCompare(bt.toLocaleUpperCase()); + if (0 != cmp) return cmp; + cmp = at.localeCompare(bt); + } + if (0 != cmp || sort_column == name_column) return cmp; + return sortfn_then_by_name(a,b,name_column); +} + +function sortfn(a,b) { + return sortfn_then_by_name(a,b,click_column); +} + +function resort(lnk) { + var span = lnk.childNodes[1]; + var table = lnk.parentNode.parentNode.parentNode.parentNode; + var rows = new Array(); + for (j=1;j<table.rows.length;j++) + rows[j-1] = table.rows[j]; + click_column = lnk.parentNode.cellIndex; + rows.sort(sortfn); + + if (prev_span != null) prev_span.innerHTML = ''; + if (span.getAttribute('sortdir')=='down') { + span.innerHTML = '↑'; + span.setAttribute('sortdir','up'); + rows.reverse(); + } else { + span.innerHTML = '↓'; + span.setAttribute('sortdir','down'); + } + for (i=0;i<rows.length;i++) + table.tBodies[0].appendChild(rows[i]); + prev_span = span; +} + +function init_sort(init_sort_column, ascending) { + var tables = document.getElementsByTagName("table"); + for (var i = 0; i < tables.length; i++) { + var table = tables[i]; + //var c = table.getAttribute("class") + //if (-1 != c.split(" ").indexOf("sort")) { + var row = table.rows[0].cells; + for (var j = 0; j < row.length; j++) { + var n = row[j]; + if (n.childNodes.length == 1 && n.childNodes[0].nodeType == 3) { + var link = document.createElement("a"); + var title = n.childNodes[0].nodeValue.replace(/:$/, ""); + link.appendChild(document.createTextNode(title)); + link.setAttribute("href", "#"); + link.setAttribute("class", "sortheader"); + link.setAttribute("onclick", "resort(this);return false;"); + var arrow = document.createElement("span"); + arrow.setAttribute("class", "sortarrow"); + arrow.appendChild(document.createTextNode(":")); + link.appendChild(arrow) + n.replaceChild(link, n.firstChild); + } + } + var lnk = row[init_sort_column].firstChild; + if (ascending) { + var span = lnk.childNodes[1]; + span.setAttribute('sortdir','down'); + } + resort(lnk); + //} + } +} + +init_sort(0, 0); + +// --> +</script> + +</body> +</html> diff --git a/xbmc/filesystem/test/data/httpdirectory/nginx-default.html b/xbmc/filesystem/test/data/httpdirectory/nginx-default.html new file mode 100644 index 0000000..77bcd75 --- /dev/null +++ b/xbmc/filesystem/test/data/httpdirectory/nginx-default.html @@ -0,0 +1,11 @@ +<html> +<head><title>Index of /</title></head> +<body> +<h1>Index of /</h1><hr><pre><a href="folder1/">folder1/</a> 01-Jan-2019 01:01 - +<a href="folder2/">folder2/</a> 02-Feb-2019 02:02 - +<a href="sample3%3A%20the%20sampling.mpg">sample3: the sampling.mpg</a> 03-Mar-2019 03:03 123 +<a href="sample%20%26%20samplability%204.mpg">sample & samplability 4.mpg</a> 04-Apr-2019 04:04 125952 +<a href="sample5.mpg">sample5.mpg</a> 05-May-2019 05:05 128974848 +<a href="sample6.mpg">sample6.mpg</a> 06-Jun-2019 06:06 132070244352 +</pre><hr></body> +</html> diff --git a/xbmc/filesystem/test/data/httpdirectory/nginx-fancyindex.html b/xbmc/filesystem/test/data/httpdirectory/nginx-fancyindex.html new file mode 100644 index 0000000..d9772df --- /dev/null +++ b/xbmc/filesystem/test/data/httpdirectory/nginx-fancyindex.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta http-equiv="content-type" content="text/html; charset=utf-8"/> + <meta name="viewport" content="width=device-width"/> + <link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo="> + <title>Files...</title> +</head> +<body> +<div class="box box-breadcrumbs"> + <div class="box-header"> + <div class="box-header-content"> + <div id="breadcrumbs" class="breadcrumbs"> + <a href="#"></a> + </div> + </div> + </div> + <div class="box-content clearfix"> + <h1>Index of: +/</h1> +<table id="list"><thead><tr><th style="width:55%"><a href="?C=N&O=A">File Name</a> <a href="?C=N&O=D"> ↓ </a></th><th style="width:20%"><a href="?C=S&O=A">File Size</a> <a href="?C=S&O=D"> ↓ </a></th><th style="width:25%"><a href="?C=M&O=A">Date</a> <a href="?C=M&O=D"> ↓ </a></th></tr></thead> +<tbody><tr><td class="link"><a href="../">Parent directory/</a></td><td class="size">-</td><td class="date">-</td></tr> +<tr><td class="link"><a href="folder1/" title="folder1">folder1/</a></td><td class="size">-</td><td class="date">2019-Jan-01 01:01</td></tr> +<tr><td class="link"><a href="folder2/" title="folder2">folder2/</a></td><td class="size">-</td><td class="date">2019-Feb-02 02:02</td></tr> +<tr><td class="link"><a href="sample3%3A%20the%20sampling.mpg" title="sample3: the sampling.mpg">sample3: the sampling.mpg</a></td><td class="size">123 B</td><td class="date">2019-Mar-03 03:03</td></tr> +<tr><td class="link"><a href="sample%20%26%20samplability%204.mpg" title="sample & samplability 4.mpg">sample & samplability 4.mpg</a></td><td class="size">123.0 KiB</td><td class="date">2019-Apr-04 04:04</td></tr> +<tr><td class="link"><a href="sample5.mpg" title="sample5.mpg">sample5.mpg</a></td><td class="size">123.0 MiB</td><td class="date">2019-May-05 05:05</td></tr> +<tr><td class="link"><a href="sample6.mpg" title="sample6.mpg">sample6.mpg</a></td><td class="size">123.0 GiB</td><td class="date">2019-Jun-06 06:06</td></tr> +</tbody></table> +</div> +</div> +</body> +</html> diff --git a/xbmc/filesystem/test/extendedlocalheader.zip b/xbmc/filesystem/test/extendedlocalheader.zip Binary files differnew file mode 100644 index 0000000..b30d92e --- /dev/null +++ b/xbmc/filesystem/test/extendedlocalheader.zip diff --git a/xbmc/filesystem/test/refRARnormal.rar b/xbmc/filesystem/test/refRARnormal.rar Binary files differnew file mode 100644 index 0000000..58fe71d --- /dev/null +++ b/xbmc/filesystem/test/refRARnormal.rar diff --git a/xbmc/filesystem/test/refRARstored.rar b/xbmc/filesystem/test/refRARstored.rar Binary files differnew file mode 100644 index 0000000..1500027 --- /dev/null +++ b/xbmc/filesystem/test/refRARstored.rar diff --git a/xbmc/filesystem/test/reffile.txt b/xbmc/filesystem/test/reffile.txt new file mode 100644 index 0000000..7a5e510 --- /dev/null +++ b/xbmc/filesystem/test/reffile.txt @@ -0,0 +1,25 @@ +About +----- +XBMC is an award-winning free and open source (GPL) software media player and +entertainment hub for digital media. XBMC is available for multiple platforms. +Created in 2003 by a group of like minded programmers, XBMC is a non-profit +project run and developed by volunteers located around the world. More than 50 +software developers have contributed to XBMC, and 100-plus translators have +worked to expand its reach, making it available in more than 30 languages. + +While XBMC functions very well as a standard media player application for your +computer, it has been designed to be the perfect companion for your HTPC. +Supporting an almost endless range of remote controls, and combined with its +beautiful interface and powerful skinning engine, XBMC feels very natural to +use from the couch and is the ideal solution for your home theater. + +Currently XBMC can be used to play almost all popular audio and video formats +around. It was designed for network playback, so you can stream your multimedia +from anywhere in the house or directly from the internet using practically any +protocol available. Use your media as-is: XBMC can play CDs and DVDs directly +from the disk or image file, almost all popular archive formats from your hard +drive, and even files inside ZIP and RAR archives. It will even scan all of +your media and automatically create a personalized library complete with box +covers, descriptions, and fanart. There are playlist and slideshow functions, a +weather forecast feature and many audio visualizations. Once installed, your +computer will become a fully functional multimedia jukebox. diff --git a/xbmc/filesystem/test/reffile.txt.rar b/xbmc/filesystem/test/reffile.txt.rar Binary files differnew file mode 100644 index 0000000..20841b8 --- /dev/null +++ b/xbmc/filesystem/test/reffile.txt.rar diff --git a/xbmc/filesystem/test/reffile.txt.zip b/xbmc/filesystem/test/reffile.txt.zip Binary files differnew file mode 100644 index 0000000..dd0572f --- /dev/null +++ b/xbmc/filesystem/test/reffile.txt.zip |