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