330 lines
12 KiB
C++
330 lines
12 KiB
C++
#include "stdafx.h"
|
|
#include <helpers/file_list_helper.h>
|
|
#include <helpers/filetimetools.h>
|
|
|
|
void RunIOTest() {
|
|
try {
|
|
abort_callback_dummy noAbort;
|
|
auto request = http_client::get()->create_request("GET");
|
|
request->run("https://www.foobar2000.org", noAbort);
|
|
} catch (std::exception const & e) {
|
|
popup_message::g_show( PFC_string_formatter() << "Network test failure:\n" << e, "Information");
|
|
return;
|
|
}
|
|
popup_message::g_show(PFC_string_formatter() << "Network test OK", "Information");
|
|
}
|
|
|
|
namespace { // anon namespace local classes for good measure
|
|
class tpc_copyFiles : public threaded_process_callback {
|
|
public:
|
|
tpc_copyFiles ( metadb_handle_list_cref items, const char * pathTo ) : m_pathTo(pathTo), m_outFS(filesystem::get(pathTo)) {
|
|
m_lstFiles.init_from_list( items );
|
|
}
|
|
|
|
void on_init(HWND p_wnd) override {
|
|
// Main thread, called before run() gets started
|
|
}
|
|
void run(threaded_process_status & p_status, abort_callback & p_abort) override {
|
|
// Worker thread
|
|
for( size_t fileWalk = 0; fileWalk < m_lstFiles.get_size(); ++ fileWalk ) {
|
|
|
|
// always do this in every timeconsuming loop
|
|
// will throw exception_aborted if the user pushed 'cancel' on us
|
|
p_abort.check();
|
|
|
|
const char * inPath = m_lstFiles[fileWalk];
|
|
p_status.set_progress(fileWalk, m_lstFiles.get_size());
|
|
p_status.set_item_path( inPath );
|
|
|
|
try {
|
|
workWithFile(inPath, p_abort);
|
|
} catch(exception_aborted) {
|
|
// User abort, just bail
|
|
throw;
|
|
} catch(std::exception const & e) {
|
|
m_errorLog << "Could not copy: " << file_path_display(inPath) << ", reason: " << e << "\n";
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
void workWithFile( const char * inPath, abort_callback & abort ) {
|
|
FB2K_console_formatter() << "File: " << file_path_display(inPath);
|
|
|
|
// Filesystem API for inPath
|
|
const filesystem::ptr inFS = filesystem::get( inPath );
|
|
pfc::string8 inFN;
|
|
// Extract filename+extension according to this filesystem's rules
|
|
// If it's HTTP or so, there might be ?key=value that needs stripping
|
|
inFS->extract_filename_ext(inPath, inFN);
|
|
|
|
pfc::string8 outPath = m_pathTo;
|
|
// Suffix with outFS path separator. On Windows local filesystem this is always a backslash.
|
|
outPath.end_with( m_outFS->pathSeparator() );
|
|
outPath += inFN;
|
|
|
|
const double openTimeout = 1.0; // wait and keep retrying for up to 1 second in case of a sharing violation error
|
|
|
|
// Not strictly needed, but we do it anyway
|
|
// Acquire a read lock on the file, so anyone trying to acquire a write lock will just wait till we have finished
|
|
auto inLock = file_lock_manager::get()->acquire_read( inPath, abort );
|
|
|
|
auto inFile = inFS->openRead( inPath, abort, openTimeout );
|
|
|
|
// Let's toy around with inFile
|
|
{
|
|
auto stats = inFile->get_stats(abort);
|
|
|
|
if ( stats.m_size != filesize_invalid ) {
|
|
FB2K_console_formatter() << "Size: " << pfc::format_file_size_short(stats.m_size);
|
|
}
|
|
if ( stats.m_timestamp != filetimestamp_invalid ) {
|
|
FB2K_console_formatter() << "Last modified: " << format_filetimestamp(stats.m_timestamp);
|
|
}
|
|
pfc::string8 contentType;
|
|
if ( inFile->get_content_type( contentType ) ) {
|
|
FB2K_console_formatter() << "Content type: " << contentType;
|
|
}
|
|
|
|
uint8_t buffer[256];
|
|
size_t got = inFile->read(buffer, sizeof(buffer), abort);
|
|
|
|
if ( got > 0 ) {
|
|
FB2K_console_formatter() << "Header bytes: " << pfc::format_hexdump( buffer, got );
|
|
}
|
|
|
|
if ( inFile->is_remote() ) {
|
|
FB2K_console_formatter() << "File is remote";
|
|
} else {
|
|
FB2K_console_formatter() << "File is local";
|
|
}
|
|
|
|
// For seekable files, reopen() seeks to the beginning.
|
|
// For nonseekable stream, reopen() restarts reading the stream.
|
|
inFile->reopen(abort);
|
|
}
|
|
|
|
// This is a glorified strcmp() for file paths.
|
|
if ( metadb::path_compare( inPath, outPath ) == 0 ) {
|
|
// Same path, go no further. Specifically don't attempt acquiring a writelock because that will never complete, unless user aborted.
|
|
FB2K_console_formatter() << "Input and output paths are the same - not copying!";
|
|
return;
|
|
}
|
|
|
|
// Required to write to files being currently played.
|
|
// See file_lock_manager documentation for details.
|
|
auto outLock = file_lock_manager::get()->acquire_write( outPath, abort );
|
|
|
|
// WARNING : if a file exists at outPath prior to this, it will be reset to zero bytes (win32 CREATE_ALWAYS semantics)
|
|
auto outFile = m_outFS->openWriteNew(outPath, abort, openTimeout);
|
|
|
|
|
|
|
|
try {
|
|
// Refer to g_transfer_file implementation details in the SDK for lowlevel reading & writing details
|
|
file::g_transfer_file(inFile, outFile, abort);
|
|
} catch(...) {
|
|
|
|
if ( inFile->is_remote() ) {
|
|
// Remote file was being downloaded? Suppress deletion of incomplete output
|
|
throw;
|
|
}
|
|
// Failed for some reason
|
|
// Release our destination file hadnle
|
|
outFile.release();
|
|
|
|
// .. and delete the incomplete file
|
|
try {
|
|
abort_callback_dummy noAbort; // we might be being aborted, don't let that prevent deletion
|
|
m_outFS->remove( outPath, noAbort );
|
|
} catch(...) {
|
|
// disregard errors - just report original copy error
|
|
}
|
|
|
|
throw; // rethrow the original copy error
|
|
}
|
|
|
|
}
|
|
|
|
void on_done(HWND p_wnd, bool p_was_aborted) override {
|
|
// All done, main thread again
|
|
|
|
if (! p_was_aborted && m_errorLog.length() > 0 ) {
|
|
popup_message::g_show(m_errorLog, "Information");
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// This is a helper class that generates a sorted list of unique file paths in this metadb_handle_list.
|
|
// The metadb_handle_list might contain duplicate tracks or multiple subsongs in the same file. m_lstFiles will list each file only once.
|
|
file_list_helper::file_list_from_metadb_handle_list m_lstFiles;
|
|
|
|
// Destination path
|
|
const pfc::string8 m_pathTo;
|
|
// Destination filesystem API. Obtained via filesystem::get() with destination path.
|
|
const filesystem::ptr m_outFS;
|
|
|
|
// Error log
|
|
pfc::string_formatter m_errorLog;
|
|
};
|
|
}
|
|
|
|
void RunCopyFiles(metadb_handle_list_cref data) {
|
|
|
|
// Detect modal dialog wars.
|
|
// If another modal dialog is active, bump it instead of allowing our modal dialog (uBrowseForFolder) to run.
|
|
// Suppress this if the relevant code is intended to be launched by a modal dialog.
|
|
if (!ModalDialogPrologue()) return;
|
|
|
|
const HWND wndParent = core_api::get_main_window();
|
|
pfc::string8 copyTo;
|
|
// shared.dll method
|
|
if (!uBrowseForFolder( wndParent, "Choose destination folder", copyTo )) return;
|
|
|
|
// shared.dll methods are win32 API wrappers and return plain paths with no protocol prepended
|
|
// Prefix with file:// before passing to fb2k filesystem methods.
|
|
// Actually the standard fb2k filesystem implementation recognizes paths even without the prefix, but we enforce it here as a good practice.
|
|
pfc::string8 copyTo2 = PFC_string_formatter() << "file://" << copyTo;
|
|
|
|
// Create worker object, a threaded_process_callback implementation.
|
|
auto worker = fb2k::service_new<tpc_copyFiles>(data, copyTo2);
|
|
const uint32_t flags = threaded_process::flag_show_abort | threaded_process::flag_show_progress | threaded_process::flag_show_item;
|
|
// Start the process asynchronously.
|
|
threaded_process::get()->run_modeless( worker, flags, wndParent, "Sample Component: Copying Files" );
|
|
|
|
// Our worker is now running.
|
|
}
|
|
|
|
|
|
namespace {
|
|
class processLLtags : public threaded_process_callback {
|
|
public:
|
|
processLLtags( metadb_handle_list_cref data ) : m_items(data) {
|
|
m_hints = metadb_io_v2::get()->create_hint_list();
|
|
}
|
|
void on_init(ctx_t p_wnd) override {
|
|
// Main thread, called before run() gets started
|
|
}
|
|
void run(threaded_process_status & p_status, abort_callback & p_abort) override {
|
|
// Worker thread
|
|
|
|
// Note:
|
|
// We should look for references to the same file (such as multiple subsongs) in the track list and update each file only once.
|
|
// But for the sake of simplicity we don't do this in a sample component.
|
|
for( size_t itemWalk = 0; itemWalk < m_items.get_size(); ++ itemWalk ) {
|
|
|
|
// always do this in every timeconsuming loop
|
|
// will throw exception_aborted if the user pushed 'cancel' on us
|
|
p_abort.check();
|
|
|
|
auto item = m_items[ itemWalk ];
|
|
p_status.set_progress( itemWalk, m_items.get_size() );
|
|
p_status.set_item_path( item->get_path() );
|
|
|
|
try {
|
|
workWithTrack(item, p_abort);
|
|
} catch(exception_aborted) {
|
|
// User abort, just bail
|
|
throw;
|
|
} catch(std::exception const & e) {
|
|
m_errorLog << "Could not update: " << item << ", reason: " << e << "\n";
|
|
}
|
|
}
|
|
}
|
|
void on_done(ctx_t p_wnd, bool p_was_aborted) override {
|
|
// All done, main thread again
|
|
if ( m_hints.is_valid() ) {
|
|
// This is the proper time to finalize the hint list
|
|
// All playlists showing these files and such will now be refreshed
|
|
m_hints->on_done();
|
|
}
|
|
|
|
if (!p_was_aborted && m_errorLog.length() > 0) {
|
|
popup_message::g_show(m_errorLog, "Information");
|
|
}
|
|
}
|
|
|
|
private:
|
|
void workWithTrack( metadb_handle_ptr item, abort_callback & abort ) {
|
|
|
|
FB2K_console_formatter() << "foo_sample will update tags on: " << item;
|
|
|
|
const auto subsong = item->get_subsong_index();
|
|
const auto path = item->get_path();
|
|
|
|
const double openTimeout = 1.0; // wait and keep retrying for up to 1 second in case of a sharing violation error
|
|
|
|
// Required to write to files being currently played.
|
|
// See file_lock_manager documentation for details.
|
|
auto lock = file_lock_manager::get()->acquire_write( path, abort );
|
|
|
|
input_info_writer::ptr writer;
|
|
input_entry::g_open_for_info_write_timeout(writer, nullptr, path, abort, openTimeout );
|
|
|
|
{ // let's toy around with info that the writer can hand to us
|
|
auto stats = writer->get_file_stats( abort );
|
|
if (stats.m_timestamp != filetimestamp_invalid) {
|
|
FB2K_console_formatter() << "Last-modified before tag update: " << format_filetimestamp_utc(stats.m_timestamp);
|
|
}
|
|
}
|
|
|
|
file_info_impl info;
|
|
writer->get_info( subsong, info, abort );
|
|
|
|
info.meta_set("comment", "foo_sample lowlevel tags write demo was here");
|
|
|
|
// Touchy subject
|
|
// Should we let the user abort an incomplete tag write?
|
|
// Let's better not
|
|
abort_callback_dummy noAbort;
|
|
|
|
// This can be called many times for files with multiple subsongs
|
|
writer->set_info( subsong, info, noAbort );
|
|
|
|
// This is called once - when we're done set_info()'ing
|
|
writer->commit( noAbort );
|
|
|
|
{ // let's toy around with info that the writer can hand to us
|
|
auto stats = writer->get_file_stats(abort);
|
|
if (stats.m_timestamp != filetimestamp_invalid) {
|
|
FB2K_console_formatter() << "Last-modified after tag update: " << format_filetimestamp_utc(stats.m_timestamp);
|
|
}
|
|
}
|
|
|
|
// Now send new info to metadb
|
|
// If we don't do this, old info may still be shown in playlists etc.
|
|
if ( true ) {
|
|
// Method #1: feed altered info directly to the hintlist.
|
|
// Makes sense here as we updated just one subsong.
|
|
auto stats = writer->get_file_stats(abort);
|
|
m_hints->add_hint(item, info, stats, true);
|
|
} else {
|
|
// Method #2: let metadb talk to our writer object (more commonly used).
|
|
// The writer is a subclass of input_info_reader and can therefore be legitimately fed to add_hint_reader()
|
|
// This will read info from all subsongs in the file and update metadb if appropriate.
|
|
m_hints->add_hint_reader(path, writer, abort);
|
|
}
|
|
}
|
|
metadb_hint_list::ptr m_hints;
|
|
const metadb_handle_list m_items;
|
|
pfc::string_formatter m_errorLog;
|
|
};
|
|
}
|
|
void RunAlterTagsLL(metadb_handle_list_cref data) {
|
|
|
|
const HWND wndParent = core_api::get_main_window();
|
|
|
|
// Our worker object, a threaded_process_callback subclass.
|
|
auto worker = fb2k::service_new< processLLtags > ( data );
|
|
|
|
const uint32_t flags = threaded_process::flag_show_abort | threaded_process::flag_show_progress | threaded_process::flag_show_item;
|
|
|
|
// Start the worker asynchronously.
|
|
threaded_process::get()->run_modeless(worker, flags, wndParent, "Sample Component: Updating Tags");
|
|
|
|
// The worker is now running.
|
|
|
|
}
|