view foosdk/sdk/foobar2000/foo_sample/IO.cpp @ 1:20d02a178406 default tip

*: check in everything else yay
author Paper <paper@tflc.us>
date Mon, 05 Jan 2026 02:15:46 -0500
parents
children
line wrap: on
line source

#include "stdafx.h"
#include <helpers/file_list_helper.h>
#include <helpers/filetimetools.h>
#include <memory>

void RunIOTest() {
	try {
		auto request = http_client::get()->create_request("GET");
		request->run("https://www.foobar2000.org", fb2k::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(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
			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 {
					auto & noAbort = fb2k::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(ctx_t 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 RunCopyFilesHere(metadb_handle_list_cref data, const char * copyTo, fb2k::hwnd_t wndParent) {
    
    // Create worker object, a threaded_process_callback implementation.
    auto worker = fb2k::service_new<tpc_copyFiles>(data, copyTo);
    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.
}
void RunCopyFiles(metadb_handle_list_cref data) {

#ifdef _WIN32
	// 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;

    RunCopyFilesHere(data, copyTo2, wndParent);
#else
    auto tracksCopy = std::make_shared<metadb_handle_list>( data );
    auto setup = fb2k::fileDialog::get()->setupOpenFolder();
    setup->setTitle("Choose destination folder");
    setup->runSimple( [tracksCopy] (fb2k::stringRef path) {
        RunCopyFilesHere(*tracksCopy, path->c_str(), core_api::get_main_window());
    } );
    
#endif
}


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
			auto & noAbort = fb2k::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 auto 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.

}