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

Handle media refresh

Reparse them instead of deleting/re-adding them. This allows us to keep
any potential meta information set by the user on the reloaded media
parent ad9253fa
......@@ -322,6 +322,26 @@ std::shared_ptr<AlbumTrack> Album::addTrack( std::shared_ptr<Media> media, unsig
return track;
}
bool Album::removeTrack( Media& media, AlbumTrack& track )
{
m_duration -= media.duration();
m_nbTracks--;
auto genre = std::static_pointer_cast<Genre>( track.genre() );
if ( genre != nullptr )
genre->updateCachedNbTracks( -1 );
auto lock = m_tracks.lock();
if ( m_tracks.isCached() == true )
{
auto it = std::find_if( begin( m_tracks.get() ), end( m_tracks.get() ), [&media]( MediaPtr m ) {
return m->id() == media.id();
});
if ( it != end( m_tracks.get() ) )
m_tracks.get().erase( it );
}
return true;
}
unsigned int Album::nbTracks() const
{
return m_nbTracks;
......
......@@ -87,6 +87,7 @@ class Album : public IAlbum, public DatabaseHelpers<Album>
///
std::shared_ptr<AlbumTrack> addTrack( std::shared_ptr<Media> media, unsigned int trackNb,
unsigned int discNumber, int64_t artistId, Genre* genre );
bool removeTrack( Media& media, AlbumTrack& albumTrack );
unsigned int nbTracks() const override;
unsigned int duration() const override;
......
......@@ -526,6 +526,25 @@ void MediaLibrary::onDiscoveredFile( std::shared_ptr<fs::IFile> fileFs,
}
}
void MediaLibrary::onUpdatedFile( std::shared_ptr<File> file,
std::shared_ptr<fs::IFile> fileFs )
{
auto mrl = fileFs->mrl();
try
{
auto task = parser::Task::create( this, std::move( file ), std::move( fileFs ) );
if ( task != nullptr && m_parser != nullptr )
m_parser->parse( std::move( task ) );
}
catch( const sqlite::errors::ConstraintViolation& ex )
{
// Most likely the file is already scheduled and we restarted the
// discovery after a crash.
LOG_WARN( "Failed to insert ", mrl, ": ", ex.what(), ". "
"Assuming the file is already scheduled for discovery" );
}
}
bool MediaLibrary::deleteFolder( const Folder& folder )
{
LOG_INFO( "deleting folder ", folder.mrl() );
......
......@@ -46,6 +46,7 @@ class Device;
class Folder;
class Genre;
class Playlist;
class File;
namespace fs
{
......@@ -82,6 +83,8 @@ class MediaLibrary : public IMediaLibrary, public IDeviceListerCb
std::shared_ptr<Folder> parentFolder,
std::shared_ptr<fs::IDirectory> parentFolderFs,
std::pair<std::shared_ptr<Playlist>, unsigned int> parentPlaylist );
void onUpdatedFile( std::shared_ptr<File> file,
std::shared_ptr<fs::IFile> fileFs );
bool deleteFolder(const Folder& folder );
......
......@@ -314,7 +314,7 @@ void FsDiscoverer::checkFiles( std::shared_ptr<fs::IDirectory> parentFolderFs,
+ " WHERE folder_id = ?";
auto files = File::fetchAll<File>( m_ml, req, parentFolder->id() );
std::vector<std::shared_ptr<fs::IFile>> filesToAdd;
std::vector<std::shared_ptr<File>> filesToRemove;
std::vector<std::pair<std::shared_ptr<File>, std::shared_ptr<fs::IFile>>> filesToRefresh;
for ( const auto& fileFs: parentFolderFs->files() )
{
if ( m_probe->stopFileDiscovery() == true )
......@@ -330,28 +330,20 @@ void FsDiscoverer::checkFiles( std::shared_ptr<fs::IDirectory> parentFolderFs,
filesToAdd.push_back( fileFs );
continue;
}
if ( fileFs->lastModificationDate() == (*it)->lastModificationDate() )
if ( fileFs->lastModificationDate() != (*it)->lastModificationDate() )
{
// Unchanged file
files.erase( it );
continue;
LOG_INFO( "Forcing file refresh ", fileFs->mrl() );
filesToRefresh.emplace_back( std::move( *it ), fileFs );
}
auto& file = (*it);
LOG_INFO( "Forcing file refresh ", fileFs->mrl() );
// Pre-cache the file's media, since we need it to remove. However, better doing it
// out of a write context, since that way, other threads can also read the database.
file->media();
filesToRemove.push_back( std::move( file ) );
filesToAdd.push_back( fileFs );
files.erase( it );
}
if ( m_probe->deleteUnseenFiles() == false )
files.clear();
using FilesT = decltype( files );
using FilesToRemoveT = decltype( filesToRemove );
using FilesToRefreshT = decltype( filesToRefresh );
using FilesToAddT = decltype( filesToAdd );
sqlite::Tools::withRetries( 3, [this, &parentFolder, &parentFolderFs]
( FilesT files, FilesToAddT filesToAdd, FilesToRemoveT filesToRemove ) {
( FilesT files, FilesToAddT filesToAdd, FilesToRefreshT filesToRefresh ) {
auto t = m_ml->getConn()->newTransaction();
for ( const auto& file : files )
{
......@@ -367,29 +359,14 @@ void FsDiscoverer::checkFiles( std::shared_ptr<fs::IDirectory> parentFolderFs,
file->destroy();
}
}
for ( auto& f : filesToRemove )
{
if ( f->type() == IFile::Type::Playlist )
{
f->destroy(); // Trigger cascade: delete Playlist, and playlist/media relations
continue;
}
auto media = f->media();
if ( media != nullptr )
media->removeFile( *f );
else
{
// If there is no media associated with this file, the file had to be removed through
// a trigger
assert( f->isDeleted() );
}
}
for ( auto& p: filesToRefresh )
m_ml->onUpdatedFile( std::move( p.first ), std::move( p.second ) );
// Insert all files at once to avoid SQL write contention
for ( auto& p : filesToAdd )
m_ml->onDiscoveredFile( p, parentFolder, parentFolderFs, m_probe->getPlaylistParent() );
t->commit();
LOG_INFO( "Done checking files in ", parentFolderFs->mrl() );
}, std::move( files ), std::move( filesToAdd ), std::move( filesToRemove ) );
}, std::move( files ), std::move( filesToAdd ), std::move( filesToRefresh ) );
}
bool FsDiscoverer::addFolder( std::shared_ptr<fs::IDirectory> folder,
......
......@@ -36,6 +36,8 @@
#include "Media.h"
#include "Playlist.h"
#include "Show.h"
#include "ShowEpisode.h"
#include "Movie.h"
#include "utils/Directory.h"
#include "utils/Filename.h"
#include "utils/Url.h"
......@@ -93,6 +95,16 @@ int MetadataAnalyzer::toInt( IItem& item, IItem::Metadata meta )
Status MetadataAnalyzer::run( IItem& item )
{
if ( item.isRefresh() )
{
bool success;
bool needRescan;
std::tie( success, needRescan ) = refreshFile( item );
if ( success == false )
return Status::Fatal;
if ( needRescan == false )
return Status::Success;
}
int nbSubitem = item.nbSubItems();
// Assume that file containing subitem(s) is a Playlist
if ( nbSubitem > 0 )
......@@ -451,6 +463,96 @@ void MetadataAnalyzer::createTracks( Media& m, const std::vector<IItem::Track>&
}
}
std::tuple<bool, bool> MetadataAnalyzer::refreshFile( IItem& item ) const
{
assert( item.media() == nullptr );
assert( item.file() != nullptr );
auto file = std::static_pointer_cast<File>( item.file() );
auto media = file->media();
file->updateFsInfo( item.fileFs()->lastModificationDate(), item.fileFs()->size() );
if ( media->duration() != item.duration() )
media->setDuration( item.duration() );
auto newTitle = item.meta( IItem::Metadata::Title );
if ( media->title() != newTitle )
media->setTitleBuffered( newTitle );
auto tracks = item.tracks();
auto isAudio = std::find_if( begin( tracks ), end( tracks ), [](const Task::Item::Track& t) {
return t.type == Task::Item::Track::Type::Video;
}) == end( tracks );
if ( isAudio == true && media->type() == IMedia::Type::Video )
media->setType( IMedia::Type::Audio );
else if ( isAudio == false && media->type() == IMedia::Type::Audio )
media->setType( IMedia::Type::Video );
bool needRescan = false;
if ( media->subType() != IMedia::SubType::Unknown )
{
needRescan = true;
switch( media->subType() )
{
case IMedia::SubType::AlbumTrack:
{
auto albumTrack = std::static_pointer_cast<AlbumTrack>( media->albumTrack() );
if ( albumTrack == nullptr )
{
assert( false );
LOG_ERROR( "Can't fetch album track associated with media ", media->id() );
break;
}
auto album = std::static_pointer_cast<Album>( albumTrack->album() );
if ( album == nullptr )
{
assert( false );
LOG_ERROR( "Can't fetch album associated to album track ",
albumTrack->id(), "(media ", media->id(), ")" );
}
album->removeTrack( *media, *albumTrack );
AlbumTrack::destroy( m_ml, albumTrack->id() );
break;
}
case IMedia::SubType::Movie:
{
auto movie = media->movie();
if ( movie == nullptr )
{
assert( false );
LOG_ERROR( "Failed to fetch movie associated with media ", media->id() );
break;
}
Movie::destroy( m_ml, movie->id() );
break;
}
case IMedia::SubType::ShowEpisode:
{
auto episode = std::static_pointer_cast<ShowEpisode>( media->showEpisode() );
if ( episode == nullptr )
{
assert( false );
LOG_ERROR( "Failed to fetch show episode associated with media ", media->id() );
break;
}
ShowEpisode::destroy( m_ml, episode->id() );
break;
}
case IMedia::SubType::Unknown:
assert( !"Unreachable" );
break;
}
media->setSubType( IMedia::SubType::Unknown );
}
if ( media->save() == false )
return std::make_tuple( false, false );
item.setMedia( std::move( media ) );
return std::make_tuple( true, needRescan );
}
/* Audio files */
bool MetadataAnalyzer::parseAudioFile( IItem& item )
......
......@@ -55,6 +55,7 @@ protected:
bool parseVideoFile( IItem& task ) const;
std::tuple<Status, bool> createFileAndMedia( IItem& item ) const;
void createTracks( Media& m, const std::vector<IItem::Track>& tracks ) const;
std::tuple<bool, bool> refreshFile( IItem& item ) const;
std::pair<std::shared_ptr<Artist>, std::shared_ptr<Artist>> findOrCreateArtist( IItem& item ) const;
std::shared_ptr<AlbumTrack> handleTrack( std::shared_ptr<Album> album, IItem& item,
std::shared_ptr<Artist> artist, Genre* genre ) const;
......
......@@ -163,6 +163,8 @@ void Parser::done( std::shared_ptr<Task> t, Status status )
m_opToDo -= m_services.size() - serviceIdx;
}
updateStats();
if ( t->item().isRefresh() == true )
Task::destroy( m_ml, t->id() );
return;
}
......
......@@ -79,7 +79,17 @@ Task::Task( MediaLibraryPtr ml, std::shared_ptr<fs::IFile> fileFs,
, m_fileId( 0 )
, m_item( this, std::move( fileFs ), std::move( parentFolder ),
std::move( parentFolderFs ), std::move( parentPlaylist ),
parentPlaylistIndex )
parentPlaylistIndex, false )
{
}
Task::Task( MediaLibraryPtr ml, std::shared_ptr<File> file,
std::shared_ptr<fs::IFile> fileFs )
: currentService( 0 )
, m_ml( ml )
, m_step( Step::None )
, m_fileId( file->id() )
, m_item( this, std::move( file ), std::move( fileFs ) )
{
}
......@@ -164,7 +174,8 @@ Task::Item::Item( ITaskCb* taskCb, std::string mrl, unsigned int subitemPosition
Task::Item::Item( ITaskCb* taskCb, std::shared_ptr<fs::IFile> fileFs,
std::shared_ptr<Folder> parentFolder,
std::shared_ptr<fs::IDirectory> parentFolderFs,
std::shared_ptr<Playlist> parentPlaylist, unsigned int parentPlaylistIndex )
std::shared_ptr<Playlist> parentPlaylist, unsigned int parentPlaylistIndex,
bool isRefresh )
: m_taskCb( taskCb )
, m_mrl( fileFs->mrl() )
, m_duration( 0 )
......@@ -173,7 +184,17 @@ Task::Item::Item( ITaskCb* taskCb, std::shared_ptr<fs::IFile> fileFs,
, m_parentFolderFs( std::move( parentFolderFs ) )
, m_parentPlaylist( std::move( parentPlaylist ) )
, m_parentPlaylistIndex( parentPlaylistIndex )
, m_isRefresh( false )
, m_isRefresh( isRefresh )
{
}
Task::Item::Item( ITaskCb* taskCb, std::shared_ptr<File> file, std::shared_ptr<fs::IFile> fileFs )
: m_taskCb( taskCb )
, m_mrl( fileFs->mrl() )
, m_duration( 0 )
, m_file( std::move( file ) )
, m_fileFs( std::move( fileFs ) )
, m_isRefresh( true )
{
}
......@@ -405,7 +426,7 @@ bool Task::restoreLinkedEntities()
m_item = Item{ this, std::move( fileFs ), std::move( parentFolder ),
std::move( parentFolderFs ), std::move( parentPlaylist ),
m_item.parentPlaylistIndex() };
m_item.parentPlaylistIndex(), m_item.isRefresh() };
if ( file != nullptr )
{
m_item.setMedia( file->media() );
......@@ -481,6 +502,19 @@ Task::create( MediaLibraryPtr ml, std::shared_ptr<fs::IFile> fileFs,
return self;
}
std::shared_ptr<Task>
Task::create( MediaLibraryPtr ml, std::shared_ptr<File> file,
std::shared_ptr<fs::IFile> fileFs )
{
auto self = std::make_shared<Task>( ml, std::move( file ), std::move( fileFs ) );
const std::string req = "INSERT INTO " + Task::Table::Name +
"(mrl, file_id, is_refresh) VALUES(?, ?, ?)";
if ( insert( ml, self, req, self->m_item.mrl(), self->m_item.file()->id(),
true ) == false )
return nullptr;
return self;
}
void Task::recoverUnscannedFiles( MediaLibraryPtr ml )
{
static const std::string req = "INSERT INTO " + Task::Table::Name +
......
......@@ -93,7 +93,9 @@ public:
Item( ITaskCb* taskCb, std::string mrl, unsigned int subitemIndex, bool isRefresh );
Item( ITaskCb* taskCb, std::shared_ptr<fs::IFile> fileFs,
std::shared_ptr<Folder> folder, std::shared_ptr<fs::IDirectory> folderFs,
std::shared_ptr<Playlist> parentPlaylist, unsigned int parentPlaylistIndex );
std::shared_ptr<Playlist> parentPlaylist, unsigned int parentPlaylistIndex,
bool isRefresh );
Item( ITaskCb* taskCb, std::shared_ptr<File> file, std::shared_ptr<fs::IFile> fileFs );
virtual std::string meta( Metadata type ) const override;
......@@ -162,6 +164,8 @@ public:
std::shared_ptr<fs::IDirectory> parentFolderFs,
std::shared_ptr<Playlist> parentPlaylist,
unsigned int parentPlaylistIndex );
Task( MediaLibraryPtr ml, std::shared_ptr<File> file,
std::shared_ptr<fs::IFile> fileFs );
/*
* We need to decouple the current parser state and the saved one.
......@@ -204,6 +208,8 @@ public:
std::shared_ptr<fs::IDirectory> parentFolderFs,
std::pair<std::shared_ptr<Playlist>,
unsigned int> parentPlaylist );
static std::shared_ptr<Task> create( MediaLibraryPtr ml, std::shared_ptr<File> file,
std::shared_ptr<fs::IFile> fsFile );
static void recoverUnscannedFiles( MediaLibraryPtr ml );
private:
......
......@@ -249,9 +249,9 @@ TEST_F( Folders, UpdateFile )
f = ml->media( filePath );
ASSERT_NE( nullptr, f );
// The file is expected to be deleted and re-added since it changed, so the
// id should have changed
ASSERT_NE( id, f->id() );
// File won't be refreshed since unittests don't have parsers (and the file
// doesn't actually exist) but let's check it's not deleted/re-added anymore
ASSERT_EQ( id, f->id() );
}
TEST_F( FoldersNoDiscover, Blacklist )
......
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