From 53ccef615148f6f3d2fe21545cf44269b2ede466 Mon Sep 17 00:00:00 2001 From: 78andyp <99039295+78andyp@users.noreply.github.com> Date: Sun, 3 Aug 2025 21:13:37 +0100 Subject: [PATCH 1/8] Refactor stacking code in CFileItemList to generate folder stacks. Improve regex to allow for additional volume qualifiers (eg. part x) --- cmake/installdata/test-reference-data.txt | 10 +- xbmc/FileItemList.cpp | 379 ++++++++---------- xbmc/FileItemList.h | 44 +- xbmc/settings/AdvancedSettings.cpp | 10 +- xbmc/video/test/TestStacks.cpp | 81 ++-- .../Movie_(2001)/Movie_(2001)_part1.iso | Bin 0 -> 1310720 bytes .../Movie_(2001)/Movie_(2001)_part2.iso | Bin 0 -> 1310720 bytes .../Movie_(2001)/Movie_(2001)_dvd1.iso | Bin 0 -> 555008 bytes .../Movie_(2001)/Movie_(2001)_dvd2.iso | Bin 0 -> 555008 bytes .../part_1/VIDEO_TS/VIDEO_TS.IFO} | 0 .../Movie_(2001)/part_2/BDMV/index.bdmv} | 0 .../Movie_(2001)_part1.mkv} | 0 .../part_2/Movie_(2001)_part2.mkv | 0 .../part_3/Movie_(2001)_part3.mkv | 0 14 files changed, 254 insertions(+), 270 deletions(-) create mode 100644 xbmc/video/test/testdata/moviestack_blurayiso/Movie_(2001)/Movie_(2001)_part1.iso create mode 100644 xbmc/video/test/testdata/moviestack_blurayiso/Movie_(2001)/Movie_(2001)_part2.iso rename xbmc/video/test/testdata/{moviestack_subfolder_parts/Movie_(2001)/cd1/Movie_(2001)_part1.mkv => moviestack_subfolder_disc_parts/Movie_(2001)/part_1/VIDEO_TS/VIDEO_TS.IFO} (100%) rename xbmc/video/test/testdata/{moviestack_subfolder_parts/Movie_(2001)/cd2/Movie_(2001)_part2.mkv => moviestack_subfolder_disc_parts/Movie_(2001)/part_2/BDMV/index.bdmv} (100%) rename xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)/{cd3/Movie_(2001)_part3.mkv => part_1/Movie_(2001)_part1.mkv} (100%) create mode 100644 xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)/part_2/Movie_(2001)_part2.mkv create mode 100644 xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)/part_3/Movie_(2001)_part3.mkv diff --git a/cmake/installdata/test-reference-data.txt b/cmake/installdata/test-reference-data.txt index c3b9384d178..610042ce6d5 100644 --- a/cmake/installdata/test-reference-data.txt +++ b/cmake/installdata/test-reference-data.txt @@ -40,9 +40,13 @@ xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part2.mkv xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part3.mkv xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)/Movie_(2001)_dvd1.iso xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)/Movie_(2001)_dvd2.iso -xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)/cd1/Movie_(2001)_part1.mkv -xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)/cd2/Movie_(2001)_part2.mkv -xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)/cd3/Movie_(2001)_part3.mkv +xbmc/video/test/testdata/moviestack_blurayiso/Movie_(2001)/Movie_(2001)_part1.iso +xbmc/video/test/testdata/moviestack_blurayiso/Movie_(2001)/Movie_(2001)_part2.iso +xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)/part_1/Movie_(2001)_part1.mkv +xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)/part_2/Movie_(2001)_part2.mkv +xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)/part_3/Movie_(2001)_part3.mkv +xbmc/video/test/testdata/moviestack_subfolder_disc_parts/Movie_(2001)/part_1/VIDEO_TS/VIDEO_TS.IFO +xbmc/video/test/testdata/moviestack_subfolder_disc_parts/Movie_(2001)/part_2/BDMV/index.bdmv xbmc/utils/test/CXBMCTinyXML-test.xml xbmc/utils/test/rss.xml xbmc/utils/test/data/bluray/BDMV/STREAM/00001.m2ts diff --git a/xbmc/FileItemList.cpp b/xbmc/FileItemList.cpp index 52ac11834b4..5a888500f6b 100644 --- a/xbmc/FileItemList.cpp +++ b/xbmc/FileItemList.cpp @@ -17,7 +17,6 @@ #include "filesystem/StackDirectory.h" #include "filesystem/VideoDatabaseDirectory.h" #include "music/MusicFileItemClassify.h" -#include "network/NetworkFileItemClassify.h" #include "playlists/PlayListFileItemClassify.h" #include "settings/AdvancedSettings.h" #include "settings/Settings.h" @@ -34,6 +33,9 @@ #include "video/VideoUtils.h" #include +#include +#include +#include using namespace KODI; using namespace XFILE; @@ -659,7 +661,32 @@ void CFileItemList::RemoveExtensions() std::ranges::for_each(m_items, [](auto& item) { item->RemoveExtension(); }); } -void CFileItemList::Stack(bool stackFiles /* = true */) +namespace +{ +void ChangeFolderToFile(const std::shared_ptr& item, const std::string& playPath) +{ + item->SetPath(playPath); // updated path (for DVD/Bluray files) + item->SetFolder(false); +} + +void ConvertDiscFoldersToFiles(std::vector> items) +{ + auto folderItems{items | std::views::filter([](const std::shared_ptr& item) + { return item->IsFolder(); })}; + for (const auto& item : folderItems) + { + if (auto playPath{VIDEO::UTILS::GetOpticalMediaPath(*item)}; !playPath.empty()) + { + CURL url(playPath); + if (url.IsProtocol("udf")) + playPath = url.GetHostName(); + ChangeFolderToFile(item, playPath); + } + } +} +} // namespace + +void CFileItemList::Stack() { std::unique_lock lock(m_lock); @@ -672,17 +699,17 @@ void CFileItemList::Stack(bool stackFiles /* = true */) // items needs to be sorted for stuff below to work properly Sort(SortByLabel, SortOrderAscending); - StackFolders(); + // Convert folder paths containing disc images to files (INDEX.BDMV or VIDEO_TS.IFO) + ConvertDiscFoldersToFiles(m_items); - if (stackFiles) - StackFiles(); -} + // Cannot stack a single item + if (m_items.size() == 1) + return; -void CFileItemList::StackFolders() -{ + // Get REs // Precompile our REs - std::vector folderRegExps = - CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_folderStackRegExps; + std::vector folderRegExps{ + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_folderStackRegExps}; if (folderRegExps.empty()) { @@ -690,241 +717,149 @@ void CFileItemList::StackFolders() return; } - // stack folders - for (const auto& item : m_items) - { - // combined the folder checks - if (item->IsFolder()) - { - // only check known fast sources? - // NOTES: - // 1. rars and zips may be on slow sources? is this supposed to be allowed? - if (!NETWORK::IsRemote(*item) || item->IsSmb() || item->IsNfs() || - URIUtils::IsInRAR(item->GetPath()) || URIUtils::IsInZIP(item->GetPath()) || - URIUtils::IsOnLAN(item->GetPath())) - { - // stack cd# folders if contains only a single video file + std::vector fileRegExps{ + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoStackRegExps}; - bool bMatch(false); - - auto expr = folderRegExps.begin(); - while (!bMatch && expr != folderRegExps.end()) - { - //CLog::LogF(LOGDEBUG,"Running expression {} on {}", expr->GetPattern(), item->GetLabel()); - bMatch = (expr->RegFind(item->GetLabel().c_str()) != -1); - if (bMatch) - { - CFileItemList items; - CDirectory::GetDirectory( - item->GetPath(), items, - CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(), DIR_FLAG_DEFAULTS); - // optimized to only traverse listing once by checking for filecount - // and recording last file item for later use - int nFiles = 0; - int index = -1; - for (int j = 0; j < items.Size(); j++) - { - if (!items[j]->IsFolder()) - { - nFiles++; - index = j; - } - - if (nFiles > 1) - break; - } - - if (nFiles == 1) - *item = *items[index]; - } - ++expr; - } - - // check for dvd folders - if (!bMatch) - { - std::string dvdPath = VIDEO::UTILS::GetOpticalMediaPath(*item); - - if (!dvdPath.empty()) - { - // NOTE: should this be done for the CD# folders too? - item->SetFolder(false); - item->SetPath(std::move(dvdPath)); - item->SetLabel2(""); - item->SetLabelPreformatted(true); - m_sortDescription.sortBy = SortByNone; /* sorting is now broken */ - } - } - } - } - } -} - -void CFileItemList::StackFiles() -{ - std::vector stackRegExps = - CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoStackRegExps; - - if (stackRegExps.empty()) + if (fileRegExps.empty()) { CLog::LogF(LOGDEBUG, "No stack expressions available. Skipping file stacking"); return; } - // now stack the files, some of which may be from the previous stack iteration - int i = 0; - while (i < Size()) + std::vector stackCandidates; + for (int i = 0; i < Size(); ++i) { - CFileItemPtr item1 = Get(i); - - // skip folders, nfo files, playlists - if (item1->IsFolder() || item1->IsParentFolder() || item1->IsNFO() || - PLAYLIST::IsPlayList(*item1)) + const auto& item{m_items[i]}; + if (item->IsFolder() || VIDEO::IsDVDFile(*item) || VIDEO::IsBDFile(*item)) { - // increment index - i++; - continue; - } + // Folder stacking (for BD/DVD files/images) + std::string folder{StringUtils::ToLower(item->GetLabel())}; + URIUtils::RemoveSlashAtEnd(folder); - int64_t size = 0; - size_t offset = 0; - std::string stackName; - std::string file1; - std::string filePath; - std::vector stack; - auto expr = stackRegExps.begin(); - - URIUtils::Split(item1->GetPath(), filePath, file1); - if (URIUtils::HasEncodedFilename(CURL(filePath))) - file1 = CURL::Decode(file1); - - int j; - while (expr != stackRegExps.end()) - { - if (expr->RegFind(file1, offset) != -1) + // Test each item against each RegExp + for (auto& regExp : folderRegExps) { - std::string title1 = expr->GetMatch(1); - const std::string volume1 = expr->GetMatch(2); - const std::string ignore1 = expr->GetMatch(3); - const std::string extension1 = expr->GetMatch(4); - if (offset) - title1 = file1.substr(0, expr->GetSubStart(2)); - j = i + 1; - while (j < Size()) - { - const CFileItemPtr item2 = Get(j); + if (regExp.RegFind(folder) == -1) + continue; - // skip folders, nfo files, playlists - if (item2->IsFolder() || item2->IsParentFolder() || item2->IsNFO() || - PLAYLIST::IsPlayList(*item2)) - { - // increment index - j++; + bool fileFound{true}; + if (item->IsFolder()) + { + // Look for media files in the folder + CFileItemList items; + if (!CDirectory::GetDirectory( + item->GetPath(), items, + CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(), + DIR_FLAG_DEFAULTS)) continue; - } - std::string file2; - std::string filePath2; - URIUtils::Split(item2->GetPath(), filePath2, file2); - if (URIUtils::HasEncodedFilename(CURL(filePath2))) - file2 = CURL::Decode(file2); - - if (expr->RegFind(file2, offset) != -1) + // Only expect one media file per folder (if >1 should be a file stack) + if (items.GetFileCount() == 1) + ChangeFolderToFile(item, items[0]->GetPath()); + else { - std::string title2 = expr->GetMatch(1); - const std::string volume2 = expr->GetMatch(2); - const std::string ignore2 = expr->GetMatch(3); - const std::string extension2 = expr->GetMatch(4); - if (offset) - title2 = file2.substr(0, expr->GetSubStart(2)); - if (StringUtils::EqualsNoCase(title1, title2)) - { - if (!StringUtils::EqualsNoCase(volume1, volume2)) - { - if (StringUtils::EqualsNoCase(ignore1, ignore2) && - StringUtils::EqualsNoCase(extension1, extension2)) - { - if (stack.empty()) - { - stackName = title1 + ignore1 + extension1; - stack.emplace_back(i); - size += item1->GetSize(); - } - stack.emplace_back(j); - size += item2->GetSize(); - } - else // Sequel - { - offset = 0; - ++expr; - break; - } - } - else if (!StringUtils::EqualsNoCase(ignore1, - ignore2)) // False positive, try again with offset - { - offset = expr->GetSubStart(3); - break; - } - else // Extension mismatch - { - offset = 0; - ++expr; - break; - } - } - else // Title mismatch - { - offset = 0; - ++expr; - break; - } + CLog::LogF(LOGDEBUG, + "Skipping folder '{}' - expected 1 media file per folder for a folder " + "stack, found {}", + item->GetPath(), items.GetFileCount()); + fileFound = false; } - else // No match 2, next expression - { - offset = 0; - ++expr; - break; - } - j++; } - if (j == Size()) - expr = stackRegExps.end(); - } - else // No match 1 - { - offset = 0; - ++expr; - } - if (stack.size() > 1) - { - // have a stack, remove the items and add the stacked item - // dont actually stack a multipart rar set, just remove all items but the first - std::string stackPath; - if (Get(stack[0])->IsRAR()) - stackPath = Get(stack[0])->GetPath(); - else + + if (fileFound) { - stackPath = CStackDirectory::ConstructStackPath(*this, stack); + // Add to stack vector + stackCandidates.emplace_back(StackCandidate{.type = StackCandidateType::FOLDER_CANDIDATE, + .title = regExp.GetMatch(1), + .volume = regExp.GetMatch(2), + .size = item->GetSize(), + .index = i}); + break; } - item1->SetPath(std::move(stackPath)); - // clean up list - for (size_t k = 1; k < stack.size(); k++) - Remove(i + 1); - // item->SetFolder(true); // don't treat stacked files as folders - // the label may be in a different char set from the filename (eg over smb - // the label is converted from utf8, but the filename is not) - if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( - CSettings::SETTING_FILELISTS_SHOWEXTENSIONS)) - URIUtils::RemoveExtension(stackName); + } + } + else if (!item->IsFolder() && !item->IsParentFolder() && !item->IsNFO() && + !PLAYLIST::IsPlayList(*item)) + { + // File stacking + std::string file; + std::string filePath; + URIUtils::Split(StringUtils::ToLower(item->GetPath()), filePath, file); + if (URIUtils::HasEncodedFilename(CURL(filePath))) + file = CURL::Decode(file); - item1->SetLabel(stackName); - item1->SetSize(size); + // Test each item against each RegExp + for (auto& regExp : fileRegExps) + { + if (regExp.RegFind(file) == -1) + continue; + + // Get components of file name + stackCandidates.emplace_back(StackCandidate{.type = StackCandidateType::FILE_CANDIDATE, + .title = regExp.GetMatch(1), + .volume = regExp.GetMatch(2), + .size = item->GetSize(), + .index = i}); break; } } - i++; } + + // Check we have stack candidates + if (stackCandidates.empty()) + return; + + // Sort stack candidates + std::ranges::sort(stackCandidates); + + // Count stack candidates + std::map countedCandidates; + for (const auto& s : stackCandidates) + ++countedCandidates[{s.type, s.title}]; + + // Find stacks + std::vector deleteItems; + for (const auto& [candidate, count] : + countedCandidates | std::views::filter([](const auto& c) { return c.second > 1; })) + { + // Find all items in this stack + std::vector stack; + int64_t size{0}; + for (const auto& stackItem : + stackCandidates | + std::views::filter([type = candidate.type, title = candidate.title](const auto& item) + { return item.type == type && item.title == title; })) + { + stack.emplace_back(stackItem.index); + size += stackItem.size; + if (stack.size() > 1) + deleteItems.emplace_back(stackItem.index); // delete all but first item in stack + } + + // Generate combined stack path + // @todo - why is RAR a special case here? a RAR file could be part of a stack. + const auto& baseItem{Get(stack[0])}; + const std::string stackPath{baseItem->IsRAR() + ? baseItem->GetPath() + : CStackDirectory::ConstructStackPath(*this, stack)}; + + // First item in stack becomes the stack + std::string stackName{candidate.title}; + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_FILELISTS_SHOWEXTENSIONS)) + URIUtils::RemoveExtension(stackName); + + // Update item + baseItem->SetPath(stackPath); + baseItem->SetLabel(stackName); + baseItem->SetSize(size); + } + + // Delete unneeded items + // Sort and delete from last to first (otherwise index is no longer correct) + std::ranges::sort(deleteItems, std::greater()); + for (int i : deleteItems) + Remove(i); } bool CFileItemList::Load(int windowID) diff --git a/xbmc/FileItemList.h b/xbmc/FileItemList.h index 06152615dd6..058be4e2019 100644 --- a/xbmc/FileItemList.h +++ b/xbmc/FileItemList.h @@ -16,6 +16,7 @@ #include "FileItem.h" #include "threads/CriticalSection.h" +#include #include #include #include @@ -35,6 +36,31 @@ public: ALWAYS }; + enum class StackCandidateType : uint8_t + { + FOLDER_CANDIDATE, + FILE_CANDIDATE + }; + + struct StackCandidate + { + StackCandidateType type; + std::string title; + std::string volume; + int64_t size; + int index; // index in m_items + + auto operator<=>(const StackCandidate&) const = default; + }; + + struct CountedStackCandidate + { + StackCandidateType type; + std::string title; + + auto operator<=>(const CountedStackCandidate& other) const = default; + }; + CFileItemList(); explicit CFileItemList(const std::string& strPath); ~CFileItemList() override; @@ -82,11 +108,9 @@ public: bool GetFastLookup() const { return m_fastLookup; } /*! \brief stack a CFileItemList - By default we stack all items (files and folders) in a CFileItemList - \param stackFiles whether to stack all items or just collapse folders (defaults to true) - \sa StackFiles,StackFolders + We stack all items (files and folders) in a CFileItemList */ - void Stack(bool stackFiles = true); + void Stack(); SortOrder GetSortOrder() const { return m_sortDescription.sortOrder; } SortBy GetSortMethod() const { return m_sortDescription.sortBy; } @@ -187,18 +211,6 @@ public: private: std::string GetDiscFileCache(int windowID) const; - /*! - \brief stack files in a CFileItemList - \sa Stack - */ - void StackFiles(); - - /*! - \brief stack folders in a CFileItemList - \sa Stack - */ - void StackFolders(); - void AddFastLookupItem(const CFileItemPtr& item); void AddFastLookupItems(const std::vector& items); diff --git a/xbmc/settings/AdvancedSettings.cpp b/xbmc/settings/AdvancedSettings.cpp index 4e62e709f18..169ce68a84d 100644 --- a/xbmc/settings/AdvancedSettings.cpp +++ b/xbmc/settings/AdvancedSettings.cpp @@ -26,7 +26,6 @@ #include "utils/StringUtils.h" #include "utils/SystemInfo.h" #include "utils/URIUtils.h" -#include "utils/Variant.h" #include "utils/XMLUtils.h" #include "utils/log.h" @@ -271,13 +270,14 @@ void CAdvancedSettings::Initialize() m_allExcludeFromScanRegExps.end()); m_folderStackRegExps = CompileRegexes({ - "((cd|dvd|dis[ck])[0-9]+)$", + "(.*?)(?:[^\\/])((?:cd|dvd|p(?:(?:ar)?t)|dis[ck])[ _.-]*[0-9])$", + "()((?:p(?:(?:ar)?t)[ _.-]*[0-9]))$", }); m_videoStackRegExps = CompileRegexes({ - "(.*?)([ _.-]*(?:cd|dvd|p(?:(?:ar)?t)|dis[ck])[ _.-]*[0-9]+)(.*?)(\\.[^.]+)$", - "(.*?)([ _.-]*(?:cd|dvd|p(?:(?:ar)?t)|dis[ck])[ _.-]*[a-d])(.*?)(\\.[^.]+)$", - "(.*?)([ ._-]*[a-d])(.*?)(\\.[^.]+)$", + "(.*?)([ _.-]*(?:cd|dvd|p(?:(?:ar)?t)|dis[ck]|file)[ _.-]*[0-9]+)(.*?)(\\.[^.]+)$", + "(.*?)([ _.-]*(?:cd|dvd|p(?:(?:ar)?t)|dis[ck])[ _.-]*[a-z])(.*?)(\\.[^.]+)$", + "^(.+)((?:[ ._-]|(?<=\\)))[a-z])()(\\.[^.]+)$", // This one is a bit too greedy to enable by default. It will stack sequels // in a flat dir structure, but is perfectly safe in a dir-per-vid one. // "(.*?)([ ._-]*[0-9])(.*?)(\\.[^.]+)$", diff --git a/xbmc/video/test/TestStacks.cpp b/xbmc/video/test/TestStacks.cpp index 6b75367d84f..a007afebe9f 100644 --- a/xbmc/video/test/TestStacks.cpp +++ b/xbmc/video/test/TestStacks.cpp @@ -9,7 +9,9 @@ #include "FileItem.h" #include "FileItemList.h" #include "filesystem/Directory.h" +#include "filesystem/StackDirectory.h" #include "test/TestUtils.h" +#include "utils/URIUtils.h" #include @@ -41,7 +43,10 @@ TEST_F(TestStacks, TestMovieFilesStackFilesAB) items.Stack(); EXPECT_EQ(items.Size(), 1); // check the single item in the stack is a stack:// - EXPECT_EQ(items.Get(0)->IsStack(), true); + if (!items.IsEmpty()) + { + EXPECT_EQ(items.Get(0)->IsStack(), true); + } } TEST_F(TestStacks, TestMovieFilesStackFilesPart) @@ -56,7 +61,10 @@ TEST_F(TestStacks, TestMovieFilesStackFilesPart) items.Stack(); EXPECT_EQ(items.Size(), 1); // check the single item in the stack is a stack:// - EXPECT_EQ(items.Get(0)->IsStack(), true); + if (!items.IsEmpty()) + { + EXPECT_EQ(items.Get(0)->IsStack(), true); + } } TEST_F(TestStacks, TestMovieFilesStackDvdIso) @@ -71,7 +79,28 @@ TEST_F(TestStacks, TestMovieFilesStackDvdIso) items.Stack(); EXPECT_EQ(items.Size(), 1); // check the single item in the stack is a stack:// - EXPECT_EQ(items.Get(0)->IsStack(), true); + if (!items.IsEmpty()) + { + EXPECT_EQ(items.Get(0)->IsStack(), true); + } +} + +TEST_F(TestStacks, TestMovieFilesStackBlurayIso) +{ + const std::string movieFolder = + XBMC_REF_FILE_PATH("xbmc/video/test/testdata/moviestack_blurayiso/Movie_(2001)"); + CFileItemList items; + CDirectory::GetDirectory(movieFolder, items, VIDEO_EXTENSIONS, DIR_FLAG_DEFAULTS); + // make sure items has 2 items (the two bluray isos) + EXPECT_EQ(items.Size(), 2); + // stack the items and make sure we end up with a single movie + items.Stack(); + EXPECT_EQ(items.Size(), 1); + // check the single item in the stack is a stack:// + if (!items.IsEmpty()) + { + EXPECT_EQ(items.Get(0)->IsStack(), true); + } } TEST_F(TestStacks, TestMovieFilesStackFolderFilesPart) @@ -86,33 +115,37 @@ TEST_F(TestStacks, TestMovieFilesStackFolderFilesPart) items.Stack(); EXPECT_EQ(items.Size(), 1); // check the single item in the stack is a stack:// - EXPECT_EQ(items.Get(0)->IsStack(), true); - EXPECT_EQ(items.Get(0)->IsFolder(), false); + if (!items.IsEmpty()) + { + EXPECT_EQ(items.Get(0)->IsStack(), true); + EXPECT_EQ(items.Get(0)->IsFolder(), false); + } } -TEST_F(TestStacks, TestMovieFilesStackFoldersPart) +TEST_F(TestStacks, TestMovieFilesStackFolderFilesDiscPart) { const std::string movieFolder = - XBMC_REF_FILE_PATH("xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)"); + XBMC_REF_FILE_PATH("xbmc/video/test/testdata/moviestack_subfolder_disc_parts/Movie_(2001)"); CFileItemList items; CDirectory::GetDirectory(movieFolder, items, "", DIR_FLAG_DEFAULTS); - // make sure items has 3 items (the three movie parts) - EXPECT_EQ(items.Size(), 3); - - EXPECT_EQ(items.Get(0)->IsFolder(), true); - EXPECT_EQ(items.Get(1)->IsFolder(), true); - EXPECT_EQ(items.Get(2)->IsFolder(), true); - + // make sure items has 2 items (the two movie parts) + EXPECT_EQ(items.Size(), 2); // stack the items and make sure we end up with a single movie - items.Stack(false); - EXPECT_EQ(items.Size(), 3); - // check the single item in the stack is a stack:// - EXPECT_EQ(items.Get(0)->IsStack(), false); - EXPECT_EQ(items.Get(1)->IsStack(), false); - EXPECT_EQ(items.Get(2)->IsStack(), false); + items.Stack(); + EXPECT_EQ(items.Size(), 1); - // Check the folders have been replaced with the files - EXPECT_EQ(items.Get(0)->IsFolder(), false); - EXPECT_EQ(items.Get(1)->IsFolder(), false); - EXPECT_EQ(items.Get(2)->IsFolder(), false); + // check the single item in the stack is a stack:// + std::shared_ptr item{items.Get(0)}; + EXPECT_EQ(item->IsStack(), true); + EXPECT_EQ(item->IsFolder(), false); + + // check bluray/dvd paths + std::vector paths; + CStackDirectory::GetPaths(item->GetPath(), paths); + EXPECT_EQ(paths.size(), 2); + if (paths.size() == 2) + { + EXPECT_EQ(URIUtils::IsDVDFile(paths[0]), true); + EXPECT_EQ(URIUtils::IsBDFile(paths[1]), true); + } } diff --git a/xbmc/video/test/testdata/moviestack_blurayiso/Movie_(2001)/Movie_(2001)_part1.iso b/xbmc/video/test/testdata/moviestack_blurayiso/Movie_(2001)/Movie_(2001)_part1.iso new file mode 100644 index 0000000000000000000000000000000000000000..18aee5b40cc6fa28af7f12013172b139dfccddac GIT binary patch literal 1310720 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 Un%@`CO#}!KAV7e?n@-^W0U936J^%m! literal 0 HcmV?d00001 diff --git a/xbmc/video/test/testdata/moviestack_blurayiso/Movie_(2001)/Movie_(2001)_part2.iso b/xbmc/video/test/testdata/moviestack_blurayiso/Movie_(2001)/Movie_(2001)_part2.iso new file mode 100644 index 0000000000000000000000000000000000000000..18aee5b40cc6fa28af7f12013172b139dfccddac GIT binary patch literal 1310720 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 Un%@`CO#}!KAV7e?n@-^W0U936J^%m! literal 0 HcmV?d00001 diff --git a/xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)/Movie_(2001)_dvd1.iso b/xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)/Movie_(2001)_dvd1.iso index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7d0f9c07fc7372d9eed39d684c387cee3c8562c4 100644 GIT binary patch literal 555008 zcmeI*OKV)$9RToi$9ha+sa8lKMcPc=Gz*2A(WI6zG)+`JiXe`RWGh(|iW@5+ur)?9 z^`>q^>9SDhCn$kHR)LT&&_EZZi*CCqWEs4h4^T+|=gvqP#g_H5e(IrpCP z8^QmcJCBo+M1TMR0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBx}v-SE! zk=|Qdf1^>`J6&B|S(w{3tQ`Z(%t&{8$=oq)M)s>(``bDsZiNe|2MFdGVPx;`C15s5dT8Of;tI^~U7odcB?= z*$^9drL=1;R`NftM1TN+UJ&@E6aRamE!seU009C72%M6@FXpE6_+KJGfB*pk1fC;- zcbAvy4e|dudc}2>009C72&8n~_}{+)0RjXF5P04Mt}VVdx0FA@^}Kale+dvEK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+zzGTzsgmA`6TA@t0=~%8%;gmB!SS>G{c+<9uRbW@=`4`IBCIDJ?EvpRUa` z@7-P~NgRlBji_CfR3>VZ#u-1-MTQK#U@!@=`oX|$17$L}`puWqbuHV=Ccwm4cx4qj{E z;6-GAaLx=5~@&pJFAkgyyeG$X| z8m)Y7wpC0--kt+5Pn-A7N^N2N!#p?eR;p}GId|6Q2i)q=Y2;%2X(&4f0RjXF98#b^ z;{P9_&c|Q0iaZvr#3P6K%IohgFV)M@X{_~L%55y-X*f4cjP1P_NQ(#%AV8p}1qLGG z|2bN?bnUa^jmXdC)}Hn8eKq(>&|?zeMW;~f}@009C72pm;lFyjB0QTfh=YWiv9eMdczdISg%AV7e?nGvW){BM8m zuT@+=Gp*K80t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNC9 z+X64d@Bh9TO?+kbx8=MtscT;P?ipsP-7AmjQr>;3v;81*^5yb*uE9!?|2h?A1PBly zK!Cuh2;|@Y|5V-TeTbOYacXkt+sdW|6IMgxUw*} zf4LJXF>u1|A2@cqt0}I?y3%+Feq(mNM8CaubNyc4TPeF)s=@YZ@c<{y1!;^n+OkN+RDyj`|ax6Jk# z|F^Fz+7gMr(j;l`^|KE=NzyI>pPPsIk$NyU& zcpKb3{=c*^zj(wJsYZYR0RjXFoU}k5|NlHr^7&VP+m7X(@qcHSz4QM6pGLm*U4ZV! zb;kS`^0qyek23ymUsrUW$T9%}1PBlya9n{R4MkYak-3_N%O!8&AJm0+c0vz{MQl9_;0t5&= zU4e`H#O}w(|A_AShlAT!2*ssGn)D!l{lEMwK)!|;+ImaDw*nuLvVi~r0t5&=3xPcU j-+$x8dH%oeQtVZ literal 0 HcmV?d00001 diff --git a/xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)/Movie_(2001)_dvd2.iso b/xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)/Movie_(2001)_dvd2.iso index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7d0f9c07fc7372d9eed39d684c387cee3c8562c4 100644 GIT binary patch literal 555008 zcmeI*OKV)$9RToi$9ha+sa8lKMcPc=Gz*2A(WI6zG)+`JiXe`RWGh(|iW@5+ur)?9 z^`>q^>9SDhCn$kHR)LT&&_EZZi*CCqWEs4h4^T+|=gvqP#g_H5e(IrpCP z8^QmcJCBo+M1TMR0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBx}v-SE! zk=|Qdf1^>`J6&B|S(w{3tQ`Z(%t&{8$=oq)M)s>(``bDsZiNe|2MFdGVPx;`C15s5dT8Of;tI^~U7odcB?= z*$^9drL=1;R`NftM1TN+UJ&@E6aRamE!seU009C72%M6@FXpE6_+KJGfB*pk1fC;- zcbAvy4e|dudc}2>009C72&8n~_}{+)0RjXF5P04Mt}VVdx0FA@^}Kale+dvEK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+zzGTzsgmA`6TA@t0=~%8%;gmB!SS>G{c+<9uRbW@=`4`IBCIDJ?EvpRUa` z@7-P~NgRlBji_CfR3>VZ#u-1-MTQK#U@!@=`oX|$17$L}`puWqbuHV=Ccwm4cx4qj{E z;6-GAaLx=5~@&pJFAkgyyeG$X| z8m)Y7wpC0--kt+5Pn-A7N^N2N!#p?eR;p}GId|6Q2i)q=Y2;%2X(&4f0RjXF98#b^ z;{P9_&c|Q0iaZvr#3P6K%IohgFV)M@X{_~L%55y-X*f4cjP1P_NQ(#%AV8p}1qLGG z|2bN?bnUa^jmXdC)}Hn8eKq(>&|?zeMW;~f}@009C72pm;lFyjB0QTfh=YWiv9eMdczdISg%AV7e?nGvW){BM8m zuT@+=Gp*K80t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNC9 z+X64d@Bh9TO?+kbx8=MtscT;P?ipsP-7AmjQr>;3v;81*^5yb*uE9!?|2h?A1PBly zK!Cuh2;|@Y|5V-TeTbOYacXkt+sdW|6IMgxUw*} zf4LJXF>u1|A2@cqt0}I?y3%+Feq(mNM8CaubNyc4TPeF)s=@YZ@c<{y1!;^n+OkN+RDyj`|ax6Jk# z|F^Fz+7gMr(j;l`^|KE=NzyI>pPPsIk$NyU& zcpKb3{=c*^zj(wJsYZYR0RjXFoU}k5|NlHr^7&VP+m7X(@qcHSz4QM6pGLm*U4ZV! zb;kS`^0qyek23ymUsrUW$T9%}1PBlya9n{R4MkYak-3_N%O!8&AJm0+c0vz{MQl9_;0t5&= zU4e`H#O}w(|A_AShlAT!2*ssGn)D!l{lEMwK)!|;+ImaDw*nuLvVi~r0t5&=3xPcU j-+$x8dH%oeQtVZ literal 0 HcmV?d00001 diff --git a/xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)/cd1/Movie_(2001)_part1.mkv b/xbmc/video/test/testdata/moviestack_subfolder_disc_parts/Movie_(2001)/part_1/VIDEO_TS/VIDEO_TS.IFO similarity index 100% rename from xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)/cd1/Movie_(2001)_part1.mkv rename to xbmc/video/test/testdata/moviestack_subfolder_disc_parts/Movie_(2001)/part_1/VIDEO_TS/VIDEO_TS.IFO diff --git a/xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)/cd2/Movie_(2001)_part2.mkv b/xbmc/video/test/testdata/moviestack_subfolder_disc_parts/Movie_(2001)/part_2/BDMV/index.bdmv similarity index 100% rename from xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)/cd2/Movie_(2001)_part2.mkv rename to xbmc/video/test/testdata/moviestack_subfolder_disc_parts/Movie_(2001)/part_2/BDMV/index.bdmv diff --git a/xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)/cd3/Movie_(2001)_part3.mkv b/xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)/part_1/Movie_(2001)_part1.mkv similarity index 100% rename from xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)/cd3/Movie_(2001)_part3.mkv rename to xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)/part_1/Movie_(2001)_part1.mkv diff --git a/xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)/part_2/Movie_(2001)_part2.mkv b/xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)/part_2/Movie_(2001)_part2.mkv new file mode 100644 index 00000000000..e69de29bb2d diff --git a/xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)/part_3/Movie_(2001)_part3.mkv b/xbmc/video/test/testdata/moviestack_subfolder_parts/Movie_(2001)/part_3/Movie_(2001)_part3.mkv new file mode 100644 index 00000000000..e69de29bb2d From cc709bfe60fe1db450bdbe6a43df23445356a6d3 Mon Sep 17 00:00:00 2001 From: 78andyp <99039295+78andyp@users.noreply.github.com> Date: Sun, 3 Aug 2025 22:21:18 +0100 Subject: [PATCH 2/8] Refactor CStackDirectory (and associated routines) to support folder stacks. --- xbmc/FileItem.cpp | 2 +- xbmc/filesystem/StackDirectory.cpp | 428 ++++++++++++++------------ xbmc/filesystem/StackDirectory.h | 64 +++- xbmc/utils/ArtUtils.cpp | 11 +- xbmc/utils/URIUtils.cpp | 32 +- xbmc/video/VideoUtils.cpp | 2 +- xbmc/video/tags/VideoTagLoaderNFO.cpp | 2 +- xbmc/video/test/TestStacks.cpp | 125 ++++++++ 8 files changed, 433 insertions(+), 233 deletions(-) diff --git a/xbmc/FileItem.cpp b/xbmc/FileItem.cpp index ea4108cd914..8b2b0e14bf6 100644 --- a/xbmc/FileItem.cpp +++ b/xbmc/FileItem.cpp @@ -1879,7 +1879,7 @@ std::string CFileItem::GetMovieName(bool bUseFolderNames /* = false */) const std::string strMovieName; if (URIUtils::IsStack(m_strPath)) - strMovieName = CStackDirectory::GetStackedTitlePath(m_strPath); + strMovieName = CStackDirectory::GetStackTitlePath(m_strPath); else strMovieName = GetBaseMoviePath(bUseFolderNames); diff --git a/xbmc/filesystem/StackDirectory.cpp b/xbmc/filesystem/StackDirectory.cpp index b54798f0be4..72a262ca874 100644 --- a/xbmc/filesystem/StackDirectory.cpp +++ b/xbmc/filesystem/StackDirectory.cpp @@ -18,204 +18,246 @@ #include "utils/URIUtils.h" #include "utils/log.h" -#include +#include +#include +#include +#include +#include namespace XFILE { - CStackDirectory::CStackDirectory() = default; +bool CStackDirectory::GetDirectory(const CURL& url, CFileItemList& items) +{ + items.Clear(); + std::vector files; + const std::string pathToUrl(url.Get()); + if (!GetPaths(pathToUrl, files)) + return false; // error in path - CStackDirectory::~CStackDirectory() = default; - - bool CStackDirectory::GetDirectory(const CURL& url, CFileItemList& items) + for (const std::string& i : files) { - items.Clear(); - std::vector 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->SetFolder(false); - items.Add(item); - } - return true; - } - - std::string CStackDirectory::GetStackedTitlePath(const std::string& strPath) - { - std::vector RegExps = - CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoStackRegExps; - return GetStackedTitlePath(strPath, RegExps); - } - - std::string CStackDirectory::GetStackedTitlePath(const std::string& strPath, - std::vector& 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::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& 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 &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 &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; + auto item = std::make_shared(i); + item->SetPath(i); + item->SetFolder(false); + items.Add(item); } + return true; } +std::string CStackDirectory::GetStackTitlePath(const std::string& strPath) +{ + CStackDirectory stack; + CFileItemList parts; + stack.GetDirectory(CURL(strPath), parts); + if (parts.Size() < 2) + { + CLog::LogF(LOGDEBUG, "Only one path. Skipping stack title path creation"); + return {}; + } + + std::vector folderRegExps{ + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_folderStackRegExps}; + + const bool isFolderStack{ + [&] + { + std::string path{StringUtils::ToLower(parts[0]->GetBaseMoviePath(true))}; + URIUtils::RemoveSlashAtEnd(path); + for (auto& regExp : folderRegExps) + if (regExp.RegFind(path) != -1) + return true; + return false; + }()}; + + const std::string commonPath{URIUtils::GetParentPath(strPath)}; + std::vector stackParts; + for (const auto& part : parts) + { + if (isFolderStack) + { + // Folder stack + std::string path{URIUtils::GetBasePath(part->GetPath())}; + URIUtils::RemoveSlashAtEnd(path); + path = URIUtils::GetFileName(path); + + // Test each item against each RegExp and save parts + for (auto& regExp : folderRegExps) + { + if (regExp.RegFind(path) != -1) + { + stackParts.emplace_back(StackPart{.title = regExp.GetMatch(1)}); + break; + } + } + } + else + { + // File stack + std::string fileName{URIUtils::GetFileName(part->GetPath())}; + + std::vector fileRegExps = + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoStackRegExps; + + // Check if source path uses URL encoding + if (URIUtils::HasEncodedFilename(CURL(commonPath))) + fileName = CURL::Decode(fileName); + + // Test each item against each RegExp and save parts + for (auto& regExp : fileRegExps) + { + if (regExp.RegFind(fileName) != -1) + { + stackParts.emplace_back( + StackPart{.title = regExp.GetMatch(1), .volume = regExp.GetMatch(3)}); + break; + } + } + } + } + + // Check all equal + std::string stackPath; + std::string stackTitle; + if (!stackParts.empty() && + std::ranges::adjacent_find(stackParts, std::not_equal_to{}) == stackParts.end()) + { + // Create stacked title + stackTitle = stackParts[0].title + stackParts[0].volume + + (isFolderStack ? "/" : URIUtils::GetExtension(parts[0]->GetPath())); + + // Check if source path uses URL encoding + if (!isFolderStack && URIUtils::HasEncodedFilename(CURL(commonPath))) + stackTitle = CURL::Encode(stackTitle); + + if (!commonPath.empty() && !stackTitle.empty()) + stackPath = commonPath + stackTitle; + } + + return stackPath; +} + +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& 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& 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& paths, + std::string& stackedPath, + const std::string& newPath) +{ + if (paths.size() < 2) + { + stackedPath.clear(); + return false; + } + + stackedPath = "stack://"; + for (auto path : paths) + { + // double escape any occurrence of commas + StringUtils::Replace(path, ",", ",,"); + stackedPath += path + " , "; + } + if (!newPath.empty()) + { + std::string path{newPath}; + StringUtils::Replace(path, ",", ",,"); + stackedPath += path + " , "; + } + stackedPath.erase(stackedPath.size() - 3); // remove last " , " + + return true; +} + +std::string CStackDirectory::GetParentPath(const std::string& stackPath) +{ + static constexpr int MAX_ITERATIONS{5}; + std::vector paths; + if (!GetPaths(stackPath, paths)) + return {}; + + // Loop until we have found a common parent path + bool first{true}; + int i = 0; // Maximum of 5 iterations + do + { + for (auto& path : paths) + { + URIUtils::RemoveSlashAtEnd(path); + if (first && (URIUtils::IsBDFile(path) || URIUtils::IsDVDFile(path))) + path = URIUtils::GetDiscBasePath(path); + else + path = URIUtils::GetParentPath(path); + } + first = false; + ++i; + } while (std::ranges::adjacent_find(paths, std::not_equal_to<>()) != paths.end() && + i <= MAX_ITERATIONS); + + if (i > MAX_ITERATIONS && std::ranges::adjacent_find(paths, std::not_equal_to<>()) != paths.end()) + { + CLog::LogF(LOGWARNING, "Failed to find common parent path after 5 iterations. Paths: [{}]", + StringUtils::Join(paths, ", ")); + return "/"; + } + return !paths[0].empty() ? paths[0] : "/"; +} +} // namespace XFILE diff --git a/xbmc/filesystem/StackDirectory.h b/xbmc/filesystem/StackDirectory.h index b5eef3da1d3..f14bab2d215 100644 --- a/xbmc/filesystem/StackDirectory.h +++ b/xbmc/filesystem/StackDirectory.h @@ -9,7 +9,6 @@ #pragma once #include "IDirectory.h" -#include "utils/RegExp.h" #include #include @@ -18,17 +17,66 @@ namespace XFILE { class CStackDirectory : public IDirectory { + typedef struct StackPart + { + std::string title; + std::string volume{}; + + auto operator<=>(const StackPart&) const = default; + } StackPart; + 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, - std::vector& RegExps); + + /*! + \brief Get the common base path/file from all the elements of the stack (for finding nfo files/metadata etc..) + \param strPath The stack:// path + \return The common base path/file + */ + static std::string GetStackTitlePath(const std::string& strPath); + + /*! + \brief Get the first element (file path) of a stack + \param strPath The stack:// path + \return The first element + */ static std::string GetFirstStackedFile(const std::string &strPath); + + /*! + \brief Extract the indvidual paths from a stack:// path + \param strPath The stack:// path + \param vecPaths The vector to fill with the individual paths + \return True if successful, false otherwise + */ static bool GetPaths(const std::string& strPath, std::vector& vecPaths); - static std::string ConstructStackPath(const CFileItemList& items, const std::vector &stack); - static bool ConstructStackPath(const std::vector &paths, std::string &stackedPath); + + /*! + \brief Construct a stack:// path from a CFileItemList with the position in the stack determined by the entry in the stack vector + \param items The CFileItemList containing the items to stack + \param stack The vector containing the positions in the CFileItemList to stack (ie. if stack[1] == 3 then the 2nd item in the stack is items[3]) + Note that stack is 0 based. + \return The constructed stack:// path + */ + static std::string ConstructStackPath(const CFileItemList& items, + const std::vector& stack); + + /*! + \brief Construct a stack:// path from a vector of paths and an optional additional path to append + \param paths The vector of paths to stack + \param stackedPath The constructed stack:// path + \param newPath An optional additional path to append to the stack + \return True if successful, false otherwise + */ + static bool ConstructStackPath(const std::vector& paths, + std::string& stackedPath, + const std::string& newPath = {}); + + /*! + \brief Get the parent path in common from all the parts of a stack:// path + \param stackPath The stack:// path + \return The parent path + */ + static std::string GetParentPath(const std::string& stackPath); }; } diff --git a/xbmc/utils/ArtUtils.cpp b/xbmc/utils/ArtUtils.cpp index 2eb360726ef..acb824c8355 100644 --- a/xbmc/utils/ArtUtils.cpp +++ b/xbmc/utils/ArtUtils.cpp @@ -214,12 +214,7 @@ std::string GetLocalArtBaseFilename(const CFileItem& item, { std::string strFile; if (item.IsStack()) - { - std::string strPath; - URIUtils::GetParentPath(item.GetPath(), strPath); - strFile = URIUtils::AddFileToFolder( - strPath, URIUtils::GetFileName(CStackDirectory::GetStackedTitlePath(item.GetPath()))); - } + strFile = CStackDirectory::GetStackTitlePath(item.GetPath()); std::string file = strFile.empty() ? item.GetPath() : strFile; if (URIUtils::IsInRAR(file) || URIUtils::IsInZIP(file)) @@ -313,7 +308,7 @@ std::string GetLocalFanart(const CFileItem& item) URIUtils::GetParentPath(item.GetPath(), path); CStackDirectory dir; std::string path2; - path2 = dir.GetStackedTitlePath(file); + path2 = dir.GetStackTitlePath(file); file = URIUtils::AddFileToFolder(path, URIUtils::GetFileName(path2)); CFileItem fan_item(dir.GetFirstStackedFile(item.GetPath()), false); std::string TBNFile(URIUtils::ReplaceExtension(GetTBNFile(fan_item), "-fanart")); @@ -396,7 +391,7 @@ std::string GetTBNFile(const CFileItem& item, int season /* = - 1 */, int episod if (CFile::Exists(returnPath)) return returnPath; - const std::string& stackPath{CStackDirectory::GetStackedTitlePath(file)}; + const std::string& stackPath{CStackDirectory::GetStackTitlePath(file)}; file = URIUtils::AddFileToFolder(path, URIUtils::GetFileName(stackPath)); } diff --git a/xbmc/utils/URIUtils.cpp b/xbmc/utils/URIUtils.cpp index b9e4be7c05f..69e12861583 100644 --- a/xbmc/utils/URIUtils.cpp +++ b/xbmc/utils/URIUtils.cpp @@ -442,26 +442,8 @@ bool URIUtils::GetParentPath(const std::string& strPath, std::string& strParent) } else if (url.IsProtocol("stack")) { - CStackDirectory dir; - CFileItemList items; - if (!dir.GetDirectory(url, items)) - return false; - CURL url2(GetDirectory(items[0]->GetPath())); - if (url2.HasParentInHostname()) - GetParentPath(url2.Get(), strParent); - else - strParent = url2.Get(); - for (const auto& item : items) - { - item->SetDVDLabel(GetDirectory(item->GetPath())); - if (url2.HasParentInHostname()) - item->SetPath(GetParentPath(item->GetDVDLabel())); - else - item->SetPath(item->GetDVDLabel()); - - GetCommonPath(strParent, item->GetPath()); - } - return true; + strParent = CStackDirectory::GetParentPath(url.Get()); + return !strParent.empty(); } else if (url.IsProtocol("multipath")) { @@ -1134,7 +1116,15 @@ bool URIUtils::IsDiscImage(const std::string& file) bool URIUtils::IsDiscImageStack(const std::string& file) { - return IsStack(file) && IsDiscImage(CStackDirectory::GetFirstStackedFile(file)); + if (IsStack(file)) + { + std::vector paths; + CStackDirectory::GetPaths(file, paths); + for (const std::string& path : paths) + if (IsDiscImage(path) || IsDVDFile(path) || IsBDFile(path)) + return true; + } + return false; } bool URIUtils::IsSpecial(const std::string& strFile) diff --git a/xbmc/video/VideoUtils.cpp b/xbmc/video/VideoUtils.cpp index 84ad6d603be..82e84743f2c 100644 --- a/xbmc/video/VideoUtils.cpp +++ b/xbmc/video/VideoUtils.cpp @@ -49,7 +49,7 @@ std::string FindTrailer(const CFileItem& item) URIUtils::GetParentPath(item.GetPath(), strPath); XFILE::CStackDirectory dir; std::string strPath2; - strPath2 = dir.GetStackedTitlePath(strFile); + strPath2 = dir.GetStackTitlePath(strFile); strFile = URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(strPath2)); CFileItem sitem(dir.GetFirstStackedFile(item.GetPath()), false); std::string strTBNFile(URIUtils::ReplaceExtension(ART::GetTBNFile(sitem), "-trailer")); diff --git a/xbmc/video/tags/VideoTagLoaderNFO.cpp b/xbmc/video/tags/VideoTagLoaderNFO.cpp index 3fe8c1132b3..68e2c0c1a0e 100644 --- a/xbmc/video/tags/VideoTagLoaderNFO.cpp +++ b/xbmc/video/tags/VideoTagLoaderNFO.cpp @@ -157,7 +157,7 @@ std::string CVideoTagLoaderNFO::FindNFO(const CFileItem& item, // else try .nfo file matching stacked title if (nfoFile.empty()) { - std::string stackedTitlePath = dir.GetStackedTitlePath(item.GetPath()); + std::string stackedTitlePath = dir.GetStackTitlePath(item.GetPath()); item2.SetPath(stackedTitlePath); nfoFile = FindNFO(item2, movieFolder); } diff --git a/xbmc/video/test/TestStacks.cpp b/xbmc/video/test/TestStacks.cpp index a007afebe9f..397a1c6ffaa 100644 --- a/xbmc/video/test/TestStacks.cpp +++ b/xbmc/video/test/TestStacks.cpp @@ -19,6 +19,10 @@ using namespace XFILE; +using ::testing::Test; +using ::testing::ValuesIn; +using ::testing::WithParamInterface; + namespace { const std::string VIDEO_EXTENSIONS = ".mpg|.mpeg|.mp4|.mkv|.mk3d|.iso"; @@ -149,3 +153,124 @@ TEST_F(TestStacks, TestMovieFilesStackFolderFilesDiscPart) EXPECT_EQ(URIUtils::IsBDFile(paths[1]), true); } } + +TEST_F(TestStacks, TestConstructStackPath) +{ + CFileItemList items; + + CFileItem item; + item.SetPath("smb://somepath/movie_part_1.mkv"); + items.Add(std::make_shared(item)); + + CFileItem item2; + item2.SetPath("smb://somepath/movie_part_2.mkv"); + items.Add(std::make_shared(item2)); + + std::vector index(2); + index[0] = 0; + index[1] = 1; + + std::string path{CStackDirectory::ConstructStackPath(items, index)}; + EXPECT_EQ(path, "stack://smb://somepath/movie_part_1.mkv , smb://somepath/movie_part_2.mkv"); + + index[0] = 1; + index[1] = 0; + + path = CStackDirectory::ConstructStackPath(items, index); + EXPECT_EQ(path, "stack://smb://somepath/movie_part_2.mkv , smb://somepath/movie_part_1.mkv"); + + std::vector paths; + paths.emplace_back("smb://somepath/movie_part_1.mkv"); + EXPECT_EQ(CStackDirectory::ConstructStackPath(paths, path), false); + + paths.emplace_back("smb://somepath/movie_part_2.mkv"); + EXPECT_EQ(CStackDirectory::ConstructStackPath(paths, path), true); + EXPECT_EQ(path, "stack://smb://somepath/movie_part_1.mkv , smb://somepath/movie_part_2.mkv"); + + EXPECT_EQ(CStackDirectory::ConstructStackPath(paths, path, "smb://somepath/movie_part_3.mkv"), + true); + EXPECT_EQ(path, "stack://smb://somepath/movie_part_1.mkv , smb://somepath/movie_part_2.mkv , " + "smb://somepath/movie_part_3.mkv"); +} + +TEST_F(TestStacks, TestGetParentPath) +{ + std::string path{"stack://smb://somepath/movie_part_1.mkv , smb://somepath/movie_part_2.mkv , " + "smb://somepath/movie_part_3.mkv"}; + std::string parent{CStackDirectory::GetParentPath(path)}; + EXPECT_EQ(parent, "smb://somepath/"); + + path = "stack://smb://somepath/BDMV/index.bdmv , smb://somepath/VIDEO_TS/VIDEO_TS.IFO"; + parent = CStackDirectory::GetParentPath(path); + EXPECT_EQ(parent, "smb://somepath/"); + + path = "stack://smb://somepath/a/b/c/d/e/movie_part_1.mkv , " + "smb://somepath/a/f/g/h/i/movie_part_2.mkv"; + parent = CStackDirectory::GetParentPath(path); + EXPECT_EQ(parent, "smb://somepath/a/"); + + path = "stack://smb://somepath/a/b/c/d/e/f/g/movie_part_1.mkv , " + "smb://somepath/a/h/i/j/k/l/m/movie_part_2.mkv"; + parent = CStackDirectory::GetParentPath(path); + EXPECT_EQ(parent, "/"); +} + +struct TestStackData +{ + const char* path; + const char* basePath; + const char* firstPath; +}; + +class TestGetStackedTitlePath : public Test, public WithParamInterface +{ +}; + +constexpr TestStackData Stacks[] = { + {.path = "stack://smb://somepath/movie_part_1.mkv , smb://somepath/movie_part_2.mkv", + .basePath = "smb://somepath/movie.mkv", + .firstPath = "smb://somepath/movie_part_1.mkv"}, + {.path = "stack://smb://somepath/movie_part_1.iso , smb://somepath/movie_part_2.iso", + .basePath = "smb://somepath/movie.iso", + .firstPath = "smb://somepath/movie_part_1.iso"}, + {.path = + "stack://smb://somepath/movie_part_1/movie.iso , smb://somepath/movie_part_2/movie.iso", + .basePath = "smb://somepath/movie/", + .firstPath = "smb://somepath/movie_part_1/movie.iso"}, + {.path = "stack://smb://somepath/movie_part_1/BDMV/index.bdmv , " + "smb://somepath/movie_part_2/VIDEO_TS/VIDEO_TS.IFO", + .basePath = "smb://somepath/movie/", + .firstPath = "smb://somepath/movie_part_1/BDMV/index.bdmv"}, + {.path = + "stack://bluray://" + "udf%3a%2f%2fsmb%253a%252f%252fsomepath%252fmovie_part_1%252fmovie.iso%2f/BDMV/" + "PLAYLIST/00800.mpls , " + "bluray://udf%3a%2f%2fsmb%253a%252f%252fsomepath%252fmovie_part_2%252fmovie.iso%2f/BDMV/" + "PLAYLIST/00800.mpls", + .basePath = "smb://somepath/movie/", + .firstPath = + "bluray://udf%3a%2f%2fsmb%253a%252f%252fsomepath%252fmovie_part_1%252fmovie.iso%2f/BDMV/" + "PLAYLIST/00800.mpls"}, +}; + +TEST_P(TestGetStackedTitlePath, GetStackedTitlePath) +{ + CFileItem item; + const std::string path{CStackDirectory::GetStackTitlePath(GetParam().path)}; + EXPECT_EQ(path, GetParam().basePath); +} + +INSTANTIATE_TEST_SUITE_P(TestStackDirectory, TestGetStackedTitlePath, ValuesIn(Stacks)); + +class TestGetFirstStackedFile : public Test, public WithParamInterface +{ +}; + +TEST_P(TestGetFirstStackedFile, GetFirstStackedFile) +{ + CFileItem item; + const std::string path{CStackDirectory::GetFirstStackedFile(GetParam().path)}; + EXPECT_EQ(path, GetParam().firstPath); +} + +INSTANTIATE_TEST_SUITE_P(TestStackDirectory, TestGetFirstStackedFile, ValuesIn(Stacks)); From 560198249527ed26e75ae63e26c2fbbfd59dc866 Mon Sep 17 00:00:00 2001 From: 78andyp <99039295+78andyp@users.noreply.github.com> Date: Thu, 14 Aug 2025 15:29:48 +0100 Subject: [PATCH 3/8] Refactor CApplicationStackHelper (and associated routines) to support folder stacks and std::chrono. --- xbmc/application/Application.cpp | 22 +- .../ApplicationMessageHandling.cpp | 5 +- .../application/ApplicationPlayerCallback.cpp | 37 +- xbmc/application/ApplicationStackHelper.cpp | 727 ++++++++++++------ xbmc/application/ApplicationStackHelper.h | 271 ++++--- xbmc/powermanagement/PowerManager.cpp | 4 +- xbmc/utils/SaveFileStateJob.cpp | 6 +- xbmc/video/VideoDatabase.cpp | 25 +- xbmc/video/VideoDatabase.h | 5 +- xbmc/video/VideoUtils.cpp | 29 +- 10 files changed, 732 insertions(+), 399 deletions(-) diff --git a/xbmc/application/Application.cpp b/xbmc/application/Application.cpp index 14742a91fc2..1be79a89e89 100644 --- a/xbmc/application/Application.cpp +++ b/xbmc/application/Application.cpp @@ -177,6 +177,7 @@ #endif #include +#include #include #include #include @@ -2484,8 +2485,8 @@ const CFileItem& CApplication::CurrentUnstackedItem() { const auto stackHelper = GetComponent(); - if (stackHelper->IsPlayingISOStack() || stackHelper->IsPlayingRegularStack()) - return stackHelper->GetCurrentStackPartFileItem(); + if (stackHelper->IsPlayingStack()) + return stackHelper->GetCurrentStackPart(); else return *m_itemCurrentFile; } @@ -2504,9 +2505,9 @@ double CApplication::GetTotalTime() const if (appPlayer->IsPlaying()) { if (stackHelper->IsPlayingRegularStack()) - rc = stackHelper->GetStackTotalTimeMs() * 0.001; + rc = static_cast(stackHelper->GetStackTotalTime().count()) / 1000.0; else - rc = appPlayer->GetTotalTime() * 0.001; + rc = static_cast(appPlayer->GetTotalTime()) / 1000.0; } return rc; @@ -2526,7 +2527,8 @@ double CApplication::GetTime() const { if (stackHelper->IsPlayingRegularStack()) { - uint64_t startOfCurrentFile = stackHelper->GetCurrentStackPartStartTimeMs(); + uint64_t startOfCurrentFile = + static_cast(stackHelper->GetCurrentStackPartStartTime().count()); rc = (startOfCurrentFile + appPlayer->GetTime()) * 0.001; } else @@ -2557,15 +2559,15 @@ void CApplication::SeekTime( double dTime ) // file if necessary, and calculate the correct seek within the new // file. Otherwise, just fall through to the usual routine if the // time is higher than our total time. - int partNumberToPlay = - stackHelper->GetStackPartNumberAtTimeMs(static_cast(dTime * 1000.0)); - uint64_t startOfNewFile = stackHelper->GetStackPartStartTimeMs(partNumberToPlay); + int partNumberToPlay = stackHelper->GetStackPartNumberAtTime( + std::chrono::milliseconds(static_cast(dTime * 1000.0))); + uint64_t startOfNewFile = stackHelper->GetStackPartStartTime(partNumberToPlay).count(); if (partNumberToPlay == stackHelper->GetCurrentPartNumber()) appPlayer->SeekTime(static_cast(dTime * 1000.0) - startOfNewFile); else { // seeking to a new file - stackHelper->SetStackPartCurrentFileItem(partNumberToPlay); - CFileItem* item = new CFileItem(stackHelper->GetCurrentStackPartFileItem()); + stackHelper->SetStackPartAsCurrent(partNumberToPlay); + CFileItem* item = new CFileItem(stackHelper->GetCurrentStackPart()); item->SetStartOffset(static_cast(dTime * 1000.0) - startOfNewFile); // don't just call "PlayFile" here, as we are quite likely called from the // player thread, so we won't be able to delete ourselves. diff --git a/xbmc/application/ApplicationMessageHandling.cpp b/xbmc/application/ApplicationMessageHandling.cpp index d037c9c4422..d305e3638a1 100644 --- a/xbmc/application/ApplicationMessageHandling.cpp +++ b/xbmc/application/ApplicationMessageHandling.cpp @@ -665,11 +665,12 @@ bool CApplicationMessageHandling::OnMessage(const CGUIMessage& message) m_app.CurrentFileItemPtr(), data); m_app.m_playerEvent.Set(); + if (const auto stackHelper{m_app.GetComponent()}; stackHelper->IsPlayingRegularStack() && stackHelper->HasNextStackPartFileItem()) { - // just play the next item in the stack - m_app.PlayFile(stackHelper->SetNextStackPartCurrentFileItem(), "", true); + // Just play the next item in the stack + m_app.PlayFile(stackHelper->SetNextStackPartAsCurrent(), "", true); return true; } diff --git a/xbmc/application/ApplicationPlayerCallback.cpp b/xbmc/application/ApplicationPlayerCallback.cpp index 0292e8b8f48..46036244947 100644 --- a/xbmc/application/ApplicationPlayerCallback.cpp +++ b/xbmc/application/ApplicationPlayerCallback.cpp @@ -39,9 +39,11 @@ #include "video/VideoFileItemClassify.h" #include "video/VideoInfoTag.h" +#include #include using namespace KODI; +using namespace std::chrono_literals; void CApplicationPlayerCallback::OnPlayBackEnded() { @@ -83,8 +85,8 @@ void CApplicationPlayerCallback::OnPlayBackStarted(const CFileItem& file) auto& components = CServiceBroker::GetAppComponents(); const auto stackHelper = components.GetComponent(); - if (stackHelper->IsPlayingISOStack() || stackHelper->IsPlayingRegularStack()) - itemCurrentFile = std::make_shared(*stackHelper->GetRegisteredStack(file)); + if (stackHelper->IsPlayingStack()) + itemCurrentFile = std::make_shared(*stackHelper->GetStack(file)); else itemCurrentFile = std::make_shared(file); @@ -97,7 +99,7 @@ void CApplicationPlayerCallback::OnPlayBackStarted(const CFileItem& file) CServiceBroker::GetJobManager()->PauseJobs(); } - stackHelper->OnPlayBackStarted(file); + stackHelper->OnPlayBackStarted(); CGUIMessage msg(GUI_MSG_PLAYBACK_STARTED, 0, 0, 0, 0, itemCurrentFile); CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg); @@ -164,23 +166,23 @@ void UpdateStackAndItem(const CFileItem& file, CBookmark& bookmark, const std::shared_ptr& stackHelper) { - if (stackHelper->GetRegisteredStackTotalTimeMs(fileItem) > 0) + if (stackHelper->GetStackTotalTime() > 0ms) { // Regular (not disc image) stack case: We have to save the bookmark on the stack. - fileItem = *stackHelper->GetRegisteredStack(file); + fileItem = *stackHelper->GetStack(file); // The bookmark coming from the player is only relative to the current part, thus needs // to be corrected with these attributes (start time will be 0 for non-stackparts). bookmark.timeInSeconds += - static_cast(stackHelper->GetRegisteredStackPartStartTimeMs(file)) / 1000.0; + static_cast(stackHelper->GetStackPartStartTime(file).count()) / 1000.0; - const uint64_t registeredStackTotalTimeMs{stackHelper->GetRegisteredStackTotalTimeMs(file)}; - if (registeredStackTotalTimeMs > 0) - bookmark.totalTimeInSeconds = static_cast(registeredStackTotalTimeMs) / 1000.0; + const auto StackTotalTimeMs{stackHelper->GetStackTotalTime()}; + if (StackTotalTimeMs > 0ms) + bookmark.totalTimeInSeconds = static_cast(StackTotalTimeMs.count()) / 1000.0; } // Any stack case: We need to save the part number. bookmark.partNumber = - stackHelper->GetRegisteredStackPartNumber(file) + 1; // CBookmark part numbers are 1-based + stackHelper->GetStackPartNumber(file) + 1; // CBookmark part numbers are 1-based } bool WithinPercentOfEnd(const CBookmark& bookmark, float ignorePercentAtEnd) @@ -232,17 +234,10 @@ void CApplicationPlayerCallback::OnPlayerCloseFile(const CFileItem& file, UpdateRemovableBlurayPath(fileItem, file.GetProperty("update_stream_details").asBoolean(false)); #endif - bool isStack{false}; - { - auto& components{CServiceBroker::GetAppComponents()}; - const auto stackHelper{components.GetComponent()}; - - std::unique_lock lock(stackHelper->m_critSection); - - isStack = (stackHelper->GetRegisteredStack(file) != nullptr); - if (isStack) - UpdateStackAndItem(file, fileItem, bookmark, stackHelper); - } + // Update the stack + const bool isStack{stackHelper->GetStack(file) != nullptr}; + if (isStack) + UpdateStackAndItem(file, fileItem, bookmark, stackHelper); if (const std::shared_ptr advancedSettings{ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()}; diff --git a/xbmc/application/ApplicationStackHelper.cpp b/xbmc/application/ApplicationStackHelper.cpp index 660665b2e6c..8e7bccf0307 100644 --- a/xbmc/application/ApplicationStackHelper.cpp +++ b/xbmc/application/ApplicationStackHelper.cpp @@ -10,326 +10,565 @@ #include "FileItem.h" #include "FileItemList.h" +#include "ServiceBroker.h" #include "URL.h" -#include "Util.h" +#include "cores/IPlayer.h" #include "cores/VideoPlayer/DVDFileInfo.h" #include "filesystem/StackDirectory.h" +#include "settings/AdvancedSettings.h" +#include "settings/MediaSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" #include "utils/URIUtils.h" #include "utils/log.h" #include "video/VideoDatabase.h" +#include "video/VideoUtils.h" +#include +#include +#include +#include +#include #include +#include using namespace XFILE; - -CApplicationStackHelper::CApplicationStackHelper(void) - : m_currentStack(new CFileItemList) -{ -} +using namespace std::chrono_literals; void CApplicationStackHelper::Clear() { m_currentStackPosition = 0; - m_currentStack->Clear(); + m_stackTotalTime = 0ms; + m_knownStackParts = 0; + m_stackPaths.clear(); + m_originalStackItems.Clear(); + m_oldStackPath.clear(); + m_stackMap.clear(); + m_wasDiscStack = false; + m_partFinished = false; + m_seekingParts = false; } -void CApplicationStackHelper::OnPlayBackStarted(const CFileItem& item) +void CApplicationStackHelper::OnPlayBackStarted() { - std::unique_lock lock(m_critSection); - - // time to clean up stack map - if (!HasRegisteredStack(item)) - m_stackmap.clear(); - else - { - auto stack = GetRegisteredStack(item); - Stackmap::iterator itr = m_stackmap.begin(); - while (itr != m_stackmap.end()) - { - if (itr->second->m_pStack != stack) - { - itr = m_stackmap.erase(itr); - } - else - { - ++itr; - } - } - } + m_partFinished = false; + m_seekingParts = false; } -bool CApplicationStackHelper::InitializeStack(const CFileItem & item) +bool CApplicationStackHelper::InitializeStack(const CFileItem& item) { - if (!item.IsStack()) - return false; - - auto stack = std::make_shared(item); + std::unique_lock stackLock(m_critSection); Clear(); - // read and determine kind of stack + + // Get a FileItem for each part of the stack + // (note this just populates m_strPath of the FileItem) + // Assumes item.GetDynPath() is the stack:// path CStackDirectory dir; - CURL path{item.GetDynPath()}; - if (!dir.GetDirectory(path, *m_currentStack) || m_currentStack->IsEmpty()) + CURL stackPath{item.GetDynPath()}; + if (!dir.GetDirectory(stackPath, m_originalStackItems) || m_originalStackItems.IsEmpty()) return false; - for (int i = 0; i < m_currentStack->Size(); i++) + + // Populate stack map + // Key is the path of each part (derived from stack:// above) + const auto stack{std::make_shared(item)}; + for (int i = 0; const auto& file : m_originalStackItems) { - // keep cross-references between stack parts and the stack - SetRegisteredStack(GetStackPartFileItem(i), stack); - SetRegisteredStackPartNumber(GetStackPartFileItem(i), i); + const std::string& path{file->GetPath()}; + const auto& part{GetOrCreateStackPartInformation(path)}; + part->stackItem = stack; + part->partNumber = i++; + m_stackPaths.emplace_back(path); + CLog::LogF(LOGDEBUG, "Populating stack with {} (part number {})", path, part->partNumber); } - m_currentStackIsDiscImageStack = URIUtils::IsDiscImageStack(item.GetDynPath()); + + // Now see what times we have for each part + CVideoDatabase db; + std::vector times; + bool haveTimes{false}; + + if (db.Open()) + haveTimes = db.GetStackTimes(item.GetDynPath(), times); + + // If no times and is a regular (file) stack then get times from files + // Not possible for BD/DVD (folder) stacks due as the playlist/title not known yet + if (!haveTimes && !IsPlayingDiscStack()) + { + std::chrono::milliseconds totalTime{0ms}; + for (int i = 0; i < m_originalStackItems.Size(); ++i) + { + int duration; + const auto& part{m_originalStackItems.Get(i)}; + if (!CDVDFileInfo::GetFileDuration(part->GetDynPath(), duration)) + { + Clear(); + CLog::LogF(LOGERROR, "Unable to get file duration from {}", part->GetDynPath()); + return false; + } + totalTime += std::chrono::milliseconds{duration}; + times.emplace_back(totalTime); + } + if (db.IsOpen()) + db.SetStackTimes(item.GetDynPath(), times); + haveTimes = true; + } + + db.Close(); + + // If now have times (saved or found above) then update stack items in stackmap + if (haveTimes) + { + const std::chrono::milliseconds totalTime{times.back()}; + for (int i = 0; i < static_cast(times.size()); ++i) + { + const auto& part{GetStackPartInformation(*m_originalStackItems.Get(i))}; + part->stackItem->SetEndOffset(times[i].count()); + part->startTime = i == 0 ? 0ms : times[i - 1]; + } + SetStackTotalTime(totalTime); // Set total time + m_knownStackParts = static_cast(times.size()); + CLog::LogF(LOGDEBUG, "Initialized stack with {} (known) parts and total time {}ms", + m_knownStackParts, totalTime.count()); + } + + // Remember if this was a disc stack + m_wasDiscStack = HasDiscParts(); return true; } -std::optional CApplicationStackHelper::InitializeStackStartPartAndOffset( - const CFileItem& item) +bool CApplicationStackHelper::ProcessNextPartInBookmark(CFileItem& item, CBookmark& bookmark) { - CVideoDatabase dbs; - int64_t startoffset = 0; + std::unique_lock stackLock(m_critSection); - // case 1: stacked ISOs - if (m_currentStackIsDiscImageStack) + if (std::optional nextPart{KODI::VIDEO::UTILS::GetNextPartFromBookmark(bookmark)}; nextPart) { - // first assume values passed to the stack - int selectedFile = item.GetStartPartNumber(); - startoffset = item.GetStartOffset(); - - // check if we instructed the stack to resume from default - if (startoffset == STARTOFFSET_RESUME) // selected file is not specified, pick the 'last' resume point - { - if (dbs.Open()) - { - CBookmark bookmark; - std::string path = item.GetDynPath(); - if (item.HasProperty("original_listitem_url") && URIUtils::IsPlugin(item.GetProperty("original_listitem_url").asString())) - path = item.GetProperty("original_listitem_url").asString(); - if (dbs.GetResumeBookMark(path, bookmark)) - { - startoffset = CUtil::ConvertSecsToMilliSecs(bookmark.timeInSeconds); - selectedFile = bookmark.partNumber; - } - dbs.Close(); - } - else - CLog::LogF(LOGERROR, "Cannot open VideoDatabase"); - } - - // make sure that the selected part is within the boundaries - if (selectedFile <= 0) - { - CLog::LogF(LOGWARNING, "Selected part {} out of range, playing part 1", selectedFile); - selectedFile = 1; - } - else if (selectedFile > m_currentStack->Size()) - { - CLog::LogF(LOGWARNING, "Selected part {} out of range, playing part {}", selectedFile, - m_currentStack->Size()); - selectedFile = m_currentStack->Size(); - } - - // set startoffset in selected item, track stack item for updating purposes, and finally play disc part - m_currentStackPosition = selectedFile - 1; - startoffset = startoffset > 0 ? STARTOFFSET_RESUME : 0; + // This updates the FileItem with the next part in the stack + item = SetStackPartAsCurrent(*nextPart); + bookmark.timeInSeconds = 0.0; + bookmark.playerState.clear(); + CLog::LogF(LOGDEBUG, "Next part in stack is {}", item.GetDynPath()); + return true; } - // case 2: all other stacks - else - { - // see if we have the info in the database - //! @todo If user changes the time speed (FPS via framerate conversion stuff) - //! then these times will be wrong. - //! Also, this is really just a hack for the slow load up times we have - //! A much better solution is a fast reader of FPS and fileLength - //! that we can use on a file to get it's time. - std::vector times; - bool haveTimes(false); - - if (dbs.Open()) - { - haveTimes = dbs.GetStackTimes(item.GetDynPath(), times); - dbs.Close(); - } - - // calculate the total time of the stack - uint64_t totalTimeMs = 0; - for (int i = 0; i < m_currentStack->Size(); i++) - { - if (haveTimes) - { - // set end time in every part - GetStackPartFileItem(i).SetEndOffset(times[i]); - } - else - { - int duration; - if (!CDVDFileInfo::GetFileDuration(GetStackPartFileItem(i).GetDynPath(), duration)) - { - m_currentStack->Clear(); - return std::nullopt; - } - totalTimeMs += duration; - // set end time in every part - GetStackPartFileItem(i).SetEndOffset(totalTimeMs); - times.push_back(totalTimeMs); - } - // set start time in every part - SetRegisteredStackPartStartTimeMs(GetStackPartFileItem(i), GetStackPartStartTimeMs(i)); - } - // set total time in every part - totalTimeMs = GetStackTotalTimeMs(); - for (int i = 0; i < m_currentStack->Size(); i++) - SetRegisteredStackTotalTimeMs(GetStackPartFileItem(i), totalTimeMs); - - uint64_t msecs = item.GetStartOffset(); - - if (!haveTimes || item.GetStartOffset() == STARTOFFSET_RESUME) - { - if (dbs.Open()) - { - // have our times now, so update the dB - if (!haveTimes && !times.empty()) - dbs.SetStackTimes(item.GetDynPath(), times); - - if (item.GetStartOffset() == STARTOFFSET_RESUME) - { - // can only resume seek here, not dvdstate - CBookmark bookmark; - std::string path = item.GetDynPath(); - if (item.HasProperty("original_listitem_url") && URIUtils::IsPlugin(item.GetProperty("original_listitem_url").asString())) - path = item.GetProperty("original_listitem_url").asString(); - if (dbs.GetResumeBookMark(path, bookmark)) - msecs = static_cast(bookmark.timeInSeconds * 1000); - else - msecs = 0; - } - dbs.Close(); - } - } - - m_currentStackPosition = GetStackPartNumberAtTimeMs(msecs); - startoffset = msecs - GetStackPartStartTimeMs(m_currentStackPosition); - } - return startoffset; + return false; } -bool CApplicationStackHelper::IsPlayingISOStack() const +static constexpr std::chrono::milliseconds STARTOFFSET_RESUME_MS{-1ms}; + +void CApplicationStackHelper::GetStackPartAndOptions(CFileItem& item, + CPlayerOptions& options, + bool restart) { - return m_currentStack->Size() > 0 && m_currentStackIsDiscImageStack; + std::unique_lock stackLock(m_critSection); + + std::chrono::milliseconds start{0ms}; + bool updated{false}; + + std::string path{item.GetDynPath()}; // stack:// path + if (item.HasProperty("original_listitem_url") && + URIUtils::IsPlugin(item.GetProperty("original_listitem_url").asString())) + path = item.GetProperty("original_listitem_url").asString(); + + if (restart) + { + m_currentStackPosition = 0; + options = {}; + } + else + { + // If resume then find start time in bookmark + start = std::chrono::milliseconds(item.GetStartOffset()); + if (start == STARTOFFSET_RESUME_MS) + { + CVideoDatabase db; + if (db.Open()) + { + CBookmark bookmark; + if (db.GetResumeBookMark(path, bookmark)) + { + updated = ProcessNextPartInBookmark(item, bookmark); + start = std::chrono::duration_cast( + std::chrono::duration(bookmark.timeInSeconds)); + options.starttime = bookmark.timeInSeconds; + options.state = bookmark.playerState; + item.SetStartOffset(STARTOFFSET_RESUME); + } + db.Close(); + } + } + } + + // Find stack part at this absolute time + if (!updated) + { + m_currentStackPosition = GetStackPartNumberAtTime(start); + item = *m_originalStackItems.Get(m_currentStackPosition); + options.starttime = + std::chrono::duration(start - GetStackPartStartTime(m_currentStackPosition)) + .count(); // Relative time in seconds + } + + CLog::LogF(LOGDEBUG, "Returning stack part {} ({}) with start time {}ms", m_currentStackPosition, + item.GetDynPath(), static_cast(options.starttime * 1000)); + + options.fullscreen = + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreenOnMovieStart && + !CMediaSettings::GetInstance().DoesMediaStartWindowed(); +} + +bool CApplicationStackHelper::UpdateDiscStackAndTimes(const CFileItem& playedFile) +{ + std::unique_lock stackLock(m_critSection); + + // Build stacktimes for disc image stacks after each disc is played + // Assumes the stack dynpaths have already been updated + if (m_stackMap.empty()) + return false; + + CVideoDatabase db; + if (!db.Open()) + return false; + + // Get existing stacktimes (if any) + const std::string newStackPath{m_stackMap.begin()->second->stackItem->GetDynPath()}; + const std::string path{!m_oldStackPath.empty() ? m_oldStackPath : newStackPath}; + CLog::LogF(LOGDEBUG, "Looking for existing stacktimes ({})", path); + + std::vector times; + const bool haveTimes{db.GetStackTimes(path, times)}; + + // See if new part played + if (GetKnownStackParts() - 1 == static_cast(times.size())) + { + std::chrono::milliseconds thisPartTime{ + playedFile.GetVideoInfoTag()->m_streamDetails.GetVideoDuration() * 1000ms}; + const std::chrono::milliseconds startPartTime{haveTimes ? times.back() : 0ms}; + const std::chrono::milliseconds totalTime{startPartTime + thisPartTime}; + CLog::LogF(LOGDEBUG, "Updating stack total time to {}ms (was {}ms)", totalTime.count(), + thisPartTime.count()); + + // Add this part's time to end of stacktimes + times.emplace_back(totalTime); + db.SetStackTimes(newStackPath, times); + db.Close(); + + // Update stack times (for bookmark and % played) + SetStackPartStartTime(playedFile, startPartTime); + SetStackTotalTime(totalTime); + + return true; + } + + db.Close(); + return true; +} + +void CApplicationStackHelper::SetNextPartBookmark(const std::string& path) +{ + std::unique_lock stackLock(m_critSection); + + // When parts are played completely but the size of subsequent parts is not known (ie. blurays) + // then setting the time and total time as the parts played and total parts will show progress as a fraction + // in the library (ie. 1 of 2 parts played will show a progress bar of 50%) + CBookmark bookmark; + bookmark.timeInSeconds = GetCurrentPartNumber(); + bookmark.totalTimeInSeconds = GetTotalPartNumbers(); + + bookmark.playerState = StringUtils::Format("{}", GetCurrentPartNumber()); + + CVideoDatabase db; + if (db.Open()) + { + db.AddBookMarkToFile(path, bookmark, CBookmark::RESUME); + db.Close(); + } +} + +bool CApplicationStackHelper::IsPlayingStack() const +{ + return !m_stackMap.empty(); +} + +bool CApplicationStackHelper::IsPlayingDiscStack() const +{ + return !m_stackMap.empty() && !IsPlayingRegularStack(); } bool CApplicationStackHelper::IsPlayingRegularStack() const { - return m_currentStack->Size() > 0 && !m_currentStackIsDiscImageStack; + return !m_stackMap.empty() && !HasDiscParts(); +} + +bool CApplicationStackHelper::IsPlayingResolvedDiscStack() const +{ + if (m_stackMap.empty() || !m_wasDiscStack) + return false; + + for (int i = 0; i <= m_currentStackPosition; ++i) + { + const std::string path{m_stackPaths[i]}; + if (URIUtils::IsDiscImage(path) || URIUtils::IsDVDFile(path) || URIUtils::IsBDFile(path)) + return false; // Stack part not resolved + } + + // All parts of stack up to the current have been resolved + return true; +} + +bool CApplicationStackHelper::HasDiscParts() const +{ + return std::ranges::any_of(m_stackPaths, + [](const std::string& path) { + return URIUtils::IsDiscImage(path) || URIUtils::IsDVDFile(path) || + URIUtils::IsBDFile(path); + }); } bool CApplicationStackHelper::HasNextStackPartFileItem() const { - return m_currentStackPosition < m_currentStack->Size() - 1; + return m_currentStackPosition < static_cast(m_stackMap.size()) - 1; } -uint64_t CApplicationStackHelper::GetStackPartEndTimeMs(int partNumber) const +bool CApplicationStackHelper::IsPlayingLastStackPart() const { - return GetStackPartFileItem(partNumber).GetEndOffset(); + return m_currentStackPosition == static_cast(m_stackMap.size()) - 1; } -uint64_t CApplicationStackHelper::GetStackTotalTimeMs() const +CFileItem& CApplicationStackHelper::SetNextStackPartAsCurrent() { - return GetStackPartEndTimeMs(m_currentStack->Size() - 1); + std::unique_lock stackLock(m_critSection); + + ++m_currentStackPosition; + return GetCurrentStackPart(); } -int CApplicationStackHelper::GetStackPartNumberAtTimeMs(uint64_t msecs) +CFileItem& CApplicationStackHelper::SetStackPartAsCurrent(int partNumber) { - if (msecs > 0) + std::unique_lock stackLock(m_critSection); + + m_currentStackPosition = partNumber; + return GetCurrentStackPart(); +} + +CFileItem& CApplicationStackHelper::GetCurrentStackPart() const +{ + return *m_originalStackItems.Get(m_currentStackPosition); +} + +std::chrono::milliseconds CApplicationStackHelper::GetStackPartEndTime(int partNumber) const +{ + if (partNumber < static_cast(m_stackPaths.size())) + if (const auto part{GetStackPartInformation(m_stackPaths[partNumber])}; part) + return std::chrono::milliseconds(part->stackItem->GetEndOffset()); + return 0ms; +} + +std::chrono::milliseconds CApplicationStackHelper::GetStackPartStartTime(int partNumber) const +{ + if (partNumber < static_cast(m_stackPaths.size())) + if (const auto part{GetStackPartInformation(m_stackPaths[partNumber])}; part) + return part->startTime; + return 0ms; +} + +std::chrono::milliseconds CApplicationStackHelper::GetCurrentStackPartStartTime() const +{ + if (m_currentStackPosition < static_cast(m_stackPaths.size())) + if (const auto part{GetStackPartInformation(m_stackPaths[m_currentStackPosition])}; part) + return part->startTime; + return 0ms; +} + +std::chrono::milliseconds CApplicationStackHelper::GetStackTotalTime() const +{ + return m_stackTotalTime; +} + +int CApplicationStackHelper::GetStackPartNumberAtTime(std::chrono::milliseconds msecs) const +{ + if (msecs > 0ms) { - // work out where to seek to - for (int partNumber = 0; partNumber < m_currentStack->Size(); partNumber++) + for (int i = m_knownStackParts - 1; i >= 0; --i) { - if (msecs < GetStackPartEndTimeMs(partNumber)) - return partNumber; + const auto startTime{GetStackPartStartTime(i)}; + const auto endTime{GetStackPartEndTime(i)}; + if (endTime > 0ms && msecs >= startTime && msecs < endTime) + return i; } } return 0; } -void CApplicationStackHelper::ClearAllRegisteredStackInformation() +std::shared_ptr CApplicationStackHelper::GetStack(const CFileItem& item) const { - m_stackmap.clear(); + if (const auto part{GetStackPartInformation(item)}; part) + return part->stackItem; + return nullptr; } -std::shared_ptr CApplicationStackHelper::GetRegisteredStack( +bool CApplicationStackHelper::IsInStack(const CFileItem& item) const +{ + auto it{m_stackMap.find(item.GetPath())}; + if (it == m_stackMap.end()) + it = m_stackMap.find(item.GetDynPath()); + return it != m_stackMap.end() && it->second != nullptr; +} + +int CApplicationStackHelper::GetStackPartNumber(const CFileItem& item) const +{ + if (const auto part{GetStackPartInformation(item)}; part) + return part->partNumber; + return 0; +} + +std::chrono::milliseconds CApplicationStackHelper::GetStackPartStartTime( const CFileItem& item) const { - return GetStackPartInformation(item.GetDynPath())->m_pStack; + if (const auto part{GetStackPartInformation(item)}; part) + return part->startTime; + return 0ms; } -bool CApplicationStackHelper::HasRegisteredStack(const CFileItem& item) const +void CApplicationStackHelper::SetStackFileIds(int fileId) { - const auto it = m_stackmap.find(item.GetDynPath()); - return it != m_stackmap.end() && it->second != nullptr; + std::unique_lock stackLock(m_critSection); + + for (const auto& stackMapItem : m_stackMap | std::views::values) + stackMapItem->stackItem->GetVideoInfoTag()->m_iFileId = fileId; } -void CApplicationStackHelper::SetRegisteredStack(const CFileItem& item, - std::shared_ptr stackItem) +void CApplicationStackHelper::SetStackPartStreamDetails(const CFileItem& item) { - GetStackPartInformation(item.GetDynPath())->m_pStack = std::move(stackItem); -} + std::unique_lock stackLock(m_critSection); -CFileItem& CApplicationStackHelper::GetStackPartFileItem(int partNumber) -{ - return *(*m_currentStack)[partNumber]; -} - -const CFileItem& CApplicationStackHelper::GetStackPartFileItem(int partNumber) const -{ - return *(*m_currentStack)[partNumber]; -} - -int CApplicationStackHelper::GetRegisteredStackPartNumber(const CFileItem& item) -{ - return GetStackPartInformation(item.GetDynPath())->m_lStackPartNumber; -} - -void CApplicationStackHelper::SetRegisteredStackPartNumber(const CFileItem& item, int partNumber) -{ - GetStackPartInformation(item.GetDynPath())->m_lStackPartNumber = partNumber; -} - -uint64_t CApplicationStackHelper::GetRegisteredStackPartStartTimeMs(const CFileItem& item) const -{ - return GetStackPartInformation(item.GetDynPath())->m_lStackPartStartTimeMs; -} - -void CApplicationStackHelper::SetRegisteredStackPartStartTimeMs(const CFileItem& item, uint64_t startTime) -{ - GetStackPartInformation(item.GetDynPath())->m_lStackPartStartTimeMs = startTime; -} - -uint64_t CApplicationStackHelper::GetRegisteredStackTotalTimeMs(const CFileItem& item) const -{ - return GetStackPartInformation(item.GetDynPath())->m_lStackTotalTimeMs; -} - -void CApplicationStackHelper::SetRegisteredStackTotalTimeMs(const CFileItem& item, uint64_t totalTime) -{ - GetStackPartInformation(item.GetDynPath())->m_lStackTotalTimeMs = totalTime; -} - -CApplicationStackHelper::StackPartInformationPtr CApplicationStackHelper::GetStackPartInformation( - const std::string& key) -{ - if (!m_stackmap.contains(key)) + if (const auto part{GetStackPartInformation(item)}; part) { - StackPartInformationPtr value(new StackPartInformation()); - m_stackmap[key] = value; + CVideoInfoTag* tag{part->stackItem->GetVideoInfoTag()}; + if (tag && item.HasVideoInfoTag()) + tag->m_streamDetails = item.GetVideoInfoTag()->m_streamDetails; } - return m_stackmap[key]; } -CApplicationStackHelper::StackPartInformationPtr CApplicationStackHelper::GetStackPartInformation( - const std::string& key) const +void CApplicationStackHelper::SetStackPartStartTime(const CFileItem& item, + std::chrono::milliseconds startTime) const { - const auto it = m_stackmap.find(key); - if (it == m_stackmap.end()) - return std::make_shared(); - return it->second; + std::unique_lock stackLock(m_critSection); + + if (const auto part{GetStackPartInformation(item)}; part) + part->startTime = startTime; +} + +void CApplicationStackHelper::SetStackDynPaths(const std::string& newPath) const +{ + for (const auto& stackMapItem : m_stackMap | std::views::values) + stackMapItem->stackItem->SetDynPath(newPath); +} + +void CApplicationStackHelper::SetStackPartPath(const CFileItem& item) +{ + std::unique_lock stackLock(m_critSection); + + if (m_stackMap.empty()) + return; + + // Get the current stack:// path and separate into individual paths + std::vector paths{}; + std::string stackedPath{m_stackMap.begin()->second->stackItem->GetDynPath()}; + CStackDirectory::GetPaths(stackedPath, paths); + + // Find the stack part to update + auto it{std::ranges::find(paths, item.GetPath())}; + if (it == paths.end()) + { + CLog::LogF(LOGERROR, "Item {} not found in stack {}", item.GetPath(), stackedPath); + return; + } + const int partNumber{static_cast(std::ranges::distance(paths.begin(), it))}; + + // Save current (old) stack:// path + m_oldStackPath = stackedPath; + + // Update the stack map with the new path for this part + // Update the key (the path) in the map + const std::string& newPath{item.GetDynPath()}; + const std::string oldPath{m_stackPaths[partNumber]}; + auto node{m_stackMap.extract(oldPath)}; + node.key() = newPath; + node.mapped()->stackItem->SetPath(newPath); + m_stackMap.insert(std::move(node)); + + // Update stack paths and file item + m_stackPaths[partNumber] = newPath; + m_originalStackItems[partNumber]->SetPath(newPath); + + // Generate new stack:// path + paths[partNumber] = newPath; + CStackDirectory::ConstructStackPath(paths, stackedPath); + SetStackDynPaths(stackedPath); +} + +std::string CApplicationStackHelper::GetStackDynPath() const +{ + return !m_stackMap.empty() ? m_stackMap.begin()->second->stackItem->GetDynPath() : std::string{}; +} + +std::string CApplicationStackHelper::GetOldStackDynPath() const +{ + return m_oldStackPath; +} + +void CApplicationStackHelper::SetStackTotalTime(const std::chrono::milliseconds totalTime) +{ + std::unique_lock stackLock(m_critSection); + + m_stackTotalTime = totalTime; +} + +void CApplicationStackHelper::SetStackPartOffsets(const CFileItem& item, + const std::chrono::milliseconds startOffset, + const std::chrono::milliseconds endOffset) const +{ + std::unique_lock stackLock(m_critSection); + + if (const auto it{m_stackMap.find(item.GetPath())}; it != m_stackMap.end()) + { + auto stackItem{it->second->stackItem}; + stackItem->SetStartOffset(startOffset.count()); + stackItem->SetEndOffset(endOffset.count()); + } +} + +void CApplicationStackHelper::IncreaseKnownStackParts() +{ + std::unique_lock stackLock(m_critSection); + + m_knownStackParts += 1; + m_oldStackPath.clear(); +} + +std::shared_ptr CApplicationStackHelper:: + GetOrCreateStackPartInformation(const std::string& key) +{ + if (!m_stackMap.contains(key)) + m_stackMap[key] = std::make_shared(); + return m_stackMap[key]; +} + +std::shared_ptr CApplicationStackHelper:: + GetStackPartInformation(const std::string& key) const +{ + if (const auto it{m_stackMap.find(key)}; it != m_stackMap.end()) + return it->second; + return std::make_shared(); +} + +std::shared_ptr CApplicationStackHelper:: + GetStackPartInformation(const CFileItem& item) const +{ + auto it{m_stackMap.find(item.GetPath())}; + if (it == m_stackMap.end()) + it = m_stackMap.find(item.GetDynPath()); + + return (it != m_stackMap.end() && it->second) ? it->second + : std::make_shared(); } diff --git a/xbmc/application/ApplicationStackHelper.h b/xbmc/application/ApplicationStackHelper.h index f35c5f7b73b..21d7b558e1c 100644 --- a/xbmc/application/ApplicationStackHelper.h +++ b/xbmc/application/ApplicationStackHelper.h @@ -8,28 +8,28 @@ #pragma once +#include "FileItemList.h" #include "application/IApplicationComponent.h" #include "threads/CriticalSection.h" +#include #include #include -#include #include +class CPlayerOptions; class CFileItem; class CFileItemList; class CApplicationStackHelper : public IApplicationComponent { public: - CApplicationStackHelper(void); - ~CApplicationStackHelper() = default; - void Clear(); - void OnPlayBackStarted(const CFileItem& item); + + void OnPlayBackStarted(); /*! - \brief Initialize stack + \brief Initialize stack and times for each part. \param item the FileItem object that is the stack */ bool InitializeStack(const CFileItem& item); @@ -37,9 +37,25 @@ public: /*! \brief Initialize stack times for each part, start & end, total time, and current part number if resume offset is specified. \param item the FileItem object that is the stack - \returns the part offset if available, nullopt in case of errors + \param options player options to update + \param restart true if playback is a restart, false otherwise */ - std::optional InitializeStackStartPartAndOffset(const CFileItem& item); + void GetStackPartAndOptions(CFileItem& item, CPlayerOptions& options, bool restart); + + /*! + \brief Updates the stack, fileItem and database stacktimes with new times. + The stack should have already been updated with the new dynpath. + \param playedFile The FileItem of the actual file played (updated in InputStream). + \return true if successful, false otherwise. + */ + bool UpdateDiscStackAndTimes(const CFileItem& playedFile); + + /*! + \brief If a disc stack is stopped between parts when the next part has not been determined (ie. playlist not selected), + then we need to save the bookmark for the next part before exiting playback. + \param path the stack:// path + */ + void SetNextPartBookmark(const std::string& path); /*! \brief returns the current part number @@ -47,169 +63,238 @@ public: int GetCurrentPartNumber() const { return m_currentStackPosition; } /*! - \brief Returns true if Application is currently playing an ISO stack + \brief returns the total number of parts */ - bool IsPlayingISOStack() const; + int GetTotalPartNumbers() const { return static_cast(m_stackMap.size()); } /*! - \brief Returns true if Application is currently playing a Regular (non-ISO) stack + \brief Returns true if Application is currently playing any stack + */ + bool IsPlayingStack() const; + + /*! + \brief Returns true if Application is currently playing a disc (ISO/BMDV/VIDEO_TS) stack + */ + bool IsPlayingDiscStack() const; + + /*! + \brief Returns true if Application is currently playing a regular (non-disc) stack */ bool IsPlayingRegularStack() const; /*! - \brief returns true if there is a next part available + \brief Returns true if Application is currently playing a disc stack where all parts up to the current one have been resolved + */ + bool IsPlayingResolvedDiscStack() const; + + /*! + \brief Returns true if there is another stack part available */ bool HasNextStackPartFileItem() const; /*! - \brief sets the next stack part as the current and returns a reference to it + \brief Returns true if playing the last part of the stack */ - const CFileItem& SetNextStackPartCurrentFileItem() - { - return GetStackPartFileItem(++m_currentStackPosition); - } + bool IsPlayingLastStackPart() const; /*! - \brief sets a given stack part as the current and returns a reference to it + \brief Sets the next stack part as the current and returns a reference to it + */ + CFileItem& SetNextStackPartAsCurrent(); + + /*! + \brief Sets a given stack part as the current and returns a reference to it \param partNumber the number of the part that needs to become the current one */ - const CFileItem& SetStackPartCurrentFileItem(int partNumber) - { - return GetStackPartFileItem(m_currentStackPosition = partNumber); - } + CFileItem& SetStackPartAsCurrent(int partNumber); /*! - \brief Returns the FileItem currently playing back as part of a (non-ISO) stack playback + \brief Returns the FileItem currently playing back as part of a stack playback */ - const CFileItem& GetCurrentStackPartFileItem() const - { - return GetStackPartFileItem(m_currentStackPosition); - } + CFileItem& GetCurrentStackPart() const; /*! - \brief Returns the end time of a FileItem part of a (non-ISO) stack playback + \brief Returns the end time of a FileItem part of a stack playback \param partNumber the requested part number in the stack */ - uint64_t GetStackPartEndTimeMs(int partNumber) const; + std::chrono::milliseconds GetStackPartEndTime(int partNumber) const; /*! - \brief Returns the start time of a FileItem part of a (non-ISO) stack playback + \brief Returns the start time of a FileItem part of a stack playback \param partNumber the requested part number in the stack */ - uint64_t GetStackPartStartTimeMs(int partNumber) const { return (partNumber > 0) ? GetStackPartEndTimeMs(partNumber - 1) : 0; } + std::chrono::milliseconds GetStackPartStartTime(int partNumber) const; /*! - \brief Returns the start time of the current FileItem part of a (non-ISO) stack playback + \brief Returns the start time of the current FileItem part of a stack playback */ - uint64_t GetCurrentStackPartStartTimeMs() const { return GetStackPartStartTimeMs(m_currentStackPosition); } + std::chrono::milliseconds GetCurrentStackPartStartTime() const; /*! - \brief Returns the total time of a (non-ISO) stack playback + \brief Returns the total time of a stack playback */ - uint64_t GetStackTotalTimeMs() const; + std::chrono::milliseconds GetStackTotalTime() const; /*! - \brief Returns the stack part number corresponding to the given timestamp in a (non-ISO) stack playback + \brief Returns the stack part number corresponding to the given timestamp in a stack playback \param msecs the requested timestamp in the stack (in milliseconds) */ - int GetStackPartNumberAtTimeMs(uint64_t msecs); + int GetStackPartNumberAtTime(std::chrono::milliseconds msecs) const; // Stack information registration methods - /*! - \brief Clear all entries in the item-stack map. To be called upon playback stopped. - */ - void ClearAllRegisteredStackInformation(); - /*! \brief Returns a smart pointer to the stack CFileItem. */ - std::shared_ptr GetRegisteredStack(const CFileItem& item) const; + std::shared_ptr GetStack(const CFileItem& item) const; /*! - \brief Returns true if there is a registered stack for the given CFileItem part. + \brief Returns true if there is a stack for the given CFileItem part. \param item the reference to the item that is part of a stack */ - bool HasRegisteredStack(const CFileItem& item) const; - - /*! - \brief Stores a smart pointer to the stack CFileItem in the item-stack map. - \param item the reference to the item that is part of a stack - \param stackItem the smart pointer to the stack CFileItem - */ - void SetRegisteredStack(const CFileItem& item, std::shared_ptr stackItem); + bool IsInStack(const CFileItem& item) const; /*! \brief Returns the part number of the part in the parameter \param item the reference to the item that is part of a stack */ - int GetRegisteredStackPartNumber(const CFileItem& item); - - /*! - \brief Stores the part number in the item-stack map. - \param item the reference to the item that is part of a stack - \param partNumber the part number of the part in other parameter - */ - void SetRegisteredStackPartNumber(const CFileItem& item, int partNumber); + int GetStackPartNumber(const CFileItem& item) const; /*! \brief Returns the start time of the part in the parameter \param item the reference to the item that is part of a stack */ - uint64_t GetRegisteredStackPartStartTimeMs(const CFileItem& item) const; + std::chrono::milliseconds GetStackPartStartTime(const CFileItem& item) const; /*! \brief Stores the part start time in the item-stack map. \param item the reference to the item that is part of a stack \param startTime the start time of the part in other parameter */ - void SetRegisteredStackPartStartTimeMs(const CFileItem& item, uint64_t startTimeMs); + void SetStackPartStartTime(const CFileItem& item, std::chrono::milliseconds startTime) const; /*! - \brief Returns the total time of the stack associated to the part in the parameter + \brief Sets the file id of the VideoInfoTag of each part in the stack. + \param fileId the file id + */ + void SetStackFileIds(int fileId); + + /*! + \brief Sets the stream details of the VideoInfoTag of the given part of the stack. \param item the reference to the item that is part of a stack */ - uint64_t GetRegisteredStackTotalTimeMs(const CFileItem& item) const; + void SetStackPartStreamDetails(const CFileItem& item); /*! - \brief Stores the stack's total time associated to the part in the item-stack map. - \param item the reference to the item that is part of a stack - \param totalTime the total time of the stack + \brief Updates the DynPath (which contains the entire stack://) of each part in the stack. + \param newPath the updated stack:// path */ - void SetRegisteredStackTotalTimeMs(const CFileItem& item, uint64_t totalTimeMs); + void SetStackDynPaths(const std::string& newPath) const; - CCriticalSection m_critSection; - -protected: /*! - \brief Returns a FileItem part of a (non-ISO) stack playback - \param partNumber the requested part number in the stack + \brief Updates the stack:// with the DynPath of the given item and then updates all parts in the stack. + \param item the FileItem in the stack that has an updated DynPath (eg. bluray://) + \sa SetStackDynPaths */ - CFileItem& GetStackPartFileItem(int partNumber); - const CFileItem& GetStackPartFileItem(int partNumber) const; + void SetStackPartPath(const CFileItem& item); + + /*! + \brief Returns the stack:// path of the stack. + */ + std::string GetStackDynPath() const; + + /*! + \brief Returns the stack:// path of the stack prior to the last resolved part being updated. + */ + std::string GetOldStackDynPath() const; + + /*! + \brief Sets the total time of the stack in each stack part. + \param totalTime the total time of the stack (in ms) + */ + void SetStackTotalTime(std::chrono::milliseconds totalTime); + + /*! + \brief Sets the starting and ending offsets of a stack part. + \param item the FileItem in the stack that has an updated DynPath (eg. bluray://) + \param startOffset the start offset in ms + \param endOffset the end offset in ms + */ + void SetStackPartOffsets(const CFileItem& item, + const std::chrono::milliseconds startOffset, + const std::chrono::milliseconds endOffset) const; + + /*! + \brief Returns the number of parts in the stack that are currently resolved (ie. a playlist has been selected and path is bluray://) + */ + int GetKnownStackParts() const { return m_knownStackParts; } + + /*! + \brief Increases the number of known (resolved) stack parts by one + */ + void IncreaseKnownStackParts(); + + /*! + \brief Returns true if any part of the stack are disc parts (ISO/BMDV/VIDEO_TS) + */ + bool HasDiscParts() const; + + /*! + \brief Returns true if any part of the stack was a disc part (ISO/BMDV/VIDEO_TS) + \ (prior to being resolved to a playlist bluray:// path) + */ + bool WasPlayingDiscStack() const { return m_wasDiscStack; } + + /*! + \brief Returns true if the current part has finished playing + */ + bool IsCurrentPartFinished() const { return m_partFinished; } + + /*! + \brief Set the status of the current playing part + \param finished true if the current part has finished playing, false otherwise + */ + void SetCurrentPartFinished(bool finished) { m_partFinished = finished; } + + /*! + \brief Returns true if currently seeking between parts + */ + bool IsSeekingParts() const { return m_seekingParts; } + + /*! + \brief Flag if currently seeking between parts + \param seeking true if currently seeking between parts, false if not + */ + void SetSeekingParts(bool seeking) { m_seekingParts = seeking; } + +private: + mutable CCriticalSection m_critSection; + + bool ProcessNextPartInBookmark(CFileItem& item, CBookmark& bookmark); class StackPartInformation { public: - StackPartInformation() - { - m_lStackPartNumber = 0; - m_lStackPartStartTimeMs = 0; - m_lStackTotalTimeMs = 0; - }; - uint64_t m_lStackPartStartTimeMs; - uint64_t m_lStackTotalTimeMs; - int m_lStackPartNumber; - std::shared_ptr m_pStack; + std::shared_ptr stackItem; + std::chrono::milliseconds startTime{std::chrono::milliseconds(0)}; + int partNumber{0}; }; - typedef std::shared_ptr StackPartInformationPtr; - typedef std::map Stackmap; - Stackmap m_stackmap; - StackPartInformationPtr GetStackPartInformation(const std::string& key); - StackPartInformationPtr GetStackPartInformation(const std::string& key) const; + using StackMap = std::map, std::less<>>; + StackMap m_stackMap; + std::shared_ptr GetOrCreateStackPartInformation(const std::string& key); + std::shared_ptr GetStackPartInformation(const std::string& key) const; + std::shared_ptr GetStackPartInformation(const CFileItem& item) const; - std::unique_ptr m_currentStack; - int m_currentStackPosition = 0; - bool m_currentStackIsDiscImageStack = false; + CFileItemList m_originalStackItems; + std::vector m_stackPaths; + + std::chrono::milliseconds m_stackTotalTime{std::chrono::milliseconds(0)}; + + int m_currentStackPosition{0}; + int m_knownStackParts{0}; + std::string m_oldStackPath{}; + bool m_wasDiscStack{false}; + bool m_partFinished{false}; + bool m_seekingParts{false}; }; diff --git a/xbmc/powermanagement/PowerManager.cpp b/xbmc/powermanagement/PowerManager.cpp index f1e8a054459..7ed530b2665 100644 --- a/xbmc/powermanagement/PowerManager.cpp +++ b/xbmc/powermanagement/PowerManager.cpp @@ -272,10 +272,10 @@ void CPowerManager::StorePlayerState() const auto stackHelper = components.GetComponent(); if (stackHelper->IsPlayingRegularStack()) m_lastPlayedFileItem->SetStartOffset(m_lastPlayedFileItem->GetStartOffset() + - stackHelper->GetCurrentStackPartStartTimeMs()); + stackHelper->GetCurrentStackPartStartTime().count()); // in case of iso stack, keep track of part number m_lastPlayedFileItem->SetStartPartNumber( - stackHelper->IsPlayingISOStack() ? stackHelper->GetCurrentPartNumber() + 1 : 1); + stackHelper->IsPlayingDiscStack() ? stackHelper->GetCurrentPartNumber() + 1 : 1); // for iso and iso stacks, keep track of playerstate m_lastPlayedFileItem->SetProperty("savedplayerstate", appPlayer->GetPlayerState()); CLog::Log(LOGDEBUG, diff --git a/xbmc/utils/SaveFileStateJob.cpp b/xbmc/utils/SaveFileStateJob.cpp index 08f8260161c..d19d5a861a6 100644 --- a/xbmc/utils/SaveFileStateJob.cpp +++ b/xbmc/utils/SaveFileStateJob.cpp @@ -31,8 +31,11 @@ #include "video/VideoDatabase.h" #include "video/VideoFileItemClassify.h" +#include + using namespace KODI; using namespace KODI::VIDEO; +using namespace std::chrono_literals; void CSaveFileState::DoWork(CFileItem& item, CBookmark& bookmark, @@ -240,8 +243,7 @@ void CSaveFileState::DoWork(CFileItem& item, // In order to properly update the list, we need to refresh the stack's resume point const auto& components = CServiceBroker::GetAppComponents(); const auto stackHelper = components.GetComponent(); - if (stackHelper->HasRegisteredStack(item) && - stackHelper->GetRegisteredStackTotalTimeMs(item) == 0) + if (stackHelper->IsInStack(item) && stackHelper->GetStackTotalTime(item) == 0ms) videodatabase.GetResumePoint(*(msgItem->GetVideoInfoTag())); CGUIMessage message(GUI_MSG_NOTIFY_ALL, CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(), 0, GUI_MSG_UPDATE_ITEM, 0, msgItem); diff --git a/xbmc/video/VideoDatabase.cpp b/xbmc/video/VideoDatabase.cpp index 7a2432a578e..70815705839 100644 --- a/xbmc/video/VideoDatabase.cpp +++ b/xbmc/video/VideoDatabase.cpp @@ -63,6 +63,8 @@ #include "video/VideoThumbLoader.h" #include +#include +#include #include #include #include @@ -78,6 +80,7 @@ using namespace KODI; using namespace KODI::MESSAGING; using namespace KODI::GUILIB; using namespace KODI::VIDEO; +using namespace std::chrono_literals; //******************************************************************************************************************************** CVideoDatabase::CVideoDatabase() = default; @@ -6192,7 +6195,8 @@ std::vector CVideoDatabase::GetAvailableArtTypesForItem(int mediaId /// \brief GetStackTimes() obtains any saved video times for the stacked file /// \retval Returns true if the stack times exist, false otherwise. -bool CVideoDatabase::GetStackTimes(const std::string &filePath, std::vector ×) +bool CVideoDatabase::GetStackTimes(const std::string& filePath, + std::vector& times) { try { @@ -6208,17 +6212,18 @@ bool CVideoDatabase::GetStackTimes(const std::string &filePath, std::vectorquery( strSQL ); if (m_pDS->num_rows() > 0) { // get the video settings info - uint64_t timeTotal = 0; + std::chrono::milliseconds timeTotal{0ms}; std::vector timeString = StringUtils::Split(m_pDS->fv("times").get_asString(), ","); times.clear(); for (const auto &i : timeString) { - const auto partTime = static_cast(atof(i.c_str()) * 1000.0); - times.emplace_back(partTime); // db stores in secs, convert to msecs + const std::chrono::milliseconds partTime{static_cast( + std::stod(i) * 1000.0)}; // Convert from seconds to milliseconds keeping fractional part + times.emplace_back(partTime); timeTotal += partTime; } m_pDS->close(); - return (timeTotal > 0); + return (timeTotal > 0ms); } m_pDS->close(); } @@ -6230,7 +6235,8 @@ bool CVideoDatabase::GetStackTimes(const std::string &filePath, std::vector ×) +void CVideoDatabase::SetStackTimes(const std::string& filePath, + const std::vector& times) { try { @@ -6246,9 +6252,10 @@ void CVideoDatabase::SetStackTimes(const std::string& filePath, const std::vecto m_pDS->exec( PrepareSQL("delete from stacktimes where idFile=%i", idFile) ); // add the items - std::string timeString = StringUtils::Format("{:.3f}", times[0] / 1000.0f); - for (unsigned int i = 1; i < times.size(); i++) - timeString += StringUtils::Format(",{:.3f}", times[i] / 1000.0f); + std::string timeString; + for (auto time : times) + timeString += StringUtils::Format("{:.3f},", static_cast(time.count()) / 1000.0f); + timeString.pop_back(); // remove trailing comma m_pDS->exec( PrepareSQL("insert into stacktimes (idFile,times) values (%i,'%s')\n", idFile, timeString.c_str()) ); } diff --git a/xbmc/video/VideoDatabase.h b/xbmc/video/VideoDatabase.h index 9593f5e61bb..86ed00e96ba 100644 --- a/xbmc/video/VideoDatabase.h +++ b/xbmc/video/VideoDatabase.h @@ -872,8 +872,9 @@ public: */ bool EraseAllForFile(const std::string& fileNameAndPath); - bool GetStackTimes(const std::string &filePath, std::vector ×); - void SetStackTimes(const std::string &filePath, const std::vector ×); + bool GetStackTimes(const std::string& filePath, std::vector& times); + void SetStackTimes(const std::string& filePath, + const std::vector& times); void GetBookMarksForFile(const std::string& strFilenameAndPath, VECBOOKMARKS& bookmarks, CBookmark::EType type = CBookmark::STANDARD, bool bAppend=false, long partNumber=0); bool AddBookMarkToFile(const std::string& strFilenameAndPath, diff --git a/xbmc/video/VideoUtils.cpp b/xbmc/video/VideoUtils.cpp index 82e84743f2c..4eb67e08490 100644 --- a/xbmc/video/VideoUtils.cpp +++ b/xbmc/video/VideoUtils.cpp @@ -33,7 +33,9 @@ #include #include +#include #include +#include #include namespace KODI::VIDEO::UTILS @@ -231,17 +233,15 @@ std::tuple GetStackResumeOffsetAndPartNumber(const CFileI } partNumber = 1; - std::vector times; + std::vector times; if (db.GetStackTimes(path, times)) { - for (size_t i = times.size(); i > 0; i--) - { - if (times[i - 1] <= static_cast(offset)) - { - partNumber = static_cast(i + 1); - break; - } - } + auto index{std::ranges::distance( + std::ranges::find_if(times | std::views::reverse, [offset](auto t) + { return t <= std::chrono::milliseconds(offset); }), + times.rend())}; + if (index < static_cast(times.size())) + partNumber = static_cast(index + 1); } } } @@ -301,12 +301,13 @@ int64_t GetStackPartResumeOffset(const CFileItem& item, unsigned int partNumber) offset = 0; - std::vector times; + std::vector times; if (db.GetStackTimes(path, times)) { const int64_t offsetToCheck{CUtil::ConvertSecsToMilliSecs(bookmark.timeInSeconds)}; - const uint64_t partBegin{partNumber == 1 ? 0 : times[partNumber - 2]}; - const uint64_t partEnd{times[partNumber - 1]}; + const uint64_t partBegin{ + partNumber == 1 ? 0 : static_cast(times[partNumber - 2].count())}; + const uint64_t partEnd{static_cast(times[partNumber - 1].count())}; if (static_cast(offsetToCheck) <= partEnd && static_cast(offsetToCheck) > partBegin) { @@ -347,9 +348,9 @@ int64_t GetStackPartStartOffset(const CFileItem& item, unsigned int partNumber) return {}; } - std::vector times; + std::vector times; if (db.GetStackTimes(path, times) && partNumber <= times.size()) - offset = times[partNumber - 2]; + offset = times[partNumber - 2].count(); } } } From 493d9c38c9db743ec24831c5efc559532d67c373 Mon Sep 17 00:00:00 2001 From: 78andyp <99039295+78andyp@users.noreply.github.com> Date: Thu, 14 Aug 2025 14:12:25 +0100 Subject: [PATCH 4/8] Remove unneeded class. --- .../DVDInputStreams/CMakeLists.txt | 2 - .../DVDInputStreams/DVDFactoryInputStream.cpp | 3 - .../DVDInputStreams/DVDInputStreamStack.cpp | 170 ------------------ .../DVDInputStreams/DVDInputStreamStack.h | 46 ----- 4 files changed, 221 deletions(-) delete mode 100644 xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamStack.cpp delete mode 100644 xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamStack.h diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/CMakeLists.txt b/xbmc/cores/VideoPlayer/DVDInputStreams/CMakeLists.txt index 6e3789c99dd..1cb7b33378b 100644 --- a/xbmc/cores/VideoPlayer/DVDInputStreams/CMakeLists.txt +++ b/xbmc/cores/VideoPlayer/DVDInputStreams/CMakeLists.txt @@ -5,7 +5,6 @@ set(SOURCES BlurayStateSerializer.cpp DVDInputStreamFile.cpp DVDInputStreamMemory.cpp DVDInputStreamNavigator.cpp - DVDInputStreamStack.cpp DVDStateSerializer.cpp InputStreamAddon.cpp InputStreamMultiSource.cpp @@ -20,7 +19,6 @@ set(HEADERS BlurayStateSerializer.h DVDInputStreamFile.h DVDInputStreamMemory.h DVDInputStreamNavigator.h - DVDInputStreamStack.h DVDStateSerializer.h DllDvdNav.h InputStreamAddon.h diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDFactoryInputStream.cpp b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDFactoryInputStream.cpp index 3a6ec2d9f10..368ae4aa2bd 100644 --- a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDFactoryInputStream.cpp +++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDFactoryInputStream.cpp @@ -16,7 +16,6 @@ #include "DVDInputStreamFFmpeg.h" #include "DVDInputStreamFile.h" #include "DVDInputStreamNavigator.h" -#include "DVDInputStreamStack.h" #include "FileItem.h" #include "InputStreamAddon.h" #include "InputStreamMultiSource.h" @@ -135,8 +134,6 @@ std::shared_ptr CDVDFactoryInputStream::CreateInputStream(IVide { return std::make_shared(fileitem); } - else if(StringUtils::StartsWithNoCase(file, "stack://")) - return std::make_shared(fileitem); CFileItem finalFileitem(fileitem); diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamStack.cpp b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamStack.cpp deleted file mode 100644 index 3788295b019..00000000000 --- a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamStack.cpp +++ /dev/null @@ -1,170 +0,0 @@ -/* - * 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 "DVDInputStreamStack.h" - -#include "FileItem.h" -#include "FileItemList.h" -#include "filesystem/File.h" -#include "filesystem/StackDirectory.h" -#include "utils/log.h" - -#include - -using namespace XFILE; - -CDVDInputStreamStack::CDVDInputStreamStack(const CFileItem& fileitem) : CDVDInputStream(DVDSTREAM_TYPE_FILE, fileitem) -{ - m_eof = true; - m_pos = 0; - m_length = 0; -} - -CDVDInputStreamStack::~CDVDInputStreamStack() -{ - Close(); -} - -bool CDVDInputStreamStack::IsEOF() -{ - return m_eof; -} - -bool CDVDInputStreamStack::Open() -{ - if (!CDVDInputStream::Open()) - return false; - - CStackDirectory dir; - CFileItemList items; - - const CURL pathToUrl(m_item.GetDynPath()); - if(!dir.GetDirectory(pathToUrl, items)) - { - CLog::Log(LOGERROR, "CDVDInputStreamStack::Open - failed to get list of stacked items"); - return false; - } - - m_length = 0; - m_eof = false; - - for(int index = 0; index < items.Size(); index++) - { - TFile file(new CFile()); - - if (!file->Open(items[index]->GetDynPath(), READ_TRUNCATED)) - { - CLog::Log(LOGERROR, "CDVDInputStreamStack::Open - failed to open stack part '{}' - skipping", - items[index]->GetDynPath()); - continue; - } - TSeg segment; - segment.file = file; - segment.length = file->GetLength(); - - if(segment.length <= 0) - { - CLog::Log(LOGERROR, - "CDVDInputStreamStack::Open - failed to get file length for '{}' - skipping", - items[index]->GetDynPath()); - continue; - } - - m_length += segment.length; - - m_files.push_back(segment); - } - - if(m_files.empty()) - return false; - - m_file = m_files[0].file; - m_eof = false; - - return true; -} - -// close file and reset everything -void CDVDInputStreamStack::Close() -{ - CDVDInputStream::Close(); - m_files.clear(); - m_file.reset(); - m_eof = true; -} - -int CDVDInputStreamStack::Read(uint8_t* buf, int buf_size) -{ - if(m_file == NULL || m_eof) - return 0; - - unsigned int ret = m_file->Read(buf, buf_size); - - if(ret > INT_MAX) - return -1; - - if(ret == 0) - { - m_eof = true; - if(Seek(m_pos, SEEK_SET) < 0) - { - CLog::Log(LOGERROR, "CDVDInputStreamStack::Read - failed to seek into next file"); - m_eof = true; - m_file.reset(); - return -1; - } - } - - m_pos += ret; - - return (int)ret; -} - -int64_t CDVDInputStreamStack::Seek(int64_t offset, int whence) -{ - int64_t pos, len; - - if (whence == SEEK_SET) - pos = offset; - else if(whence == SEEK_CUR) - pos = offset + m_pos; - else if(whence == SEEK_END) - pos = offset + m_length; - else - return -1; - - len = 0; - for(TSegVec::iterator it = m_files.begin(); it != m_files.end(); ++it) - { - if(len + it->length > pos) - { - TFile file = it->file; - int64_t file_pos = pos - len; - if(file->GetPosition() != file_pos) - { - if(file->Seek(file_pos, SEEK_SET) < 0) - return false; - } - - m_file = file; - m_pos = pos; - m_eof = false; - return pos; - } - len += it->length; - } - - return -1; -} - -int64_t CDVDInputStreamStack::GetLength() -{ - return m_length; -} - - diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamStack.h b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamStack.h deleted file mode 100644 index 02befe8eb7c..00000000000 --- a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamStack.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 "DVDInputStream.h" - -#include -#include - -class CDVDInputStreamStack : public CDVDInputStream -{ -public: - explicit CDVDInputStreamStack(const CFileItem& fileitem); - ~CDVDInputStreamStack() override; - - bool Open() override; - void Close() override; - int Read(uint8_t* buf, int buf_size) override; - int64_t Seek(int64_t offset, int whence) override; - bool IsEOF() override; - int64_t GetLength() override; - -protected: - - typedef std::shared_ptr TFile; - - struct TSeg - { - TFile file; - int64_t length; - }; - - typedef std::vector TSegVec; - - TSegVec m_files; ///< collection of open ptr's to all files in stack - TFile m_file; ///< currently active file - bool m_eof; - int64_t m_pos; - int64_t m_length; -}; From 9ebe53510134bef07a0789a55b858db52193a912 Mon Sep 17 00:00:00 2001 From: 78andyp <78andyp@kodi.tv> Date: Sat, 8 Nov 2025 19:03:45 +0000 Subject: [PATCH 5/8] Prepare CDVDInputStream for dynamically updating folder stacks. --- .../DVDInputStreams/DVDInputStream.cpp | 68 ++++++++++++++----- .../DVDInputStreams/DVDInputStream.h | 3 + .../DVDInputStreams/DVDInputStreamBluray.cpp | 6 ++ .../DVDInputStreams/DVDInputStreamBluray.h | 1 + .../DVDInputStreamNavigator.cpp | 5 ++ .../DVDInputStreams/DVDInputStreamNavigator.h | 1 + 6 files changed, 66 insertions(+), 18 deletions(-) diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStream.cpp b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStream.cpp index c2670a6d175..54b5ffbfba2 100644 --- a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStream.cpp +++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStream.cpp @@ -8,7 +8,10 @@ #include "DVDInputStream.h" +#include "ServiceBroker.h" #include "URL.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationStackHelper.h" #include "cores/VideoPlayer/Interface/InputStreamConstants.h" #include "utils/URIUtils.h" #include "utils/log.h" @@ -246,26 +249,55 @@ CDVDInputStream::UpdateState CDVDInputStream::UpdatePlaylistDetails( item.GetVideoInfoTag()->m_iTrack = playlist; CLog::LogF(LOGDEBUG, "Main playlist {}", playlist); - // Update DynPath here as needed to save video settings - switch (type) + if (type == DVDSTREAM_TYPE_DVD && item.GetProperty("update_stream_details").asBoolean(false) && + item.HasVideoInfoTag()) { - case DVDSTREAM_TYPE_BLURAY: - { - const std::string path{item.GetDynPath()}; - item.SetDynPath(URIUtils::GetBlurayPlaylistPath(path, playlist)); - break; - } - case DVDSTREAM_TYPE_DVD: - { - // Only update streamdetails if not already set (ie. from NFO) - if (item.GetProperty("update_stream_details").asBoolean(false) && item.HasVideoInfoTag()) - item.GetVideoInfoTag()->m_streamDetails = it3->details; - - break; - } - default: - break; + // Update streamdetails for DVD titles (bluray handled when playlist selected) + item.GetVideoInfoTag()->m_streamDetails = it3->details; + } + else if (type == DVDSTREAM_TYPE_BLURAY) + { + // Covert dynpath to a bluray:// path with playlist + const std::string path{item.GetDynPath()}; + item.SetDynPath(URIUtils::GetBlurayPlaylistPath(path, playlist)); } return stoppedBeforeEnd ? NONE : FINISHED; } + +void CDVDInputStream::UpdateStackItem(CFileItem& item, std::chrono::milliseconds length) +{ + auto& components{CServiceBroker::GetAppComponents()}; + const auto& stackHelper{components.GetComponent()}; + if (stackHelper->GetStack(item) != nullptr && + stackHelper->GetStackPartNumber(item) >= stackHelper->GetKnownStackParts()) + { + stackHelper->IncreaseKnownStackParts(); + CLog::LogF(LOGDEBUG, "Playing new stack part"); + + // Dynamically update stack for bluray:// + if (URIUtils::IsProtocol(item.GetDynPath(), "bluray")) + { + std::chrono::milliseconds time{stackHelper->GetStackTotalTime()}; + stackHelper->SetStackTotalTime(time + length); + stackHelper->SetStackPartStartTime(item, time); + stackHelper->SetStackPartOffsets(item, time, time + length); + CLog::LogF(LOGDEBUG, "Updated stack times"); + + const std::chrono::milliseconds end{time.count() + length.count()}; + item.SetStartOffset(time.count()); + item.SetEndOffset(end.count()); + + const int part{stackHelper->GetCurrentPartNumber()}; + item.SetStartPartNumber(part); + CLog::LogF(LOGDEBUG, "Playing part {} - absolute stack offsets {}ms - {}ms", part, + time.count(), end.count()); + + // Update streamdetails in stack part if they aren't updated from the stream in VideoPlayer (ie. already in item) + if (!item.GetProperty("update_stream_details").asBoolean(false)) + stackHelper->SetStackPartStreamDetails(item); + + stackHelper->SetStackPartPath(item); + } + } +} diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStream.h b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStream.h index 53e2f9cf614..410deaae7e0 100644 --- a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStream.h +++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStream.h @@ -222,6 +222,7 @@ public: { return UpdateState::NONE; } + virtual void UpdateStack(CFileItem& item) {} struct PlaylistInformation { @@ -243,6 +244,8 @@ public: double time, bool& closed); + static void UpdateStackItem(CFileItem& item, std::chrono::milliseconds length); + protected: DVDStreamType m_streamType; BitstreamStats m_stats; diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamBluray.cpp b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamBluray.cpp index 64673bfedfa..c4059efc454 100644 --- a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamBluray.cpp +++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamBluray.cpp @@ -1359,3 +1359,9 @@ CDVDInputStream::UpdateState CDVDInputStreamBluray::UpdateCurrentState(CFileItem return UpdatePlaylistDetails(DVDSTREAM_TYPE_BLURAY, m_playedPlaylists, item, time, closed); } + +void CDVDInputStreamBluray::UpdateStack(CFileItem& item) +{ + return UpdateStackItem(item, + m_titleInfo ? std::chrono::milliseconds(m_titleInfo->duration / 90) : 0ms); +} diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamBluray.h b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamBluray.h index 0536dc8e711..01ae691bb5f 100644 --- a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamBluray.h +++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamBluray.h @@ -142,6 +142,7 @@ public: void SaveCurrentState(const CStreamDetails& details) override; UpdateState UpdateCurrentState(CFileItem& item, double time, bool& closed) override; + void UpdateStack(CFileItem& item) override; protected: struct SPlane; diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamNavigator.cpp b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamNavigator.cpp index 0ce881a6886..7464998b6ee 100644 --- a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamNavigator.cpp +++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamNavigator.cpp @@ -1605,3 +1605,8 @@ CDVDInputStream::UpdateState CDVDInputStreamNavigator::UpdateCurrentState(CFileI return UpdatePlaylistDetails(DVDSTREAM_TYPE_DVD, m_playedPlaylists, item, time, closed); } + +void CDVDInputStreamNavigator::UpdateStack(CFileItem& item) +{ + return UpdateStackItem(item, 0ms); +} diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamNavigator.h b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamNavigator.h index 63f82605841..aef3ca68f5e 100644 --- a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamNavigator.h +++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamNavigator.h @@ -147,6 +147,7 @@ public: void SaveCurrentState(const CStreamDetails& details) override; UpdateState UpdateCurrentState(CFileItem& item, double time, bool& closed) override; + void UpdateStack(CFileItem& item) override; protected: From f76118090f713d1b3405d824f3fdacda77376140 Mon Sep 17 00:00:00 2001 From: 78andyp <99039295+78andyp@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:53:58 +0100 Subject: [PATCH 6/8] Other routines needed to handle dynamically updating folder stacks. --- .../resources/strings.po | 8 +- xbmc/PlayListPlayer.cpp | 9 +- xbmc/cores/VideoPlayer/VideoPlayer.cpp | 5 +- xbmc/utils/SaveFileStateJob.cpp | 45 ++-- xbmc/video/VideoDatabase.cpp | 90 ++++---- xbmc/video/VideoUtils.cpp | 200 ++++++------------ xbmc/video/VideoUtils.h | 16 +- xbmc/video/guilib/VideoGUIUtils.cpp | 30 +-- .../video/guilib/VideoPlayActionProcessor.cpp | 6 +- 9 files changed, 189 insertions(+), 220 deletions(-) diff --git a/addons/resource.language.en_gb/resources/strings.po b/addons/resource.language.en_gb/resources/strings.po index 00415bf34e8..8efe3d4646f 100644 --- a/addons/resource.language.en_gb/resources/strings.po +++ b/addons/resource.language.en_gb/resources/strings.po @@ -5948,7 +5948,13 @@ msgctxt "#12022" msgid "Resume from {0:s}" msgstr "" -#empty strings from id 12023 to 12307 +#. Label for resuming playback from a certain part +#: xbmc/video/VideoUtils.cpp +msgctxt "#12023" +msgid "Resume from" +msgstr "" + +#empty strings from id 12024 to 12307 #. Label of the show password button in the keyboard dialog #: xbmc/dialogs/GUIDialogKeyboardGeneric.cpp diff --git a/xbmc/PlayListPlayer.cpp b/xbmc/PlayListPlayer.cpp index bcfea353789..87e7236cbe5 100644 --- a/xbmc/PlayListPlayer.cpp +++ b/xbmc/PlayListPlayer.cpp @@ -944,7 +944,14 @@ void PLAYLIST::CPlayListPlayer::OnApplicationMessage(KODI::MESSAGING::ThreadMess { // Discard the current playlist, if TMSG_MEDIA_PLAY gets posted with just a single item. // Otherwise items may fail to play, when started while a playlist is playing. - Reset(); + // But a single item in a stack is allowed. + if (m_iCurrentPlayList != Id::TYPE_NONE) + { + + CPlayList& playlist{GetPlaylist(m_iCurrentPlayList)}; + if (!URIUtils::IsStack(playlist[m_iCurrentSong]->GetDynPath())) + Reset(); + } CFileItem *item = static_cast(pMsg->lpVoid); g_application.PlayFile(*item, "", pMsg->param1 != 0); diff --git a/xbmc/cores/VideoPlayer/VideoPlayer.cpp b/xbmc/cores/VideoPlayer/VideoPlayer.cpp index 5d04d55e03b..879dff14add 100644 --- a/xbmc/cores/VideoPlayer/VideoPlayer.cpp +++ b/xbmc/cores/VideoPlayer/VideoPlayer.cpp @@ -33,6 +33,7 @@ #include "VideoPlayerRadioRDS.h" #include "VideoPlayerVideo.h" #include "application/Application.h" +#include "application/ApplicationStackHelper.h" #include "cores/DataCacheCore.h" #include "cores/EdlEdit.h" #include "cores/FFmpeg.h" @@ -46,7 +47,6 @@ #include "interfaces/AnnouncementManager.h" #include "jobs/JobQueue.h" #include "messaging/ApplicationMessenger.h" -#include "network/NetworkFileItemClassify.h" #include "settings/AdvancedSettings.h" #include "settings/Settings.h" #include "settings/SettingsComponent.h" @@ -1393,6 +1393,9 @@ void CVideoPlayer::Prepare() if (!discStateRestored) OpenDefaultStreams(); + // Update stack and offsets in fileItem (for Blurays/DVDs) + m_pInputStream->UpdateStack(fileItem); + /* * Check to see if the demuxer should start at something other than time 0. This will be the case * if there was a start time specified as part of the "Start from where last stopped" (aka diff --git a/xbmc/utils/SaveFileStateJob.cpp b/xbmc/utils/SaveFileStateJob.cpp index d19d5a861a6..9abda640b43 100644 --- a/xbmc/utils/SaveFileStateJob.cpp +++ b/xbmc/utils/SaveFileStateJob.cpp @@ -15,7 +15,6 @@ #include "URIUtils.h" #include "URL.h" #include "Util.h" -#include "application/ApplicationStackHelper.h" #include "guilib/GUIComponent.h" #include "guilib/GUIMessage.h" #include "guilib/GUIWindowManager.h" @@ -42,16 +41,20 @@ void CSaveFileState::DoWork(CFileItem& item, bool updatePlayCount) { std::string progressTrackingFile = item.GetPath(); - - if (URIUtils::IsBlurayPath(item.GetDynPath()) && - (item.GetVideoContentType() == VideoDbContentType::MOVIES || - item.GetVideoContentType() == VideoDbContentType::EPISODES || - item.GetVideoContentType() == VideoDbContentType::UNKNOWN /* Removable bluray */)) + if (item.IsStack() || + (URIUtils::IsBlurayPath(item.GetDynPath()) && + (item.GetVideoContentType() == VideoDbContentType::MOVIES || + item.GetVideoContentType() == VideoDbContentType::EPISODES || + item.GetVideoContentType() == VideoDbContentType::UNKNOWN /* Removable bluray */))) + { progressTrackingFile = item.GetDynPath(); + } else if (item.HasVideoInfoTag() && IsVideoDb(item)) + { progressTrackingFile = item.GetVideoInfoTag() ->m_strFileNameAndPath; // we need the file url of the video db item to create the bookmark + } else if (item.HasProperty("original_listitem_url")) { // only use original_listitem_url for Python, UPnP and Bluray sources @@ -209,16 +212,29 @@ void CSaveFileState::DoWork(CFileItem& item, // See if idFile of library item needs updating const CVideoInfoTag* tag{item.HasVideoInfoTag() ? item.GetVideoInfoTag() : nullptr}; - if (tag && tag->m_iFileId >= 0 && - !(tag->m_iDbId < 0 && item.GetVideoContentType() != VideoDbContentType::UNKNOWN) && - URIUtils::IsBlurayPath(item.GetDynPath()) && - tag->m_strFileNameAndPath != item.GetDynPath()) + const bool updateNeeded{ + [&item, &tag] + { + if (!tag || tag->m_iFileId < 0) + return false; // No tag or file to update + if (tag->m_iDbId < 0 && item.GetVideoContentType() != VideoDbContentType::UNKNOWN) + return false; // No video db item to update + if (URIUtils::IsBlurayPath(item.GetDynPath()) && + !URIUtils::IsStack(tag->m_strFileNameAndPath) && + tag->m_strFileNameAndPath != item.GetDynPath()) + return true; // Bluray path to update + if (item.GetProperty("new_stack_path").asBoolean(false)) + return true; // Stack path to update + return false; + }()}; + + if (updateNeeded) { videodatabase.BeginTransaction(); // tag->m_iFileId contains the idFile originally played and may be different to the idFile // in the movie table entry if it's a non-default video version const int newFileId{videodatabase.SetFileForMedia( - item.GetDynPath(), item.GetVideoContentType(), tag->m_iDbId, + progressTrackingFile, item.GetVideoContentType(), tag->m_iDbId, CVideoDatabase::FileRecord{.m_idFile = tag->m_iFileId, .m_playCount = tag->GetPlayCount(), .m_lastPlayed = tag->m_lastPlayed, @@ -239,13 +255,6 @@ void CSaveFileState::DoWork(CFileItem& item, if (item.HasProperty("original_listitem_url")) msgItem->SetPath(item.GetProperty("original_listitem_url").asString()); - // Could be part of an ISO stack. In this case the bookmark is saved onto the part. - // In order to properly update the list, we need to refresh the stack's resume point - const auto& components = CServiceBroker::GetAppComponents(); - const auto stackHelper = components.GetComponent(); - if (stackHelper->IsInStack(item) && stackHelper->GetStackTotalTime(item) == 0ms) - videodatabase.GetResumePoint(*(msgItem->GetVideoInfoTag())); - CGUIMessage message(GUI_MSG_NOTIFY_ALL, CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(), 0, GUI_MSG_UPDATE_ITEM, 0, msgItem); CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message); } diff --git a/xbmc/video/VideoDatabase.cpp b/xbmc/video/VideoDatabase.cpp index 70815705839..a4aa2119904 100644 --- a/xbmc/video/VideoDatabase.cpp +++ b/xbmc/video/VideoDatabase.cpp @@ -1097,15 +1097,14 @@ int CVideoDatabase::AddOrUpdateFile(const std::string& fileAndPath, int CVideoDatabase::AddFile(const CFileItem& item) { - if (URIUtils::IsBlurayPath(item.GetDynPath())) + if (URIUtils::IsBlurayPath(item.GetDynPath()) || item.IsStack()) return AddFile(item.GetDynPath()); if (IsVideoDb(item) && item.HasVideoInfoTag()) { const auto videoInfoTag = item.GetVideoInfoTag(); if (videoInfoTag->m_iFileId != -1) return videoInfoTag->m_iFileId; - else - return AddFile(*videoInfoTag); + return AddFile(*videoInfoTag); } return AddFile(item.GetPath()); } @@ -3829,56 +3828,47 @@ void CVideoDatabase::GetBookMarksForFile(const std::string& strFilenameAndPath, { try { - if (URIUtils::IsDiscImageStack(strFilenameAndPath)) - { - CStackDirectory dir; - CFileItemList fileList; - const CURL pathToUrl(strFilenameAndPath); - dir.GetDirectory(pathToUrl, fileList); - if (!bAppend) - bookmarks.clear(); - for (int i = fileList.Size() - 1; i >= 0; i--) // put the bookmarks of the highest part first in the list - GetBookMarksForFile(fileList[i]->GetPath(), bookmarks, type, true, (i+1)); - } - else - { - int idFile = GetFileId(strFilenameAndPath); - if (idFile < 0) return ; - if (!bAppend) - bookmarks.erase(bookmarks.begin(), bookmarks.end()); - if (nullptr == m_pDB) - return; - if (nullptr == m_pDS) - return; + int idFile = GetFileId(strFilenameAndPath); + if (idFile < 0) + return; + if (!bAppend) + bookmarks.erase(bookmarks.begin(), bookmarks.end()); + if (nullptr == m_pDB) + return; + if (nullptr == m_pDS) + return; - std::string strSQL = - PrepareSQL("select * from bookmark where idFile=%i and type=%i order by timeInSeconds", - idFile, static_cast(type)); - m_pDS->query( strSQL ); - while (!m_pDS->eof()) + std::string strSQL = + PrepareSQL("select * from bookmark where idFile=%i and type=%i order by timeInSeconds", + idFile, static_cast(type)); + m_pDS->query(strSQL); + while (!m_pDS->eof()) + { + CBookmark bookmark; + bookmark.timeInSeconds = m_pDS->fv("timeInSeconds").get_asDouble(); + bookmark.partNumber = partNumber; + bookmark.totalTimeInSeconds = m_pDS->fv("totalTimeInSeconds").get_asDouble(); + bookmark.thumbNailImage = m_pDS->fv("thumbNailImage").get_asString(); + bookmark.playerState = m_pDS->fv("playerState").get_asString(); + bookmark.player = m_pDS->fv("player").get_asString(); + bookmark.type = type; + if (type == CBookmark::EPISODE) { - CBookmark bookmark; - bookmark.timeInSeconds = m_pDS->fv("timeInSeconds").get_asDouble(); - bookmark.partNumber = partNumber; - bookmark.totalTimeInSeconds = m_pDS->fv("totalTimeInSeconds").get_asDouble(); - bookmark.thumbNailImage = m_pDS->fv("thumbNailImage").get_asString(); - bookmark.playerState = m_pDS->fv("playerState").get_asString(); - bookmark.player = m_pDS->fv("player").get_asString(); - bookmark.type = type; - if (type == CBookmark::EPISODE) - { - std::string strSQL2=PrepareSQL("select c%02d, c%02d from episode where c%02d=%i order by c%02d, c%02d", VIDEODB_ID_EPISODE_EPISODE, VIDEODB_ID_EPISODE_SEASON, VIDEODB_ID_EPISODE_BOOKMARK, m_pDS->fv("idBookmark").get_asInt(), VIDEODB_ID_EPISODE_SORTSEASON, VIDEODB_ID_EPISODE_SORTEPISODE); - m_pDS2->query(strSQL2); - bookmark.episodeNumber = m_pDS2->fv(0).get_asInt(); - bookmark.seasonNumber = m_pDS2->fv(1).get_asInt(); - m_pDS2->close(); - } - bookmarks.emplace_back(bookmark); - m_pDS->next(); + std::string strSQL2 = + PrepareSQL("select c%02d, c%02d from episode where c%02d=%i order by c%02d, c%02d", + VIDEODB_ID_EPISODE_EPISODE, VIDEODB_ID_EPISODE_SEASON, + VIDEODB_ID_EPISODE_BOOKMARK, m_pDS->fv("idBookmark").get_asInt(), + VIDEODB_ID_EPISODE_SORTSEASON, VIDEODB_ID_EPISODE_SORTEPISODE); + m_pDS2->query(strSQL2); + bookmark.episodeNumber = m_pDS2->fv(0).get_asInt(); + bookmark.seasonNumber = m_pDS2->fv(1).get_asInt(); + m_pDS2->close(); } - //sort(bookmarks.begin(), bookmarks.end(), SortBookmarks); - m_pDS->close(); + bookmarks.emplace_back(bookmark); + m_pDS->next(); } + //sort(bookmarks.begin(), bookmarks.end(), SortBookmarks); + m_pDS->close(); } catch (...) { @@ -7509,7 +7499,7 @@ void CVideoDatabase::UpdateFanart(const CFileItem& item, VideoDbContentType type CDateTime CVideoDatabase::SetPlayCount(const CFileItem& item, int count, const CDateTime& date) { int id{-1}; - if (!URIUtils::IsBlurayPath(item.GetDynPath()) && item.HasProperty("original_listitem_url") && + if (item.HasProperty("original_listitem_url") && URIUtils::IsPlugin(item.GetProperty("original_listitem_url").asString())) { CFileItem item2(item); diff --git a/xbmc/video/VideoUtils.cpp b/xbmc/video/VideoUtils.cpp index 4eb67e08490..d206e271e25 100644 --- a/xbmc/video/VideoUtils.cpp +++ b/xbmc/video/VideoUtils.cpp @@ -27,6 +27,7 @@ #include "utils/FileUtils.h" #include "utils/StringUtils.h" #include "utils/URIUtils.h" +#include "utils/XBMCTinyXML2.h" #include "utils/log.h" #include "video/VideoDatabase.h" #include "video/VideoInfoTag.h" @@ -187,133 +188,88 @@ bool IsAutoPlayNextItem(const std::string& content) return setting && CSettingUtils::FindIntInList(setting, settingValue); } -std::tuple GetStackResumeOffsetAndPartNumber(const CFileItem& item) +std::optional GetNextPartFromBookmark(const CBookmark& bookmark) { - int64_t offset{-1}; - unsigned int partNumber{0}; - if (item.IsStack()) + if (!bookmark.HasSavedPlayerState()) + return std::nullopt; + + CXBMCTinyXML2 xmlDoc; + if (!xmlDoc.Parse(bookmark.playerState)) + return std::nullopt; + + tinyxml2::XMLHandle hRoot(xmlDoc.RootElement()); + if (!hRoot.ToElement() || !StringUtils::EqualsNoCase(hRoot.ToElement()->Value(), "nextpart")) + return std::nullopt; + + return std::stoi(hRoot.ToElement()->GetText()); +} + +namespace +{ +bool GetStackTimes(const std::string& path, std::vector& times) +{ + CVideoDatabase db; + if (!db.Open()) + { + CLog::LogF(LOGERROR, "Cannot open VideoDatabase"); + return false; + } + return db.GetStackTimes(path, times); +} +} // namespace + +std::optional> GetStackResumeOffsetAndPartNumber( + const CFileItem& item) +{ + if (item.IsStack() && item.HasVideoInfoTag()) { const std::string& path{item.GetDynPath()}; - if (URIUtils::IsDiscImageStack(path)) + const CBookmark bookmark{item.GetVideoInfoTag()->GetResumePoint()}; + if (bookmark.IsPartWay()) { - // disc image stacks - every part can have its own resume point - CVideoDatabase db; - if (!db.Open()) + if (std::optional nextPart{GetNextPartFromBookmark(bookmark)}; nextPart) + return std::make_optional(std::make_tuple(0, *nextPart + 1)); + + int64_t offset{CUtil::ConvertSecsToMilliSecs(bookmark.timeInSeconds)}; + unsigned int partNumber{1}; + std::vector times; + if (GetStackTimes(path, times)) { - CLog::LogF(LOGERROR, "Cannot open VideoDatabase"); - return {}; - } - - CBookmark bookmark; - if (db.GetResumeBookMark(path, bookmark)) - { - offset = CUtil::ConvertSecsToMilliSecs(bookmark.timeInSeconds); - partNumber = static_cast(bookmark.partNumber); - } - } - else - { - // all other stacks - there is only one resume point for the whole stack - if (item.HasVideoInfoTag()) - { - const CBookmark bookmark{item.GetVideoInfoTag()->GetResumePoint()}; - if (bookmark.IsPartWay()) - { - offset = CUtil::ConvertSecsToMilliSecs(bookmark.timeInSeconds); - - //! @todo Should the part number be set when loading/setting tags's bookmark from db, - //! like done for disc image stacks? - //partNumber = static_cast(bookmark.partNumber); - - CVideoDatabase db; - if (!db.Open()) - { - CLog::LogF(LOGERROR, "Cannot open VideoDatabase"); - return {}; - } - - partNumber = 1; - std::vector times; - if (db.GetStackTimes(path, times)) - { - auto index{std::ranges::distance( - std::ranges::find_if(times | std::views::reverse, [offset](auto t) - { return t <= std::chrono::milliseconds(offset); }), - times.rend())}; - if (index < static_cast(times.size())) - partNumber = static_cast(index + 1); - } - } + // Look backwards through the parts to find the part we are in + const auto index{std::ranges::distance( + std::ranges::find_if(times | std::views::reverse, [offset](auto t) + { return t <= std::chrono::milliseconds(offset); }), + times.rend())}; + if (index >= 0 && index < static_cast(times.size())) + partNumber = static_cast(index + 1); } + return std::make_optional(std::make_tuple(offset, partNumber)); } } - return {offset, partNumber}; + return std::nullopt; } int64_t GetStackPartResumeOffset(const CFileItem& item, unsigned int partNumber) { int64_t offset{-1}; - if (item.IsStack() && partNumber > 0) + if (item.IsStack() && item.HasVideoInfoTag() && partNumber > 0) { const std::string& path{item.GetDynPath()}; - if (URIUtils::IsDiscImageStack(path)) + const CBookmark bookmark{item.GetVideoInfoTag()->GetResumePoint()}; + if (bookmark.IsPartWay()) { - // disc image stacks - every part can have its own resume point - CVideoDatabase db; - if (!db.Open()) + std::vector times; + if (GetStackTimes(path, times)) { - CLog::LogF(LOGERROR, "Cannot open VideoDatabase"); - return offset; - } - - std::vector bookmarks; - db.GetBookMarksForFile(path, bookmarks, CBookmark::RESUME); - for (const auto& bookmark : bookmarks) - { - if (bookmark.partNumber == static_cast(partNumber)) + offset = 0; + const int64_t offsetToCheck{CUtil::ConvertSecsToMilliSecs(bookmark.timeInSeconds)}; + const uint64_t partBegin{ + partNumber == 1 ? 0 : static_cast(times[partNumber - 2].count())}; + const uint64_t partEnd{static_cast(times[partNumber - 1].count())}; + if (static_cast(offsetToCheck) <= partEnd && + static_cast(offsetToCheck) > partBegin) { - offset = CUtil::ConvertSecsToMilliSecs(bookmark.timeInSeconds); - break; - } - } - } - else - { - // all other stacks - there is only one resume point for the whole stack - if (item.HasVideoInfoTag()) - { - const CBookmark bookmark{item.GetVideoInfoTag()->GetResumePoint()}; - if (bookmark.IsPartWay()) - { - //! @todo Should the part number be set when loading/setting tags's bookmark from db, - //! like done for disc image stacks? - //if (bookmark.partNumber == static_cast(partNumber)) - // offset = CUtil::ConvertSecsToMilliSecs(bookmark.timeInSeconds); - //else - // offset = 0; - - CVideoDatabase db; - if (!db.Open()) - { - CLog::LogF(LOGERROR, "Cannot open VideoDatabase"); - return offset; - } - - offset = 0; - - std::vector times; - if (db.GetStackTimes(path, times)) - { - const int64_t offsetToCheck{CUtil::ConvertSecsToMilliSecs(bookmark.timeInSeconds)}; - const uint64_t partBegin{ - partNumber == 1 ? 0 : static_cast(times[partNumber - 2].count())}; - const uint64_t partEnd{static_cast(times[partNumber - 1].count())}; - if (static_cast(offsetToCheck) <= partEnd && - static_cast(offsetToCheck) > partBegin) - { - offset = offsetToCheck; - } - } + offset = offsetToCheck; } } } @@ -327,31 +283,13 @@ int64_t GetStackPartStartOffset(const CFileItem& item, unsigned int partNumber) if (item.IsStack() && partNumber > 0) { const std::string& path{item.GetDynPath()}; - if (URIUtils::IsDiscImageStack(path)) - { - // disc image stacks - every part starts at offset 0, correct part is selected via part naumber + if (partNumber == 1) offset = 0; - } else { - // all other stacks - start offset for a part is relative to beginning of stack - if (partNumber == 1) - { - offset = 0; - } - else - { - CVideoDatabase db; - if (!db.Open()) - { - CLog::LogF(LOGERROR, "Cannot open VideoDatabase"); - return {}; - } - - std::vector times; - if (db.GetStackTimes(path, times) && partNumber <= times.size()) - offset = times[partNumber - 2].count(); - } + std::vector times; + if (GetStackTimes(path, times) && partNumber <= times.size()) + offset = times[partNumber - 2].count(); } } return offset; diff --git a/xbmc/video/VideoUtils.h b/xbmc/video/VideoUtils.h index 3cae223f780..9d5b6fa54ce 100644 --- a/xbmc/video/VideoUtils.h +++ b/xbmc/video/VideoUtils.h @@ -8,8 +8,11 @@ #pragma once +#include "video/Bookmark.h" + #include #include +#include #include #include @@ -45,12 +48,19 @@ bool IsAutoPlayNextItem(const CFileItem& item); */ bool IsAutoPlayNextItem(const std::string& content); +/*! \brief Parses a playerState string from a bookmark and returns the next stack part number if available. + \param bookmark The bookmark to parse + \return std::nullopt if no nextpart tag, or the next part number if available. + */ +std::optional GetNextPartFromBookmark(const CBookmark& bookmark); + /*! \brief Get the resume offset and part number for the given stack item. - \param item The stack item to retrieve the offset for - \return The offset or -1 if not found and the part number or 0 if not available + \param item The stack item to retrieve the offset for. + \return std::nullopt if nothing found, or the part number and offset. */ -std::tuple GetStackResumeOffsetAndPartNumber(const CFileItem& item); +std::optional> GetStackResumeOffsetAndPartNumber( + const CFileItem& item); /*! \brief Get the resume offset for a part of a stack item. diff --git a/xbmc/video/guilib/VideoGUIUtils.cpp b/xbmc/video/guilib/VideoGUIUtils.cpp index d9f528a22b3..f7382105a8a 100644 --- a/xbmc/video/guilib/VideoGUIUtils.cpp +++ b/xbmc/video/guilib/VideoGUIUtils.cpp @@ -27,11 +27,9 @@ #include "music/MusicFileItemClassify.h" #include "network/NetworkFileItemClassify.h" #include "playlists/PlayList.h" -#include "playlists/PlayListFactory.h" #include "playlists/PlayListFileItemClassify.h" #include "profiles/ProfileManager.h" #include "settings/MediaSettings.h" -#include "settings/SettingUtils.h" #include "settings/Settings.h" #include "settings/SettingsComponent.h" #include "threads/IRunnable.h" @@ -659,23 +657,29 @@ std::string GetResumeString(const CFileItem& item) std::string GetResumeString(int64_t startOffset, unsigned int partNumber) { + std::string resumeString; if (startOffset > 0) { - std::string resumeString{StringUtils::Format( - g_localizeStrings.Get(12022), - StringUtils::SecondsToTimeString(CUtil::ConvertMilliSecsToSecsInt(startOffset), - TIME_FORMAT_HH_MM_SS))}; - if (partNumber > 0) - { - const std::string partString{StringUtils::Format(g_localizeStrings.Get(23051), partNumber)}; - resumeString += " (" + partString + ")"; - } - return resumeString; + resumeString = + StringUtils::Format(g_localizeStrings.Get(12022), + StringUtils::SecondsToTimeString( + static_cast(CUtil::ConvertMilliSecsToSecsInt(startOffset)), + TIME_FORMAT_HH_MM_SS)); // Resume from ##:##:## } else { - return g_localizeStrings.Get(13362); // Continue watching + if (partNumber > 0) + resumeString = g_localizeStrings.Get(12023); // Resume from + else + resumeString = g_localizeStrings.Get(13362); // Continue watching } + if (partNumber > 0) + { + const std::string partString{ + StringUtils::Format(g_localizeStrings.Get(23051), partNumber)}; // Part # + resumeString += startOffset > 0 ? " (" + partString + ")" : " " + partString; + } + return resumeString; } } // namespace KODI::VIDEO::UTILS diff --git a/xbmc/video/guilib/VideoPlayActionProcessor.cpp b/xbmc/video/guilib/VideoPlayActionProcessor.cpp index 8e92bb556e3..a880301fad1 100644 --- a/xbmc/video/guilib/VideoPlayActionProcessor.cpp +++ b/xbmc/video/guilib/VideoPlayActionProcessor.cpp @@ -105,9 +105,11 @@ Action CVideoPlayActionProcessor::ChoosePlayOrResume() const } else if (URIUtils::IsStack(GetItem()->GetDynPath())) { - const auto [offset, partNumber] = VIDEO::UTILS::GetStackResumeOffsetAndPartNumber(*GetItem()); - if (offset > 0) + if (const auto resume{UTILS::GetStackResumeOffsetAndPartNumber(*GetItem())}; resume) + { + const auto& [offset, partNumber] = *resume; return ChoosePlayOrResume(VIDEO::UTILS::GetResumeString(offset, partNumber)); + } } else { From b764a8f544ab440442b2da58b8bfa12265a9d689 Mon Sep 17 00:00:00 2001 From: 78andyp <99039295+78andyp@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:50:58 +0100 Subject: [PATCH 7/8] Support folder stacks in CApplication. --- xbmc/application/Application.cpp | 42 +++--- xbmc/application/Application.h | 5 +- .../ApplicationMessageHandling.cpp | 15 +- xbmc/application/ApplicationPlay.cpp | 135 +++++++----------- xbmc/application/ApplicationPlay.h | 15 +- 5 files changed, 97 insertions(+), 115 deletions(-) diff --git a/xbmc/application/Application.cpp b/xbmc/application/Application.cpp index 1be79a89e89..26875307557 100644 --- a/xbmc/application/Application.cpp +++ b/xbmc/application/Application.cpp @@ -55,7 +55,6 @@ #include "dialogs/GUIDialogKaiToast.h" #include "events/EventLog.h" #include "events/NotificationEvent.h" -#include "favourites/FavouritesService.h" #ifdef HAVE_LIBBLURAY #include "filesystem/BlurayDiscCache.h" #endif @@ -2001,7 +2000,10 @@ bool CApplication::PlayFile(CFileItem item, const std::string& player, bool bRes result == RESULT_ERROR) return false; else if (result == RESULT_NO_PLAYLIST_SELECTED) + { + m_cancelPlayback = true; return true; // Special case; not to be treated as error. + } // Special handling for disc stubs. //! @todo Shouldn't disc stubs also be handled via appPlayer->OpenFile()? @@ -2016,15 +2018,15 @@ bool CApplication::PlayFile(CFileItem item, const std::string& player, bool bRes // pushed some delay message into the threadmessage list, they are not // expected be processed after or during the new item playback starting. // so we clean up previous playing item's playback callback delay messages here. - static constexpr const std::array previousMsgsIgnoredByNewPlaying{GUI_MSG_PLAYBACK_STARTED, - GUI_MSG_PLAYBACK_ENDED, - GUI_MSG_PLAYBACK_STOPPED, - GUI_MSG_PLAYLIST_CHANGED, - GUI_MSG_PLAYLISTPLAYER_STOPPED, - GUI_MSG_PLAYLISTPLAYER_STARTED, - GUI_MSG_PLAYLISTPLAYER_CHANGED, - GUI_MSG_QUEUE_NEXT_ITEM, - 0}; + static constexpr std::array previousMsgsIgnoredByNewPlaying{GUI_MSG_PLAYBACK_STARTED, + GUI_MSG_PLAYBACK_ENDED, + GUI_MSG_PLAYBACK_STOPPED, + GUI_MSG_PLAYLIST_CHANGED, + GUI_MSG_PLAYLISTPLAYER_STOPPED, + GUI_MSG_PLAYLISTPLAYER_STARTED, + GUI_MSG_PLAYLISTPLAYER_CHANGED, + GUI_MSG_QUEUE_NEXT_ITEM, + 0}; if (const int dMsgCount{ CServiceBroker::GetGUI()->GetWindowManager().RemoveThreadMessageByMessageIds( &previousMsgsIgnoredByNewPlaying[0])}; @@ -2504,7 +2506,7 @@ double CApplication::GetTotalTime() const if (appPlayer->IsPlaying()) { - if (stackHelper->IsPlayingRegularStack()) + if (stackHelper->IsPlayingRegularStack() || stackHelper->IsPlayingResolvedDiscStack()) rc = static_cast(stackHelper->GetStackTotalTime().count()) / 1000.0; else rc = static_cast(appPlayer->GetTotalTime()) / 1000.0; @@ -2525,7 +2527,7 @@ double CApplication::GetTime() const if (appPlayer->IsPlaying()) { - if (stackHelper->IsPlayingRegularStack()) + if (stackHelper->IsPlayingRegularStack() || stackHelper->IsPlayingResolvedDiscStack()) { uint64_t startOfCurrentFile = static_cast(stackHelper->GetCurrentStackPartStartTime().count()); @@ -2553,7 +2555,7 @@ void CApplication::SeekTime( double dTime ) if (!appPlayer->CanSeek()) return; - if (stackHelper->IsPlayingRegularStack()) + if (stackHelper->IsPlayingRegularStack() || stackHelper->IsPlayingResolvedDiscStack()) { // find the item in the stack we are seeking to, and load the new // file if necessary, and calculate the correct seek within the new @@ -2563,12 +2565,16 @@ void CApplication::SeekTime( double dTime ) std::chrono::milliseconds(static_cast(dTime * 1000.0))); uint64_t startOfNewFile = stackHelper->GetStackPartStartTime(partNumberToPlay).count(); if (partNumberToPlay == stackHelper->GetCurrentPartNumber()) - appPlayer->SeekTime(static_cast(dTime * 1000.0) - startOfNewFile); + appPlayer->SeekTime(static_cast(dTime * 1000.0) - + static_cast(startOfNewFile)); else - { // seeking to a new file + { + // seeking to a new file stackHelper->SetStackPartAsCurrent(partNumberToPlay); + stackHelper->SetSeekingParts(true); CFileItem* item = new CFileItem(stackHelper->GetCurrentStackPart()); - item->SetStartOffset(static_cast(dTime * 1000.0) - startOfNewFile); + item->SetStartOffset(static_cast(dTime * 1000.0) - + static_cast(startOfNewFile)); // don't just call "PlayFile" here, as we are quite likely called from the // player thread, so we won't be able to delete ourselves. CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, 1, 0, static_cast(item)); @@ -2595,7 +2601,7 @@ float CApplication::GetPercentage() const return (float)(GetTime() / tag.GetDuration() * 100); } - if (stackHelper->IsPlayingRegularStack()) + if (stackHelper->IsPlayingRegularStack() || stackHelper->IsPlayingResolvedDiscStack()) { double totalTime = GetTotalTime(); if (totalTime > 0.0) @@ -2639,7 +2645,7 @@ void CApplication::SeekPercentage(float percent) { if (!appPlayer->CanSeek()) return; - if (stackHelper->IsPlayingRegularStack()) + if (stackHelper->IsPlayingRegularStack() || stackHelper->IsPlayingResolvedDiscStack()) SeekTime(static_cast(percent) * 0.01 * GetTotalTime()); else appPlayer->SeekPercentage(percent); diff --git a/xbmc/application/Application.h b/xbmc/application/Application.h index 4a0bde63a6b..8f6d71c8608 100644 --- a/xbmc/application/Application.h +++ b/xbmc/application/Application.h @@ -168,6 +168,8 @@ public: bool ExecuteXBMCAction(std::string action, const std::shared_ptr& item = NULL); + bool WasPlaybackCancelled() const { return m_cancelPlayback; } + #ifdef HAS_OPTICAL_DRIVE std::unique_ptr m_Autorun; #endif @@ -219,14 +221,13 @@ protected: bool m_bInitializing = true; int m_nextPlaylistItem = -1; + bool m_cancelPlayback{false}; std::chrono::time_point m_lastRenderTime; bool m_skipGuiRender = false; std::unique_ptr m_musicInfoScanner; - bool PlayStack(CFileItem& item, bool bRestart); - std::unique_ptr m_pInertialScrollingHandler; std::vector> diff --git a/xbmc/application/ApplicationMessageHandling.cpp b/xbmc/application/ApplicationMessageHandling.cpp index d305e3638a1..97fff9f60f2 100644 --- a/xbmc/application/ApplicationMessageHandling.cpp +++ b/xbmc/application/ApplicationMessageHandling.cpp @@ -667,11 +667,18 @@ bool CApplicationMessageHandling::OnMessage(const CGUIMessage& message) m_app.m_playerEvent.Set(); if (const auto stackHelper{m_app.GetComponent()}; - stackHelper->IsPlayingRegularStack() && stackHelper->HasNextStackPartFileItem()) + stackHelper->IsPlayingStack() && stackHelper->HasNextStackPartFileItem()) { - // Just play the next item in the stack - m_app.PlayFile(stackHelper->SetNextStackPartAsCurrent(), "", true); - return true; + // If current stack part finished then play the next part + if (stackHelper->IsCurrentPartFinished()) + { + m_app.PlayFile(stackHelper->SetNextStackPartAsCurrent(), "", true); + if (!m_app.WasPlaybackCancelled()) + return true; + + // Selection of next part playlist cancelled so create bookmark for next part + stackHelper->SetNextPartBookmark(m_app.CurrentFileItem().GetDynPath()); + } } // For EPG playlist items we keep the player open to ensure continuous viewing experience. diff --git a/xbmc/application/ApplicationPlay.cpp b/xbmc/application/ApplicationPlay.cpp index ba51eb9dbef..30ef14a5c13 100644 --- a/xbmc/application/ApplicationPlay.cpp +++ b/xbmc/application/ApplicationPlay.cpp @@ -12,7 +12,6 @@ #include "FileItem.h" #include "PlayListPlayer.h" #include "ServiceBroker.h" -#include "ServiceManager.h" #include "Util.h" #include "cores/AudioEngine/Interfaces/AE.h" #include "cores/playercorefactory/PlayerCoreFactory.h" @@ -26,7 +25,6 @@ #include "settings/MediaSettings.h" #include "settings/Settings.h" #include "settings/SettingsComponent.h" -#include "storage/MediaManager.h" #include "utils/DiscsUtils.h" #include "utils/URIUtils.h" #include "utils/log.h" @@ -71,33 +69,6 @@ bool CApplicationPlay::ResolvePath() return true; } -bool CApplicationPlay::ResolveStack() -{ - if (!m_stackHelper.InitializeStack(m_item)) - return false; - - // Particularly inefficient on startup as, if times are not saved in the database, each video - // is opened in turn to determine its length. A faster calculation of video time would improve - // this substantially. - const auto startOffset{m_stackHelper.InitializeStackStartPartAndOffset(m_item)}; - if (!startOffset.has_value()) - { - CLog::LogF(LOGERROR, "Failed to obtain start offset for stack {}. Aborting playback.", - m_item.GetDynPath()); - return false; - } - const std::string savedPlayerState{m_item.GetProperty("savedplayerstate").asString("")}; - - // Replace stack:// FileItem with the individual stack part FileItem - m_item = m_stackHelper.GetCurrentStackPartFileItem(); - - m_item.SetStartOffset(startOffset.value()); - if (!savedPlayerState.empty()) - m_item.SetProperty("savedplayerstate", savedPlayerState); - - return true; -} - namespace { bool GetEpisodeBookmark(const CFileItem& item, CPlayerOptions& options, CVideoDatabase& db) @@ -118,7 +89,7 @@ bool GetEpisodeBookmark(const CFileItem& item, CPlayerOptions& options, CVideoDa } } // namespace -void CApplicationPlay::GetOptionsAndUpdateItem(bool restart) +void CApplicationPlay::GetOptionsAndUpdateItem() { if (m_item.HasProperty("StartPercent")) { @@ -127,10 +98,7 @@ void CApplicationPlay::GetOptionsAndUpdateItem(bool restart) } m_options.starttime = CUtil::ConvertMilliSecsToSecs(m_item.GetStartOffset()); - if (restart && m_item.HasVideoInfoTag()) - m_options.state = m_item.GetVideoInfoTag()->GetResumePoint().playerState; - - if (VIDEO::IsVideo(m_item) && (!restart || m_stackHelper.IsPlayingISOStack())) + if (VIDEO::IsVideo(m_item)) { if (m_item.HasProperty("savedplayerstate")) { @@ -196,6 +164,29 @@ void CApplicationPlay::GetOptionsAndUpdateItem(bool restart) } } +namespace +{ +bool IsSimpleMenuAllowed(const CFileItem& item, + const std::string& player, + const bool forceSelection) +{ + const CPlayerCoreFactory& playerCoreFactory{CServiceBroker::GetPlayerCoreFactory()}; + const std::string defaultPlayer{player.empty() ? playerCoreFactory.GetDefaultPlayer(item) + : player}; + + // No video selection when using external or remote players (they handle it if supported) + const bool isExternalPlayer{playerCoreFactory.IsExternalPlayer(defaultPlayer)}; + const bool isRemotePlayer{playerCoreFactory.IsRemotePlayer(defaultPlayer)}; + + // Check if simple menu is enabled or if we are forced to select a playlist + const bool isSimpleMenu{forceSelection || + CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CSettings::SETTING_DISC_PLAYBACK) == BD_PLAYBACK_SIMPLE_MENU}; + + return !isExternalPlayer && !isRemotePlayer && isSimpleMenu; +} +} // namespace + bool CApplicationPlay::GetPlaylistIfDisc() { // See if disc image is a Blu-ray (as an image could be a DVD as well) or if the path is a BDMV folder @@ -207,38 +198,17 @@ bool CApplicationPlay::GetPlaylistIfDisc() const bool forceBlurayPlaylistSelection{forceSelection && URIUtils::IsBlurayPath(m_item.GetDynPath())}; - if ((isBluray && !(m_options.startpercent > 0.0 || m_options.starttime > 0.0)) || - forceBlurayPlaylistSelection) + if (((isBluray && !(m_options.startpercent > 0.0 || m_options.starttime > 0.0)) || + forceBlurayPlaylistSelection) && + IsSimpleMenuAllowed(m_item, m_player, forceSelection)) { - const bool isSimpleMenuAllowed{ - [this, forceSelection]() - { - const CPlayerCoreFactory& playerCoreFactory{CServiceBroker::GetPlayerCoreFactory()}; - const std::string defaultPlayer{ - m_player.empty() ? playerCoreFactory.GetDefaultPlayer(m_item) : m_player}; + if (!CGUIDialogSimpleMenu::ShowPlaylistSelection(m_item)) + return false; - // No video selection when using external or remote players (they handle it if supported) - const bool isExternalPlayer{playerCoreFactory.IsExternalPlayer(defaultPlayer)}; - const bool isRemotePlayer{playerCoreFactory.IsRemotePlayer(defaultPlayer)}; - - // Check if simple menu is enabled or if we are forced to select a playlist - const bool isSimpleMenu{forceSelection || - CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( - CSettings::SETTING_DISC_PLAYBACK) == BD_PLAYBACK_SIMPLE_MENU}; - - return !isExternalPlayer && !isRemotePlayer && isSimpleMenu; - }()}; - - if (isSimpleMenuAllowed) - { - if (!CGUIDialogSimpleMenu::ShowPlaylistSelection(m_item)) - return false; - - // Reset any resume state as new playlist chosen - m_options.starttime = m_options.startpercent = 0.0; - m_options.state = {}; - m_item.ClearProperty("force_playlist_selection"); - } + // Reset any resume state as new playlist chosen + m_options.starttime = m_options.startpercent = 0.0; + m_options.state = {}; + m_item.ClearProperty("force_playlist_selection"); } return true; @@ -296,14 +266,6 @@ void CApplicationPlay::DetermineFullScreen() { m_options.fullscreen = ShouldGoFullScreen(VIDEO_PLAYLIST); } - else if (m_stackHelper.IsPlayingRegularStack()) - { - if (m_stackHelper.GetCurrentPartNumber() == 0 || - m_stackHelper.GetRegisteredStack(m_item)->GetStartOffset() != 0) - m_options.fullscreen = ShouldGoFullScreen(VIDEO); - else - m_options.fullscreen = false; - } else m_options.fullscreen = ShouldGoFullScreen(VIDEO); } @@ -314,13 +276,19 @@ CApplicationPlay::GatherPlaybackDetailsResult CApplicationPlay::GatherPlaybackDe m_item = item; m_player = std::move(player); - if (m_item.IsStack()) + // Deal with stacks + // This retrieves the individual stack part FileItem from the stack + // and also generates the PlayerOptions for the stack part. + bool isStack{item.IsStack()}; + if (isStack) { - if (!ResolveStack()) + if (!m_stackHelper.InitializeStack(item)) + { + CLog::LogF(LOGERROR, "Failed to initialise stack for {}. Aborting playback.", + item.GetDynPath()); return GatherPlaybackDetailsResult::RESULT_ERROR; - - m_player.clear(); - restart = true; + } + m_stackHelper.GetStackPartAndOptions(m_item, m_options, restart); } // Ensure the MIME type has been retrieved for http:// and shout:// streams @@ -336,11 +304,18 @@ CApplicationPlay::GatherPlaybackDetailsResult CApplicationPlay::GatherPlaybackDe if (!ResolvePath()) return GatherPlaybackDetailsResult::RESULT_ERROR; - m_options = {}; - GetOptionsAndUpdateItem(restart); + if (!isStack) + { + // May be set even if restarting (eg. moving between stack parts) + m_options = {}; + m_options.starttime = CUtil::ConvertMilliSecsToSecs(item.GetStartOffset()); + + GetOptionsAndUpdateItem(); + } if (!GetPlaylistIfDisc()) - return GatherPlaybackDetailsResult::RESULT_NO_PLAYLIST_SELECTED; + return GatherPlaybackDetailsResult:: + RESULT_NO_PLAYLIST_SELECTED; // Playlist needed but none selected (ie. user cancelled) so abort playback DetermineFullScreen(); diff --git a/xbmc/application/ApplicationPlay.h b/xbmc/application/ApplicationPlay.h index 77094d6a980..65e4343326e 100644 --- a/xbmc/application/ApplicationPlay.h +++ b/xbmc/application/ApplicationPlay.h @@ -11,6 +11,7 @@ #include "FileItem.h" #include "cores/IPlayer.h" +#include #include class CApplicationStackHelper; @@ -23,8 +24,9 @@ class CApplicationPlay { public: explicit CApplicationPlay(CApplicationStackHelper& stackHelper) : m_stackHelper(stackHelper) {} + CApplicationPlay() = delete; - enum class GatherPlaybackDetailsResult + enum class GatherPlaybackDetailsResult : uint8_t { RESULT_SUCCESS, // all details gathered RESULT_ERROR, // not all details gathered @@ -64,26 +66,17 @@ public: const CPlayerOptions& GetPlayerOptions() const { return m_options; } private: - CApplicationPlay() = delete; - /*! * \brief Resolves m_item's vfs dynpath to an actual file path. * \return true if resolved successfully, false otherwise */ bool ResolvePath(); - /*! - * \brief Extracts a specific playable part from m_item's path, if m_item has a stack:// path. - * \return true if resolved successfully, false otherwise - */ - bool ResolveStack(); - /*! * \brief Determines if there is a resume point for m_item, updates the player options accordingly. * Also resolves a removable media path if needed - * \param restart Set to true if playback should restart from beginning */ - void GetOptionsAndUpdateItem(bool restart); + void GetOptionsAndUpdateItem(); /*! * \brief If m_item is a bluray that has not been played before and simple menu is enabled, then From 496cd0f2e38fad642b56e1bbf3d1ed4ea6c2e554 Mon Sep 17 00:00:00 2001 From: 78andyp <99039295+78andyp@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:53:26 +0100 Subject: [PATCH 8/8] Support folder stacks in CApplicationPlayerCallback. --- .../application/ApplicationPlayerCallback.cpp | 191 ++++++++++++++---- 1 file changed, 156 insertions(+), 35 deletions(-) diff --git a/xbmc/application/ApplicationPlayerCallback.cpp b/xbmc/application/ApplicationPlayerCallback.cpp index 46036244947..30ad8e95443 100644 --- a/xbmc/application/ApplicationPlayerCallback.cpp +++ b/xbmc/application/ApplicationPlayerCallback.cpp @@ -11,7 +11,6 @@ #include "FileItem.h" #include "FileItemList.h" #include "GUIUserMessages.h" -#include "PlayListPlayer.h" #include "ServiceBroker.h" #include "URL.h" #include "application/ApplicationComponents.h" @@ -33,6 +32,7 @@ #include "settings/SettingsComponent.h" #include "storage/MediaManager.h" #include "utils/SaveFileStateJob.h" +#include "utils/StringUtils.h" #include "utils/URIUtils.h" #include "utils/log.h" #include "video/VideoDatabase.h" @@ -86,7 +86,15 @@ void CApplicationPlayerCallback::OnPlayBackStarted(const CFileItem& file) const auto stackHelper = components.GetComponent(); if (stackHelper->IsPlayingStack()) - itemCurrentFile = std::make_shared(*stackHelper->GetStack(file)); + { + if (const auto part{stackHelper->GetStack(file)}; part) + itemCurrentFile = std::make_shared(*part); + else + { + CLog::LogF(LOGERROR, "Stack part {} not found in stack", file.GetPath()); + return; + } + } else itemCurrentFile = std::make_shared(file); @@ -161,30 +169,6 @@ void UpdateRemovableBlurayPath(CFileItem& fileItem, bool updateStreamDetails) } } -void UpdateStackAndItem(const CFileItem& file, - CFileItem& fileItem, - CBookmark& bookmark, - const std::shared_ptr& stackHelper) -{ - if (stackHelper->GetStackTotalTime() > 0ms) - { - // Regular (not disc image) stack case: We have to save the bookmark on the stack. - fileItem = *stackHelper->GetStack(file); - - // The bookmark coming from the player is only relative to the current part, thus needs - // to be corrected with these attributes (start time will be 0 for non-stackparts). - bookmark.timeInSeconds += - static_cast(stackHelper->GetStackPartStartTime(file).count()) / 1000.0; - - const auto StackTotalTimeMs{stackHelper->GetStackTotalTime()}; - if (StackTotalTimeMs > 0ms) - bookmark.totalTimeInSeconds = static_cast(StackTotalTimeMs.count()) / 1000.0; - } - // Any stack case: We need to save the part number. - bookmark.partNumber = - stackHelper->GetStackPartNumber(file) + 1; // CBookmark part numbers are 1-based -} - bool WithinPercentOfEnd(const CBookmark& bookmark, float ignorePercentAtEnd) { return ignorePercentAtEnd > 0.0f && @@ -192,6 +176,146 @@ bool WithinPercentOfEnd(const CBookmark& bookmark, float ignorePercentAtEnd) (static_cast(ignorePercentAtEnd) * bookmark.totalTimeInSeconds / 100.0); } +void ConvertRelativeStackTimesToAbsolute( + CBookmark& bookmark, + const CFileItem& file, + const std::shared_ptr& stackHelper) +{ + // The bookmark from player is relative; needs to be corrected for absolute position within stack + bookmark.timeInSeconds += + static_cast(stackHelper->GetStackPartStartTime(file).count()) / 1000.0; + bookmark.totalTimeInSeconds = + static_cast(stackHelper->GetStackTotalTime().count()) / 1000.0; +} + +bool UpdateDiscStackBookmark(CBookmark& bookmark, + const CFileItem& file, + const std::shared_ptr& stackHelper) +{ + const std::shared_ptr advancedSettings{ + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()}; + + // Define finished as all parts have been played to end and overall stack has been played to within + // videoIgnorePercentAtEnd of the end (if defined) + const bool finished{ + [&] + { + if (!stackHelper->IsPlayingLastStackPart()) + return false; // Not finished if not playing last part + if (!stackHelper->IsSeekingParts() && + !file.GetProperty("stopped_before_end").asBoolean(false)) + return true; // For disc stacks, if not flagged then we have not stopped early (decision made in InputStream unless seeking cross-parts) + if (WithinPercentOfEnd(bookmark, advancedSettings->m_videoIgnorePercentAtEnd)) + return true; // Within videoIgnorePercentAtEnd of the end so consider watched + return false; + }()}; + + const bool currentPartFinished{!file.GetProperty("stopped_before_end").asBoolean(false)}; + const bool allStackPartsPlayed{stackHelper->IsPlayingLastStackPart()}; + const bool noMainTitle{file.GetProperty("no_main_title").asBoolean(false)}; + + bookmark.partNumber = stackHelper->GetStackPartNumber(file); + stackHelper->SetCurrentPartFinished(currentPartFinished); + if (currentPartFinished) + { + if (noMainTitle) + { + if (stackHelper->GetCurrentPartNumber() > 0) + { + // Not played main title of this part yet + bookmark.timeInSeconds = bookmark.partNumber; + bookmark.totalTimeInSeconds = stackHelper->GetTotalPartNumbers(); + bookmark.playerState = StringUtils::Format("{}", bookmark.partNumber); + } + else + { + // Not played anything yet + bookmark.timeInSeconds = bookmark.totalTimeInSeconds = 0; + return false; + } + } + else + { + if (finished) + { + // Finished entire stack + bookmark.timeInSeconds = -1.0; + } + else + { + // Ended in menu (or non-main title) with part(s) still to play + // Set the bookmark as a fraction + // (eg. played parts 1 and 2 of 4 = 2/4 - so the progress will show as 50% in the library) + bookmark.partNumber += 1; + bookmark.timeInSeconds = bookmark.partNumber; + bookmark.totalTimeInSeconds = stackHelper->GetTotalPartNumbers(); + bookmark.playerState = StringUtils::Format("{}", bookmark.partNumber); + } + } + } + else + { + // Not finished current part + if (allStackPartsPlayed && + WithinPercentOfEnd(bookmark, advancedSettings->m_videoIgnorePercentAtEnd)) + bookmark.timeInSeconds = -1.0; + else if (stackHelper->GetCurrentPartNumber() == 0 && + bookmark.timeInSeconds < advancedSettings->m_videoIgnoreSecondsAtStart) + bookmark.timeInSeconds = 0.0; + else + ConvertRelativeStackTimesToAbsolute(bookmark, file, stackHelper); + } + return true; +} + +void UpdateStackAndItem(const CFileItem& file, + CFileItem& fileItem, + CBookmark& bookmark, + const std::shared_ptr& stackHelper) +{ + // Get stack component (current fileItem refers to single part) + if (const auto part{stackHelper->GetStack(file)}; part) + fileItem = *part; + else + { + CLog::LogF(LOGERROR, "Stack part {} not found in stack", file.GetPath()); + return; + } + + if (stackHelper->WasPlayingDiscStack()) + { + stackHelper->UpdateDiscStackAndTimes(file); + + if (file.GetProperty("update_stream_details").asBoolean(false)) + { + fileItem.GetVideoInfoTag()->m_streamDetails = + file.GetVideoInfoTag()->m_streamDetails; // Update streamdetails + } + + const std::string oldStackPath{stackHelper->GetOldStackDynPath()}; + if (!oldStackPath.empty()) + { + fileItem.SetProperty("new_stack_path", true); + fileItem.SetProperty("old_stack_path", oldStackPath); + } + + // Also update video info tag with total time of the stack (as this is read for the library display) + fileItem.GetVideoInfoTag()->m_streamDetails.SetVideoDuration( + 0, static_cast( + std::chrono::duration_cast(stackHelper->GetStackTotalTime()) + .count())); + + // Update bookmark + if (!UpdateDiscStackBookmark(bookmark, file, stackHelper)) + fileItem.GetVideoInfoTag() + ->m_streamDetails.Reset(); // Don't save streamdetails as nothing played + } + else + { + ConvertRelativeStackTimesToAbsolute(bookmark, file, stackHelper); + } +} + bool UpdatePlayCount(const CFileItem& fileItem, const CBookmark& bookmark) { if (bookmark.timeInSeconds < 0.0) @@ -217,6 +341,9 @@ bool UpdatePlayCount(const CFileItem& fileItem, const CBookmark& bookmark) void CApplicationPlayerCallback::OnPlayerCloseFile(const CFileItem& file, const CBookmark& bookmarkParam) { + auto& components{CServiceBroker::GetAppComponents()}; + const auto stackHelper{components.GetComponent()}; + CFileItem fileItem{file}; CBookmark bookmark{bookmarkParam}; @@ -235,8 +362,7 @@ void CApplicationPlayerCallback::OnPlayerCloseFile(const CFileItem& file, #endif // Update the stack - const bool isStack{stackHelper->GetStack(file) != nullptr}; - if (isStack) + if (stackHelper->GetStack(file) != nullptr) UpdateStackAndItem(file, fileItem, bookmark, stackHelper); if (const std::shared_ptr advancedSettings{ @@ -249,13 +375,6 @@ void CApplicationPlayerCallback::OnPlayerCloseFile(const CFileItem& file, { bookmark.timeInSeconds = 0.0; // Not played enough to bookmark (bookmark cleared) } - else if (isStack) - { - // Bookmark will be saved, so update total time from stack - fileItem.GetVideoInfoTag()->m_streamDetails.SetVideoDuration( - 0, - static_cast(bookmark.totalTimeInSeconds)); // Update VideoInfoTag with total time - } if (CServiceBroker::GetSettingsComponent() ->GetProfileManager() @@ -263,6 +382,8 @@ void CApplicationPlayerCallback::OnPlayerCloseFile(const CFileItem& file, .canWriteDatabases()) { CSaveFileState::DoWork(fileItem, bookmark, UpdatePlayCount(fileItem, bookmark)); + + stackHelper->SetStackFileIds(fileItem.GetVideoInfoTag()->m_iFileId); } }