You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
275 lines
7.7 KiB
C++
275 lines
7.7 KiB
C++
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
// Copyright The Music Player Daemon Project
|
|
// mpd-dbcreate "Hot diggity daffodil!"
|
|
// Jay Moore - dewdude@pickmy.org
|
|
|
|
#include "config.h"
|
|
#include "Instance.hxx"
|
|
#include "Main.hxx"
|
|
#include "config/Data.hxx"
|
|
#include "config/Param.hxx"
|
|
#include "config/Block.hxx"
|
|
#include "decoder/DecoderList.hxx"
|
|
#include "playlist/PlaylistRegistry.hxx"
|
|
#include "fs/AllocatedPath.hxx"
|
|
#include "lib/icu/Init.hxx"
|
|
#include "lib/fmt/RuntimeError.hxx"
|
|
#include "Log.hxx"
|
|
#include "LogInit.hxx"
|
|
#include "tag/Config.hxx"
|
|
#include "db/Configured.hxx"
|
|
#include "db/plugins/simple/SimpleDatabasePlugin.hxx"
|
|
#include "db/update/Service.hxx"
|
|
#include "storage/Configured.hxx"
|
|
#include "storage/CompositeStorage.hxx"
|
|
#include "input/Init.hxx"
|
|
#include "util/UriExtract.hxx"
|
|
|
|
#ifdef ENABLE_ARCHIVE
|
|
#include "archive/ArchiveList.hxx"
|
|
#endif
|
|
|
|
#include <iostream>
|
|
#include <memory>
|
|
#include <thread>
|
|
#include <chrono>
|
|
#include "event/CoarseTimerEvent.hxx"
|
|
#include "util/BindMethod.hxx"
|
|
|
|
// Channel mode shared with FilteredSongUpdate
|
|
enum class ChannelMode {
|
|
STEREO,
|
|
MULTICHANNEL,
|
|
ALL
|
|
};
|
|
|
|
// Helper class to check update completion with a timer
|
|
class UpdateChecker {
|
|
Instance &instance;
|
|
CoarseTimerEvent timer;
|
|
bool verbose;
|
|
int progress_counter = 0;
|
|
|
|
public:
|
|
UpdateChecker(Instance &_instance, bool _verbose)
|
|
: instance(_instance),
|
|
timer(instance.event_loop, BIND_THIS_METHOD(OnTimer)),
|
|
verbose(_verbose) {}
|
|
|
|
void Start() {
|
|
timer.Schedule(std::chrono::milliseconds(100));
|
|
}
|
|
|
|
private:
|
|
void OnTimer() noexcept {
|
|
if (instance.update->GetId() == 0) {
|
|
// Update complete, break the event loop
|
|
instance.event_loop.Break();
|
|
} else {
|
|
// Not done yet, check again in 100ms
|
|
timer.Schedule(std::chrono::milliseconds(100));
|
|
|
|
// Show progress every 10 seconds (100 checks)
|
|
if (verbose && ++progress_counter >= 100) {
|
|
progress_counter = 0;
|
|
std::cerr << "." << std::flush;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
static ChannelMode channel_mode = ChannelMode::ALL;
|
|
static std::string music_directory;
|
|
static AllocatedPath database_path = nullptr;
|
|
static bool verbose = false;
|
|
static bool update_mode = false;
|
|
|
|
// Global instance pointer required by other MPD components
|
|
// This must be defined here as we're not linking with Main.cxx
|
|
Instance *global_instance = nullptr;
|
|
|
|
extern "C" __attribute__((visibility("default"))) ChannelMode GetChannelMode() noexcept {
|
|
return channel_mode;
|
|
}
|
|
|
|
static void PrintUsage() {
|
|
std::cout << "mpd-dbcreate | Jay's MPD DB Creator - Hot Diggity Daffodil!\n"
|
|
<< "Usage: mpd-dbcreate --music-dir /path/to/scan --database /path/to/mpd.db [options]\n\n"
|
|
<< "Options:\n"
|
|
<< " --music-dir <path> Music directory\n"
|
|
<< " --database <path> Database file\n"
|
|
<< " --update Update existing database (incremental scan)\n"
|
|
<< " --stereo Stereo only\n"
|
|
<< " --multichannel Multichannel only\n"
|
|
<< " --all All (default)\n"
|
|
<< " --verbose Verbose output\n"
|
|
<< " --help Show help\n";
|
|
}
|
|
|
|
static void ParseArgs(int argc, char *argv[]) {
|
|
for (int i = 1; i < argc; i++) {
|
|
std::string arg = argv[i];
|
|
if (arg == "--help") {
|
|
PrintUsage();
|
|
exit(0);
|
|
} else if (arg == "--update") {
|
|
update_mode = true;
|
|
} else if (arg == "--stereo") {
|
|
channel_mode = ChannelMode::STEREO;
|
|
} else if (arg == "--multichannel") {
|
|
channel_mode = ChannelMode::MULTICHANNEL;
|
|
} else if (arg == "--all") {
|
|
channel_mode = ChannelMode::ALL;
|
|
} else if (arg == "--verbose") {
|
|
verbose = true;
|
|
} else if (arg == "--music-dir") {
|
|
if (++i >= argc)
|
|
throw std::runtime_error("--music-dir needs arg");
|
|
music_directory = argv[i];
|
|
} else if (arg == "--database") {
|
|
if (++i >= argc)
|
|
throw std::runtime_error("--database needs arg");
|
|
database_path = AllocatedPath::FromUTF8Throw(argv[i]);
|
|
} else {
|
|
throw FmtRuntimeError("Unknown: {}", arg);
|
|
}
|
|
}
|
|
if (music_directory.empty() || database_path.IsNull())
|
|
throw std::runtime_error("--music-dir and --database required");
|
|
}
|
|
|
|
int main(int argc, char *argv[]) {
|
|
try {
|
|
ParseArgs(argc, argv);
|
|
|
|
// Initialize
|
|
const ScopeIcuInit icu_init;
|
|
log_early_init(verbose);
|
|
// Steal logging back from systemd for verbose
|
|
if (!verbose) {
|
|
setup_log_output();
|
|
}
|
|
|
|
// Config
|
|
ConfigData config;
|
|
config.AddParam(ConfigOption::MUSIC_DIR,
|
|
ConfigParam(music_directory.c_str()));
|
|
|
|
ConfigBlock db_block;
|
|
db_block.AddBlockParam("plugin", "simple");
|
|
db_block.AddBlockParam("path", database_path.ToUTF8().c_str());
|
|
config.AddBlock(ConfigBlockOption::DATABASE, std::move(db_block));
|
|
|
|
// Initialize subsystems
|
|
TagLoadConfig(config);
|
|
decoder_plugin_init_all(config);
|
|
const ScopePlaylistPluginsInit playlist_init(config);
|
|
#ifdef ENABLE_ARCHIVE
|
|
const ScopeArchivePluginsInit archive_init;
|
|
#endif
|
|
|
|
// Create Instance - this contains event loop
|
|
Instance instance;
|
|
global_instance = &instance;
|
|
instance.io_thread.Start();
|
|
instance.rtio_thread.Start();
|
|
|
|
// Initialize input plugins with IO thread event loop
|
|
const ScopeInputPluginsInit input_init(config,
|
|
instance.io_thread.GetEventLoop());
|
|
|
|
// Create database
|
|
instance.database = CreateConfiguredDatabase(config,
|
|
instance.event_loop,
|
|
instance.io_thread.GetEventLoop(),
|
|
instance);
|
|
|
|
auto *simple_db = dynamic_cast<SimpleDatabase *>(instance.database.get());
|
|
if (!simple_db)
|
|
throw std::runtime_error("Not simple database");
|
|
|
|
instance.database->Open();
|
|
|
|
// Create storage
|
|
auto configured_storage = CreateConfiguredStorage(config,
|
|
instance.io_thread.GetEventLoop());
|
|
|
|
if (!configured_storage) {
|
|
throw std::runtime_error("Failed to create storage for music directory");
|
|
}
|
|
|
|
// Create CompositeStorage and mount the configured storage
|
|
auto *composite = new CompositeStorage();
|
|
instance.storage = composite;
|
|
|
|
if (configured_storage) {
|
|
composite->Mount("", std::move(configured_storage));
|
|
}
|
|
|
|
if (verbose) {
|
|
std::cerr << "Music directory: " << music_directory << "\n";
|
|
std::cerr << "Database path: " << database_path.ToUTF8() << "\n";
|
|
std::cerr << "Mode: " << (update_mode ? "UPDATE (incremental)" : "CREATE (full scan)") << "\n";
|
|
std::cerr << "Channel Mode: ";
|
|
if (channel_mode == ChannelMode::STEREO) {
|
|
std::cerr << "STEREO (filtering out multichannel)\n";
|
|
} else if (channel_mode == ChannelMode::MULTICHANNEL) {
|
|
std::cerr << "MULTICHANNEL (filtering out stereo)\n";
|
|
} else {
|
|
std::cerr << "ALL (no filtering)\n";
|
|
}
|
|
std::cerr << (update_mode ? "Updating" : "Scanning");
|
|
std::cerr.flush();
|
|
}
|
|
|
|
// Create update service
|
|
instance.update = new UpdateService(config,
|
|
instance.event_loop,
|
|
*simple_db,
|
|
*composite,
|
|
instance);
|
|
|
|
// Start scan - use 'false' for discard parameter when in update mode
|
|
// to perform incremental update, 'true' for full rescan
|
|
instance.update->Enqueue("", !update_mode);
|
|
|
|
// Create update checker to monitor completion
|
|
UpdateChecker checker(instance, verbose);
|
|
checker.Start();
|
|
|
|
// Run the event loop - it will process update events and our timer
|
|
// The loop will break when the timer detects update completion
|
|
instance.event_loop.Run();
|
|
|
|
if (verbose)
|
|
std::cerr << "\n";
|
|
|
|
// Save
|
|
simple_db->Save();
|
|
|
|
// Clean up update service first while event loops are still running
|
|
delete instance.update;
|
|
instance.update = nullptr;
|
|
|
|
// Close database
|
|
instance.database->Close();
|
|
instance.database.reset();
|
|
|
|
// Clean up storage
|
|
delete instance.storage;
|
|
instance.storage = nullptr;
|
|
|
|
// Now stop threads after cleanup
|
|
instance.rtio_thread.Stop();
|
|
instance.io_thread.Stop();
|
|
|
|
if (verbose)
|
|
std::cerr << "Done!\n";
|
|
|
|
return 0;
|
|
} catch (const std::exception &e) {
|
|
std::cerr << "Error: " << e.what() << "\n";
|
|
return 1;
|
|
}
|
|
}
|