Mercurial > foo_out_sdl
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 0:e9bb126753e7 | 1:20d02a178406 |
|---|---|
| 1 #include "stdafx.h" | |
| 2 #include <helpers/file_list_helper.h> | |
| 3 #include <helpers/filetimetools.h> | |
| 4 #include <memory> | |
| 5 | |
| 6 void RunIOTest() { | |
| 7 try { | |
| 8 auto request = http_client::get()->create_request("GET"); | |
| 9 request->run("https://www.foobar2000.org", fb2k::noAbort); | |
| 10 } catch (std::exception const & e) { | |
| 11 popup_message::g_show( PFC_string_formatter() << "Network test failure:\n" << e, "Information"); | |
| 12 return; | |
| 13 } | |
| 14 popup_message::g_show(PFC_string_formatter() << "Network test OK", "Information"); | |
| 15 } | |
| 16 | |
| 17 namespace { // anon namespace local classes for good measure | |
| 18 class tpc_copyFiles : public threaded_process_callback { | |
| 19 public: | |
| 20 tpc_copyFiles ( metadb_handle_list_cref items, const char * pathTo ) : m_pathTo(pathTo), m_outFS(filesystem::get(pathTo)) { | |
| 21 m_lstFiles.init_from_list( items ); | |
| 22 } | |
| 23 | |
| 24 void on_init(ctx_t p_wnd) override { | |
| 25 // Main thread, called before run() gets started | |
| 26 } | |
| 27 void run(threaded_process_status & p_status, abort_callback & p_abort) override { | |
| 28 // Worker thread | |
| 29 for( size_t fileWalk = 0; fileWalk < m_lstFiles.get_size(); ++ fileWalk ) { | |
| 30 | |
| 31 // always do this in every timeconsuming loop | |
| 32 // will throw exception_aborted if the user pushed 'cancel' on us | |
| 33 p_abort.check(); | |
| 34 | |
| 35 const char * inPath = m_lstFiles[fileWalk]; | |
| 36 p_status.set_progress(fileWalk, m_lstFiles.get_size()); | |
| 37 p_status.set_item_path( inPath ); | |
| 38 | |
| 39 try { | |
| 40 workWithFile(inPath, p_abort); | |
| 41 } catch(exception_aborted) { | |
| 42 // User abort, just bail | |
| 43 throw; | |
| 44 } catch(std::exception const & e) { | |
| 45 m_errorLog << "Could not copy: " << file_path_display(inPath) << ", reason: " << e << "\n"; | |
| 46 } | |
| 47 | |
| 48 } | |
| 49 } | |
| 50 | |
| 51 void workWithFile( const char * inPath, abort_callback & abort ) { | |
| 52 FB2K_console_formatter() << "File: " << file_path_display(inPath); | |
| 53 | |
| 54 // Filesystem API for inPath | |
| 55 const filesystem::ptr inFS = filesystem::get( inPath ); | |
| 56 pfc::string8 inFN; | |
| 57 // Extract filename+extension according to this filesystem's rules | |
| 58 // If it's HTTP or so, there might be ?key=value that needs stripping | |
| 59 inFS->extract_filename_ext(inPath, inFN); | |
| 60 | |
| 61 pfc::string8 outPath = m_pathTo; | |
| 62 // Suffix with outFS path separator. On Windows local filesystem this is always a backslash. | |
| 63 outPath.end_with( m_outFS->pathSeparator() ); | |
| 64 outPath += inFN; | |
| 65 | |
| 66 const double openTimeout = 1.0; // wait and keep retrying for up to 1 second in case of a sharing violation error | |
| 67 | |
| 68 // Not strictly needed, but we do it anyway | |
| 69 // Acquire a read lock on the file, so anyone trying to acquire a write lock will just wait till we have finished | |
| 70 auto inLock = file_lock_manager::get()->acquire_read( inPath, abort ); | |
| 71 | |
| 72 auto inFile = inFS->openRead( inPath, abort, openTimeout ); | |
| 73 | |
| 74 // Let's toy around with inFile | |
| 75 { | |
| 76 auto stats = inFile->get_stats(abort); | |
| 77 | |
| 78 if ( stats.m_size != filesize_invalid ) { | |
| 79 FB2K_console_formatter() << "Size: " << pfc::format_file_size_short(stats.m_size); | |
| 80 } | |
| 81 if ( stats.m_timestamp != filetimestamp_invalid ) { | |
| 82 FB2K_console_formatter() << "Last modified: " << format_filetimestamp(stats.m_timestamp); | |
| 83 } | |
| 84 pfc::string8 contentType; | |
| 85 if ( inFile->get_content_type( contentType ) ) { | |
| 86 FB2K_console_formatter() << "Content type: " << contentType; | |
| 87 } | |
| 88 | |
| 89 uint8_t buffer[256]; | |
| 90 size_t got = inFile->read(buffer, sizeof(buffer), abort); | |
| 91 | |
| 92 if ( got > 0 ) { | |
| 93 FB2K_console_formatter() << "Header bytes: " << pfc::format_hexdump( buffer, got ); | |
| 94 } | |
| 95 | |
| 96 if ( inFile->is_remote() ) { | |
| 97 FB2K_console_formatter() << "File is remote"; | |
| 98 } else { | |
| 99 FB2K_console_formatter() << "File is local"; | |
| 100 } | |
| 101 | |
| 102 // For seekable files, reopen() seeks to the beginning. | |
| 103 // For nonseekable stream, reopen() restarts reading the stream. | |
| 104 inFile->reopen(abort); | |
| 105 } | |
| 106 | |
| 107 // This is a glorified strcmp() for file paths. | |
| 108 if ( metadb::path_compare( inPath, outPath ) == 0 ) { | |
| 109 // Same path, go no further. Specifically don't attempt acquiring a writelock because that will never complete, unless user aborted. | |
| 110 FB2K_console_formatter() << "Input and output paths are the same - not copying!"; | |
| 111 return; | |
| 112 } | |
| 113 | |
| 114 // Required to write to files being currently played. | |
| 115 // See file_lock_manager documentation for details. | |
| 116 auto outLock = file_lock_manager::get()->acquire_write( outPath, abort ); | |
| 117 | |
| 118 // WARNING : if a file exists at outPath prior to this, it will be reset to zero bytes (win32 CREATE_ALWAYS semantics) | |
| 119 auto outFile = m_outFS->openWriteNew(outPath, abort, openTimeout); | |
| 120 | |
| 121 | |
| 122 | |
| 123 try { | |
| 124 // Refer to g_transfer_file implementation details in the SDK for lowlevel reading & writing details | |
| 125 file::g_transfer_file(inFile, outFile, abort); | |
| 126 } catch(...) { | |
| 127 | |
| 128 if ( inFile->is_remote() ) { | |
| 129 // Remote file was being downloaded? Suppress deletion of incomplete output | |
| 130 throw; | |
| 131 } | |
| 132 // Failed for some reason | |
| 133 // Release our destination file hadnle | |
| 134 outFile.release(); | |
| 135 | |
| 136 // .. and delete the incomplete file | |
| 137 try { | |
| 138 auto & noAbort = fb2k::noAbort; // we might be being aborted, don't let that prevent deletion | |
| 139 m_outFS->remove( outPath, noAbort ); | |
| 140 } catch(...) { | |
| 141 // disregard errors - just report original copy error | |
| 142 } | |
| 143 | |
| 144 throw; // rethrow the original copy error | |
| 145 } | |
| 146 | |
| 147 } | |
| 148 | |
| 149 void on_done(ctx_t p_wnd, bool p_was_aborted) override { | |
| 150 // All done, main thread again | |
| 151 | |
| 152 if (! p_was_aborted && m_errorLog.length() > 0 ) { | |
| 153 popup_message::g_show(m_errorLog, "Information"); | |
| 154 } | |
| 155 | |
| 156 } | |
| 157 | |
| 158 | |
| 159 | |
| 160 // This is a helper class that generates a sorted list of unique file paths in this metadb_handle_list. | |
| 161 // The metadb_handle_list might contain duplicate tracks or multiple subsongs in the same file. m_lstFiles will list each file only once. | |
| 162 file_list_helper::file_list_from_metadb_handle_list m_lstFiles; | |
| 163 | |
| 164 // Destination path | |
| 165 const pfc::string8 m_pathTo; | |
| 166 // Destination filesystem API. Obtained via filesystem::get() with destination path. | |
| 167 const filesystem::ptr m_outFS; | |
| 168 | |
| 169 // Error log | |
| 170 pfc::string_formatter m_errorLog; | |
| 171 }; | |
| 172 } | |
| 173 | |
| 174 void RunCopyFilesHere(metadb_handle_list_cref data, const char * copyTo, fb2k::hwnd_t wndParent) { | |
| 175 | |
| 176 // Create worker object, a threaded_process_callback implementation. | |
| 177 auto worker = fb2k::service_new<tpc_copyFiles>(data, copyTo); | |
| 178 const uint32_t flags = threaded_process::flag_show_abort | threaded_process::flag_show_progress | threaded_process::flag_show_item; | |
| 179 // Start the process asynchronously. | |
| 180 threaded_process::get()->run_modeless( worker, flags, wndParent, "Sample Component: Copying Files" ); | |
| 181 | |
| 182 // Our worker is now running. | |
| 183 } | |
| 184 void RunCopyFiles(metadb_handle_list_cref data) { | |
| 185 | |
| 186 #ifdef _WIN32 | |
| 187 // Detect modal dialog wars. | |
| 188 // If another modal dialog is active, bump it instead of allowing our modal dialog (uBrowseForFolder) to run. | |
| 189 // Suppress this if the relevant code is intended to be launched by a modal dialog. | |
| 190 if (!ModalDialogPrologue()) return; | |
| 191 | |
| 192 const HWND wndParent = core_api::get_main_window(); | |
| 193 pfc::string8 copyTo; | |
| 194 // shared.dll method | |
| 195 if (!uBrowseForFolder( wndParent, "Choose destination folder", copyTo )) return; | |
| 196 | |
| 197 // shared.dll methods are win32 API wrappers and return plain paths with no protocol prepended | |
| 198 // Prefix with file:// before passing to fb2k filesystem methods. | |
| 199 // Actually the standard fb2k filesystem implementation recognizes paths even without the prefix, but we enforce it here as a good practice. | |
| 200 pfc::string8 copyTo2 = PFC_string_formatter() << "file://" << copyTo; | |
| 201 | |
| 202 RunCopyFilesHere(data, copyTo2, wndParent); | |
| 203 #else | |
| 204 auto tracksCopy = std::make_shared<metadb_handle_list>( data ); | |
| 205 auto setup = fb2k::fileDialog::get()->setupOpenFolder(); | |
| 206 setup->setTitle("Choose destination folder"); | |
| 207 setup->runSimple( [tracksCopy] (fb2k::stringRef path) { | |
| 208 RunCopyFilesHere(*tracksCopy, path->c_str(), core_api::get_main_window()); | |
| 209 } ); | |
| 210 | |
| 211 #endif | |
| 212 } | |
| 213 | |
| 214 | |
| 215 namespace { | |
| 216 class processLLtags : public threaded_process_callback { | |
| 217 public: | |
| 218 processLLtags( metadb_handle_list_cref data ) : m_items(data) { | |
| 219 m_hints = metadb_io_v2::get()->create_hint_list(); | |
| 220 } | |
| 221 void on_init(ctx_t p_wnd) override { | |
| 222 // Main thread, called before run() gets started | |
| 223 } | |
| 224 void run(threaded_process_status & p_status, abort_callback & p_abort) override { | |
| 225 // Worker thread | |
| 226 | |
| 227 // Note: | |
| 228 // We should look for references to the same file (such as multiple subsongs) in the track list and update each file only once. | |
| 229 // But for the sake of simplicity we don't do this in a sample component. | |
| 230 for( size_t itemWalk = 0; itemWalk < m_items.get_size(); ++ itemWalk ) { | |
| 231 | |
| 232 // always do this in every timeconsuming loop | |
| 233 // will throw exception_aborted if the user pushed 'cancel' on us | |
| 234 p_abort.check(); | |
| 235 | |
| 236 auto item = m_items[ itemWalk ]; | |
| 237 p_status.set_progress( itemWalk, m_items.get_size() ); | |
| 238 p_status.set_item_path( item->get_path() ); | |
| 239 | |
| 240 try { | |
| 241 workWithTrack(item, p_abort); | |
| 242 } catch(exception_aborted) { | |
| 243 // User abort, just bail | |
| 244 throw; | |
| 245 } catch(std::exception const & e) { | |
| 246 m_errorLog << "Could not update: " << item << ", reason: " << e << "\n"; | |
| 247 } | |
| 248 } | |
| 249 } | |
| 250 void on_done(ctx_t p_wnd, bool p_was_aborted) override { | |
| 251 // All done, main thread again | |
| 252 if ( m_hints.is_valid() ) { | |
| 253 // This is the proper time to finalize the hint list | |
| 254 // All playlists showing these files and such will now be refreshed | |
| 255 m_hints->on_done(); | |
| 256 } | |
| 257 | |
| 258 if (!p_was_aborted && m_errorLog.length() > 0) { | |
| 259 popup_message::g_show(m_errorLog, "Information"); | |
| 260 } | |
| 261 } | |
| 262 | |
| 263 private: | |
| 264 void workWithTrack( metadb_handle_ptr item, abort_callback & abort ) { | |
| 265 | |
| 266 FB2K_console_formatter() << "foo_sample will update tags on: " << item; | |
| 267 | |
| 268 const auto subsong = item->get_subsong_index(); | |
| 269 const auto path = item->get_path(); | |
| 270 | |
| 271 const double openTimeout = 1.0; // wait and keep retrying for up to 1 second in case of a sharing violation error | |
| 272 | |
| 273 // Required to write to files being currently played. | |
| 274 // See file_lock_manager documentation for details. | |
| 275 auto lock = file_lock_manager::get()->acquire_write( path, abort ); | |
| 276 | |
| 277 input_info_writer::ptr writer; | |
| 278 input_entry::g_open_for_info_write_timeout(writer, nullptr, path, abort, openTimeout ); | |
| 279 | |
| 280 { // let's toy around with info that the writer can hand to us | |
| 281 auto stats = writer->get_file_stats( abort ); | |
| 282 if (stats.m_timestamp != filetimestamp_invalid) { | |
| 283 FB2K_console_formatter() << "Last-modified before tag update: " << format_filetimestamp_utc(stats.m_timestamp); | |
| 284 } | |
| 285 } | |
| 286 | |
| 287 file_info_impl info; | |
| 288 writer->get_info( subsong, info, abort ); | |
| 289 | |
| 290 info.meta_set("comment", "foo_sample lowlevel tags write demo was here"); | |
| 291 | |
| 292 // Touchy subject | |
| 293 // Should we let the user abort an incomplete tag write? | |
| 294 // Let's better not | |
| 295 auto & noAbort = fb2k::noAbort; | |
| 296 | |
| 297 // This can be called many times for files with multiple subsongs | |
| 298 writer->set_info( subsong, info, noAbort ); | |
| 299 | |
| 300 // This is called once - when we're done set_info()'ing | |
| 301 writer->commit( noAbort ); | |
| 302 | |
| 303 { // let's toy around with info that the writer can hand to us | |
| 304 auto stats = writer->get_file_stats(abort); | |
| 305 if (stats.m_timestamp != filetimestamp_invalid) { | |
| 306 FB2K_console_formatter() << "Last-modified after tag update: " << format_filetimestamp_utc(stats.m_timestamp); | |
| 307 } | |
| 308 } | |
| 309 | |
| 310 // Now send new info to metadb | |
| 311 // If we don't do this, old info may still be shown in playlists etc. | |
| 312 if ( true ) { | |
| 313 // Method #1: feed altered info directly to the hintlist. | |
| 314 // Makes sense here as we updated just one subsong. | |
| 315 auto stats = writer->get_file_stats(abort); | |
| 316 m_hints->add_hint(item, info, stats, true); | |
| 317 } else { | |
| 318 // Method #2: let metadb talk to our writer object (more commonly used). | |
| 319 // The writer is a subclass of input_info_reader and can therefore be legitimately fed to add_hint_reader() | |
| 320 // This will read info from all subsongs in the file and update metadb if appropriate. | |
| 321 m_hints->add_hint_reader(path, writer, abort); | |
| 322 } | |
| 323 } | |
| 324 metadb_hint_list::ptr m_hints; | |
| 325 const metadb_handle_list m_items; | |
| 326 pfc::string_formatter m_errorLog; | |
| 327 }; | |
| 328 } | |
| 329 void RunAlterTagsLL(metadb_handle_list_cref data) { | |
| 330 | |
| 331 const auto wndParent = core_api::get_main_window(); | |
| 332 | |
| 333 // Our worker object, a threaded_process_callback subclass. | |
| 334 auto worker = fb2k::service_new< processLLtags > ( data ); | |
| 335 | |
| 336 const uint32_t flags = threaded_process::flag_show_abort | threaded_process::flag_show_progress | threaded_process::flag_show_item; | |
| 337 | |
| 338 // Start the worker asynchronously. | |
| 339 threaded_process::get()->run_modeless(worker, flags, wndParent, "Sample Component: Updating Tags"); | |
| 340 | |
| 341 // The worker is now running. | |
| 342 | |
| 343 } |
