mirror of https://github.com/xbmc/xbmc
Merge pull request #27048 from 78andyp/folderstacks
[Video] Folder Stacks
This commit is contained in:
commit
8cb3418713
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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])(.*?)(\\.[^.]+)$",
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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> ×)
|
||||
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> ×)
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -872,8 +872,9 @@ public:
|
|||
*/
|
||||
bool EraseAllForFile(const std::string& fileNameAndPath);
|
||||
|
||||
bool GetStackTimes(const std::string &filePath, std::vector<uint64_t> ×);
|
||||
void SetStackTimes(const std::string &filePath, const std::vector<uint64_t> ×);
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
BIN
xbmc/video/test/testdata/moviestack_blurayiso/Movie_(2001)/Movie_(2001)_part1.iso
vendored
Normal file
BIN
xbmc/video/test/testdata/moviestack_blurayiso/Movie_(2001)/Movie_(2001)_part1.iso
vendored
Normal file
Binary file not shown.
BIN
xbmc/video/test/testdata/moviestack_blurayiso/Movie_(2001)/Movie_(2001)_part2.iso
vendored
Normal file
BIN
xbmc/video/test/testdata/moviestack_blurayiso/Movie_(2001)/Movie_(2001)_part2.iso
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue