Mercurial > foo_out_sdl
comparison foosdk/sdk/foobar2000/foo_sample/rating.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 | |
| 3 | |
| 4 /* | |
| 5 ======================================================================== | |
| 6 Sample implementation of metadb_index_client and a rating database. | |
| 7 ======================================================================== | |
| 8 | |
| 9 The rating data is all maintained by metadb backend, we only present and alter it when asked to. | |
| 10 Relevant classes: | |
| 11 metadb_index_client_impl - turns track info into a database key to which our data gets pinned. | |
| 12 init_stage_callback_impl - initializes ourselves at the proper moment of the app lifetime. | |
| 13 initquit_imp - clean up cached objects on app shutdown | |
| 14 metadb_display_field_provider_impl - publishes our %foo_sample_...% fields via title formatting. | |
| 15 contextmenu_rating - context menu command to cycle rating values. | |
| 16 mainmenu_rating - main menu command to show a dump of all present ratings. | |
| 17 track_property_provider_impl - serves info for the Properties dialog | |
| 18 */ | |
| 19 | |
| 20 namespace { | |
| 21 | |
| 22 // I am foo_sample and these are *my* GUIDs. | |
| 23 // Replace with your own when reusing. | |
| 24 // Always recreate guid_foo_sample_rating_index if your metadb_index_client_impl hashing semantics changed or else you run into inconsistent/nonsensical data. | |
| 25 static const GUID guid_foo_sample_track_rating_index = { 0x88cf3f09, 0x26a8, 0x42ef,{ 0xb7, 0xb8, 0x42, 0x21, 0xb9, 0x62, 0x26, 0x78 } }; | |
| 26 static const GUID guid_foo_sample_album_rating_index = { 0xd94ba576, 0x7fab, 0x4f1b,{ 0xbe, 0x5e, 0x4f, 0x8e, 0x9d, 0x5f, 0x30, 0xf1 } }; | |
| 27 static const GUID guid_foo_sample_rating_contextmenu1 = { 0x5d71c93, 0x5d38, 0x4e63,{ 0x97, 0x66, 0x8f, 0xb7, 0x6d, 0xc7, 0xc5, 0x9e } }; | |
| 28 static const GUID guid_foo_sample_rating_contextmenu2 = { 0xf3972846, 0x7c32, 0x44fa,{ 0xbd, 0xa, 0x68, 0x86, 0x65, 0x69, 0x4b, 0x7d } }; | |
| 29 static const GUID guid_foo_sample_rating_contextmenu3 = { 0x67a6d984, 0xe499, 0x4f86,{ 0xb9, 0xcb, 0x66, 0x8e, 0x59, 0xb8, 0xd0, 0xe6 } }; | |
| 30 static const GUID guid_foo_sample_rating_contextmenu4 = { 0x4541dfa5, 0x7976, 0x43aa,{ 0xb9, 0x73, 0x10, 0xc3, 0x26, 0x55, 0x5a, 0x5c } }; | |
| 31 | |
| 32 static const GUID guid_foo_sample_contextmenu_group = { 0x572de7f4, 0xcbdf, 0x479a,{ 0x97, 0x26, 0xa, 0xb0, 0x97, 0x47, 0x69, 0xe3 } }; | |
| 33 static const GUID guid_foo_sample_rating_mainmenu = { 0x53327baa, 0xbaa4, 0x478e,{ 0x87, 0x24, 0xf7, 0x38, 0x4, 0x15, 0xf7, 0x27 } }; | |
| 34 static const GUID guid_foo_sample_mainmenu_group = { 0x44963e7a, 0x4b2a, 0x4588,{ 0xb0, 0x17, 0xa8, 0x69, 0x18, 0xcb, 0x8a, 0xa5 } }; | |
| 35 | |
| 36 // Patterns by which we pin our data to. | |
| 37 // If multiple songs in the library evaluate to the same string, they will be considered the same by our component, | |
| 38 // and data applied to one will also show up with the rest. | |
| 39 // Always recreate relevant index GUIDs if you change these | |
| 40 static const char strTrackRatingPinTo[] = "%artist% - %title%"; | |
| 41 static const char strAlbumRatingPinTo[] = "%album artist% - %album%"; | |
| 42 | |
| 43 | |
| 44 // Our group in Properties dialog / Details tab, see track_property_provider_impl | |
| 45 static const char strPropertiesGroup[] = "Sample Component"; | |
| 46 | |
| 47 // Retain pinned data for four weeks if there are no matching items in library | |
| 48 static const t_filetimestamp retentionPeriod = system_time_periods::week * 4; | |
| 49 | |
| 50 // A class that turns metadata + location info into hashes to which our data gets pinned by the backend. | |
| 51 class metadb_index_client_impl : public metadb_index_client { | |
| 52 public: | |
| 53 metadb_index_client_impl( const char * pinTo ) { | |
| 54 static_api_ptr_t<titleformat_compiler>()->compile_force(m_keyObj, pinTo); | |
| 55 } | |
| 56 | |
| 57 metadb_index_hash transform(const file_info & info, const playable_location & location) { | |
| 58 pfc::string_formatter str; | |
| 59 m_keyObj->run_simple( location, &info, str ); | |
| 60 // Make MD5 hash of the string, then reduce it to 64-bit metadb_index_hash | |
| 61 return static_api_ptr_t<hasher_md5>()->process_single_string( str ).xorHalve(); | |
| 62 } | |
| 63 private: | |
| 64 titleformat_object::ptr m_keyObj; | |
| 65 }; | |
| 66 | |
| 67 static metadb_index_client_impl * clientByGUID( const GUID & guid ) { | |
| 68 // Static instances, never destroyed (deallocated with the process), created first time we get here | |
| 69 // Using service_impl_single_t, reference counting disabled | |
| 70 // This is somewhat ugly, operating on raw pointers instead of service_ptr, but OK for this purpose | |
| 71 static metadb_index_client_impl * g_clientTrack = new service_impl_single_t<metadb_index_client_impl>(strTrackRatingPinTo); | |
| 72 static metadb_index_client_impl * g_clientAlbum = new service_impl_single_t<metadb_index_client_impl>(strAlbumRatingPinTo); | |
| 73 | |
| 74 PFC_ASSERT( guid == guid_foo_sample_album_rating_index || guid == guid_foo_sample_track_rating_index ); | |
| 75 return (guid == guid_foo_sample_album_rating_index ) ? g_clientAlbum : g_clientTrack; | |
| 76 } | |
| 77 | |
| 78 // Static cached ptr to metadb_index_manager | |
| 79 // Cached because we'll be calling it a lot on per-track basis, let's not pass it everywhere to low level functions | |
| 80 // Obtaining the pointer from core is reasonably efficient - log(n) to the number of known service classes, but not good enough for something potentially called hundreds of times | |
| 81 static metadb_index_manager::ptr theAPI() { | |
| 82 // Raw ptr instead service_ptr, don't release on DLL unload, would cause issues | |
| 83 static metadb_index_manager * cached = metadb_index_manager::get().detach(); | |
| 84 return cached; | |
| 85 } | |
| 86 | |
| 87 // An init_stage_callback to hook ourselves into the metadb | |
| 88 // We need to do this properly early to prevent dispatch_global_refresh() from new fields that we added from hammering playlists etc | |
| 89 class init_stage_callback_impl : public init_stage_callback { | |
| 90 public: | |
| 91 void on_init_stage(t_uint32 stage) { | |
| 92 if (stage == init_stages::before_config_read) { | |
| 93 auto api = theAPI(); | |
| 94 // Important, handle the exceptions here! | |
| 95 // This will fail if the files holding our data are somehow corrupted. | |
| 96 try { | |
| 97 api->add(clientByGUID(guid_foo_sample_track_rating_index), guid_foo_sample_track_rating_index, retentionPeriod); | |
| 98 api->add(clientByGUID(guid_foo_sample_album_rating_index), guid_foo_sample_album_rating_index, retentionPeriod); | |
| 99 } catch (std::exception const & e) { | |
| 100 api->remove(guid_foo_sample_track_rating_index); | |
| 101 api->remove(guid_foo_sample_album_rating_index); | |
| 102 FB2K_console_formatter() << "[foo_sample rating] Critical initialization failure: " << e; | |
| 103 return; | |
| 104 } | |
| 105 api->dispatch_global_refresh(); | |
| 106 } | |
| 107 } | |
| 108 }; | |
| 109 | |
| 110 static service_factory_single_t<init_stage_callback_impl> g_init_stage_callback_impl; | |
| 111 | |
| 112 typedef uint32_t rating_t; | |
| 113 static const rating_t rating_invalid = 0; | |
| 114 static const rating_t rating_max = 5; | |
| 115 | |
| 116 struct record_t { | |
| 117 rating_t m_rating = rating_invalid; | |
| 118 pfc::string8 m_comment; | |
| 119 }; | |
| 120 | |
| 121 static record_t record_get(const GUID & indexID, metadb_index_hash hash) { | |
| 122 mem_block_container_impl temp; // this will receive our BLOB | |
| 123 theAPI()->get_user_data( indexID, hash, temp ); | |
| 124 if ( temp.get_size() > 0 ) { | |
| 125 try { | |
| 126 // Parse the BLOB using stream formatters | |
| 127 stream_reader_formatter_simple_ref< /* using big endian data? nope */ false > reader(temp.get_ptr(), temp.get_size()); | |
| 128 | |
| 129 record_t ret; | |
| 130 reader >> ret.m_rating; // little endian uint32 got | |
| 131 | |
| 132 if ( reader.get_remaining() > 0 ) { | |
| 133 // more data left in the stream? | |
| 134 // note that this is a stream_reader_formatter_simple_ref method, regular stream formatters do not know the size or seek around, only read the stream sequentially | |
| 135 reader >> ret.m_comment; // this reads uint32 prefix indicating string size in bytes, then the actual string in UTF-8 characters } | |
| 136 } // otherwise we leave the string empty | |
| 137 | |
| 138 // if we attempted to read past the EOF, we'd land in the exception_io_data handler below | |
| 139 | |
| 140 return ret; | |
| 141 | |
| 142 } catch (exception_io_data const &) { | |
| 143 // we get here as a result of stream formatter data error | |
| 144 // fall thru to return a blank record | |
| 145 } | |
| 146 } | |
| 147 return record_t(); | |
| 148 } | |
| 149 | |
| 150 void record_set( const GUID & indexID, metadb_index_hash hash, const record_t & record) { | |
| 151 | |
| 152 stream_writer_formatter_simple< /* using bing endian data? nope */ false > writer; | |
| 153 writer << record.m_rating; | |
| 154 if ( record.m_comment.length() > 0 ) { | |
| 155 // bother with this only if the comment is not blank | |
| 156 writer << record.m_comment; // uint32 size + UTF-8 bytes | |
| 157 } | |
| 158 | |
| 159 theAPI()->set_user_data( indexID, hash, writer.m_buffer.get_ptr(), writer.m_buffer.get_size() ); | |
| 160 } | |
| 161 | |
| 162 static rating_t rating_get( const GUID & indexID, metadb_index_hash hash) { | |
| 163 return record_get(indexID, hash).m_rating; | |
| 164 } | |
| 165 | |
| 166 | |
| 167 // Returns true if the value was actually changed | |
| 168 static bool rating_set( const GUID & indexID, metadb_index_hash hash, rating_t rating) { | |
| 169 bool bChanged = false; | |
| 170 auto rec = record_get(indexID, hash); | |
| 171 if ( rec.m_rating != rating ) { | |
| 172 rec.m_rating = rating; | |
| 173 record_set( indexID, hash, rec); | |
| 174 bChanged = true; | |
| 175 } | |
| 176 return bChanged; | |
| 177 } | |
| 178 | |
| 179 static bool comment_set( const GUID & indexID, metadb_index_hash hash, const char * strComment ) { | |
| 180 auto rec = record_get(indexID, hash ); | |
| 181 bool bChanged = false; | |
| 182 if ( ! rec.m_comment.equals( strComment ) ) { | |
| 183 rec.m_comment = strComment; | |
| 184 record_set(indexID, hash, rec); | |
| 185 bChanged = true; | |
| 186 } | |
| 187 return bChanged; | |
| 188 } | |
| 189 | |
| 190 // Provider of our title formatting fields. | |
| 191 class metadb_display_field_provider_impl : public metadb_display_field_provider_v2 { | |
| 192 public: | |
| 193 t_uint32 get_field_count() override { | |
| 194 return 6; | |
| 195 } | |
| 196 void get_field_name(t_uint32 index, pfc::string_base & out) override { | |
| 197 PFC_ASSERT(index < get_field_count()); | |
| 198 switch(index) { | |
| 199 case 0: | |
| 200 out = "foo_sample_track_rating"; break; | |
| 201 case 1: | |
| 202 out = "foo_sample_album_rating"; break; | |
| 203 case 2: | |
| 204 out = "foo_sample_track_comment"; break; | |
| 205 case 3: | |
| 206 out = "foo_sample_album_comment"; break; | |
| 207 case 4: | |
| 208 out = "foo_sample_track_hash"; break; | |
| 209 case 5: | |
| 210 out = "foo_sample_album_hash"; break; | |
| 211 default: | |
| 212 PFC_ASSERT(!"Should never get here"); | |
| 213 } | |
| 214 } | |
| 215 | |
| 216 bool process_field(t_uint32 index, metadb_handle* handle, titleformat_text_out* out) override { | |
| 217 return process_field_v2(index, handle, handle->query_v2_(), out); | |
| 218 } | |
| 219 bool process_field_v2(t_uint32 index, metadb_handle * handle, metadb_v2::rec_t const& metarec, titleformat_text_out * out) override { | |
| 220 PFC_ASSERT( index < get_field_count() ); | |
| 221 | |
| 222 const GUID whichID = ((index%2) == 1) ? guid_foo_sample_album_rating_index : guid_foo_sample_track_rating_index; | |
| 223 | |
| 224 if (!metarec.info.is_valid()) return false; | |
| 225 | |
| 226 record_t rec; | |
| 227 const auto hash = clientByGUID(whichID)->transform(metarec.info->info(), handle->get_location()); | |
| 228 | |
| 229 if ( index < 4 ) { | |
| 230 rec = record_get(whichID, hash); | |
| 231 } | |
| 232 | |
| 233 | |
| 234 if ( index < 2 ) { | |
| 235 // rating | |
| 236 | |
| 237 if (rec.m_rating == rating_invalid) return false; | |
| 238 | |
| 239 out->write_int(titleformat_inputtypes::meta, rec.m_rating); | |
| 240 | |
| 241 return true; | |
| 242 } else if ( index < 4 ) { | |
| 243 // comment | |
| 244 | |
| 245 if ( rec.m_comment.length() == 0 ) return false; | |
| 246 | |
| 247 out->write( titleformat_inputtypes::meta, rec.m_comment.c_str() ); | |
| 248 | |
| 249 return true; | |
| 250 } else { | |
| 251 out->write(titleformat_inputtypes::meta, pfc::format_hex(hash,16) ); | |
| 252 return true; | |
| 253 } | |
| 254 } | |
| 255 }; | |
| 256 | |
| 257 static service_factory_single_t<metadb_display_field_provider_impl> g_metadb_display_field_provider_impl; | |
| 258 | |
| 259 static void cycleRating( const GUID & whichID, metadb_handle_list_cref tracks) { | |
| 260 | |
| 261 const size_t count = tracks.get_count(); | |
| 262 if (count == 0) return; | |
| 263 | |
| 264 auto client = clientByGUID(whichID); | |
| 265 | |
| 266 rating_t rating = rating_invalid; | |
| 267 | |
| 268 // Sorted/dedup'd set of all hashes of p_data items. | |
| 269 // pfc::avltree_t<> is pfc equivalent of std::set<>. | |
| 270 // We go the avltree_t<> route because more than one track in p_data might produce the same hash value, see metadb_index_client_impl / strPinTo | |
| 271 pfc::avltree_t<metadb_index_hash> allHashes; | |
| 272 for (size_t w = 0; w < count; ++w) { | |
| 273 metadb_index_hash hash; | |
| 274 if (client->hashHandle(tracks[w], hash)) { | |
| 275 allHashes += hash; | |
| 276 | |
| 277 // Take original rating to increment from the first selected song | |
| 278 if (w == 0) rating = rating_get(whichID, hash); | |
| 279 } | |
| 280 } | |
| 281 | |
| 282 if (allHashes.get_count() == 0) { | |
| 283 FB2K_console_formatter() << "[foo_sample rating] Could not hash any of the tracks due to unavailable metadata, bailing"; | |
| 284 return; | |
| 285 } | |
| 286 | |
| 287 // Now cycle the rating value | |
| 288 if (rating == rating_invalid) rating = 1; | |
| 289 else if (rating >= rating_max) rating = rating_invalid; | |
| 290 else ++rating; | |
| 291 | |
| 292 // Now set the new rating | |
| 293 pfc::list_t<metadb_index_hash> lstChanged; // Linear list of hashes that actually changed | |
| 294 for (auto iter = allHashes.first(); iter.is_valid(); ++iter) { | |
| 295 const metadb_index_hash hash = *iter; | |
| 296 if (rating_set(whichID, hash, rating) ) { // rating_set returns true if the value actually changed, false if old equals new and no change was made | |
| 297 lstChanged += hash; | |
| 298 } | |
| 299 } | |
| 300 | |
| 301 FB2K_console_formatter() << "[foo_sample rating] " << lstChanged.get_count() << " entries updated"; | |
| 302 if (lstChanged.get_count() > 0) { | |
| 303 // This gracefully tells everyone about what just changed, in one pass regardless of how many items got altered | |
| 304 theAPI()->dispatch_refresh(whichID, lstChanged); | |
| 305 } | |
| 306 | |
| 307 } | |
| 308 | |
| 309 static void cycleComment( const GUID & whichID, metadb_handle_list_cref tracks ) { | |
| 310 const size_t count = tracks.get_count(); | |
| 311 if (count == 0) return; | |
| 312 | |
| 313 auto client = clientByGUID(whichID); | |
| 314 | |
| 315 pfc::string8 comment; | |
| 316 | |
| 317 // Sorted/dedup'd set of all hashes of p_data items. | |
| 318 // pfc::avltree_t<> is pfc equivalent of std::set<>. | |
| 319 // We go the avltree_t<> route because more than one track in p_data might produce the same hash value, see metadb_index_client_impl / strPinTo | |
| 320 pfc::avltree_t<metadb_index_hash> allHashes; | |
| 321 for (size_t w = 0; w < count; ++w) { | |
| 322 metadb_index_hash hash; | |
| 323 if (client->hashHandle(tracks[w], hash)) { | |
| 324 allHashes += hash; | |
| 325 | |
| 326 // Take original rating to increment from the first selected song | |
| 327 if (w == 0) comment = record_get(whichID, hash).m_comment; | |
| 328 } | |
| 329 } | |
| 330 | |
| 331 if (allHashes.get_count() == 0) { | |
| 332 FB2K_console_formatter() << "[foo_sample rating] Could not hash any of the tracks due to unavailable metadata, bailing"; | |
| 333 return; | |
| 334 } | |
| 335 | |
| 336 // Now cycle the comment value | |
| 337 if ( comment.equals("") ) comment = "foo"; | |
| 338 else if ( comment.equals("foo") ) comment = "bar"; | |
| 339 else comment = ""; | |
| 340 | |
| 341 // Now apply the new comment | |
| 342 pfc::list_t<metadb_index_hash> lstChanged; // Linear list of hashes that actually changed | |
| 343 for (auto iter = allHashes.first(); iter.is_valid(); ++iter) { | |
| 344 const metadb_index_hash hash = *iter; | |
| 345 | |
| 346 if ( comment_set(whichID, hash, comment) ) { | |
| 347 lstChanged += hash; | |
| 348 } | |
| 349 } | |
| 350 | |
| 351 FB2K_console_formatter() << "[foo_sample rating] " << lstChanged.get_count() << " entries updated"; | |
| 352 if (lstChanged.get_count() > 0) { | |
| 353 // This gracefully tells everyone about what just changed, in one pass regardless of how many items got altered | |
| 354 theAPI()->dispatch_refresh(whichID, lstChanged); | |
| 355 } | |
| 356 } | |
| 357 | |
| 358 class contextmenu_rating : public contextmenu_item_simple { | |
| 359 public: | |
| 360 GUID get_parent() { | |
| 361 return guid_foo_sample_contextmenu_group; | |
| 362 } | |
| 363 unsigned get_num_items() { | |
| 364 return 4; | |
| 365 } | |
| 366 void get_item_name(unsigned p_index, pfc::string_base & p_out) { | |
| 367 PFC_ASSERT( p_index < get_num_items() ); | |
| 368 switch(p_index) { | |
| 369 case 0: | |
| 370 p_out = "Cycle track rating"; break; | |
| 371 case 1: | |
| 372 p_out = "Cycle album rating"; break; | |
| 373 case 2: | |
| 374 p_out = "Cycle track comment"; break; | |
| 375 case 3: | |
| 376 p_out = "Cycle album comment"; break; | |
| 377 } | |
| 378 | |
| 379 } | |
| 380 void context_command(unsigned p_index, metadb_handle_list_cref p_data, const GUID& p_caller) { | |
| 381 PFC_ASSERT( p_index < get_num_items() ); | |
| 382 | |
| 383 const GUID whichID = ((p_index%2) == 1) ? guid_foo_sample_album_rating_index : guid_foo_sample_track_rating_index; | |
| 384 | |
| 385 if ( p_index < 2 ) { | |
| 386 // rating | |
| 387 cycleRating( whichID, p_data ); | |
| 388 } else { | |
| 389 cycleComment( whichID, p_data ); | |
| 390 | |
| 391 } | |
| 392 | |
| 393 } | |
| 394 GUID get_item_guid(unsigned p_index) { | |
| 395 switch(p_index) { | |
| 396 case 0: return guid_foo_sample_rating_contextmenu1; | |
| 397 case 1: return guid_foo_sample_rating_contextmenu2; | |
| 398 case 2: return guid_foo_sample_rating_contextmenu3; | |
| 399 case 3: return guid_foo_sample_rating_contextmenu4; | |
| 400 default: uBugCheck(); | |
| 401 } | |
| 402 } | |
| 403 bool get_item_description(unsigned p_index, pfc::string_base & p_out) { | |
| 404 PFC_ASSERT( p_index < get_num_items() ); | |
| 405 switch( p_index ) { | |
| 406 case 0: | |
| 407 p_out = "Alters foo_sample's track rating on one or more selected tracks. Use %foo_sample_track_rating% to display the rating."; | |
| 408 return true; | |
| 409 case 1: | |
| 410 p_out = "Alters foo_sample's album rating on one or more selected tracks. Use %foo_sample_album_rating% to display the rating."; | |
| 411 return true; | |
| 412 case 2: | |
| 413 p_out = "Alters foo_sample's track comment on one or more selected tracks. Use %foo_sample_track_comment% to display the comment."; | |
| 414 return true; | |
| 415 case 3: | |
| 416 p_out = "Alters foo_sample's album comment on one or more selected tracks. Use %foo_sample_album_comment% to display the comment."; | |
| 417 return true; | |
| 418 default: | |
| 419 PFC_ASSERT(!"Should not get here"); | |
| 420 return false; | |
| 421 } | |
| 422 } | |
| 423 }; | |
| 424 | |
| 425 static contextmenu_item_factory_t< contextmenu_rating > g_contextmenu_rating; | |
| 426 | |
| 427 static pfc::string_formatter formatRatingDump(const GUID & whichID) { | |
| 428 auto api = theAPI(); | |
| 429 pfc::list_t<metadb_index_hash> hashes; | |
| 430 api->get_all_hashes(whichID, hashes); | |
| 431 pfc::string_formatter message; | |
| 432 message << "The database contains " << hashes.get_count() << " hashes.\n"; | |
| 433 for( size_t hashWalk = 0; hashWalk < hashes.get_count(); ++ hashWalk ) { | |
| 434 auto hash = hashes[hashWalk]; | |
| 435 message << pfc::format_hex( hash, 8 ) << " : "; | |
| 436 auto rec = record_get(whichID, hash); | |
| 437 if ( rec.m_rating == rating_invalid ) message << "no rating"; | |
| 438 else message << "rating " << rec.m_rating; | |
| 439 if ( rec.m_comment.length() > 0 ) { | |
| 440 message << ", comment: " << rec.m_comment; | |
| 441 } | |
| 442 | |
| 443 metadb_handle_list tracks; | |
| 444 | |
| 445 // Note that this returns only handles present in the media library | |
| 446 | |
| 447 // Extra work is required if the user has no media library but only playlists, | |
| 448 // have to walk the playlists and match hashes by yourself instead of calling this method | |
| 449 api->get_ML_handles(whichID, hash, tracks); | |
| 450 | |
| 451 | |
| 452 if ( tracks.get_count() == 0 ) message << ", no matching tracks in Media Library\n"; | |
| 453 else { | |
| 454 message << ", " << tracks.get_count() << " matching track(s)\n"; | |
| 455 for( size_t w = 0; w < tracks.get_count(); ++ w ) { | |
| 456 // pfc string formatter operator<< for metadb_handle prints the location | |
| 457 message << tracks[w] << "\n"; | |
| 458 } | |
| 459 } | |
| 460 } | |
| 461 | |
| 462 return message; | |
| 463 } | |
| 464 | |
| 465 class mainmenu_rating : public mainmenu_commands { | |
| 466 public: | |
| 467 t_uint32 get_command_count() { | |
| 468 return 1; | |
| 469 } | |
| 470 GUID get_command(t_uint32 p_index) { | |
| 471 return guid_foo_sample_rating_mainmenu; | |
| 472 } | |
| 473 void get_name(t_uint32 p_index, pfc::string_base & p_out) { | |
| 474 PFC_ASSERT( p_index == 0 ); | |
| 475 p_out = "Dump rating database"; | |
| 476 } | |
| 477 bool get_description(t_uint32 p_index, pfc::string_base & p_out) { | |
| 478 PFC_ASSERT(p_index == 0); | |
| 479 p_out = "Shows a dump of the foo_sample rating database."; return true; | |
| 480 } | |
| 481 GUID get_parent() { | |
| 482 return guid_foo_sample_mainmenu_group; | |
| 483 } | |
| 484 void execute(t_uint32 p_index, service_ptr_t<service_base> p_callback) { | |
| 485 PFC_ASSERT( p_index == 0 ); | |
| 486 | |
| 487 try { | |
| 488 | |
| 489 pfc::string_formatter dump; | |
| 490 dump << "==== TRACK RATING DUMP ====\n" << formatRatingDump( guid_foo_sample_track_rating_index ) << "\n\n"; | |
| 491 dump << "==== ALBUM RATING DUMP ====\n" << formatRatingDump( guid_foo_sample_album_rating_index ) << "\n\n"; | |
| 492 | |
| 493 popup_message::g_show(dump, "foo_sample rating dump"); | |
| 494 } catch(std::exception const & e) { | |
| 495 // should not really get here | |
| 496 popup_message::g_complain("Rating dump failed", e); | |
| 497 } | |
| 498 } | |
| 499 }; | |
| 500 static service_factory_single_t<mainmenu_rating> g_mainmenu_rating; | |
| 501 | |
| 502 | |
| 503 // This class provides our information for the properties dialog | |
| 504 class track_property_provider_impl : public track_property_provider_v2 { | |
| 505 public: | |
| 506 void workThisIndex(GUID const & whichID, const char * whichName, double priorityBase, metadb_handle_list_cref p_tracks, track_property_callback & p_out) { | |
| 507 auto client = clientByGUID( whichID ); | |
| 508 pfc::avltree_t<metadb_index_hash> hashes; | |
| 509 const size_t trackCount = p_tracks.get_count(); | |
| 510 for (size_t trackWalk = 0; trackWalk < trackCount; ++trackWalk) { | |
| 511 metadb_index_hash hash; | |
| 512 if (client->hashHandle(p_tracks[trackWalk], hash)) { | |
| 513 hashes += hash; | |
| 514 } | |
| 515 } | |
| 516 | |
| 517 pfc::string8 strAverage = "N/A", strMin = "N/A", strMax = "N/A"; | |
| 518 pfc::string8 strComment; | |
| 519 | |
| 520 { | |
| 521 size_t count = 0; | |
| 522 rating_t minval = rating_invalid; | |
| 523 rating_t maxval = rating_invalid; | |
| 524 uint64_t accum = 0; | |
| 525 bool bFirst = true; | |
| 526 bool bVarComments = false; | |
| 527 for (auto i = hashes.first(); i.is_valid(); ++i) { | |
| 528 auto rec = record_get(whichID, *i); | |
| 529 auto r = rec.m_rating; | |
| 530 if (r != rating_invalid) { | |
| 531 ++count; | |
| 532 accum += r; | |
| 533 | |
| 534 if (minval == rating_invalid || minval > r) minval = r; | |
| 535 if (maxval == rating_invalid || maxval < r) maxval = r; | |
| 536 } | |
| 537 | |
| 538 | |
| 539 if ( bFirst ) { | |
| 540 strComment = rec.m_comment; | |
| 541 } else if ( ! bVarComments ) { | |
| 542 if ( strComment != rec.m_comment ) { | |
| 543 bVarComments = true; | |
| 544 strComment = "<various>"; | |
| 545 } | |
| 546 } | |
| 547 | |
| 548 bFirst = false; | |
| 549 } | |
| 550 | |
| 551 | |
| 552 if (count > 0) { | |
| 553 strMin = pfc::format_uint(minval); | |
| 554 strMax = pfc::format_uint(maxval); | |
| 555 strAverage = pfc::format_float((double)accum / (double)count, 0, 3); | |
| 556 } | |
| 557 } | |
| 558 | |
| 559 p_out.set_property(strPropertiesGroup, priorityBase + 0, PFC_string_formatter() << "Average " << whichName << " Rating", strAverage); | |
| 560 p_out.set_property(strPropertiesGroup, priorityBase + 1, PFC_string_formatter() << "Minimum " << whichName << " Rating", strMin); | |
| 561 p_out.set_property(strPropertiesGroup, priorityBase + 2, PFC_string_formatter() << "Maximum " << whichName << " Rating", strMax); | |
| 562 if ( strComment.length() > 0 ) { | |
| 563 p_out.set_property(strPropertiesGroup, priorityBase + 3, PFC_string_formatter() << whichName << " Comment", strComment); | |
| 564 } | |
| 565 } | |
| 566 void enumerate_properties(metadb_handle_list_cref p_tracks, track_property_callback & p_out) { | |
| 567 workThisIndex( guid_foo_sample_track_rating_index, "Track", 0, p_tracks, p_out ); | |
| 568 workThisIndex( guid_foo_sample_album_rating_index, "Album", 10, p_tracks, p_out); | |
| 569 } | |
| 570 void enumerate_properties_v2(metadb_handle_list_cref p_tracks, track_property_callback_v2 & p_out) { | |
| 571 if ( p_out.is_group_wanted( strPropertiesGroup ) ) { | |
| 572 enumerate_properties( p_tracks, p_out ); | |
| 573 } | |
| 574 } | |
| 575 | |
| 576 bool is_our_tech_info(const char * p_name) { | |
| 577 // If we do stuff with tech infos read from the file itself (see file_info::info_* methods), signal whether this field belongs to us | |
| 578 // We don't do any of this, hence false | |
| 579 return false; | |
| 580 } | |
| 581 | |
| 582 }; | |
| 583 | |
| 584 | |
| 585 static service_factory_single_t<track_property_provider_impl> g_track_property_provider_impl; | |
| 586 } |
