diff 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 diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/foosdk/sdk/foobar2000/foo_sample/IO.cpp	Mon Jan 05 02:15:46 2026 -0500
@@ -0,0 +1,343 @@
+#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.
+
+}