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