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