1 /*
2  *  Copyright (C) 2005-2018 Team Kodi
3  *  This file is part of Kodi - https://kodi.tv
4  *
5  *  SPDX-License-Identifier: GPL-2.0-or-later
6  *  See LICENSES/README.md for more information.
7  */
8 
9 #include "Album.h"
10 
11 #include "FileItem.h"
12 #include "ServiceBroker.h"
13 #include "music/tags/MusicInfoTag.h"
14 #include "settings/AdvancedSettings.h"
15 #include "settings/SettingsComponent.h"
16 #include "utils/MathUtils.h"
17 #include "utils/StringUtils.h"
18 #include "utils/XMLUtils.h"
19 #include "utils/log.h"
20 
21 #include <algorithm>
22 
23 using namespace MUSIC_INFO;
24 
25 typedef struct ReleaseTypeInfo {
26   CAlbum::ReleaseType type;
27   std::string name;
28 } ReleaseTypeInfo;
29 
30 ReleaseTypeInfo releaseTypes[] = {
31   { CAlbum::Album,  "album" },
32   { CAlbum::Single, "single" }
33 };
34 
CAlbum(const CFileItem & item)35 CAlbum::CAlbum(const CFileItem& item)
36 {
37   Reset();
38   const CMusicInfoTag& tag = *item.GetMusicInfoTag();
39   strAlbum = tag.GetAlbum();
40   strMusicBrainzAlbumID = tag.GetMusicBrainzAlbumID();
41   strReleaseGroupMBID = tag.GetMusicBrainzReleaseGroupID();
42   genre = tag.GetGenre();
43   strArtistDesc = tag.GetAlbumArtistString();
44   //Set sort string before processing artist credits
45   strArtistSort = tag.GetAlbumArtistSort();
46   // Determine artist credits from various tag arrays, inc fallback to song artist names
47   SetArtistCredits(tag.GetAlbumArtist(), tag.GetMusicBrainzAlbumArtistHints(), tag.GetMusicBrainzAlbumArtistID(),
48                    tag.GetArtist(), tag.GetMusicBrainzArtistHints(), tag.GetMusicBrainzArtistID());
49 
50   strOrigReleaseDate = tag.GetOriginalDate();
51   strReleaseDate = tag.GetReleaseDate();
52   strLabel = tag.GetRecordLabel();
53   strType = tag.GetMusicBrainzReleaseType();
54   bCompilation = tag.GetCompilation();
55   iTimesPlayed = 0;
56   bBoxedSet = tag.GetBoxset();
57   dateAdded.Reset();
58   dateUpdated.Reset();
59   lastPlayed.Reset();
60   releaseType = tag.GetAlbumReleaseType();
61   strReleaseStatus = tag.GetAlbumReleaseStatus();
62 }
63 
SetArtistCredits(const std::vector<std::string> & names,const std::vector<std::string> & hints,const std::vector<std::string> & mbids,const std::vector<std::string> & artistnames,const std::vector<std::string> & artisthints,const std::vector<std::string> & artistmbids)64 void CAlbum::SetArtistCredits(const std::vector<std::string>& names, const std::vector<std::string>& hints,
65                               const std::vector<std::string>& mbids,
66                               const std::vector<std::string>& artistnames, const std::vector<std::string>& artisthints,
67                               const std::vector<std::string>& artistmbids)
68 {
69   std::vector<std::string> albumartistHints = hints;
70   //Split the artist sort string to try and get sort names for individual artists
71   auto artistSort = StringUtils::Split(strArtistSort, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
72   artistCredits.clear();
73 
74   if (!mbids.empty())
75   { // Have musicbrainz artist info, so use it
76 
77     // Vector of possible separators in the order least likely to be part of artist name
78     const std::vector<std::string> separators{ " feat. ", ";", ":", "|", "#", "/", ",", "&" };
79 
80     // Establish tag consistency
81     // Do the number of musicbrainz ids and *both* number of names and number of hints mis-match?
82     if (albumartistHints.size() != mbids.size() && names.size() != mbids.size())
83     {
84       // Tags mis-match - report it and then try to fix
85       CLog::Log(LOGDEBUG, "Mis-match in song file albumartist tags: %i mbid %i name album: %s %s",
86         (int)mbids.size(),
87         (int)names.size(),
88         strAlbum.c_str(), strArtistDesc.c_str());
89       /*
90       Most likely we have no hints and a single artist name like "Artist1 feat. Artist2"
91       or "Composer; Conductor, Orchestra, Soloist" or "Artist1/Artist2" where the
92       expected single item separator (default = space-slash-space) as not been used.
93       Comma and slash (no spaces) are poor delimiters as could be in name e.g. AC/DC,
94       but here treat them as such in attempt to find artist names.
95       When there are hints they could be poorly formatted using unexpected separators,
96       so attempt to split them. Or we could have more hints or artist names than
97       musicbrainz so ignore them but raise warning.
98       */
99 
100       // Do hints exist yet mis-match
101       if (!albumartistHints.empty() && albumartistHints.size() != mbids.size())
102       {
103         if (names.size() == mbids.size())
104           // Album artist name count matches, use that as hints
105           albumartistHints = names;
106         else if (albumartistHints.size() < mbids.size())
107         { // Try splitting the hints until have matching number
108           albumartistHints = StringUtils::SplitMulti(albumartistHints, separators, mbids.size());
109         }
110         else
111           // Extra hints, discard them.
112           albumartistHints.resize(mbids.size());
113       }
114       // Do hints not exist or still mis-match, try album artists
115       if (albumartistHints.size() != mbids.size())
116         albumartistHints = names;
117       // Still mis-match, try splitting the hints (now artists) until have matching number
118       if (albumartistHints.size() < mbids.size())
119         albumartistHints = StringUtils::SplitMulti(albumartistHints, separators, mbids.size());
120       // Try matching on artists or artist hints field, if it is reliable
121       if (albumartistHints.size() != mbids.size())
122       {
123         if (!artistmbids.empty() &&
124           (artistmbids.size() == artistnames.size() ||
125             artistmbids.size() == artisthints.size()))
126         {
127           for (size_t i = 0; i < mbids.size(); i++)
128           {
129             for (size_t j = 0; j < artistmbids.size(); j++)
130             {
131               if (mbids[i] == artistmbids[j])
132               {
133                 if (albumartistHints.size() < i + 1)
134                   albumartistHints.resize(i + 1);
135                 if (artistmbids.size() == artisthints.size())
136                   albumartistHints[i] = artisthints[j];
137                 else
138                   albumartistHints[i] = artistnames[j];
139               }
140             }
141           }
142         }
143       }
144     }
145     else
146     { // Either hints or album artists (or both) name matches number of musicbrainz id
147       // If hints mis-match, use album artists
148       if (albumartistHints.size() != mbids.size())
149         albumartistHints = names;
150     }
151 
152     // Try to get number of artist sort names and musicbrainz ids to match. Split sort names
153     // further using multiple possible delimiters, over single separator applied in Tag loader
154     if (artistSort.size() != mbids.size())
155       artistSort = StringUtils::SplitMulti(artistSort, { ";", ":", "|", "#" });
156 
157     for (size_t i = 0; i < mbids.size(); i++)
158     {
159       std::string artistId = mbids[i];
160       std::string artistName;
161       /*
162          We try and get the musicbrainz id <-> name matching from the hints and match on the same index.
163          Some album artist hints could be blank (if populated from artist or artist hints).
164          If not found, use the musicbrainz id and hope we later on can update that entry.
165          If we have more names than musicbrainz id they are ignored, but raise a warning.
166       */
167       if (i < albumartistHints.size())
168         artistName = albumartistHints[i];
169       if (artistName.empty())
170         artistName = artistId;
171 
172       // Use artist sort name providing we have as many as we have mbid,
173       // otherwise something is wrong with them so ignore and leave blank
174       if (artistSort.size() == mbids.size())
175         artistCredits.emplace_back(StringUtils::Trim(artistName), StringUtils::Trim(artistSort[i]), artistId);
176       else
177         artistCredits.emplace_back(StringUtils::Trim(artistName), "", artistId);
178     }
179   }
180   else
181   {
182     /*
183       No musicbrainz album artist ids so fill artist names directly.
184       This method only called during scanning when there is a musicbrainz album id, so
185       means mbid tags are incomplete. But could also be called by JSON to SetAlbumDetails
186       Try to separate album artist names further, and trim blank space.
187     */
188     std::vector<std::string> albumArtists = names;
189     if (albumartistHints.size() > albumArtists.size())
190       // Make use of hints (ALBUMARTISTS tag), when present, to separate artist names
191       albumArtists = albumartistHints;
192     else
193       // Split album artist names further using multiple possible delimiters, over single separator applied in Tag loader
194       albumArtists = StringUtils::SplitMulti(albumArtists, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicArtistSeparators);
195 
196     if (artistSort.size() != albumArtists.size())
197       // Split artist sort names further using multiple possible delimiters, over single separator applied in Tag loader
198       artistSort = StringUtils::SplitMulti(artistSort, { ";", ":", "|", "#" });
199 
200     for (size_t i = 0; i < albumArtists.size(); i++)
201     {
202       artistCredits.emplace_back(StringUtils::Trim(albumArtists[i]));
203       // Set artist sort name providing we have as many as we have artists,
204       // otherwise something is wrong with them so ignore rather than guess.
205       if (artistSort.size() == albumArtists.size())
206         artistCredits.back().SetSortName(StringUtils::Trim(artistSort[i]));
207     }
208   }
209 }
210 
MergeScrapedAlbum(const CAlbum & source,bool override)211 void CAlbum::MergeScrapedAlbum(const CAlbum& source, bool override /* = true */)
212 {
213   /*
214    Initial scraping of album information when there is a Musicbrainz album ID derived from
215    tags is done directly using that ID, otherwise the lookup is based on album and artist names
216    but this can sometimes mis-identify the album (i.e. classical music has many "Symphony No. 5").
217    It is useful to store the scraped mbid, but we need to be able to correct any mistakes. Hence
218    a manual refresh of album information uses either the mbid as derived from tags or the album
219    and artist names, not any previously scraped mbid.
220 
221    When overwritting the data derived from tags, AND the original and scraped album have the same
222    Musicbrainz album ID, then merging is used to keep Kodi up to date with changes in the Musicbrainz
223    database including album artist credits, song artist credits and song titles. However it is ony
224    appropriate when the music files are tagged with mbids, these are taken as definative, scraped
225    mbids can not be depended on in this way.
226 
227    When the album is megerd in this deep way it is flagged so that the database album update is aware
228    artist credits and songs need to be updated too.
229   */
230 
231   bArtistSongMerge = override && !bScrapedMBID
232     && !source.strMusicBrainzAlbumID.empty() && !strMusicBrainzAlbumID.empty()
233     && (strMusicBrainzAlbumID.compare(source.strMusicBrainzAlbumID) == 0);
234 
235   /*
236    Musicbrainz album (release) ID and release group ID values derived from music file tags are
237    always taken as accurate and so can not be overwritten by a scraped value. When the album does
238    not already have an mbid or has a previously scraped mbid, merge the new scraped value,
239    flagging it as being from the scraper rather than derived from music file tags.
240   */
241   if (!source.strMusicBrainzAlbumID.empty() && (strMusicBrainzAlbumID.empty() || bScrapedMBID))
242   {
243     strMusicBrainzAlbumID = source.strMusicBrainzAlbumID;
244     bScrapedMBID = true;
245   }
246   if (!source.strReleaseGroupMBID.empty() && (strReleaseGroupMBID.empty() || bScrapedMBID))
247   {
248     strReleaseGroupMBID = source.strReleaseGroupMBID;
249   }
250 
251   /*
252    Scraping can return different album artists from the originals derived from tags, even when
253    doing a lookup on artist name.
254 
255    When overwritting the data derived from tags, AND the original and scraped album have the same
256    Musicbrainz album ID, then merging an album replaces both the album artsts and the song artists
257    with those scraped (providing they are not empty).
258 
259    When not doing that kind of merge, for any matching artist names the Musicbrainz artist id
260    returned by the scraper can be used to populate any previously missing Musicbrainz artist id values.
261   */
262   if (bArtistSongMerge && !source.artistCredits.empty())
263   {
264     artistCredits = source.artistCredits; // Replace artists and store mbid returned by scraper
265     strArtistDesc.clear();  // @todo: set artist display string e.g. "artist1 & artist2" when scraped
266   }
267   else
268   {
269     // Compare original album artists with those scraped (ignoring order), and set any missing mbid
270     for (auto &artistCredit : artistCredits)
271     {
272       if (artistCredit.GetMusicBrainzArtistID().empty())
273       {
274         for (const auto& sourceartistCredit : source.artistCredits)
275         {
276           if (StringUtils::EqualsNoCase(artistCredit.GetArtist(), sourceartistCredit.GetArtist()))
277           {
278             artistCredit.SetMusicBrainzArtistID(sourceartistCredit.GetMusicBrainzArtistID());
279             artistCredit.SetScrapedMBID(true);
280             break;
281           }
282         }
283       }
284     }
285   }
286 
287   //@todo: scraped album genre needs adding to genre and album_genre tables, this just changes the string
288   if ((override && !source.genre.empty()) || genre.empty())
289     genre = source.genre;
290   if ((override && !source.strAlbum.empty()) || strAlbum.empty())
291     strAlbum = source.strAlbum;
292   //@todo: validate ISO8601 format YYYY, YYYY-MM, or YYYY-MM-DD
293   if ((override && !source.strReleaseDate.empty()) || strReleaseDate.empty())
294     strReleaseDate = source.strReleaseDate;
295   if ((override && !source.strOrigReleaseDate.empty()) || strOrigReleaseDate.empty())
296     strOrigReleaseDate = source.strOrigReleaseDate;
297 
298   if (override)
299     bCompilation = source.bCompilation;
300   //  iTimesPlayed = source.iTimesPlayed; // times played is derived from songs
301 
302   if ((override && !source.strArtistSort.empty()) || strArtistSort.empty())
303     strArtistSort = source.strArtistSort;
304   for (const auto& i : source.art)
305   {
306     if (override || art.find(i.first) == art.end())
307       art[i.first] = i.second;
308   }
309   if((override && !source.strLabel.empty()) || strLabel.empty())
310     strLabel = source.strLabel;
311   thumbURL = source.thumbURL;
312   moods = source.moods;
313   styles = source.styles;
314   themes = source.themes;
315   strReview = source.strReview;
316   if ((override && !source.strType.empty()) || strType.empty())
317     strType = source.strType;
318 //  strPath = source.strPath; // don't merge the path
319   if ((override && !source.strReleaseStatus.empty()) || strReleaseStatus.empty())
320     strReleaseStatus = source.strReleaseStatus;
321   fRating = source.fRating;
322   iUserrating = source.iUserrating;
323   iVotes = source.iVotes;
324 
325   /*
326    When overwritting the data derived from tags, AND the original and scraped album have the same
327    Musicbrainz album ID, update the local songs with scaped Musicbrainz information including the
328    artist credits.
329   */
330   if (bArtistSongMerge)
331   {
332     for (auto &song : songs)
333     {
334       if (!song.strMusicBrainzTrackID.empty())
335         for (const auto& sourceSong : source.songs)
336           if ((sourceSong.strMusicBrainzTrackID == song.strMusicBrainzTrackID) && (sourceSong.iTrack == song.iTrack))
337             song.MergeScrapedSong(sourceSong, override);
338     }
339   }
340 }
341 
GetGenreString() const342 std::string CAlbum::GetGenreString() const
343 {
344   return StringUtils::Join(genre, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
345 }
346 
GetAlbumArtist() const347 const std::vector<std::string> CAlbum::GetAlbumArtist() const
348 {
349   //Get artist names as vector from artist credits
350   std::vector<std::string> albumartists;
351   for (const auto& artistCredit : artistCredits)
352   {
353     albumartists.push_back(artistCredit.GetArtist());
354   }
355   return albumartists;
356 }
357 
GetMusicBrainzAlbumArtistID() const358 const std::vector<std::string> CAlbum::GetMusicBrainzAlbumArtistID() const
359 {
360   //Get artist MusicBrainz IDs as vector from artist credits
361   std::vector<std::string> musicBrainzID;
362   for (const auto& artistCredit : artistCredits)
363   {
364     musicBrainzID.push_back(artistCredit.GetMusicBrainzArtistID());
365   }
366   return musicBrainzID;
367 }
368 
GetAlbumArtistString() const369 const std::string CAlbum::GetAlbumArtistString() const
370 {
371   //Artist description may be different from the artists in artistcredits (see ALBUMARTISTS tag processing)
372   //but is takes precedence as a string because artistcredits is not always filled during processing
373   if (!strArtistDesc.empty())
374     return strArtistDesc;
375   std::vector<std::string> artistvector;
376   for (const auto& i : artistCredits)
377     artistvector.emplace_back(i.GetArtist());
378   std::string artistString;
379   if (!artistvector.empty())
380     artistString = StringUtils::Join(artistvector, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
381   return artistString;
382 }
383 
GetAlbumArtistSort() const384 const std::string CAlbum::GetAlbumArtistSort() const
385 {
386   //The stored artist sort name string takes precidence but a
387   //value could be created from individual sort names held in artistcredits
388   if (!strArtistSort.empty())
389     return strArtistSort;
390   std::vector<std::string> artistvector;
391   for (const auto& artistcredit : artistCredits)
392     if (!artistcredit.GetSortName().empty())
393       artistvector.emplace_back(artistcredit.GetSortName());
394   std::string artistString;
395   if (!artistvector.empty())
396     artistString = StringUtils::Join(artistvector, "; ");
397   return artistString;
398 }
399 
GetArtistIDArray() const400 const std::vector<int> CAlbum::GetArtistIDArray() const
401 {
402   // Get album artist IDs for json rpc
403   std::vector<int> artistids;
404   for (const auto& artistCredit : artistCredits)
405     artistids.push_back(artistCredit.GetArtistId());
406   return artistids;
407 }
408 
409 
GetReleaseType() const410 std::string CAlbum::GetReleaseType() const
411 {
412   return ReleaseTypeToString(releaseType);
413 }
414 
SetReleaseType(const std::string & strReleaseType)415 void CAlbum::SetReleaseType(const std::string& strReleaseType)
416 {
417   releaseType = ReleaseTypeFromString(strReleaseType);
418 }
419 
SetDateAdded(const std::string & strDateAdded)420 void CAlbum::SetDateAdded(const std::string& strDateAdded)
421 {
422   dateAdded.SetFromDBDateTime(strDateAdded);
423 }
424 
SetDateUpdated(const std::string & strDateUpdated)425 void CAlbum::SetDateUpdated(const std::string& strDateUpdated)
426 {
427   dateUpdated.SetFromDBDateTime(strDateUpdated);
428 }
429 
SetDateNew(const std::string & strDateNew)430 void CAlbum::SetDateNew(const std::string& strDateNew)
431 {
432   dateNew.SetFromDBDateTime(strDateNew);
433 }
434 
SetLastPlayed(const std::string & strLastPlayed)435 void CAlbum::SetLastPlayed(const std::string& strLastPlayed)
436 {
437   lastPlayed.SetFromDBDateTime(strLastPlayed);
438 }
439 
ReleaseTypeToString(CAlbum::ReleaseType releaseType)440 std::string CAlbum::ReleaseTypeToString(CAlbum::ReleaseType releaseType)
441 {
442   for (const ReleaseTypeInfo& releaseTypeInfo : releaseTypes)
443   {
444     if (releaseTypeInfo.type == releaseType)
445       return releaseTypeInfo.name;
446   }
447 
448   return "album";
449 }
450 
ReleaseTypeFromString(const std::string & strReleaseType)451 CAlbum::ReleaseType CAlbum::ReleaseTypeFromString(const std::string& strReleaseType)
452 {
453   for (const ReleaseTypeInfo& releaseTypeInfo : releaseTypes)
454   {
455     if (releaseTypeInfo.name == strReleaseType)
456       return releaseTypeInfo.type;
457   }
458 
459   return Album;
460 }
461 
operator <(const CAlbum & a) const462 bool CAlbum::operator<(const CAlbum &a) const
463 {
464   if (strMusicBrainzAlbumID.empty() && a.strMusicBrainzAlbumID.empty())
465   {
466     if (strAlbum < a.strAlbum) return true;
467     if (strAlbum > a.strAlbum) return false;
468 
469     // This will do an std::vector compare (i.e. item by item)
470     if (GetAlbumArtist() < a.GetAlbumArtist()) return true;
471     if (GetAlbumArtist() > a.GetAlbumArtist()) return false;
472     return false;
473   }
474 
475   if (strMusicBrainzAlbumID < a.strMusicBrainzAlbumID) return true;
476   if (strMusicBrainzAlbumID > a.strMusicBrainzAlbumID) return false;
477   return false;
478 }
479 
Load(const TiXmlElement * album,bool append,bool prioritise)480 bool CAlbum::Load(const TiXmlElement *album, bool append, bool prioritise)
481 {
482   if (!album) return false;
483   if (!append)
484     Reset();
485 
486   const std::string itemSeparator = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
487 
488   XMLUtils::GetString(album,              "title", strAlbum);
489   XMLUtils::GetString(album, "musicbrainzalbumid", strMusicBrainzAlbumID);
490   XMLUtils::GetString(album, "musicbrainzreleasegroupid", strReleaseGroupMBID);
491   XMLUtils::GetBoolean(album, "scrapedmbid", bScrapedMBID);
492   XMLUtils::GetString(album, "artistdesc", strArtistDesc);
493   std::vector<std::string> artist; // Support old style <artist></artist> for backwards compatibility
494   XMLUtils::GetStringArray(album, "artist", artist, prioritise, itemSeparator);
495   XMLUtils::GetStringArray(album, "genre", genre, prioritise, itemSeparator);
496   XMLUtils::GetStringArray(album, "style", styles, prioritise, itemSeparator);
497   XMLUtils::GetStringArray(album, "mood", moods, prioritise, itemSeparator);
498   XMLUtils::GetStringArray(album, "theme", themes, prioritise, itemSeparator);
499   XMLUtils::GetBoolean(album, "compilation", bCompilation);
500   XMLUtils::GetBoolean(album, "boxset", bBoxedSet);
501 
502   XMLUtils::GetString(album,"review",strReview);
503   XMLUtils::GetString(album,"label",strLabel);
504   XMLUtils::GetInt(album, "duration", iAlbumDuration);
505   XMLUtils::GetString(album,"type",strType);
506   XMLUtils::GetString(album, "releasestatus", strReleaseStatus);
507 
508   XMLUtils::GetString(album, "releasedate", strReleaseDate);
509   StringUtils::Trim(strReleaseDate);  // @todo: validate ISO8601 format
510   // Support old style <year></year> for backwards compatibility
511   if (strReleaseDate.empty())
512   {
513     int year;
514     XMLUtils::GetInt(album, "year", year);
515     if (year > 0)
516       strReleaseDate = StringUtils::Format("%04i", year);
517   }
518   XMLUtils::GetString(album, "originalreleasedate", strOrigReleaseDate);
519 
520   const TiXmlElement* rElement = album->FirstChildElement("rating");
521   if (rElement)
522   {
523     float rating = 0;
524     float max_rating = 10;
525     XMLUtils::GetFloat(album, "rating", rating);
526     if (rElement->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS && max_rating>=1)
527       rating *= (10.f / max_rating); // Normalise the Rating to between 0 and 10
528     if (rating > 10.f)
529       rating = 10.f;
530     fRating = rating;
531   }
532   const TiXmlElement* userrating = album->FirstChildElement("userrating");
533   if (userrating)
534   {
535     float rating = 0;
536     float max_rating = 10;
537     XMLUtils::GetFloat(album, "userrating", rating);
538     if (userrating->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS && max_rating >= 1)
539       rating *= (10.f / max_rating); // Normalise the Rating to between 0 and 10
540     if (rating > 10.f)
541       rating = 10.f;
542     iUserrating = MathUtils::round_int(rating);
543   }
544   XMLUtils::GetInt(album, "votes", iVotes);
545 
546   size_t iThumbCount = thumbURL.GetUrls().size();
547   std::string xmlAdd = thumbURL.GetData();
548   const TiXmlElement* thumb = album->FirstChildElement("thumb");
549   while (thumb)
550   {
551     thumbURL.ParseAndAppendUrl(thumb);
552     if (prioritise)
553     {
554       std::string temp;
555       temp << *thumb;
556       xmlAdd = temp+xmlAdd;
557     }
558     thumb = thumb->NextSiblingElement("thumb");
559   }
560   // prioritise thumbs from nfos
561   if (prioritise && iThumbCount && iThumbCount != thumbURL.GetUrls().size())
562   {
563     auto thumbUrls = thumbURL.GetUrls();
564     rotate(thumbUrls.begin(), thumbUrls.begin() + iThumbCount, thumbUrls.end());
565     thumbURL.SetUrls(thumbUrls);
566     thumbURL.SetData(xmlAdd);
567   }
568 
569   const TiXmlElement* albumArtistCreditsNode = album->FirstChildElement("albumArtistCredits");
570   if (albumArtistCreditsNode)
571     artistCredits.clear();
572 
573   while (albumArtistCreditsNode)
574   {
575     if (albumArtistCreditsNode->FirstChild())
576     {
577       CArtistCredit artistCredit;
578       XMLUtils::GetString(albumArtistCreditsNode,  "artist",               artistCredit.m_strArtist);
579       XMLUtils::GetString(albumArtistCreditsNode,  "musicBrainzArtistID",  artistCredit.m_strMusicBrainzArtistID);
580       artistCredits.push_back(artistCredit);
581     }
582 
583     albumArtistCreditsNode = albumArtistCreditsNode->NextSiblingElement("albumArtistCredits");
584   }
585 
586   // Support old style <artist></artist> for backwards compatibility
587   // .nfo files should ideally be updated to use the artist credits structure above
588   // or removed entirely in preference for better tags (MusicBrainz?)
589   if (artistCredits.empty() && !artist.empty())
590   {
591     for (const auto& it : artist)
592     {
593       CArtistCredit artistCredit(it);
594       artistCredits.push_back(artistCredit);
595     }
596   }
597 
598   std::string strReleaseType;
599   if (XMLUtils::GetString(album, "releasetype", strReleaseType))
600     SetReleaseType(strReleaseType);
601   else
602     releaseType = Album;
603 
604   return true;
605 }
606 
Save(TiXmlNode * node,const std::string & tag,const std::string & strPath)607 bool CAlbum::Save(TiXmlNode *node, const std::string &tag, const std::string& strPath)
608 {
609   if (!node) return false;
610 
611   // we start with a <tag> tag
612   TiXmlElement albumElement(tag.c_str());
613   TiXmlNode *album = node->InsertEndChild(albumElement);
614 
615   if (!album) return false;
616 
617   XMLUtils::SetString(album,                    "title", strAlbum);
618   XMLUtils::SetString(album,       "musicbrainzalbumid", strMusicBrainzAlbumID);
619   XMLUtils::SetString(album, "musicbrainzreleasegroupid", strReleaseGroupMBID);
620   XMLUtils::SetBoolean(album, "scrapedmbid", bScrapedMBID);
621   XMLUtils::SetString(album,              "artistdesc", strArtistDesc); //Can be different from artist credits
622   XMLUtils::SetStringArray(album,               "genre", genre);
623   XMLUtils::SetStringArray(album,               "style", styles);
624   XMLUtils::SetStringArray(album,                "mood", moods);
625   XMLUtils::SetStringArray(album,               "theme", themes);
626   XMLUtils::SetBoolean(album,      "compilation", bCompilation);
627   XMLUtils::SetBoolean(album, "boxset", bBoxedSet);
628 
629   XMLUtils::SetString(album,      "review", strReview);
630   XMLUtils::SetString(album,        "type", strType);
631   XMLUtils::SetString(album, "releasestatus", strReleaseStatus);
632   XMLUtils::SetString(album, "releasedate", strReleaseDate);
633   XMLUtils::SetString(album, "originalreleasedate", strOrigReleaseDate);
634   XMLUtils::SetString(album,       "label", strLabel);
635   XMLUtils::SetInt(album, "duration", iAlbumDuration);
636   if (thumbURL.HasData())
637   {
638     CXBMCTinyXML doc;
639     doc.Parse(thumbURL.GetData());
640     const TiXmlNode* thumb = doc.FirstChild("thumb");
641     while (thumb)
642     {
643       album->InsertEndChild(*thumb);
644       thumb = thumb->NextSibling("thumb");
645     }
646   }
647   XMLUtils::SetString(album,        "path", strPath);
648 
649   auto* rating = XMLUtils::SetFloat(album, "rating", fRating);
650   if (rating)
651     rating->ToElement()->SetAttribute("max", 10);
652 
653   auto* userrating = XMLUtils::SetInt(album, "userrating", iUserrating);
654   if (userrating)
655     userrating->ToElement()->SetAttribute("max", 10);
656 
657   XMLUtils::SetInt(album,           "votes", iVotes);
658 
659   for (const auto& artistCredit : artistCredits)
660   {
661     // add an <albumArtistCredits> tag
662     TiXmlElement albumArtistCreditsElement("albumArtistCredits");
663     TiXmlNode *albumArtistCreditsNode = album->InsertEndChild(albumArtistCreditsElement);
664     XMLUtils::SetString(albumArtistCreditsNode, "artist", artistCredit.m_strArtist);
665     XMLUtils::SetString(albumArtistCreditsNode, "musicBrainzArtistID",
666                         artistCredit.m_strMusicBrainzArtistID);
667   }
668 
669   XMLUtils::SetString(album, "releasetype", GetReleaseType());
670 
671   return true;
672 }
673 
674