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 "LabelFormatter.h"
10 
11 #include "FileItem.h"
12 #include "RegExp.h"
13 #include "ServiceBroker.h"
14 #include "StringUtils.h"
15 #include "URIUtils.h"
16 #include "Util.h"
17 #include "Variant.h"
18 #include "guilib/LocalizeStrings.h"
19 #include "music/tags/MusicInfoTag.h"
20 #include "pictures/PictureInfoTag.h"
21 #include "settings/AdvancedSettings.h"
22 #include "settings/Settings.h"
23 #include "settings/SettingsComponent.h"
24 #include "video/VideoInfoTag.h"
25 
26 #include <cassert>
27 #include <cstdlib>
28 #include <inttypes.h>
29 
30 using namespace MUSIC_INFO;
31 
32 /* LabelFormatter
33  * ==============
34  *
35  * The purpose of this class is to parse a mask string of the form
36  *
37  *  [%N. ][%T] - [%A][ (%Y)]
38  *
39  * and provide methods to format up a CFileItem's label(s).
40  *
41  * The %N/%A/%B masks are replaced with the corresponding metadata (if available).
42  *
43  * Square brackets are treated as a metadata block.  Anything inside the block other
44  * than the metadata mask is treated as either a prefix or postfix to the metadata. This
45  * information is only included in the formatted string when the metadata is non-empty.
46  *
47  * Any metadata tags not enclosed with square brackets are treated as if it were immediately
48  * enclosed - i.e. with no prefix or postfix.
49  *
50  * The special characters %, [, and ] can be produced using %%, %[, and %] respectively.
51  *
52  * Any static text outside of the metadata blocks is only shown if the blocks on either side
53  * (or just one side in the case of an end) are both non-empty.
54  *
55  * Examples (using the above expression):
56  *
57  *   Track  Title  Artist  Year     Resulting Label
58  *   -----  -----  ------  ----     ---------------
59  *     10    "40"    U2    1983     10. "40" - U2 (1983)
60  *           "40"    U2    1983     "40" - U2 (1983)
61  *     10            U2    1983     10. U2 (1983)
62  *     10    "40"          1983     "40" (1983)
63  *     10    "40"    U2             10. "40" - U2
64  *     10    "40"                   10. "40"
65  *
66  * Available metadata masks:
67  *
68  *  %A - Artist
69  *  %B - Album
70  *  %C - Programs count
71  *  %D - Duration
72  *  %E - episode number
73  *  %F - FileName
74  *  %G - Genre
75  *  %H - season*100+episode
76  *  %I - Size
77  *  %J - Date
78  *  %K - Movie/Game title
79  *  %L - existing Label
80  *  %M - number of episodes
81  *  %N - Track Number
82  *  %O - mpaa rating
83  *  %P - production code
84  *  %Q - file time
85  *  %R - Movie rating
86  *  %S - Disc Number
87  *  %T - Title
88  *  %U - studio
89  *  %V - Playcount
90  *  %W - Listeners
91  *  %X - Bitrate
92  *  %Y - Year
93  *  %Z - tvshow title
94  *  %a - Date Added
95  *  %b - Total number of discs
96  *  %c - Relevance - Used for actors' appearances
97  *  %d - Date and Time
98  *  %e - Original release date
99  *  %f - bpm
100  *  %p - Last Played
101  *  %r - User Rating
102  *  *t - Date Taken (suitable for Pictures)
103  */
104 
105 #define MASK_CHARS "NSATBGYFLDIJRCKMEPHZOQUVXWabcdefiprstuv"
106 
CLabelFormatter(const std::string & mask,const std::string & mask2)107 CLabelFormatter::CLabelFormatter(const std::string &mask, const std::string &mask2)
108 {
109   // assemble our label masks
110   AssembleMask(0, mask);
111   AssembleMask(1, mask2);
112   // save a bool for faster lookups
113   m_hideFileExtensions = !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_SHOWEXTENSIONS);
114 }
115 
GetContent(unsigned int label,const CFileItem * item) const116 std::string CLabelFormatter::GetContent(unsigned int label, const CFileItem *item) const
117 {
118   assert(label < 2);
119   assert(m_staticContent[label].size() == m_dynamicContent[label].size() + 1);
120 
121   if (!item) return "";
122 
123   std::string strLabel, dynamicLeft, dynamicRight;
124   for (unsigned int i = 0; i < m_dynamicContent[label].size(); i++)
125   {
126     dynamicRight = GetMaskContent(m_dynamicContent[label][i], item);
127     if ((i == 0 || !dynamicLeft.empty()) && !dynamicRight.empty())
128       strLabel += m_staticContent[label][i];
129     strLabel += dynamicRight;
130     dynamicLeft = dynamicRight;
131   }
132   if (!dynamicLeft.empty())
133     strLabel += m_staticContent[label][m_dynamicContent[label].size()];
134 
135   return strLabel;
136 }
137 
FormatLabel(CFileItem * item) const138 void CLabelFormatter::FormatLabel(CFileItem *item) const
139 {
140   std::string maskedLabel = GetContent(0, item);
141   if (!maskedLabel.empty())
142     item->SetLabel(maskedLabel);
143   else if (!item->m_bIsFolder && m_hideFileExtensions)
144     item->RemoveExtension();
145 }
146 
FormatLabel2(CFileItem * item) const147 void CLabelFormatter::FormatLabel2(CFileItem *item) const
148 {
149   item->SetLabel2(GetContent(1, item));
150 }
151 
GetMaskContent(const CMaskString & mask,const CFileItem * item) const152 std::string CLabelFormatter::GetMaskContent(const CMaskString &mask, const CFileItem *item) const
153 {
154   if (!item) return "";
155   const CMusicInfoTag *music = item->GetMusicInfoTag();
156   const CVideoInfoTag *movie = item->GetVideoInfoTag();
157   const CPictureInfoTag *pic = item->GetPictureInfoTag();
158   std::string value;
159   switch (mask.m_content)
160   {
161   case 'N':
162     if (music && music->GetTrackNumber() > 0)
163       value = StringUtils::Format("%2.2i", music->GetTrackNumber());
164     if (movie&& movie->m_iTrack > 0)
165       value = StringUtils::Format("%2.2i", movie->m_iTrack);
166     break;
167   case 'S':
168     if (music && music->GetDiscNumber() > 0)
169       value = StringUtils::Format("%2.2i", music->GetDiscNumber());
170     break;
171   case 'A':
172     if (music && music->GetArtistString().size())
173       value = music->GetArtistString();
174     if (movie && movie->m_artist.size())
175       value = StringUtils::Join(movie->m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
176     break;
177   case 'T':
178     if (music && music->GetTitle().size())
179       value = music->GetTitle();
180     if (movie && movie->m_strTitle.size())
181       value = movie->m_strTitle;
182     break;
183   case 'Z':
184     if (movie && !movie->m_strShowTitle.empty())
185       value = movie->m_strShowTitle;
186     break;
187   case 'B':
188     if (music && music->GetAlbum().size())
189       value = music->GetAlbum();
190     else if (movie)
191       value = movie->m_strAlbum;
192     break;
193   case 'G':
194     if (music && music->GetGenre().size())
195       value = StringUtils::Join(music->GetGenre(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
196     if (movie && movie->m_genre.size())
197       value = StringUtils::Join(movie->m_genre, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
198     break;
199   case 'Y':
200     if (music)
201       value = music->GetYearString();
202     if (movie)
203     {
204       if (movie->m_firstAired.IsValid())
205         value = movie->m_firstAired.GetAsLocalizedDate();
206       else if (movie->HasYear())
207         value = StringUtils::Format("%i", movie->GetYear());
208     }
209     break;
210   case 'F': // filename
211     value = CUtil::GetTitleFromPath(item->GetPath(), item->m_bIsFolder && !item->IsFileFolder());
212     break;
213   case 'L':
214     value = item->GetLabel();
215     // is the label the actual file or folder name?
216     if (value == URIUtils::GetFileName(item->GetPath()))
217     { // label is the same as filename, clean it up as appropriate
218       value = CUtil::GetTitleFromPath(item->GetPath(), item->m_bIsFolder && !item->IsFileFolder());
219     }
220     break;
221   case 'D':
222     { // duration
223       int nDuration=0;
224       if (music)
225         nDuration = music->GetDuration();
226       if (movie)
227         nDuration = movie->GetDuration();
228       if (nDuration > 0)
229         value = StringUtils::SecondsToTimeString(nDuration, (nDuration >= 3600) ? TIME_FORMAT_H_MM_SS : TIME_FORMAT_MM_SS);
230       else if (item->m_dwSize > 0)
231         value = StringUtils::SizeToString(item->m_dwSize);
232     }
233     break;
234   case 'I': // size
235     if( (item->m_bIsFolder && item->m_dwSize != 0) || item->m_dwSize >= 0 )
236       value = StringUtils::SizeToString(item->m_dwSize);
237     break;
238   case 'J': // date
239     if (item->m_dateTime.IsValid())
240       value = item->m_dateTime.GetAsLocalizedDate();
241     break;
242   case 'Q': // time
243     if (item->m_dateTime.IsValid())
244       value = item->m_dateTime.GetAsLocalizedTime("", false);
245     break;
246   case 'R': // rating
247     if (music && music->GetRating() != 0.f)
248       value = StringUtils::Format("%.1f", music->GetRating());
249     else if (movie && movie->GetRating().rating != 0.f)
250       value = StringUtils::Format("%.1f", movie->GetRating().rating);
251     break;
252   case 'C': // programs count
253     value = StringUtils::Format("%i", item->m_iprogramCount);
254     break;
255   case 'c': // relevance
256     value = StringUtils::Format("%i", movie->m_relevance);
257     break;
258   case 'K':
259     value = item->m_strTitle;
260     break;
261   case 'M':
262     if (movie && movie->m_iEpisode > 0)
263       value = StringUtils::Format("%i %s",
264                                   movie->m_iEpisode,
265                                   g_localizeStrings.Get(movie->m_iEpisode == 1 ? 20452 : 20453).c_str());
266     break;
267   case 'E':
268     if (movie && movie->m_iEpisode > 0)
269     { // episode number
270       if (movie->m_iSeason == 0)
271         value = StringUtils::Format("S%2.2i", movie->m_iEpisode);
272       else
273         value = StringUtils::Format("%2.2i", movie->m_iEpisode);
274     }
275     break;
276   case 'P':
277     if (movie) // tvshow production code
278       value = movie->m_strProductionCode;
279     break;
280   case 'H':
281     if (movie && movie->m_iEpisode > 0)
282     { // season*100+episode number
283       if (movie->m_iSeason == 0)
284         value = StringUtils::Format("S%2.2i", movie->m_iEpisode);
285       else
286         value = StringUtils::Format("%ix%2.2i", movie->m_iSeason,movie->m_iEpisode);
287     }
288     break;
289   case 'O':
290     if (movie)
291     {// MPAA Rating
292       value = movie->m_strMPAARating;
293     }
294     break;
295   case 'U':
296     if (movie && !movie->m_studio.empty())
297     {// Studios
298       value = StringUtils::Join(movie ->m_studio, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
299     }
300     break;
301   case 'V': // Playcount
302     if (music)
303       value = StringUtils::Format("%i", music->GetPlayCount());
304     if (movie)
305       value = StringUtils::Format("%i", movie->GetPlayCount());
306     break;
307   case 'X': // Bitrate
308     if( !item->m_bIsFolder && item->m_dwSize != 0 )
309       value = StringUtils::Format("%" PRId64" kbps", item->m_dwSize);
310     break;
311    case 'W': // Listeners
312     if( !item->m_bIsFolder && music && music->GetListeners() != 0 )
313      value = StringUtils::Format("%i %s",
314                                  music->GetListeners(),
315                                  g_localizeStrings.Get(music->GetListeners() == 1 ? 20454 : 20455).c_str());
316     break;
317   case 'a': // Date Added
318     if (movie && movie->m_dateAdded.IsValid())
319       value = movie->m_dateAdded.GetAsLocalizedDate();
320     if (music && music->GetDateAdded().IsValid())
321       value = music->GetDateAdded().GetAsLocalizedDate();
322     break;
323   case 'b': // Total number of discs
324     if (music)
325       value = StringUtils::Format("%i", music->GetTotalDiscs());
326     break;
327   case 'e': // Original release date
328     if (music)
329     {
330       value = music->GetOriginalDate();
331       if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bMusicLibraryUseISODates)
332         value = StringUtils::ISODateToLocalizedDate(value);
333       break;
334     }
335   case 'd': // date and time
336     if (item->m_dateTime.IsValid())
337       value = item->m_dateTime.GetAsLocalizedDateTime();
338     break;
339   case 'p': // Last played
340     if (movie && movie->m_lastPlayed.IsValid())
341       value = movie->m_lastPlayed.GetAsLocalizedDate();
342     if (music && music->GetLastPlayed().IsValid())
343       value = music->GetLastPlayed().GetAsLocalizedDate();
344     break;
345   case 'r': // userrating
346     if (movie && movie->m_iUserRating != 0)
347       value = StringUtils::Format("%i", movie->m_iUserRating);
348     if (music && music->GetUserrating() != 0)
349       value = StringUtils::Format("%i", music->GetUserrating());
350     break;
351   case 't': // Date Taken
352     if (pic && pic->GetDateTimeTaken().IsValid())
353       value = pic->GetDateTimeTaken().GetAsLocalizedDate();
354     break;
355   case 's': // Addon status
356     if (item->HasProperty("Addon.Status"))
357       value = item->GetProperty("Addon.Status").asString();
358     break;
359   case 'i': // Install date
360     if (item->HasAddonInfo() && item->GetAddonInfo()->InstallDate().IsValid())
361       value = item->GetAddonInfo()->InstallDate().GetAsLocalizedDate();
362     break;
363   case 'u': // Last used
364     if (item->HasAddonInfo() && item->GetAddonInfo()->LastUsed().IsValid())
365       value = item->GetAddonInfo()->LastUsed().GetAsLocalizedDate();
366     break;
367   case 'v': // Last updated
368     if (item->HasAddonInfo() && item->GetAddonInfo()->LastUpdated().IsValid())
369       value = item->GetAddonInfo()->LastUpdated().GetAsLocalizedDate();
370     break;
371   case 'f': // BPM
372     if (music)
373       value = StringUtils::Format("%i", music->GetBPM());
374     break;
375   }
376   if (!value.empty())
377     return mask.m_prefix + value + mask.m_postfix;
378   return "";
379 }
380 
SplitMask(unsigned int label,const std::string & mask)381 void CLabelFormatter::SplitMask(unsigned int label, const std::string &mask)
382 {
383   assert(label < 2);
384   CRegExp reg;
385   reg.RegComp("%([" MASK_CHARS "])");
386   std::string work(mask);
387   int findStart = -1;
388   while ((findStart = reg.RegFind(work.c_str())) >= 0)
389   { // we've found a match
390     m_staticContent[label].push_back(work.substr(0, findStart));
391     m_dynamicContent[label].emplace_back("", reg.GetMatch(1)[0], "");
392     work = work.substr(findStart + reg.GetFindLen());
393   }
394   m_staticContent[label].push_back(work);
395 }
396 
AssembleMask(unsigned int label,const std::string & mask)397 void CLabelFormatter::AssembleMask(unsigned int label, const std::string& mask)
398 {
399   assert(label < 2);
400   m_staticContent[label].clear();
401   m_dynamicContent[label].clear();
402 
403   // we want to match [<prefix>%A<postfix]
404   // but allow %%, %[, %] to be in the prefix and postfix.  Anything before the first [
405   // could be a mask that's not surrounded with [], so pass to SplitMask.
406   CRegExp reg;
407   reg.RegComp("(^|[^%])\\[(([^%]|%%|%\\]|%\\[)*)%([" MASK_CHARS "])(([^%]|%%|%\\]|%\\[)*)\\]");
408   std::string work(mask);
409   int findStart = -1;
410   while ((findStart = reg.RegFind(work.c_str())) >= 0)
411   { // we've found a match for a pre/postfixed string
412     // send anything
413     SplitMask(label, work.substr(0, findStart) + reg.GetMatch(1));
414     m_dynamicContent[label].emplace_back(reg.GetMatch(2), reg.GetMatch(4)[0], reg.GetMatch(5));
415     work = work.substr(findStart + reg.GetFindLen());
416   }
417   SplitMask(label, work);
418   assert(m_staticContent[label].size() == m_dynamicContent[label].size() + 1);
419 }
420 
FillMusicTag(const std::string & fileName,CMusicInfoTag * tag) const421 bool CLabelFormatter::FillMusicTag(const std::string &fileName, CMusicInfoTag *tag) const
422 {
423   // run through and find static content to split the string up
424   size_t pos1 = fileName.find(m_staticContent[0][0], 0);
425   if (pos1 == std::string::npos)
426     return false;
427   for (unsigned int i = 1; i < m_staticContent[0].size(); i++)
428   {
429     size_t pos2 = m_staticContent[0][i].size() ? fileName.find(m_staticContent[0][i], pos1) : fileName.size();
430     if (pos2 == std::string::npos)
431       return false;
432     // found static content - thus we have the dynamic content surrounded
433     FillMusicMaskContent(m_dynamicContent[0][i - 1].m_content, fileName.substr(pos1, pos2 - pos1), tag);
434     pos1 = pos2 + m_staticContent[0][i].size();
435   }
436   return true;
437 }
438 
FillMusicMaskContent(const char mask,const std::string & value,CMusicInfoTag * tag) const439 void CLabelFormatter::FillMusicMaskContent(const char mask, const std::string &value, CMusicInfoTag *tag) const
440 {
441   if (!tag) return;
442   switch (mask)
443   {
444   case 'N':
445     tag->SetTrackNumber(atol(value.c_str()));
446     break;
447   case 'S':
448     tag->SetDiscNumber(atol(value.c_str()));
449     break;
450   case 'A':
451     tag->SetArtist(value);
452     break;
453   case 'T':
454     tag->SetTitle(value);
455     break;
456   case 'B':
457     tag->SetAlbum(value);
458     break;
459   case 'G':
460     tag->SetGenre(value);
461     break;
462   case 'Y':
463     tag->SetYear(atol(value.c_str()));
464     break;
465   case 'D':
466     tag->SetDuration(StringUtils::TimeStringToSeconds(value));
467     break;
468   case 'R': // rating
469     tag->SetRating(value[0]);
470     break;
471   case 'r': // userrating
472     tag->SetUserrating(value[0]);
473     break;
474   case 'b': // total discs
475     tag->SetTotalDiscs(atol(value.c_str()));
476     break;
477   }
478 }
479 
480