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 }