1 /// 2 /// Template method implementations for music library. 3 /// @file musiclibraryimpl.h - pianod 4 /// @author Perette Barella 5 /// @date 2014-12-09 6 /// @copyright Copyright 2014-2020 Devious Fish. All rights reserved. 7 /// 8 9 #pragma once 10 11 #include "fileio.h" 12 #include "parsnip.h" 13 #include "fundamentals.h" 14 #include "utility.h" 15 #include "musickeys.h" 16 17 namespace MusicLibrary { 18 19 /* 20 * Supporting functions 21 */ persistentId(MusicThingie::Type item_type,const std::string & key)22 inline std::string persistentId (MusicThingie::Type item_type, 23 const std::string &key) { 24 const char type_code [2] = { static_cast <char> (item_type), '\0' }; 25 return type_code + std::to_string (create_key_from_string (key)); 26 } 27 28 29 30 /** 31 * Customized hash tables. 32 * 33 * These add persist/restore capabilities, and provide for items 34 * that are cross-linked with parent objects. 35 */ 36 template <class TThing, class TParent> ~ThingieContainer()37 ThingieContainer<TThing, TParent>::~ThingieContainer() { 38 #ifndef NDEBUG 39 for (auto &item : *this) { 40 assert (item.second->getUseCount() == 1); 41 } 42 #endif 43 clear(); 44 } 45 46 /** Remove items according to the predicate. 47 @param pred Predicate, which returns true to indicate removal. */ 48 template <class TThing, class TParent> purge(bool pred (const TThing *))49 void ThingieContainer<TThing, TParent>::purge (bool pred(const TThing *)) { 50 auto it = this->begin(); 51 while (it != this->end()) { 52 auto element = it++; 53 if (pred (element->second)) { 54 element->second->release(); 55 this->erase (element); 56 } 57 } 58 } 59 60 61 /** Remove all items from the hash table. */ 62 template <class TThing, class TParent> clear()63 void ThingieContainer<TThing, TParent>::clear() { 64 while (this->begin() != this->end()) { 65 this->begin()->second->release(); 66 this->erase (this->begin()); 67 } 68 } 69 70 /** Get a thing by its id. */ 71 template <class TThing, class TParent> getById(const std::string & key)72 TThing *ThingieContainer<TThing, TParent>::getById (const std::string &key) const { 73 const auto iterator = this->find (key); 74 return (iterator == this->end() ? nullptr : iterator->second); 75 } 76 77 /** Get a thing by its id, getting the ID from JSON data. */ 78 template <class TThing, class TParent> getById(const Parsnip::SerialData & data,const char * field)79 TThing *ThingieContainer<TThing, TParent>::getById (const Parsnip::SerialData &data, 80 const char *field) { 81 return getById (data [field].asString()); 82 } 83 84 85 /** Search the things looking for a name and parent match. */ 86 template <class TThing, class TParent> getByName(const std::string & name,TParent * parent)87 TThing *ThingieContainer<TThing, TParent>::getByName(const std::string &name, 88 TParent *parent) const { 89 for (auto item : *this) { 90 if (item.second->parent() == parent && *(item.second) == name) { 91 return item.second; 92 } 93 } 94 return nullptr; 95 } 96 97 98 /** Construct unique, random ID for a new item. 99 @param item_type The type code for the item. 100 @return The unique ID string. */ 101 template <class TThing, class TParent> getNewId(MusicThingie::Type item_type)102 std::string ThingieContainer<TThing, TParent>::getNewId (MusicThingie::Type item_type) const { 103 const char type [2] = { static_cast <char> (item_type), '\0' }; 104 while (true) { 105 long candidate = random(); 106 std::string newid = type + std::to_string (candidate); 107 if (!getById (newid)) 108 return newid; 109 } 110 } 111 112 113 /** Construct a new item instance and add it to the hash by its ID. 114 @param name The new item's name. 115 @param id The item's ID, if known, or the empty string. 116 @param parent The parent object to which the new item will be linked. */ 117 template <class TThing, class TParent> addItem(const std::string & name,std::string id,TParent * parent)118 TThing *ThingieContainer<TThing, TParent>::addItem (const std::string &name, 119 std::string id, TParent *parent) { 120 std::string newid (id.empty() ? getNewId (TThing::typetype ()) : id); 121 TThing *thing = new TThing (parent, newid, name); 122 assert (this->find (newid) == this->end()); 123 (*this) [newid] = thing; 124 thing->retain(); 125 return thing; 126 } 127 128 /** Retrieve an item by ID or by name. 129 - If ID is given, that is used. 130 - Otherwise, a "persistent ID" based on hashing the name is tried; 131 if a match is found, use that. 132 - Otherwise, try searching by name and parent. If found, use that. 133 - Otherwise, create a new item with the name and a new ID assigned. 134 @param name The name to search for. 135 @param id The ID to search for. 136 @param parent The parent item. */ 137 template <class TThing, class TParent> addOrGetItem(const std::string & name,std::string id,TParent * parent)138 TThing *ThingieContainer<TThing, TParent>::addOrGetItem (const std::string &name, 139 std::string id, TParent *parent) { 140 assert (parent); 141 TThing *thing; 142 if (id.empty()) { 143 // Hash the name for a candidate ID 144 id = persistentId (TThing::typetype (), name); 145 thing = getById (id); 146 if (thing && 147 thing->parent() == parent && 148 *thing == name) { 149 return thing; 150 } 151 // If there was no match, leave ID set to the candidate ID. 152 if (thing) 153 id = ""; 154 thing = getByName (name, parent); 155 } else { 156 thing = getById (id); 157 } 158 if (thing) 159 return thing; 160 return addItem (name, id, parent); 161 } 162 163 /** Reconstitute an item from a persisted file. 164 @param node The JSON node with data to be restored. 165 @param parent The parent to which the new record will be attached. 166 @return A newly constructed item, attached to its parent. */ 167 template <class TThing, class TParent> addOrGetItem(const Parsnip::SerialData & data,TParent * parent,const std::string & namefield,const std::string & idfield)168 TThing *ThingieContainer<TThing, TParent>::addOrGetItem (const Parsnip::SerialData &data, 169 TParent *parent, 170 const std::string &namefield, 171 const std::string &idfield) { 172 const std::string &name = data [namefield].asString(); 173 const std::string &id = data [idfield].asString(); 174 TThing *item = addOrGetItem (name, id, parent); 175 assert (item); 176 item->restore (data); 177 return item; 178 } 179 180 181 182 /* 183 * Library Template 184 */ 185 186 template <class TSong, class TAlbum, class TArtist> purge(void)187 void Library<TSong, TAlbum, TArtist>::purge (void) { 188 albums.purge ([] (const TAlbum *album)->bool { 189 return (album->getUseCount() == 1 190 && album->empty()); 191 }); 192 artists.purge ([] (const TArtist *artist)->bool { 193 return (artist->getUseCount() == 1 194 && artist->empty()); 195 }); 196 } 197 198 template <class TSong, class TAlbum, class TArtist> removePlaylist(Playlist * play)199 bool Library<TSong, TAlbum, TArtist>::removePlaylist (Playlist *play) { 200 auto it = playlists.find (play->playlistId()); 201 assert (it != playlists.end()); 202 playlists.erase (it); 203 unpopulatePlaylist (play); 204 play->release(); 205 return true; 206 }; 207 208 template <class TSong, class TAlbum, class TArtist> seedsForPlaylist(const Playlist * playlist)209 ThingieList Library<TSong, TAlbum, TArtist>::seedsForPlaylist (const Playlist *playlist) { 210 ThingieList results; 211 for (auto &item : playlist->seeds) { 212 assert (!item.empty()); 213 // Type code is the first character of the ID. 214 const MusicThingie::Type type = (MusicThingie::Type) item.at (0); 215 MusicThingie *thing = nullptr; 216 switch (type) { 217 case MusicThingie::Type::Song: 218 thing = songs.getById (item); 219 break; 220 case MusicThingie::Type::Album: 221 thing = albums.getById (item); 222 break; 223 case MusicThingie::Type::Artist: 224 thing = artists.getById (item); 225 break; 226 default: 227 assert (0); 228 break; 229 } 230 if (thing) { 231 results.push_back (thing); 232 } 233 } 234 return results; 235 } 236 237 238 /// Get a list of all songs in the library. 239 template <class TSong, class TAlbum, class TArtist> getAllSongs(void)240 SongList Library<TSong, TAlbum, TArtist>::getAllSongs (void) { 241 SongList list; 242 list.reserve (songs.size()); 243 for (auto item : songs) { 244 list.push_back (item.second); 245 } 246 return list; 247 } 248 249 /// Get a list of all songs matching a filter. 250 template <class TSong, class TAlbum, class TArtist> getMatchingSongs(const Filter & criteria)251 SongList Library<TSong, TAlbum, TArtist>::getMatchingSongs (const Filter &criteria) { 252 SongList list; 253 list.reserve (songs.size()); 254 for (auto &item : songs) { 255 if (criteria.matches (item.second)) { 256 list.push_back (item.second); 257 } 258 } 259 return list; 260 } 261 262 /** Retrieve suggestions. Whereas getMatchingSongs only 263 returns songs (several songs on an artist match), this 264 returns only the artist or album and not more discrete 265 data when an encompassing concept matches. */ 266 template <class TSong, class TAlbum, class TArtist> getSuggestions(const Filter & criteria,SearchRange what)267 ThingieList Library<TSong, TAlbum, TArtist>::getSuggestions (const Filter &criteria, 268 SearchRange what) { 269 bool exhaustive = deepSearch (what); 270 ThingieList list; 271 for (auto &artist : artists) { 272 bool matches_artist = criteria.matches (artist.second); 273 if (matches_artist) 274 list.push_back (artist.second); 275 if (!matches_artist || exhaustive) { 276 for (auto album : artist.second->getAlbums()) { 277 bool matches_album = criteria.matches (album); 278 if (matches_album) 279 list.push_back (album); 280 if (!matches_album || exhaustive) { 281 for (auto song : album->getSongs()) { 282 if (criteria.matches (song)) { 283 list.push_back (song); 284 } 285 } 286 } 287 } 288 } 289 } 290 return list; 291 } 292 293 294 /// Get a list of all songs belonging to enabled playlists. 295 template <class TSong, class TAlbum, class TArtist> getMixSongs(void)296 SongList Library<TSong, TAlbum, TArtist>::getMixSongs (void) { 297 SongList list; 298 list.reserve (songs.size()); 299 for (auto item : songs) { 300 if (item.second->_playlist && item.second->_playlist->enabled) { 301 list.push_back (item.second); 302 } 303 } 304 return list; 305 } 306 307 308 /** Get a list of all songs assigned to a playlist. 309 @param reassess If false, only assigned songs are returned. 310 If true, songs in other playlists are also considered. */ 311 template <class TSong, class TAlbum, class TArtist> getPlaylistSongs(const Playlist * play,bool reassess)312 SongList Library<TSong, TAlbum, TArtist>::getPlaylistSongs (const Playlist *play, bool reassess) { 313 SongList list; 314 list.reserve (songs.size()); 315 for (auto item : songs) { 316 if ((item.second->_playlist == play) || 317 (reassess && item.second->_playlist && play->appliesTo (item.second))) { 318 list.push_back (item.second); 319 } 320 } 321 return list; 322 } 323 324 325 /** Find a playlist for a song, preferring enabled playlists. 326 First, aim for enabled playlists; 327 if not found then recurse and aim for disabled playlists. */ 328 template <class TSong, class TAlbum, class TArtist> findPlaylistForSong(TSong * song,bool enabled)329 Playlist *Library<TSong, TAlbum, TArtist>::findPlaylistForSong (TSong *song, bool enabled) { 330 for (auto playlist : playlists) { 331 if (playlist.second->enabled == enabled) { 332 if (playlist.second->appliesTo (song)) { 333 return playlist.second; 334 } 335 } 336 } 337 if (enabled == false) return nullptr; 338 return findPlaylistForSong (song, false); 339 } 340 341 342 /** Review songs and assign them to a new candidate if they match. This is 343 applicable when a playlist has been created or just been added to the mix. 344 @param aggressive If true, songs with enabled playlists are considered for reassignment. 345 If false, only songs without a playlist or with a disabled playlist are considered. */ 346 template <class TSong, class TAlbum, class TArtist> populatePlaylist(Playlist * play,bool aggressive)347 void Library<TSong, TAlbum, TArtist>::populatePlaylist (Playlist *play, bool aggressive) { 348 for (auto item : songs) { 349 // Only grab songs that aren't assigned or whose playlist isn't enabled, unless we're aggressive 350 if (aggressive || !item.second->_playlist || !item.second->_playlist->enabled) { 351 // Don't waste time if we already own it 352 if (item.second->_playlist != play) { 353 if (play->appliesTo (item.second)) { 354 item.second->_playlist = play; 355 } 356 } 357 } 358 } 359 } 360 361 /** Reassign all a playlists' songs to some other playlist. 362 Applicable when song has been removed from the mix, or 363 playlist is about to be deleted. 364 @warning May reassign current playlists, if no better ones are found 365 and the current playlist is still in the playlist set. */ 366 template <class TSong, class TAlbum, class TArtist> unpopulatePlaylist(Playlist * play)367 void Library<TSong, TAlbum, TArtist>::unpopulatePlaylist (Playlist *play) { 368 for (auto &item : songs) { 369 // Reassign only if it belongs to this playlist 370 if (item.second->_playlist == play) { 371 item.second->_playlist = findPlaylistForSong (item.second); 372 } 373 } 374 } 375 /** Retrieve anything stored in the library by its ID. 376 @param type The type of the thing. 377 @param id The ID of the thing to retrieve. 378 @return The thing, or a nullptr if not found. */ 379 template <class TSong, class TAlbum, class TArtist> getById(MusicThingie::Type type,const std::string & id)380 MusicThingie *Library<TSong, TAlbum, TArtist>::getById (MusicThingie::Type type, 381 const std::string &id) { 382 MusicThingie *thing = nullptr; 383 switch (type) { 384 case MusicThingie::Type::Playlist: 385 { 386 MusicLibrary::Playlist *play = playlists.getById (id); 387 if (!play) return nullptr; 388 populatePlaylist (play, true); 389 thing = play; 390 break; 391 } 392 case MusicThingie::Type::Artist: 393 thing = artists.getById (id); 394 break; 395 case MusicThingie::Type::Album: 396 thing = albums.getById (id); 397 break; 398 case MusicThingie::Type::Song: 399 thing = songs.getById (id); 400 break; 401 default: 402 // Asked for a suggestion, seed, etc. 403 // Not used by this source, may nevertheless be asked for one. 404 break; 405 } 406 return thing; 407 } 408 409 410 /** Retrieve all songs for a playlist, which may be a meta playlist */ 411 template <class TSong, class TAlbum, class TArtist> getSongsForPlaylist(PianodPlaylist * playlist)412 SongList Library<TSong, TAlbum, TArtist>::getSongsForPlaylist (PianodPlaylist *playlist) { 413 switch (playlist->playlistType()) { 414 case PianodPlaylist::SINGLE: 415 case PianodPlaylist::TRANSIENT: 416 return playlist->songs(); 417 case PianodPlaylist::MIX: 418 return getMixSongs(); 419 case PianodPlaylist::EVERYTHING: 420 return getAllSongs(); 421 } 422 assert (0); 423 return SongList(); 424 } 425 426 427 template <class TSong, class TAlbum, class TArtist> createPlaylist(const std::string & name,MusicThingie::Type type,MusicThingie * from)428 PianodPlaylist *Library<TSong, TAlbum, TArtist>::createPlaylist (const std::string &name, 429 MusicThingie::Type type, 430 MusicThingie *from) { 431 assert (from); 432 assert (from->source() == source); 433 assert (!playlists.getByName (name, this)); 434 if (playlists.getByName (name, this)) 435 throw CommandError (E_DUPLICATE); 436 Playlist *play = playlists.addOrGetItem (name, this); 437 play->selector = Filter::None; 438 populatePlaylist (play); 439 markDirty (IMPORTANT); 440 try { 441 play->seed (type, from, true); 442 } catch (...) { 443 removePlaylist (play); 444 throw; 445 } 446 return play; 447 } 448 449 450 template <class TSong, class TAlbum, class TArtist> createPlaylist(const std::string & name,const Filter & filter)451 PianodPlaylist *Library<TSong, TAlbum, TArtist>::createPlaylist (const std::string &name, 452 const Filter &filter) { 453 assert (filter.canPersist()); 454 assert (!playlists.getByName (name, this)); 455 Playlist *play = playlists.addOrGetItem (name, this); 456 play->selector = filter; 457 populatePlaylist (play); 458 markDirty (IMPORTANT); 459 return play; 460 } 461 462 template <class TSong, class TAlbum, class TArtist> formTransientPlaylist(const Filter & criteria)463 PianodPlaylist *Library<TSong, TAlbum, TArtist>::formTransientPlaylist (const Filter &criteria) { 464 return new TransientPlaylist (this, criteria); 465 } 466 467 468 469 /** Persist library data into a file */ 470 template <class TSong, class TAlbum, class TArtist> writeIndexToFile(const std::string & filename)471 bool Library<TSong, TAlbum, TArtist>::writeIndexToFile (const std::string &filename) const { 472 try { 473 Parsnip::SerialData all_artists {Parsnip::SerialData::List}; 474 for (const auto &artist : artists) { 475 all_artists.push_back (artist.second->persist()); 476 } 477 478 Parsnip::SerialData all_playlists {Parsnip::SerialData::List}; 479 for (const auto &playlist : playlists) { 480 assert (playlist.second->selector.canPersist()); 481 all_playlists.push_back (playlist.second->persist ()); 482 } 483 484 Parsnip::SerialData media = {Parsnip::SerialData::Dictionary, 485 MusicStorage::MediaArtists, std::move (all_artists) 486 }; 487 persist (media); 488 489 Parsnip::SerialData doc {Parsnip::SerialData::Dictionary, 490 MusicStorage::LibraryMedia, std::move (media), 491 MusicStorage::LibraryPlaylists, std::move (all_playlists) 492 }; 493 return carefullyWriteFile (filename, doc); 494 } catch (const std::exception &e) { 495 flog (LOG_WHERE (LOG_ERROR), "Could not serialize user data: ", e.what()); 496 return false; 497 } 498 } 499 500 template <class TSong, class TAlbum, class TArtist> restoreIndexFromFile(const std::string & filename)501 bool Library<TSong, TAlbum, TArtist>::restoreIndexFromFile (const std::string &filename) { 502 const Parsnip::SerialData index = retrieveJsonFile(filename); 503 504 // Restore the regular tree 505 MusicAutoReleasePool pool; 506 for (const auto &artist : index [MusicStorage::LibraryMedia][MusicStorage::MediaArtists]) { 507 auto newartist = artists.addOrGetItem (artist, this, MusicStorage::ArtistName, MusicStorage::ArtistId); 508 if (!newartist) 509 continue; 510 for (const auto &album : artist [MusicStorage::ArtistAlbums]) { 511 auto newalbum = albums.addOrGetItem (album, newartist, MusicStorage::AlbumName, MusicStorage::AlbumId); 512 if (!newalbum) 513 continue; 514 for (const auto &track : album [MusicStorage::AlbumSongs]) { 515 songs.addOrGetItem(track, newalbum, MusicStorage::SongName, MusicStorage::SongId); 516 } 517 } 518 } 519 // Restore compilations 520 for (const auto &artist : index [MusicStorage::LibraryMedia][MusicStorage::MediaArtists]) { 521 for (const auto &album : artist [MusicStorage::ArtistAlbums]) { 522 if (isCompilationAlbum (album)) { 523 for (const auto &track : album [MusicStorage::AlbumSongs]) { 524 auto thesong = songs.getById (track, MusicStorage::SongId); 525 assert (thesong); 526 // Check if compilation song has an artist; if so, set it up. 527 if (track.contains (MusicStorage::ArtistId)) { 528 auto trackartist = artists.getById (track, MusicStorage::ArtistId); 529 if (!trackartist) 530 flog (LOG_WHERE (LOG_WARNING), "Could not find song artist for ", 531 (*thesong)()); 532 else 533 thesong->artist (trackartist); 534 } 535 } 536 } 537 } 538 } 539 // Restore the playlists 540 for (const auto &playlist : index [MusicStorage::LibraryPlaylists]) { 541 playlists.addOrGetItem (playlist, this, MusicStorage::PlaylistName, MusicStorage::PlaylistId); 542 } 543 return true; 544 } 545 546 } 547 548