Commit 8eef7fe4 authored by Hugo Beauzée-Luyssen's avatar Hugo Beauzée-Luyssen

Allow folders and their content to be listed

Fix #42
parent f693046c
Pipeline #1074 passed with stages
in 11 minutes and 39 seconds
......@@ -23,6 +23,9 @@
#pragma once
#include <string>
#include "IQuery.h"
#include "IMedia.h"
#include "IMediaLibrary.h"
namespace medialibrary
{
......@@ -48,7 +51,49 @@ public:
* from being discovered.
*/
virtual bool isBanned() const = 0;
virtual uint32_t nbMedia() const = 0;
/**
* @brief media Returns the media contained by this folder.
* @param type The media type, or IMedia::Type::Unknown for all types
* @param params A query parameter instance, or nullptr for the default
* @return A Query object to be used to fetch the results
*
* This function will only return the media contained in the folder, not
* the media contained in subfolders.
* A media is considered to be in a directory when the main file representing
* it is part of the directory.
* For instance, in this file hierarchy:
* .
* ├── a
* │ ├── c
* │ │ └── NakedMoleRat.asf
* │ └── seaotter_themovie.srt
* └── b
* └── seaotter_themovie.mkv
* Media of 'a' would be empty (since the only file is a subtitle file and
* not the actual media, and NakedMoleRat.asf
* is in a subfolder)
* Media of 'c' would contain NakedMoleRat.asf
* Media of 'b' would contain seaotter_themovie.mkv
*/
virtual Query<IMedia> media( IMedia::Type type,
const QueryParameters* params ) const = 0;
/**
* @brief subfolders Returns the subfolders contained folder
* @return A query object to be used to fetch the results
*
* all of the folder subfolders, regardless of the folder content.
* For instance, in this hierarchy:
* ├── a
* │ └── w
* │ └── x
* a->subfolders() would return w; w->subfolders would return x, even though
* x is empty.
* This is done for optimization purposes, as keeping track of the entire
* folder hierarchy would be quite heavy.
* As an alternative, it is possible to use IMediaLibrary::folders to return
* a flattened list of all folders that contain media.
*/
virtual Query<IFolder> subfolders( const QueryParameters* params ) const = 0;
};
}
......@@ -65,6 +65,8 @@ enum class SortingCriteria
Album,
Filename,
TrackNumber,
// Valid for folders only. Default order is descending
NbMedia,
};
......@@ -387,6 +389,29 @@ class IMediaLibrary
*/
virtual bool setDiscoverNetworkEnabled( bool enable ) = 0;
virtual Query<IFolder> entryPoints() const = 0;
/**
* @brief folders Returns a flattened list of all folders containing at least a media
* @param params A query parameters object
* @return A query object to be used to fetch the results
*
* This is flattened, ie.
* ├── a
* │ └── w
* │ └── x
* │ └── y
* │ └── z
* │ └── DogMeme.avi
* ├── c
* │ └── NakedMoleRat.asf
*
* would return a query containing 'z' and 'c' as the other folders are
* not containing any media.
* In case a non flattened list is desired, the
* entryPoints() & IFolder::subFolders() function should be used.
*/
virtual Query<IFolder> folders( const QueryParameters* params = nullptr ) const = 0;
virtual Query<IFolder> searchFolders( const std::string& pattern,
const QueryParameters* params ) const = 0;
virtual FolderPtr folder( const std::string& mrl ) const = 0;
virtual void removeEntryPoint( const std::string& entryPoint ) = 0;
/**
......
......@@ -30,6 +30,7 @@
#include "Media.h"
#include "database/SqliteTools.h"
#include "database/SqliteQuery.h"
#include "medialibrary/filesystem/IDirectory.h"
#include "medialibrary/filesystem/IDevice.h"
#include "medialibrary/filesystem/IFileSystemFactory.h"
......@@ -54,7 +55,7 @@ Folder::Folder( MediaLibraryPtr ml, sqlite::Row& row )
, m_isBanned( row.load<decltype(m_isBanned)>( 4 ) )
, m_deviceId( row.load<decltype(m_deviceId)>( 5 ) )
, m_isRemovable( row.load<decltype(m_isRemovable)>( 6 ) )
, m_nbMedia( row.load<decltype(m_nbMedia)>( 7 ) )
// Skip nbMedia
{
}
......@@ -254,6 +255,45 @@ std::shared_ptr<Folder> Folder::fromMrl( MediaLibraryPtr ml, const std::string&
return folder;
}
std::string Folder::sortRequest( const QueryParameters* params )
{
auto sort = params != nullptr ? params->sort : SortingCriteria::Default;
auto desc = params != nullptr ? params->desc : false;
std::string req = "ORDER BY ";
switch ( sort )
{
case SortingCriteria::NbMedia:
req += "nb_media";
desc = !desc;
break;
default:
LOG_WARN( "Unsupported sorting criteria, falling back to Default (alpha)" );
/* fall-through */
case SortingCriteria::Default:
case SortingCriteria::Alpha:
req += "name";
}
if ( desc == true )
req += " DESC";
return req;
}
Query<IFolder> Folder::withMedia(MediaLibraryPtr ml , const QueryParameters* params)
{
static std::string req = "FROM " + Table::Name + " WHERE nb_media > 0";
return make_query<Folder, IFolder>( ml, "*", req, sortRequest( params ) );
}
Query<IFolder> Folder::searchWithMedia( MediaLibraryPtr ml, const std::string& pattern,
const QueryParameters* params )
{
std::string req = "FROM " + Table::Name + " f WHERE f.id_folder IN "
"(SELECT rowid FROM " + Table::Name + "Fts WHERE " +
Table::Name + "Fts MATCH '*' || ? || '*') "
"AND nb_media > 0";
return make_query<Folder, IFolder>( ml, "*", req, sortRequest( params ), pattern );
}
int64_t Folder::id() const
{
return m_id;
......@@ -380,9 +420,15 @@ bool Folder::isRootFolder() const
return m_parent == 0;
}
uint32_t Folder::nbMedia() const
Query<IMedia> Folder::media( IMedia::Type type, const QueryParameters* params ) const
{
return Media::fromFolderId( m_ml, type, m_id, params );
}
Query<IFolder> Folder::subfolders( const QueryParameters* params ) const
{
return m_nbMedia;
static const std::string req = "FROM " + Table::Name + " WHERE parent_id = ?";
return make_query<Folder, IFolder>( m_ml, "*", req, sortRequest( params ), m_id );
}
std::vector<std::shared_ptr<Folder>> Folder::fetchRootFolders( MediaLibraryPtr ml )
......
......@@ -64,6 +64,9 @@ public:
static std::shared_ptr<Folder> fromMrl(MediaLibraryPtr ml, const std::string& mrl );
static std::shared_ptr<Folder> bannedFolder(MediaLibraryPtr ml, const std::string& mrl );
static Query<IFolder> withMedia( MediaLibraryPtr ml, const QueryParameters* params );
static Query<IFolder> searchWithMedia( MediaLibraryPtr ml, const std::string& pattern,
const QueryParameters* params );
virtual int64_t id() const override;
virtual const std::string& mrl() const override;
......@@ -79,7 +82,9 @@ public:
virtual bool isPresent() const override;
virtual bool isBanned() const override;
bool isRootFolder() const;
virtual uint32_t nbMedia() const override;
virtual Query<IMedia> media( IMedia::Type type,
const QueryParameters* params ) const override;
virtual Query<IFolder> subfolders( const QueryParameters* params ) const override;
enum class BannedType
{
......@@ -90,6 +95,9 @@ public:
static std::shared_ptr<Folder> fromMrl( MediaLibraryPtr ml, const std::string& mrl, BannedType bannedType );
private:
static std::string sortRequest( const QueryParameters* params );
private:
MediaLibraryPtr m_ml;
......@@ -102,7 +110,6 @@ private:
const bool m_isBanned;
const int64_t m_deviceId;
const bool m_isRemovable;
uint32_t m_nbMedia;
mutable std::string m_deviceMountpoint;
mutable std::shared_ptr<Device> m_device;
......
......@@ -500,6 +500,9 @@ std::string Media::addRequestJoin( const QueryParameters* params, bool forceFile
case SortingCriteria::TrackNumber:
albumTrack = true;
break;
case SortingCriteria::NbMedia:
// Unrelated to media requests
break;
}
std::string req;
// Use "LEFT JOIN to allow for ordering different media type
......@@ -921,6 +924,26 @@ Query<IMedia> Media::fetchStreamHistory(MediaLibraryPtr ml)
IMedia::Type::Stream );
}
Query<IMedia> Media::fromFolderId( MediaLibraryPtr ml, IMedia::Type type,
int64_t folderId, const QueryParameters* params )
{
// This assumes the folder is present, as folders are not expected to be
// manipulated when the device is not present
std::string req = "FROM " + Table::Name + " m WHERE folder_id = ?";
if ( type != Type::Unknown )
{
req += " AND type = ?";
req += addRequestJoin( params, false, false );
return make_query<Media, IMedia>( ml, "*", req, sortRequest( params ),
folderId, type );
}
// Don't explicitely filter by type since only video/audio media have a
// non NULL folder_id
req += addRequestJoin( params, false, false );
return make_query<Media, IMedia>( ml, "*", req, sortRequest( params ),
folderId );
}
void Media::clearHistory( MediaLibraryPtr ml )
{
auto dbConn = ml->getConn();
......
......@@ -154,6 +154,9 @@ class Media : public IMedia, public DatabaseHelpers<Media>
int64_t playlistId, const QueryParameters* params );
static Query<IMedia> fetchHistory( MediaLibraryPtr ml );
static Query<IMedia> fetchStreamHistory( MediaLibraryPtr ml );
static Query<IMedia> fromFolderId( MediaLibraryPtr ml, Type type,
int64_t folderId,
const QueryParameters* params );
static void clearHistory( MediaLibraryPtr ml );
static void removeOldMedia( MediaLibraryPtr ml, std::chrono::seconds maxLifeTime );
......
......@@ -1380,6 +1380,19 @@ Query<IFolder> MediaLibrary::entryPoints() const
return make_query<Folder, IFolder>( this, "*", req, "" );
}
Query<IFolder> MediaLibrary::folders( const QueryParameters* params ) const
{
return Folder::withMedia( this, params );
}
Query<IFolder> MediaLibrary::searchFolders( const std::string& pattern,
const QueryParameters* params ) const
{
if ( validateSearchPattern( pattern ) == false )
return {};
return Folder::searchWithMedia( this, pattern, params );
}
FolderPtr MediaLibrary::folder( const std::string& mrl ) const
{
return Folder::fromMrl( this, mrl, Folder::BannedType::Any );
......
......@@ -143,6 +143,9 @@ class MediaLibrary : public IMediaLibrary, public IDeviceListerCb
virtual void discover( const std::string& entryPoint ) override;
virtual bool setDiscoverNetworkEnabled( bool enabled ) override;
virtual Query<IFolder> entryPoints() const override;
virtual Query<IFolder> folders( const QueryParameters* params = nullptr ) const override;
virtual Query<IFolder> searchFolders( const std::string& pattern,
const QueryParameters* params ) const override;
virtual FolderPtr folder( const std::string& mrl ) const override;
virtual void removeEntryPoint( const std::string& entryPoint ) override;
virtual void banFolder( const std::string& path ) override;
......
......@@ -460,8 +460,8 @@ TEST_F( Folders, NbMedia )
auto subFolder = ml->folder( 2 );
ASSERT_EQ( "file:///a/", root->mrl() );
ASSERT_EQ( "file:///a/folder/", subFolder->mrl() );
ASSERT_EQ( 2u, root->nbMedia() );
ASSERT_EQ( 1u, subFolder->nbMedia() );
ASSERT_EQ( 2u, root->media( IMedia::Type::Unknown, nullptr )->count() );
ASSERT_EQ( 1u, subFolder->media( IMedia::Type::Unknown, nullptr )->count() );
// Do not watch for live changes
ml.reset();
fsMock->removeFile( mock::FileSystemFactory::SubFolder + "subfile.mp4" );
......@@ -471,8 +471,94 @@ TEST_F( Folders, NbMedia )
root = ml->folder( 1 );
subFolder = ml->folder( 2 );
ASSERT_EQ( 2u, root->nbMedia() );
ASSERT_EQ( 0u, subFolder->nbMedia() );
ASSERT_EQ( 2u, root->media( IMedia::Type::Unknown, nullptr )->count() );
ASSERT_EQ( 0u, subFolder->media( IMedia::Type::Unknown, nullptr )->count() );
}
TEST_F( FoldersNoDiscover, ListWithMedia )
{
auto newFolder = mock::FileSystemFactory::Root + "empty/";
fsMock->addFolder( newFolder );
ml->discover( mock::FileSystemFactory::Root );
bool discovered = cbMock->waitDiscovery();
ASSERT_TRUE( discovered );
QueryParameters params{};
params.sort = SortingCriteria::NbMedia;
auto folders = ml->folders( &params )->all();
ASSERT_EQ( 2u, folders.size() );
ASSERT_EQ( folders[0]->mrl(), mock::FileSystemFactory::Root );
ASSERT_EQ( 2u, folders[0]->media( IMedia::Type::Unknown, nullptr )->count() );
ASSERT_EQ( folders[1]->mrl(), mock::FileSystemFactory::SubFolder );
ASSERT_EQ( 1u, folders[1]->media( IMedia::Type::Unknown, nullptr )->count() );
params.desc = true;
folders = ml->folders( &params )->all();
ASSERT_EQ( 2u, folders.size() );
ASSERT_EQ( folders[1]->mrl(), mock::FileSystemFactory::Root );
ASSERT_EQ( 2u, folders[1]->media( IMedia::Type::Unknown, nullptr )->count() );
ASSERT_EQ( folders[0]->mrl(), mock::FileSystemFactory::SubFolder );
ASSERT_EQ( 1u, folders[0]->media( IMedia::Type::Unknown, nullptr )->count() );
}
TEST_F( FoldersNoDiscover, ListSubFolders )
{
auto newFolder = mock::FileSystemFactory::Root + "empty/";
fsMock->addFolder( newFolder );
ml->discover( mock::FileSystemFactory::Root );
bool discovered = cbMock->waitDiscovery();
ASSERT_TRUE( discovered );
auto entryPoints = ml->entryPoints()->all();
ASSERT_EQ( 1u, entryPoints.size() );
auto root = entryPoints[0];
QueryParameters params{};
params.sort = SortingCriteria::NbMedia;
auto rootSubFolders = root->subfolders( &params )->all();
ASSERT_EQ( 2u, rootSubFolders.size() );
ASSERT_EQ( mock::FileSystemFactory::SubFolder, rootSubFolders[0]->mrl() );
auto sfMedia = rootSubFolders[0]->media( IMedia::Type::Unknown, nullptr )->all();
ASSERT_EQ( 1u, sfMedia.size() );
ASSERT_EQ( newFolder, rootSubFolders[1]->mrl() );
ASSERT_EQ( 0u, rootSubFolders[1]->media( IMedia::Type::Unknown, nullptr )->count() );
auto media = std::static_pointer_cast<Media>( sfMedia[0] );
media->setType( IMedia::Type::Video );
media->save();
// Check fetching by type now
ASSERT_EQ( 0u, rootSubFolders[0]->media( IMedia::Type::Audio, nullptr )->count() );
ASSERT_EQ( 1u, rootSubFolders[0]->media( IMedia::Type::Video, nullptr )->count() );
// Double check with a fetch all instead of counting
auto allMedia = rootSubFolders[0]->media( IMedia::Type::Video, nullptr )->all();
ASSERT_EQ( 1u, allMedia.size() );
ASSERT_EQ( media->id(), allMedia[0]->id() );
}
TEST_F( FoldersNoDiscover, SearchFolders )
{
// Add an empty folder matching the search pattern
auto newFolder = mock::FileSystemFactory::Root + "empty/folder/";
fsMock->addFolder( newFolder );
// Add a non empty sub folder also matching the pattern
auto newSubFolder = mock::FileSystemFactory::Root + "empty/folder/fold/";
fsMock->addFolder( newSubFolder );
fsMock->addFile( newSubFolder + "some file.avi" );
fsMock->addFile( newSubFolder + "some other file.avi" );
ml->discover( mock::FileSystemFactory::Root );
bool discovered = cbMock->waitDiscovery();
ASSERT_TRUE( discovered );
QueryParameters params{};
params.sort = SortingCriteria::NbMedia;
auto folders = ml->searchFolders( "fold", &params )->all();
ASSERT_EQ( 2u, folders.size() );
ASSERT_EQ( newSubFolder, folders[0]->mrl() );
ASSERT_EQ( mock::FileSystemFactory::SubFolder, folders[1]->mrl() );
}
TEST_F( FoldersNoDiscover, Name )
......
......@@ -218,7 +218,7 @@ TEST_F( DbModel, Upgrade13to14 )
auto folder = ml->folder( 1 );
ASSERT_NE( nullptr, folder );
ASSERT_EQ( 2u, folder->nbMedia() );
ASSERT_EQ( 2u, folder->media( IMedia::Type::Unknown, nullptr )->count() );
ASSERT_EQ( "folder", folder->name() );
CheckNbTriggers( 34 );
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment