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