1 ///
2 /// Method implementations for music library types.
3 ///	@file		musiclibrarytypes.cpp - pianod
4 ///	@author		Perette Barella
5 ///	@date		2014-12-09
6 ///	@copyright	Copyright 2014-2020 Devious Fish.  All rights reserved.
7 ///
8 
9 #include <config.h>
10 
11 #include <string>
12 #include <set>
13 
14 #include <parsnip.h>
15 #include <football.h>
16 
17 #include "musiclibrary.h"
18 #include "musiclibraryparameters.h"
19 #include "utility.h"
20 #include "datastore.h"
21 #include "user.h"
22 #include "mediaunit.h"
23 #include "musickeys.h"
24 
25 using namespace std;
26 
27 /// Element and attribute keys for ratings.
28 namespace MusicLibrary {
29     static const std::string noPlaylistName = "Bibliotheque";
30 
31 
32     /** Make a list of unique strings.  Used for artists on compilation albums
33         and genres on playlists. */
34     class NameAggregator: public set<string> {
35     public:
36         /** Return the results of aggregation.
37             @param joiner The string with which to connect members.
38             @param limit Approximate maximum length of the returned string.
39             @param tail A string to append if members are dropped. */
str(const string & joiner,string::size_type limit=0xffff,const string & tail="...") const40         string str(const string &joiner, string::size_type limit = 0xffff, const string &tail = "...") const {
41             std::string result;
42             bool dropped = false;
43             for (auto &item : *this) {
44                 if (!item.empty()) {
45                     if (result.empty() || (result.size() + joiner.size() + item.size() < limit)) {
46                         if (!result.empty())
47                             result += joiner;
48                         result += item;
49                     } else {
50                         dropped = true;
51                     }
52                 }
53             }
54             if (dropped) {
55                 result += tail;
56             }
57             return result;
58         }
59     };
60 
61 
62     /*
63      *                  Playlists
64      */
appliesTo(const PianodSong * song) const65     bool Playlist::appliesTo (const PianodSong *song) const {
66         if (selector.matches (song)) return true;
67         if (seeds.find (song->songId()) != seeds.end()) return true;
68         if (seeds.find (song->albumId()) != seeds.end()) return true;
69         if (seeds.find (song->artistId()) != seeds.end()) return true;
70         return false;
71     };
72 
canSeed(MusicThingie::Type) const73     bool Playlist::canSeed (MusicThingie::Type) const {
74         return true;
75     }
76 
seed(MusicThingie::Type seed_type,const MusicThingie * music) const77     bool Playlist::seed (MusicThingie::Type seed_type,
78                          const MusicThingie *music) const {
79         assert (music->source() == this->source());
80         if (seed_type == MusicThingie::Type::Playlist)
81             throw CommandError (E_MEDIA_VALUE);
82         const std::string &seed_id = music->id (seed_type);
83         if (seed_id.empty()) return false;
84         return (seeds.find (seed_id) != seeds.end());
85     }
86 
seed(MusicThingie::Type seed_type,MusicThingie * music,bool value)87     void Playlist::seed (MusicThingie::Type seed_type,
88                          MusicThingie *music,
89                          bool value) {
90         assert (music->source() == this->source());
91         if (seed_type == MusicThingie::Type::Playlist)
92             throw CommandError (E_MEDIA_VALUE);
93         const std::string &seed_id = music->id (seed_type);
94         if (seed_id.empty())
95             throw CommandError (E_INVALID);
96         assert (static_cast <MusicThingie::Type> (seed_id [0]) == seed_type);
97         SeedSet::const_iterator it = seeds.find (seed_id);
98         if ((value && it != seeds.end()) || (!value && it == seeds.end())) {
99             // Already in correct state.
100             return;
101         }
102         if (value) {
103             pair<SeedSet::iterator, bool> result = seeds.insert (seed_id);
104             if (!result.second)
105                 throw CommandError (E_NAK);
106         } else {
107             seeds.erase (it);
108             invalidateSeeds (music);
109         }
110         genres_dirty = true;
111         _library->populatePlaylist (this);
112         _library->markDirty (Foundation::NOMINAL);
113     }
114 
getSeeds(void) const115     ThingieList Playlist::getSeeds (void) const {
116         return _library->seedsForPlaylist (this);
117     };
118 
songs()119     SongList Playlist::songs () {
120         return _library->getPlaylistSongs (this, true);
121     }
122 
songs(const Filter & filter)123     SongList Playlist::songs (const Filter &filter) {
124         SongList candidates = this->songs();
125         SongList results;
126         results.reserve (candidates.size());
127         for (auto song : candidates) {
128             if (filter.matches (song)) {
129                 results.push_back (song);
130             }
131         }
132         return results;
133     }
134 
135 
invalidateSeeds(const MusicThingie * music)136     void Playlist::invalidateSeeds(const MusicThingie *music) {
137         auto artist = dynamic_cast<const Artist *>(music);
138         if (artist) {
139             for (auto album : artist->albums) {
140                 for (auto song : album->_songs) {
141                     if (song->_playlist == this) song->_playlist = nullptr;
142                 }
143             }
144             return;
145         }
146         // To ensure we invalidate all related seeds,
147         // do it from our parent artist.
148         auto album = dynamic_cast<const Album *>(music);
149         if (album) {
150             invalidateSeeds (album->_artist);
151         } else {
152             auto song = dynamic_cast<const Song *>(music);
153             assert (song);
154             invalidateSeeds (song->_album->_artist);
155         }
156     }
157 
rename(const std::string & newname)158     void Playlist::rename (const std::string &newname) {
159         _name = newname;
160         return;
161     }
162 
erase()163     void Playlist::erase () {
164         if (!library()->removePlaylist (this)) {
165             throw CommandError (E_NAK);
166         }
167     }
168 
169 
playlist(void) const170     PianodPlaylist *Song::playlist (void) const {
171         return _playlist;
172     };
173 
persist() const174     Parsnip::SerialData Playlist::persist () const {
175         Parsnip::SerialData seed_list {Parsnip::SerialData::List};
176         for (const auto &seed : seeds) {
177             seed_list.push_back (seed);
178         }
179 
180         return Parsnip::SerialData { Parsnip::SerialData::Dictionary,
181             MusicStorage::PlaylistId, _id,
182             MusicStorage::PlaylistName, _name,
183             MusicStorage::PlaylistEnabled, enabled,
184             MusicStorage::PlaylistSelector, selector.toString(),
185             MusicStorage::PlaylistSeeds, std::move (seed_list)
186         };
187     };
188 
restore(const Parsnip::SerialData & data)189     void Playlist::restore (const Parsnip::SerialData &data) {
190         enabled = data [MusicStorage::PlaylistEnabled].asBoolean();
191         try {
192             selector = data [MusicStorage::PlaylistSelector].asString();
193         } catch (const CommandError &err) {
194             flog (LOG_WHERE (LOG_WARNING),
195                   "Playlist ", playlistName(), " has an invalid filter expression: ", err.reason());
196             selector = Filter::None;
197         } catch (const Parsnip::SerializationException &err) {
198             flog (LOG_WHERE (LOG_WARNING),
199                   "Playlist ", playlistName(), ": ", err.what());
200             selector = Filter::None;
201         }
202         for (const auto &seed : data [MusicStorage::PlaylistSeeds]) {
203             seeds.insert (seed.asString());
204         }
205         genres_dirty = true;
206     }
207 
calculateGenres() const208     void Playlist::calculateGenres () const {
209         NameAggregator genres;
210         for (auto song : _library->getPlaylistSongs (this)) {
211             const char *gen = song->genre().c_str();
212             while (isspace (*gen))
213                 gen++;
214             while (*gen) {
215                 // Grab everything up to one of the genre separators
216                 string::size_type len = strcspn (gen, "/,+");
217                 if (len) {
218                     genres.insert (trim (string {gen, len}));
219                 }
220                 // Skip past the genre and any separators
221                 gen += len;
222                 gen += strspn (gen, "/,+ \t");
223             }
224         }
225 
226         // Assemble the list into a string
227         _genres = genres.str (", ", 50);
228         genres_dirty = false;
229     }
230 
231     /** (Re)calculate the current genres list for a playlist. */
genre(void) const232     const std::string &Playlist::genre (void) const {
233         if (genres_dirty) {
234             calculateGenres();
235         }
236         return _genres;
237     }
238 
239 
240     /*
241      *                  Transient Playlist
242      */
243 
TransientPlaylist(Foundation * const library,const Filter & criteria)244     TransientPlaylist::TransientPlaylist (Foundation *const library, const Filter &criteria) :
245     Playlist (library, "transient", "Transient") {
246         selector = criteria;
247     }
248 
songs()249     SongList TransientPlaylist::songs () {
250         return _library->getMatchingSongs (selector);
251     }
252 
253 
254 
255 
256 
257     /*
258      *                  Artists
259      */
260 
261     /** Create a new artist.
262         @param library The library in which the artist will reside.
263         @param id A unique ID for the artist.
264         @param name The name of the artist. */
Artist(Foundation * const library,const std::string & id,const std::string & name)265     Artist::Artist (Foundation *const library,
266                     const std::string &id, const std::string &name)
267     : _library (library), _id (id), _name (name) {
268     };
~Artist()269     Artist::~Artist() {
270         assert (albums.empty());
271     }
272 
273     /** Get all songs belonging to all albums by this artist.
274         However, since compilation albums don't belong to the artist,
275         this does not return the artist's songs that are on complations.
276         @return All songs on albums belonging to this artist. */
songs()277     SongList Artist::songs () {
278         int song_count = 0;
279         for (auto album : albums) {
280             song_count += album->_songs.size();
281         }
282         SongList songs, album_songs;
283         songs.reserve (song_count);
284         for (auto album : albums) {
285             album_songs = album->songs();
286             songs.join (album_songs);
287         }
288         return songs;
289     };
290 
persist() const291     Parsnip::SerialData Artist::persist () const {
292         Parsnip::SerialData all_albums { Parsnip::SerialData::List };
293 
294         for (auto album : albums) {
295             all_albums.push_back (album->persist ());
296         }
297         return Parsnip::SerialData { Parsnip::SerialData::Dictionary,
298             MusicStorage::ArtistId, _id,
299             MusicStorage::ArtistName, _name,
300             MusicStorage::ArtistAlbums, std::move (all_albums)
301         };
302     };
restore(const Parsnip::SerialData & data)303     void Artist::restore (const Parsnip::SerialData &data) {
304         // Nothing to do in this base class.
305     }
306 
307 
308 
309 
310     /*
311      *                  Albums
312      */
313 
314     /** Create a new album, and register itself with its parent.
315         @param parent The album's artist.
316         @param id A unique ID for the album.
317         @param name The name of the album. */
Album(Artist * const parent,const std::string & id,const std::string & name)318     Album::Album (Artist *const parent, const std::string &id, const std::string &name)
319     : _artist (parent), _id (id), _name (name) {
320         _artist->albums.push_back (this);
321         _artist->retain();
322     };
323 
324     /** Destroy an album by unregistering from its parent artist. */
~Album(void)325     Album::~Album (void) {
326         assert (_songs.empty());
327         auto it = find (_artist->albums.begin(), _artist->albums.end(), this);
328         assert (it != _artist->albums.end());
329         if (it != _artist->albums.end())
330             _artist->albums.erase (it);
331         _artist->release();
332     }
333 
334 
335     /** Get the current artist, or list of artists if a compilation. */
artist(void) const336     const string &Album::artist (void) const {
337         if (compilation() && artists_dirty)
338             calculateArtists();
339         return (compilation() ? _artists : _artist->_name);
340     };
341 
342     /** (Re)calculate the current artists list for a compilation. */
calculateArtists(void) const343     void Album::calculateArtists (void) const {
344         assert (compilation()); // Harmless but wasteful on non-compilation
345         NameAggregator artists;
346         for (auto song : _songs) {
347             artists.insert (trim (song->artist()));
348         }
349         _artists = artists.str("; ");
350         artists_dirty = false;
351     }
352 
compilation() const353     bool Album::compilation () const {
354         return (_songs.empty() ? false :
355                 _songs.front()->_artist != nullptr);
356     };
357 
358 
359     /** Mark an album as a compilation.
360         Set all song artists the current artist.
361         Detach the item from its artist and stick it into a special compilation
362         artist.
363         @param compilation_artist The compilation artist to move the album to. */
compilation(Artist * compilation_artist)364     void Album::compilation (Artist *compilation_artist) {
365         assert (compilation_artist);
366 
367         // If already a compilation, do nothing.
368         if (compilation()) return;
369 
370         // Move artist name into existing songs.
371         for (auto song : _songs) {
372             assert (!song->_artist);
373             if (song->_artist) {
374                 flog (LOG_WARNING, operator()(), " was ambiguously a compilation");
375                 if (song->_artist != compilation_artist) {
376                     flog (LOG_WARNING, operator()(), " is not the compilation artist ",
377                           (*compilation_artist)());
378                 }
379             }
380             song->artist (_artist);
381         }
382 
383         // Remove album from existing artist
384         auto it = find (_artist->albums.begin(), _artist->albums.end(), this);
385         assert (it != _artist->albums.end());
386         if (it != _artist->albums.end())
387             _artist->albums.erase (it);
388         _artist->release();
389 
390         // Add to new artist
391         _artist = compilation_artist;
392         _artist->albums.push_back (this);
393         compilation_artist->retain();
394     };
395 
396     /** Get all songs belonging to the album.
397         @return The songs belonging to the album. */
songs()398     SongList Album::songs () {
399         sort (_songs.begin(), _songs.end(), [] (Song *a, Song *b) {
400             int result = a->trackNumber() - b->trackNumber();
401             if (result == 0) {
402                 result = compare_title_order(a->title(), b->title());
403             }
404             return result < 0;
405         });
406         SongList list;
407         list.reserve (_songs.size());
408         for (auto song : _songs) {
409             list.push_back (song);
410         }
411         return list;
412     };
413 
414 
persist() const415     Parsnip::SerialData Album::persist () const {
416         Parsnip::SerialData all_songs { Parsnip::SerialData::List };
417         for (auto song : _songs) {
418             all_songs.push_back (song->persist());
419         }
420         Parsnip::SerialData album { Parsnip::SerialData::Dictionary,
421             MusicStorage::AlbumId, _id,
422             MusicStorage::AlbumName, _name,
423             MusicStorage::AlbumSongs, std::move (all_songs)
424         };
425 
426         if (compilation())
427             album [MusicStorage::AlbumIsCompilation] = true;
428 
429         return album;
430     };
431 
restore(const Parsnip::SerialData & data)432     void Album::restore (const Parsnip::SerialData &data) {
433     }
434 
435 
436 
437     /*
438      *                  Songs
439      */
440 
441     /** Add a song, and register itself with its parent album.
442         @param parent The song's album.
443         @param id A unique ID for the song.
444         @param name The name of the song. */
Song(Album * const parent,const std::string & id,const std::string & name)445     Song::Song (Album *const parent, const std::string &id, const std::string &name)
446     : _album (parent), _id (id), _name (name) {
447         _album->_songs.push_back (this);
448         _album->retain();
449         _album->artists_dirty = true;
450     };
artist(void) const451     const string &Song::artist (void) const {
452         return (_artist ? _artist->_name : _album->_artist->_name);
453     };
454 
~Song(void)455     Song::~Song (void) {
456         if (_artist) {
457             _artist->release();
458         }
459         auto it = find (_album->_songs.begin(), _album->_songs.end(), this);
460         assert (it != _album->_songs.end());
461         if (it != _album->_songs.end())
462             _album->_songs.erase (it);
463         _album->artists_dirty = true;
464         _album->release();
465     };
466 
467     /** Set the artist for this song.  This applies only for compilation albums.
468         @param artist The artist of the song. */
artist(Artist * artist)469     void Song::artist (Artist *artist) {
470         if (_artist)
471             _artist->release();
472         _artist = artist;
473         _artist->retain();
474     };
475 
rate(Rating value,User * user)476     RESPONSE_CODE Song::rate (Rating value, User *user) {
477         assert (user);
478         auto ratings = UserData::Ratings::retrieve (user, UserData::Key::TrackRatings, source()->key());
479         if (!ratings) {
480             return E_RESOURCE;
481         }
482         try {
483             (*ratings) [songId()] = value;
484             user->updateData();
485             return S_OK;
486         } catch (const bad_alloc &) {
487             return E_RESOURCE;
488         }
489     }
490 
rating(const User * user) const491     Rating Song::rating (const User *user) const {
492         assert (user);
493         auto ratings = UserData::Ratings::retrieve (user, UserData::Key::TrackRatings, source()->key());
494         if (!ratings) return Rating::UNRATED;
495         return ratings->get (songId(), Rating::UNRATED);
496     }
497 
rateOverplayed(User * user)498     RESPONSE_CODE Song::rateOverplayed (User *user) {
499         if (!user) return E_LOGINREQUIRED;
500         auto overplays = UserData::OverplayedList::retrieve (user, source()->key());
501         if (!overplays) {
502             return E_RESOURCE;
503         }
504         try {
505             // 30 days in seconds
506             (*overplays) [songId()] = time(nullptr) + 86400 * 30;
507             user->updateData();
508             return S_OK;
509         } catch (const bad_alloc &) {
510             return E_RESOURCE;
511         }
512     }
513 
persist() const514     Parsnip::SerialData Song::persist () const {
515         Parsnip::SerialData song { Parsnip::SerialData::Dictionary,
516             MusicStorage::SongId, _id,
517             MusicStorage::SongName, _name,
518             MusicStorage::SongGenre, _genre
519         };
520 
521         if (_artist) {
522             song [MusicStorage::ArtistId] = _artist->_id;
523             song [MusicStorage::ArtistName] = _artist->_name;
524         }
525         if (_duration)
526             song [MusicStorage::SongDuration] = _duration;
527         if (_year)
528             song [MusicStorage::SongYear] = _year;
529         if (_trackNumber)
530             song [MusicStorage::SongTrackNumber] = _trackNumber;
531         if (last_played)
532             song [MusicStorage::SongLastPlayed] = last_played;
533         return (song);
534     };
535 
restore(const Parsnip::SerialData & data)536     void Song::restore(const Parsnip::SerialData &data) {
537         _genre = data [MusicStorage::SongGenre].asString();
538         if (data.contains (MusicStorage::SongDuration))
539             _duration = data [MusicStorage::SongDuration].asInteger();
540         if (data.contains (MusicStorage::SongYear))
541             _year = data [MusicStorage::SongYear].asInteger();
542         if (data.contains (MusicStorage::SongTrackNumber))
543             _trackNumber = data [MusicStorage::SongTrackNumber].asInteger();
544         if (data.contains (MusicStorage::SongLastPlayed))
545             last_played = data [MusicStorage::SongLastPlayed].as<time_t> ();
546     }
547 }
548