Merge pull request #27048 from 78andyp/folderstacks

[Video] Folder Stacks
This commit is contained in:
78andyp 2025-12-11 14:39:21 +00:00 committed by GitHub
commit 8cb3418713
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 1901 additions and 1485 deletions

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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 <algorithm>
#include <map>
#include <ranges>
#include <vector>
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<CFileItem>& item, const std::string& playPath)
{
item->SetPath(playPath); // updated path (for DVD/Bluray files)
item->SetFolder(false);
}
void ConvertDiscFoldersToFiles(std::vector<std::shared_ptr<CFileItem>> items)
{
auto folderItems{items | std::views::filter([](const std::shared_ptr<CFileItem>& 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<CRegExp> folderRegExps =
CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_folderStackRegExps;
std::vector<CRegExp> 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<CRegExp> 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<CRegExp> 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<StackCandidate> 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<int> 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<CountedStackCandidate, int> countedCandidates;
for (const auto& s : stackCandidates)
++countedCandidates[{s.type, s.title}];
// Find stacks
std::vector<int> 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<int> 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)

View File

@ -16,6 +16,7 @@
#include "FileItem.h"
#include "threads/CriticalSection.h"
#include <compare>
#include <map>
#include <string>
#include <string_view>
@ -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<CFileItemPtr>& items);

View File

@ -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<CFileItem*>(pMsg->lpVoid);
g_application.PlayFile(*item, "", pMsg->param1 != 0);

View File

@ -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
@ -177,6 +176,7 @@
#endif
#include <array>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <memory>
@ -2000,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()?
@ -2015,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])};
@ -2484,8 +2487,8 @@ const CFileItem& CApplication::CurrentUnstackedItem()
{
const auto stackHelper = GetComponent<CApplicationStackHelper>();
if (stackHelper->IsPlayingISOStack() || stackHelper->IsPlayingRegularStack())
return stackHelper->GetCurrentStackPartFileItem();
if (stackHelper->IsPlayingStack())
return stackHelper->GetCurrentStackPart();
else
return *m_itemCurrentFile;
}
@ -2503,10 +2506,10 @@ double CApplication::GetTotalTime() const
if (appPlayer->IsPlaying())
{
if (stackHelper->IsPlayingRegularStack())
rc = stackHelper->GetStackTotalTimeMs() * 0.001;
if (stackHelper->IsPlayingRegularStack() || stackHelper->IsPlayingResolvedDiscStack())
rc = static_cast<double>(stackHelper->GetStackTotalTime().count()) / 1000.0;
else
rc = appPlayer->GetTotalTime() * 0.001;
rc = static_cast<double>(appPlayer->GetTotalTime()) / 1000.0;
}
return rc;
@ -2524,9 +2527,10 @@ double CApplication::GetTime() const
if (appPlayer->IsPlaying())
{
if (stackHelper->IsPlayingRegularStack())
if (stackHelper->IsPlayingRegularStack() || stackHelper->IsPlayingResolvedDiscStack())
{
uint64_t startOfCurrentFile = stackHelper->GetCurrentStackPartStartTimeMs();
uint64_t startOfCurrentFile =
static_cast<uint64_t>(stackHelper->GetCurrentStackPartStartTime().count());
rc = (startOfCurrentFile + appPlayer->GetTime()) * 0.001;
}
else
@ -2551,22 +2555,26 @@ 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
// file. Otherwise, just fall through to the usual routine if the
// time is higher than our total time.
int partNumberToPlay =
stackHelper->GetStackPartNumberAtTimeMs(static_cast<uint64_t>(dTime * 1000.0));
uint64_t startOfNewFile = stackHelper->GetStackPartStartTimeMs(partNumberToPlay);
int partNumberToPlay = stackHelper->GetStackPartNumberAtTime(
std::chrono::milliseconds(static_cast<int64_t>(dTime * 1000.0)));
uint64_t startOfNewFile = stackHelper->GetStackPartStartTime(partNumberToPlay).count();
if (partNumberToPlay == stackHelper->GetCurrentPartNumber())
appPlayer->SeekTime(static_cast<uint64_t>(dTime * 1000.0) - startOfNewFile);
appPlayer->SeekTime(static_cast<int64_t>(dTime * 1000.0) -
static_cast<int64_t>(startOfNewFile));
else
{ // seeking to a new file
stackHelper->SetStackPartCurrentFileItem(partNumberToPlay);
CFileItem* item = new CFileItem(stackHelper->GetCurrentStackPartFileItem());
item->SetStartOffset(static_cast<uint64_t>(dTime * 1000.0) - startOfNewFile);
{
// seeking to a new file
stackHelper->SetStackPartAsCurrent(partNumberToPlay);
stackHelper->SetSeekingParts(true);
CFileItem* item = new CFileItem(stackHelper->GetCurrentStackPart());
item->SetStartOffset(static_cast<int64_t>(dTime * 1000.0) -
static_cast<int64_t>(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<void*>(item));
@ -2593,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)
@ -2637,7 +2645,7 @@ void CApplication::SeekPercentage(float percent)
{
if (!appPlayer->CanSeek())
return;
if (stackHelper->IsPlayingRegularStack())
if (stackHelper->IsPlayingRegularStack() || stackHelper->IsPlayingResolvedDiscStack())
SeekTime(static_cast<double>(percent) * 0.01 * GetTotalTime());
else
appPlayer->SeekPercentage(percent);

View File

@ -168,6 +168,8 @@ public:
bool ExecuteXBMCAction(std::string action, const std::shared_ptr<CGUIListItem>& item = NULL);
bool WasPlaybackCancelled() const { return m_cancelPlayback; }
#ifdef HAS_OPTICAL_DRIVE
std::unique_ptr<MEDIA_DETECT::CAutorun> m_Autorun;
#endif
@ -219,14 +221,13 @@ protected:
bool m_bInitializing = true;
int m_nextPlaylistItem = -1;
bool m_cancelPlayback{false};
std::chrono::time_point<std::chrono::steady_clock> m_lastRenderTime;
bool m_skipGuiRender = false;
std::unique_ptr<MUSIC_INFO::CMusicInfoScanner> m_musicInfoScanner;
bool PlayStack(CFileItem& item, bool bRestart);
std::unique_ptr<CInertialScrollingHandler> m_pInertialScrollingHandler;
std::vector<std::shared_ptr<ADDON::CAddonInfo>>

View File

@ -665,12 +665,20 @@ bool CApplicationMessageHandling::OnMessage(const CGUIMessage& message)
m_app.CurrentFileItemPtr(), data);
m_app.m_playerEvent.Set();
if (const auto stackHelper{m_app.GetComponent<CApplicationStackHelper>()};
stackHelper->IsPlayingRegularStack() && stackHelper->HasNextStackPartFileItem())
stackHelper->IsPlayingStack() && stackHelper->HasNextStackPartFileItem())
{
// just play the next item in the stack
m_app.PlayFile(stackHelper->SetNextStackPartCurrentFileItem(), "", 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.

View File

@ -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();

View File

@ -11,6 +11,7 @@
#include "FileItem.h"
#include "cores/IPlayer.h"
#include <cstdint>
#include <string>
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

View File

@ -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,15 +32,18 @@
#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"
#include "video/VideoFileItemClassify.h"
#include "video/VideoInfoTag.h"
#include <chrono>
#include <memory>
using namespace KODI;
using namespace std::chrono_literals;
void CApplicationPlayerCallback::OnPlayBackEnded()
{
@ -83,8 +85,16 @@ void CApplicationPlayerCallback::OnPlayBackStarted(const CFileItem& file)
auto& components = CServiceBroker::GetAppComponents();
const auto stackHelper = components.GetComponent<CApplicationStackHelper>();
if (stackHelper->IsPlayingISOStack() || stackHelper->IsPlayingRegularStack())
itemCurrentFile = std::make_shared<CFileItem>(*stackHelper->GetRegisteredStack(file));
if (stackHelper->IsPlayingStack())
{
if (const auto part{stackHelper->GetStack(file)}; part)
itemCurrentFile = std::make_shared<CFileItem>(*part);
else
{
CLog::LogF(LOGERROR, "Stack part {} not found in stack", file.GetPath());
return;
}
}
else
itemCurrentFile = std::make_shared<CFileItem>(file);
@ -97,7 +107,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);
@ -159,30 +169,6 @@ void UpdateRemovableBlurayPath(CFileItem& fileItem, bool updateStreamDetails)
}
}
void UpdateStackAndItem(const CFileItem& file,
CFileItem& fileItem,
CBookmark& bookmark,
const std::shared_ptr<CApplicationStackHelper>& stackHelper)
{
if (stackHelper->GetRegisteredStackTotalTimeMs(fileItem) > 0)
{
// Regular (not disc image) stack case: We have to save the bookmark on the stack.
fileItem = *stackHelper->GetRegisteredStack(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<double>(stackHelper->GetRegisteredStackPartStartTimeMs(file)) / 1000.0;
const uint64_t registeredStackTotalTimeMs{stackHelper->GetRegisteredStackTotalTimeMs(file)};
if (registeredStackTotalTimeMs > 0)
bookmark.totalTimeInSeconds = static_cast<double>(registeredStackTotalTimeMs) / 1000.0;
}
// Any stack case: We need to save the part number.
bookmark.partNumber =
stackHelper->GetRegisteredStackPartNumber(file) + 1; // CBookmark part numbers are 1-based
}
bool WithinPercentOfEnd(const CBookmark& bookmark, float ignorePercentAtEnd)
{
return ignorePercentAtEnd > 0.0f &&
@ -190,6 +176,146 @@ bool WithinPercentOfEnd(const CBookmark& bookmark, float ignorePercentAtEnd)
(static_cast<double>(ignorePercentAtEnd) * bookmark.totalTimeInSeconds / 100.0);
}
void ConvertRelativeStackTimesToAbsolute(
CBookmark& bookmark,
const CFileItem& file,
const std::shared_ptr<CApplicationStackHelper>& stackHelper)
{
// The bookmark from player is relative; needs to be corrected for absolute position within stack
bookmark.timeInSeconds +=
static_cast<double>(stackHelper->GetStackPartStartTime(file).count()) / 1000.0;
bookmark.totalTimeInSeconds =
static_cast<double>(stackHelper->GetStackTotalTime().count()) / 1000.0;
}
bool UpdateDiscStackBookmark(CBookmark& bookmark,
const CFileItem& file,
const std::shared_ptr<CApplicationStackHelper>& stackHelper)
{
const std::shared_ptr<CAdvancedSettings> 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("<nextpart>{}</nextpart>", 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("<nextpart>{}</nextpart>", 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<CApplicationStackHelper>& 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<int>(
std::chrono::duration_cast<std::chrono::seconds>(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)
@ -215,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<CApplicationStackHelper>()};
CFileItem fileItem{file};
CBookmark bookmark{bookmarkParam};
@ -232,17 +361,9 @@ 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<CApplicationStackHelper>()};
std::unique_lock lock(stackHelper->m_critSection);
isStack = (stackHelper->GetRegisteredStack(file) != nullptr);
if (isStack)
UpdateStackAndItem(file, fileItem, bookmark, stackHelper);
}
// Update the stack
if (stackHelper->GetStack(file) != nullptr)
UpdateStackAndItem(file, fileItem, bookmark, stackHelper);
if (const std::shared_ptr<CAdvancedSettings> advancedSettings{
CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()};
@ -254,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<int>(bookmark.totalTimeInSeconds)); // Update VideoInfoTag with total time
}
if (CServiceBroker::GetSettingsComponent()
->GetProfileManager()
@ -268,6 +382,8 @@ void CApplicationPlayerCallback::OnPlayerCloseFile(const CFileItem& file,
.canWriteDatabases())
{
CSaveFileState::DoWork(fileItem, bookmark, UpdatePlayCount(fileItem, bookmark));
stackHelper->SetStackFileIds(fileItem.GetVideoInfoTag()->m_iFileId);
}
}

View File

@ -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 <algorithm>
#include <chrono>
#include <map>
#include <ranges>
#include <string>
#include <utility>
#include <vector>
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<CFileItem>(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<CFileItem>(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<std::chrono::milliseconds> 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<int>(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<int>(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<int64_t> 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<int> 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<uint64_t> 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<uint64_t>(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::milliseconds>(
std::chrono::duration<double>(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<double>(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<int>(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<std::chrono::milliseconds> times;
const bool haveTimes{db.GetStackTimes(path, times)};
// See if new part played
if (GetKnownStackParts() - 1 == static_cast<int>(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("<nextpart>{}</nextpart>", 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<int>(m_stackMap.size()) - 1;
}
uint64_t CApplicationStackHelper::GetStackPartEndTimeMs(int partNumber) const
bool CApplicationStackHelper::IsPlayingLastStackPart() const
{
return GetStackPartFileItem(partNumber).GetEndOffset();
return m_currentStackPosition == static_cast<int>(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<int>(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<int>(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<int>(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<const CFileItem> CApplicationStackHelper::GetStack(const CFileItem& item) const
{
m_stackmap.clear();
if (const auto part{GetStackPartInformation(item)}; part)
return part->stackItem;
return nullptr;
}
std::shared_ptr<const CFileItem> 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<CFileItem> 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<StackPartInformation>();
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<std::string> 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<int>(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::StackPartInformation> CApplicationStackHelper::
GetOrCreateStackPartInformation(const std::string& key)
{
if (!m_stackMap.contains(key))
m_stackMap[key] = std::make_shared<StackPartInformation>();
return m_stackMap[key];
}
std::shared_ptr<CApplicationStackHelper::StackPartInformation> 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<StackPartInformation>();
}
std::shared_ptr<CApplicationStackHelper::StackPartInformation> 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<StackPartInformation>();
}

View File

@ -8,28 +8,28 @@
#pragma once
#include "FileItemList.h"
#include "application/IApplicationComponent.h"
#include "threads/CriticalSection.h"
#include <chrono>
#include <map>
#include <memory>
#include <optional>
#include <string>
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<int64_t> 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<int>(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<const CFileItem> GetRegisteredStack(const CFileItem& item) const;
std::shared_ptr<const CFileItem> 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<CFileItem> 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<CFileItem> m_pStack;
std::shared_ptr<CFileItem> stackItem;
std::chrono::milliseconds startTime{std::chrono::milliseconds(0)};
int partNumber{0};
};
typedef std::shared_ptr<StackPartInformation> StackPartInformationPtr;
typedef std::map<std::string, StackPartInformationPtr> Stackmap;
Stackmap m_stackmap;
StackPartInformationPtr GetStackPartInformation(const std::string& key);
StackPartInformationPtr GetStackPartInformation(const std::string& key) const;
using StackMap = std::map<std::string, std::shared_ptr<StackPartInformation>, std::less<>>;
StackMap m_stackMap;
std::shared_ptr<StackPartInformation> GetOrCreateStackPartInformation(const std::string& key);
std::shared_ptr<StackPartInformation> GetStackPartInformation(const std::string& key) const;
std::shared_ptr<StackPartInformation> GetStackPartInformation(const CFileItem& item) const;
std::unique_ptr<CFileItemList> m_currentStack;
int m_currentStackPosition = 0;
bool m_currentStackIsDiscImageStack = false;
CFileItemList m_originalStackItems;
std::vector<std::string> 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};
};

View File

@ -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

View File

@ -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<CDVDInputStream> CDVDFactoryInputStream::CreateInputStream(IVide
{
return std::make_shared<CDVDInputStreamFFmpeg>(fileitem);
}
else if(StringUtils::StartsWithNoCase(file, "stack://"))
return std::make_shared<CDVDInputStreamStack>(fileitem);
CFileItem finalFileitem(fileitem);

View File

@ -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<CApplicationStackHelper>()};
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);
}
}
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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:

View File

@ -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 <limits.h>
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;
}

View File

@ -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 <memory>
#include <vector>
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<XFILE::CFile> TFile;
struct TSeg
{
TFile file;
int64_t length;
};
typedef std::vector<TSeg> 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;
};

View File

@ -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"
@ -1405,6 +1405,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

View File

@ -18,204 +18,246 @@
#include "utils/URIUtils.h"
#include "utils/log.h"
#include <stdlib.h>
#include <algorithm>
#include <array>
#include <functional>
#include <string>
#include <vector>
namespace XFILE
{
CStackDirectory::CStackDirectory() = default;
bool CStackDirectory::GetDirectory(const CURL& url, CFileItemList& items)
{
items.Clear();
std::vector<std::string> files;
const std::string pathToUrl(url.Get());
if (!GetPaths(pathToUrl, files))
return false; // error in path
CStackDirectory::~CStackDirectory() = default;
bool CStackDirectory::GetDirectory(const CURL& url, CFileItemList& items)
for (const std::string& i : files)
{
items.Clear();
std::vector<std::string> files;
const std::string pathToUrl(url.Get());
if (!GetPaths(pathToUrl, files))
return false; // error in path
for (const std::string& i : files)
{
CFileItemPtr item(new CFileItem(i));
item->SetPath(i);
item->SetFolder(false);
items.Add(item);
}
return true;
}
std::string CStackDirectory::GetStackedTitlePath(const std::string& strPath)
{
std::vector<CRegExp> RegExps =
CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoStackRegExps;
return GetStackedTitlePath(strPath, RegExps);
}
std::string CStackDirectory::GetStackedTitlePath(const std::string& strPath,
std::vector<CRegExp>& RegExps)
{
CStackDirectory stack;
CFileItemList files;
std::string strStackTitlePath,
strCommonDir = URIUtils::GetParentPath(strPath);
const CURL pathToUrl(strPath);
stack.GetDirectory(pathToUrl, files);
if (files.Size() > 1)
{
std::string strStackTitle;
std::string File1 = URIUtils::GetFileName(files[0]->GetPath());
std::string File2 = URIUtils::GetFileName(files[1]->GetPath());
// Check if source path uses URL encoding
if (URIUtils::HasEncodedFilename(CURL(strCommonDir)))
{
File1 = CURL::Decode(File1);
File2 = CURL::Decode(File2);
}
std::vector<CRegExp>::iterator itRegExp = RegExps.begin();
int offset = 0;
while (itRegExp != RegExps.end())
{
if (itRegExp->RegFind(File1, offset) != -1)
{
std::string Title1 = itRegExp->GetMatch(1),
Volume1 = itRegExp->GetMatch(2),
Ignore1 = itRegExp->GetMatch(3),
Extension1 = itRegExp->GetMatch(4);
if (offset)
Title1 = File1.substr(0, itRegExp->GetSubStart(2));
if (itRegExp->RegFind(File2, offset) != -1)
{
std::string Title2 = itRegExp->GetMatch(1),
Volume2 = itRegExp->GetMatch(2),
Ignore2 = itRegExp->GetMatch(3),
Extension2 = itRegExp->GetMatch(4);
if (offset)
Title2 = File2.substr(0, itRegExp->GetSubStart(2));
if (StringUtils::EqualsNoCase(Title1, Title2))
{
if (!StringUtils::EqualsNoCase(Volume1, Volume2))
{
if (StringUtils::EqualsNoCase(Ignore1, Ignore2) &&
StringUtils::EqualsNoCase(Extension1, Extension2))
{
// got it
strStackTitle = Title1 + Ignore1 + Extension1;
// Check if source path uses URL encoding
if (URIUtils::HasEncodedFilename(CURL(strCommonDir)))
strStackTitle = CURL::Encode(strStackTitle);
itRegExp = RegExps.end();
break;
}
else // Invalid stack
break;
}
else // Early match, retry with offset
{
offset = itRegExp->GetSubStart(3);
continue;
}
}
}
}
offset = 0;
++itRegExp;
}
if (!strCommonDir.empty() && !strStackTitle.empty())
strStackTitlePath = strCommonDir + strStackTitle;
}
return strStackTitlePath;
}
std::string CStackDirectory::GetFirstStackedFile(const std::string &strPath)
{
// the stacked files are always in volume order, so just get up to the first filename
// occurrence of " , "
std::string file, folder;
size_t pos = strPath.find(" , ");
if (pos != std::string::npos)
URIUtils::Split(strPath.substr(0, pos), folder, file);
else
URIUtils::Split(strPath, folder, file); // single filed stacks - should really not happen
// remove "stack://" from the folder
folder = folder.substr(8);
StringUtils::Replace(file, ",,", ",");
return URIUtils::AddFileToFolder(folder, file);
}
bool CStackDirectory::GetPaths(const std::string& strPath, std::vector<std::string>& vecPaths)
{
// format is:
// stack://file1 , file2 , file3 , file4
// filenames with commas are double escaped (ie replaced with ,,), thus the " , " separator used.
std::string path = strPath;
// remove stack:// from the beginning
path = path.substr(8);
vecPaths = StringUtils::Split(path, " , ");
if (vecPaths.empty())
return false;
// because " , " is used as a separator any "," in the real paths are double escaped
for (std::string& itPath : vecPaths)
StringUtils::Replace(itPath, ",,", ",");
return true;
}
std::string CStackDirectory::ConstructStackPath(const CFileItemList &items, const std::vector<int> &stack)
{
// no checks on the range of stack here.
// we replace all instances of comma's with double comma's, then separate
// the files using " , ".
std::string stackedPath = "stack://";
std::string folder, file;
URIUtils::Split(items[stack[0]]->GetPath(), folder, file);
stackedPath += folder;
// double escape any occurrence of commas
StringUtils::Replace(file, ",", ",,");
stackedPath += file;
for (unsigned int i = 1; i < stack.size(); ++i)
{
stackedPath += " , ";
file = items[stack[i]]->GetPath();
// double escape any occurrence of commas
StringUtils::Replace(file, ",", ",,");
stackedPath += file;
}
return stackedPath;
}
bool CStackDirectory::ConstructStackPath(const std::vector<std::string> &paths, std::string& stackedPath)
{
if (paths.size() < 2)
return false;
stackedPath = "stack://";
std::string folder, file;
URIUtils::Split(paths[0], folder, file);
stackedPath += folder;
// double escape any occurrence of commas
StringUtils::Replace(file, ",", ",,");
stackedPath += file;
for (unsigned int i = 1; i < paths.size(); ++i)
{
stackedPath += " , ";
file = paths[i];
// double escape any occurrence of commas
StringUtils::Replace(file, ",", ",,");
stackedPath += file;
}
return true;
auto item = std::make_shared<CFileItem>(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<CRegExp> 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<StackPart> 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<CRegExp> 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<std::string>& vecPaths)
{
// format is:
// stack://file1 , file2 , file3 , file4
// filenames with commas are double escaped (ie replaced with ,,), thus the " , " separator used.
std::string path = strPath;
// remove stack:// from the beginning
path = path.substr(8);
vecPaths = StringUtils::Split(path, " , ");
if (vecPaths.empty())
return false;
// because " , " is used as a separator any "," in the real paths are double escaped
for (std::string& itPath : vecPaths)
StringUtils::Replace(itPath, ",,", ",");
return true;
}
std::string CStackDirectory::ConstructStackPath(const CFileItemList& items,
const std::vector<int>& stack)
{
// no checks on the range of stack here.
// we replace all instances of comma's with double comma's, then separate
// the files using " , ".
std::string stackedPath = "stack://";
std::string folder, file;
URIUtils::Split(items[stack[0]]->GetPath(), folder, file);
stackedPath += folder;
// double escape any occurrence of commas
StringUtils::Replace(file, ",", ",,");
stackedPath += file;
for (unsigned int i = 1; i < stack.size(); ++i)
{
stackedPath += " , ";
file = items[stack[i]]->GetPath();
// double escape any occurrence of commas
StringUtils::Replace(file, ",", ",,");
stackedPath += file;
}
return stackedPath;
}
bool CStackDirectory::ConstructStackPath(const std::vector<std::string>& paths,
std::string& stackedPath,
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<std::string> 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

View File

@ -9,7 +9,6 @@
#pragma once
#include "IDirectory.h"
#include "utils/RegExp.h"
#include <string>
#include <vector>
@ -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<CRegExp>& 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<std::string>& vecPaths);
static std::string ConstructStackPath(const CFileItemList& items, const std::vector<int> &stack);
static bool ConstructStackPath(const std::vector<std::string> &paths, std::string &stackedPath);
/*!
\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<int>& 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<std::string>& 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);
};
}

View File

@ -272,10 +272,10 @@ void CPowerManager::StorePlayerState()
const auto stackHelper = components.GetComponent<CApplicationStackHelper>();
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,

View File

@ -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])(.*?)(\\.[^.]+)$",

View File

@ -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));
}

View File

@ -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"
@ -31,24 +30,31 @@
#include "video/VideoDatabase.h"
#include "video/VideoFileItemClassify.h"
#include <chrono>
using namespace KODI;
using namespace KODI::VIDEO;
using namespace std::chrono_literals;
void CSaveFileState::DoWork(CFileItem& item,
CBookmark& bookmark,
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
@ -206,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,
@ -236,14 +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<CApplicationStackHelper>();
if (stackHelper->HasRegisteredStack(item) &&
stackHelper->GetRegisteredStackTotalTimeMs(item) == 0)
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);
}

View File

@ -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<std::string> 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)

View File

@ -63,6 +63,8 @@
#include "video/VideoThumbLoader.h"
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <map>
#include <memory>
#include <ranges>
@ -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;
@ -1094,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());
}
@ -3826,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<int>(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<int>(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 (...)
{
@ -6192,7 +6185,8 @@ std::vector<std::string> 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<uint64_t> &times)
bool CVideoDatabase::GetStackTimes(const std::string& filePath,
std::vector<std::chrono::milliseconds>& times)
{
try
{
@ -6208,17 +6202,18 @@ bool CVideoDatabase::GetStackTimes(const std::string &filePath, std::vector<uint
m_pDS->query( strSQL );
if (m_pDS->num_rows() > 0)
{ // get the video settings info
uint64_t timeTotal = 0;
std::chrono::milliseconds timeTotal{0ms};
std::vector<std::string> timeString = StringUtils::Split(m_pDS->fv("times").get_asString(), ",");
times.clear();
for (const auto &i : timeString)
{
const auto partTime = static_cast<uint64_t>(atof(i.c_str()) * 1000.0);
times.emplace_back(partTime); // db stores in secs, convert to msecs
const std::chrono::milliseconds partTime{static_cast<int64_t>(
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 +6225,8 @@ bool CVideoDatabase::GetStackTimes(const std::string &filePath, std::vector<uint
}
/// \brief Sets the stack times for a particular video file
void CVideoDatabase::SetStackTimes(const std::string& filePath, const std::vector<uint64_t> &times)
void CVideoDatabase::SetStackTimes(const std::string& filePath,
const std::vector<std::chrono::milliseconds>& times)
{
try
{
@ -6246,9 +6242,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<float>(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()) );
}
@ -7502,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);

View File

@ -872,8 +872,9 @@ public:
*/
bool EraseAllForFile(const std::string& fileNameAndPath);
bool GetStackTimes(const std::string &filePath, std::vector<uint64_t> &times);
void SetStackTimes(const std::string &filePath, const std::vector<uint64_t> &times);
bool GetStackTimes(const std::string& filePath, std::vector<std::chrono::milliseconds>& times);
void SetStackTimes(const std::string& filePath,
const std::vector<std::chrono::milliseconds>& 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,

View File

@ -27,13 +27,16 @@
#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"
#include <algorithm>
#include <array>
#include <chrono>
#include <cstdint>
#include <ranges>
#include <vector>
namespace KODI::VIDEO::UTILS
@ -49,7 +52,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"));
@ -185,134 +188,88 @@ bool IsAutoPlayNextItem(const std::string& content)
return setting && CSettingUtils::FindIntInList(setting, settingValue);
}
std::tuple<int64_t, unsigned int> GetStackResumeOffsetAndPartNumber(const CFileItem& item)
std::optional<int> 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<std::chrono::milliseconds>& times)
{
CVideoDatabase db;
if (!db.Open())
{
CLog::LogF(LOGERROR, "Cannot open VideoDatabase");
return false;
}
return db.GetStackTimes(path, times);
}
} // namespace
std::optional<std::tuple<int64_t, unsigned int>> 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<int> 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<std::chrono::milliseconds> 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<unsigned int>(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<unsigned int>(bookmark.partNumber);
CVideoDatabase db;
if (!db.Open())
{
CLog::LogF(LOGERROR, "Cannot open VideoDatabase");
return {};
}
partNumber = 1;
std::vector<uint64_t> times;
if (db.GetStackTimes(path, times))
{
for (size_t i = times.size(); i > 0; i--)
{
if (times[i - 1] <= static_cast<uint64_t>(offset))
{
partNumber = static_cast<unsigned int>(i + 1);
break;
}
}
}
}
// 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<int>(times.size()))
partNumber = static_cast<unsigned int>(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<std::chrono::milliseconds> times;
if (GetStackTimes(path, times))
{
CLog::LogF(LOGERROR, "Cannot open VideoDatabase");
return offset;
}
std::vector<CBookmark> bookmarks;
db.GetBookMarksForFile(path, bookmarks, CBookmark::RESUME);
for (const auto& bookmark : bookmarks)
{
if (bookmark.partNumber == static_cast<long>(partNumber))
offset = 0;
const int64_t offsetToCheck{CUtil::ConvertSecsToMilliSecs(bookmark.timeInSeconds)};
const uint64_t partBegin{
partNumber == 1 ? 0 : static_cast<uint64_t>(times[partNumber - 2].count())};
const uint64_t partEnd{static_cast<uint64_t>(times[partNumber - 1].count())};
if (static_cast<uint64_t>(offsetToCheck) <= partEnd &&
static_cast<uint64_t>(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<long>(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<uint64_t> 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]};
if (static_cast<uint64_t>(offsetToCheck) <= partEnd &&
static_cast<uint64_t>(offsetToCheck) > partBegin)
{
offset = offsetToCheck;
}
}
offset = offsetToCheck;
}
}
}
@ -326,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<uint64_t> times;
if (db.GetStackTimes(path, times) && partNumber <= times.size())
offset = times[partNumber - 2];
}
std::vector<std::chrono::milliseconds> times;
if (GetStackTimes(path, times) && partNumber <= times.size())
offset = times[partNumber - 2].count();
}
}
return offset;

View File

@ -8,8 +8,11 @@
#pragma once
#include "video/Bookmark.h"
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <tuple>
@ -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<int> 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<int64_t, unsigned int> GetStackResumeOffsetAndPartNumber(const CFileItem& item);
std::optional<std::tuple<int64_t, unsigned int>> GetStackResumeOffsetAndPartNumber(
const CFileItem& item);
/*!
\brief Get the resume offset for a part of a stack item.

View File

@ -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<long>(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

View File

@ -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
{

View File

@ -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);
}

View File

@ -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 <string>
@ -17,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";
@ -41,7 +47,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 +65,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 +83,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 +119,158 @@ 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<CFileItem> item{items.Get(0)};
EXPECT_EQ(item->IsStack(), true);
EXPECT_EQ(item->IsFolder(), false);
// check bluray/dvd paths
std::vector<std::string> 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);
}
}
TEST_F(TestStacks, TestConstructStackPath)
{
CFileItemList items;
CFileItem item;
item.SetPath("smb://somepath/movie_part_1.mkv");
items.Add(std::make_shared<CFileItem>(item));
CFileItem item2;
item2.SetPath("smb://somepath/movie_part_2.mkv");
items.Add(std::make_shared<CFileItem>(item2));
std::vector<int> 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<std::string> 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<TestStackData>
{
};
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<TestStackData>
{
};
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));