|
1
|
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 }
|