/*
* Copyright (C) 2013 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see .
*/
#include "MediaScanner.hpp"
#include
#include
#include
#include
#include "cover/CoverArtGrabber.hpp"
#include "database/Artist.hpp"
#include "database/Cluster.hpp"
#include "database/Release.hpp"
#include "database/ScanSettings.hpp"
#include "database/Track.hpp"
#include "utils/Logger.hpp"
#include "utils/Path.hpp"
#include "utils/Utils.hpp"
using namespace Database;
namespace {
Wt::WDate
getNextMonday(Wt::WDate current)
{
do
{
current = current.addDays(1);
} while (current.dayOfWeek() != 1);
return current;
}
Wt::WDate
getNextFirstOfMonth(Wt::WDate current)
{
do
{
current = current.addDays(1);
} while (current.day() != 1);
return current;
}
bool
isFileSupported(const boost::filesystem::path& file, const std::set& extensions)
{
return (extensions.find(file.extension()) != extensions.end());
}
bool
isPathInParentPath(const boost::filesystem::path& path, const boost::filesystem::path& parentPath)
{
boost::filesystem::path curPath = path;
while (curPath.has_parent_path())
{
curPath = curPath.parent_path();
if (curPath == parentPath)
return true;
}
return false;
}
Artist::pointer
getArtist(Wt::Dbo::Session& session, const std::string& name, const std::string& mbid)
{
Artist::pointer artist;
// First try to get by MBID
if (!mbid.empty())
{
artist = Artist::getByMBID(session, mbid);
if (!artist)
artist = Artist::create(session, name, mbid);
return artist;
}
// Fall back on artist name (collisions may occur)
if (!name.empty())
{
for (Artist::pointer sameNamedArtist : Artist::getByName(session, name))
{
if (sameNamedArtist->getMBID().empty())
{
artist = sameNamedArtist;
break;
}
}
// No Artist found with the same name and without MBID -> creating
if (!artist)
artist = Artist::create(session, name);
return artist;
}
return Artist::pointer();
}
Release::pointer
getRelease(Wt::Dbo::Session& session, const std::string& name, const std::string& mbid)
{
Release::pointer release;
// First try to get by MBID
if (!mbid.empty())
{
release = Release::getByMBID(session, mbid );
if (!release)
release = Release::create(session, name, mbid);
return release;
}
// Fall back on release name (collisions may occur)
if (!name.empty())
{
for (Release::pointer sameNamedRelease : Release::getByName(session, name))
{
if (sameNamedRelease->getMBID().empty())
{
release = sameNamedRelease;
break;
}
}
// No release found with the same name and without MBID -> creating
if (!release)
release = Release::create(session, name);
return release;
}
return Release::pointer();
}
std::vector
getClusters(Wt::Dbo::Session& session, const MetaData::Clusters& clustersNames)
{
std::vector< Cluster::pointer > clusters;
for (auto clusterNames : clustersNames)
{
auto clusterType = ClusterType::getByName(session, clusterNames.first);
if (!clusterType)
continue;
for (auto clusterName : clusterNames.second)
{
auto cluster = clusterType->getCluster(clusterName);
if (!cluster)
cluster = Cluster::create(session, clusterType, clusterName);
clusters.push_back(cluster);
}
}
return clusters;
}
} // namespace
namespace Scanner {
MediaScanner::MediaScanner(Wt::Dbo::SqlConnectionPool& connectionPool)
: _running(false),
_scheduleTimer(_ioService),
_db(connectionPool)
{
_ioService.setThreadCount(1);
refreshScanSettings();
}
void
MediaScanner::restart(void)
{
stop();
start();
}
void
MediaScanner::start(void)
{
_running = true;
// post some jobs in the io_service
scheduleScan();
_ioService.start();
}
void
MediaScanner::stop(void)
{
_running = false;
_scheduleTimer.cancel();
_ioService.stop();
}
void
MediaScanner::scheduleImmediateScan()
{
_ioService.post([=]()
{
LMS_LOG(DBUPDATER, INFO) << "Schedule immediate scan";
scheduleScan(std::chrono::seconds(0));
});
}
void
MediaScanner::reschedule()
{
_ioService.post([=]()
{
LMS_LOG(DBUPDATER, INFO) << "Rescheduling scan";
scheduleScan();
});
}
void
MediaScanner::scheduleScan()
{
refreshScanSettings();
Wt::WDateTime now = Wt::WLocalDateTime::currentServerDateTime().toUTC();
Wt::WDate nextScanDate;
switch (_updatePeriod)
{
case ScanSettings::UpdatePeriod::Daily:
if (now.time() < _startTime)
nextScanDate = now.date();
else
nextScanDate = now.date().addDays(1);
break;
case ScanSettings::UpdatePeriod::Weekly:
if (now.time() < _startTime && now.date().dayOfWeek() == 1)
nextScanDate = now.date();
else
nextScanDate = getNextMonday(now.date());
break;
case ScanSettings::UpdatePeriod::Monthly:
if (now.time() < _startTime && now.date().day() == 1)
nextScanDate = now.date();
else
nextScanDate = getNextFirstOfMonth(now.date());
break;
case ScanSettings::UpdatePeriod::Never:
LMS_LOG(DBUPDATER, INFO) << "Auto scan disabled!";
break;
}
if (nextScanDate.isValid())
scheduleScan( Wt::WDateTime(nextScanDate, _startTime).toTimePoint() );
}
void
MediaScanner::scheduleScan(std::chrono::seconds duration)
{
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan in " << duration.count() << " seconds";
_scheduleTimer.expires_from_now(duration);
_scheduleTimer.async_wait( std::bind( &MediaScanner::scan, this, std::placeholders::_1) );
}
void
MediaScanner::scheduleScan(std::chrono::system_clock::time_point timePoint)
{
std::time_t t = std::chrono::system_clock::to_time_t(timePoint);
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan at " << std::string(std::ctime(&t));
_scheduleTimer.expires_at(timePoint);
_scheduleTimer.async_wait(std::bind(&MediaScanner::scan, this, std::placeholders::_1));
}
void
MediaScanner::scan(boost::system::error_code err)
{
if (err)
return;
LMS_LOG(UI, INFO) << "New scan started!";
refreshScanSettings();
bool forceScan = false;
Stats stats;
removeMissingTracks(stats);
LMS_LOG(UI, INFO) << "Checks complete, force scan = " << forceScan;
LMS_LOG(DBUPDATER, INFO) << "scaning media directory '" << _mediaDirectory.string() << "'...";
scanMediaDirectory(_mediaDirectory, forceScan, stats);
LMS_LOG(DBUPDATER, INFO) << "scaning media directory '" << _mediaDirectory.string() << "' DONE";
if (_running)
{
removeOrphanEntries();
checkDuplicatedAudioFiles(stats);
}
LMS_LOG(DBUPDATER, INFO) << "Scan " << (_running ? "complete" : "aborted") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.scanErrors << ", not imported = " << stats.incompleteScans << "), duplicates = " << stats.nbDuplicates() << " (hash = " << stats.duplicateHashes << ", mbid = " << stats.duplicateMBID << ")";
// Save the last scan only if it has been completed
if (_running)
{
scheduleScan();
scanComplete().emit(stats);
}
}
void
MediaScanner::refreshScanSettings()
{
Wt::Dbo::Transaction transaction(_db.getSession());
auto scanSettings = ScanSettings::get(_db.getSession());
LMS_LOG(DBUPDATER, INFO) << "Using scan settings version " << scanSettings->getScanVersion();
_scanVersion = scanSettings->getScanVersion();
_startTime = scanSettings->getUpdateStartTime();
_updatePeriod = scanSettings->getUpdatePeriod();
_fileExtensions = scanSettings->getAudioFileExtensions();
_mediaDirectory = scanSettings->getMediaDirectory();
auto clusterTypes = scanSettings->getClusterTypes();
std::set clusterTypeNames;
std::transform(clusterTypes.begin(), clusterTypes.end(),
std::inserter(clusterTypeNames, clusterTypeNames.begin()),
[](ClusterType::pointer clusterType) -> std::string { return clusterType->getName(); });
_metadataParser.setClusterTypeNames(clusterTypeNames);
}
void
MediaScanner::scanAudioFile(const boost::filesystem::path& file, bool forceScan, Stats& stats)
{
auto lastWriteTime = Wt::WDateTime::fromTime_t(boost::filesystem::last_write_time(file));
if (!forceScan)
{
// Skip file if last write is the same
Wt::Dbo::Transaction transaction(_db.getSession());
Wt::Dbo::ptr