[cosmetics] update date in GPL header
[vuplus_xbmc] / xbmc / utils / LabelFormatter.cpp
1 /*
2  *      Copyright (C) 2005-2013 Team XBMC
3  *      http://www.xbmc.org
4  *
5  *  This Program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2, or (at your option)
8  *  any later version.
9  *
10  *  This Program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with XBMC; see the file COPYING.  If not, see
17  *  <http://www.gnu.org/licenses/>.
18  *
19  */
20
21 #include "LabelFormatter.h"
22 #include "settings/GUISettings.h"
23 #include "settings/AdvancedSettings.h"
24 #include "RegExp.h"
25 #include "Util.h"
26 #include "video/VideoInfoTag.h"
27 #include "music/tags/MusicInfoTag.h"
28 #include "FileItem.h"
29 #include "StringUtils.h"
30 #include "URIUtils.h"
31 #include "guilib/LocalizeStrings.h"
32
33 using namespace MUSIC_INFO;
34
35 /* LabelFormatter
36  * ==============
37  *
38  * The purpose of this class is to parse a mask string of the form
39  *
40  *  [%N. ][%T] - [%A][ (%Y)]
41  *
42  * and provide methods to format up a CFileItem's label(s).
43  *
44  * The %N/%A/%B masks are replaced with the corresponding metadata (if available).
45  *
46  * Square brackets are treated as a metadata block.  Anything inside the block other
47  * than the metadata mask is treated as either a prefix or postfix to the metadata. This
48  * information is only included in the formatted string when the metadata is non-empty.
49  *
50  * Any metadata tags not enclosed with square brackets are treated as if it were immediately
51  * enclosed - i.e. with no prefix or postfix.
52  *
53  * The special characters %, [, and ] can be produced using %%, %[, and %] respectively.
54  *
55  * Any static text outside of the metadata blocks is only shown if the blocks on either side
56  * (or just one side in the case of an end) are both non-empty.
57  *
58  * Examples (using the above expression):
59  *
60  *   Track  Title  Artist  Year     Resulting Label
61  *   -----  -----  ------  ----     ---------------
62  *     10    "40"    U2    1983     10. "40" - U2 (1983)
63  *           "40"    U2    1983     "40" - U2 (1983)
64  *     10            U2    1983     10. U2 (1983)
65  *     10    "40"          1983     "40" (1983)
66  *     10    "40"    U2             10. "40" - U2
67  *     10    "40"                   10. "40"
68  *
69  * Available metadata masks:
70  *
71  *  %A - Artist
72  *  %B - Album
73  *  %C - Programs count
74  *  %D - Duration
75  *  %E - episode number
76  *  %F - FileName
77  *  %G - Genre
78  *  %H - season*100+episode
79  *  %I - Size
80  *  %J - Date
81  *  %K - Movie/Game title
82  *  %L - existing Label
83  *  %M - number of episodes
84  *  %N - Track Number
85  *  %O - mpaa rating
86  *  %P - production code
87  *  %Q - file time
88  *  %R - Movie rating
89  *  %S - Disc Number
90  *  %T - Title
91  *  %U - studio
92  *  %V - Playcount
93  *  %W - Listeners
94  *  %X - Bitrate
95  *  %Y - Year
96  *  %Z - tvshow title
97  *  %a - Date Added
98  *  %p - Last Played
99  */
100
101 #define MASK_CHARS "NSATBGYFLDIJRCKMEPHZOQUVXWap"
102
103 CLabelFormatter::CLabelFormatter(const CStdString &mask, const CStdString &mask2)
104 {
105   // assemble our label masks
106   AssembleMask(0, mask);
107   AssembleMask(1, mask2);
108   // save a bool for faster lookups
109   m_hideFileExtensions = !g_guiSettings.GetBool("filelists.showextensions");
110 }
111
112 CStdString CLabelFormatter::GetContent(unsigned int label, const CFileItem *item) const
113 {
114   assert(label < 2);
115   assert(m_staticContent[label].size() == m_dynamicContent[label].size() + 1);
116
117   if (!item) return "";
118
119   CStdString strLabel, dynamicLeft, dynamicRight;
120   for (unsigned int i = 0; i < m_dynamicContent[label].size(); i++)
121   {
122     dynamicRight = GetMaskContent(m_dynamicContent[label][i], item);
123     if ((i == 0 || !dynamicLeft.IsEmpty()) && !dynamicRight.IsEmpty())
124       strLabel += m_staticContent[label][i];
125     strLabel += dynamicRight;
126     dynamicLeft = dynamicRight;
127   }
128   if (!dynamicLeft.IsEmpty())
129     strLabel += m_staticContent[label][m_dynamicContent[label].size()];
130
131   return strLabel;
132 }
133
134 void CLabelFormatter::FormatLabel(CFileItem *item) const
135 {
136   CStdString maskedLabel = GetContent(0, item);
137   if (!maskedLabel.IsEmpty())
138     item->SetLabel(maskedLabel);
139   else if (!item->m_bIsFolder && m_hideFileExtensions)
140     item->RemoveExtension();
141 }
142
143 void CLabelFormatter::FormatLabel2(CFileItem *item) const
144 {
145   item->SetLabel2(GetContent(1, item));
146 }
147
148 CStdString CLabelFormatter::GetMaskContent(const CMaskString &mask, const CFileItem *item) const
149 {
150   if (!item) return "";
151   const CMusicInfoTag *music = item->GetMusicInfoTag();
152   const CVideoInfoTag *movie = item->GetVideoInfoTag();
153   CStdString value;
154   switch (mask.m_content)
155   {
156   case 'N':
157     if (music && music->GetTrackNumber() > 0)
158       value.Format("%02.2i", music->GetTrackNumber());
159     if (movie&& movie->m_iTrack > 0)
160       value.Format("%02.2i", movie->m_iTrack);
161     break;
162   case 'S':
163     if (music && music->GetDiscNumber() > 0)
164       value.Format("%02.2i", music->GetDiscNumber());
165     break;
166   case 'A':
167     if (music && music->GetArtist().size())
168       value = StringUtils::Join(music->GetArtist(), g_advancedSettings.m_musicItemSeparator);
169     if (movie && movie->m_artist.size())
170       value = StringUtils::Join(movie->m_artist, g_advancedSettings.m_videoItemSeparator);
171     break;
172   case 'T':
173     if (music && music->GetTitle().size())
174       value = music->GetTitle();
175     if (movie && movie->m_strTitle.size())
176       value = movie->m_strTitle;
177     break;
178   case 'Z':
179     if (movie && !movie->m_strShowTitle.IsEmpty())
180       value = movie->m_strShowTitle;
181     break;
182   case 'B':
183     if (music && music->GetAlbum().size())
184       value = music->GetAlbum();
185     else if (movie)
186       value = movie->m_strAlbum;
187     break;
188   case 'G':
189     if (music && music->GetGenre().size())
190       value = StringUtils::Join(music->GetGenre(), g_advancedSettings.m_musicItemSeparator);
191     if (movie && movie->m_genre.size())
192       value = StringUtils::Join(movie->m_genre, g_advancedSettings.m_videoItemSeparator);
193     break;
194   case 'Y':
195     if (music)
196       value = music->GetYearString();
197     if (movie)
198     {
199       if (movie->m_firstAired.IsValid())
200         value = movie->m_firstAired.GetAsLocalizedDate();
201       else if (movie->m_premiered.IsValid())
202         value = movie->m_premiered.GetAsLocalizedDate();
203       else if (movie->m_iYear > 0)
204         value.Format("%i",movie->m_iYear);
205     }
206     break;
207   case 'F': // filename
208     value = CUtil::GetTitleFromPath(item->GetPath(), item->m_bIsFolder && !item->IsFileFolder());
209     break;
210   case 'L':
211     value = item->GetLabel();
212     // is the label the actual file or folder name?
213     if (value == URIUtils::GetFileName(item->GetPath()))
214     { // label is the same as filename, clean it up as appropriate
215       value = CUtil::GetTitleFromPath(item->GetPath(), item->m_bIsFolder && !item->IsFileFolder());
216     }
217     break;
218   case 'D':
219     { // duration
220       int nDuration=0;
221       if (music)
222         nDuration = music->GetDuration();
223       if (movie)
224         nDuration = movie->GetDuration();
225       if (nDuration > 0)
226         value = StringUtils::SecondsToTimeString(nDuration, (nDuration >= 3600) ? TIME_FORMAT_H_MM_SS : TIME_FORMAT_MM_SS);
227       else if (item->m_dwSize > 0)
228         value = StringUtils::SizeToString(item->m_dwSize);
229     }
230     break;
231   case 'I': // size
232     if( !item->m_bIsFolder || item->m_dwSize != 0 )
233       value = StringUtils::SizeToString(item->m_dwSize);
234     break;
235   case 'J': // date
236     if (item->m_dateTime.IsValid())
237       value = item->m_dateTime.GetAsLocalizedDate();
238     break;
239   case 'Q': // time
240     if (item->m_dateTime.IsValid())
241       value = item->m_dateTime.GetAsLocalizedTime("", false);
242     break;
243   case 'R': // rating
244     if (music && music->GetRating() != '0')
245       value = music->GetRating();
246     else if (movie && movie->m_fRating != 0.f)
247       value.Format("%.1f", movie->m_fRating);
248     break;
249   case 'C': // programs count
250     value.Format("%i", item->m_iprogramCount);
251     break;
252   case 'K':
253     value = item->m_strTitle;
254     break;
255   case 'M':
256     if (movie && movie->m_iEpisode > 0)
257       value.Format("%i %s", movie->m_iEpisode,g_localizeStrings.Get(movie->m_iEpisode == 1 ? 20452 : 20453));
258     break;
259   case 'E':
260     if (movie && movie->m_iEpisode > 0)
261     { // episode number
262       if (movie->m_iSpecialSortEpisode > 0)
263         value.Format("S%02.2i", movie->m_iEpisode);
264       else
265         value.Format("%02.2i", movie->m_iEpisode);
266     }
267     break;
268   case 'P':
269     if (movie) // tvshow production code
270       value = movie->m_strProductionCode;
271     break;
272   case 'H':
273     if (movie && movie->m_iEpisode > 0)
274     { // season*100+episode number
275       if (movie->m_iSpecialSortSeason > 0)
276         value.Format("Sx%02.2i", movie->m_iEpisode);
277       else
278         value.Format("%ix%02.2i", movie->m_iSeason,movie->m_iEpisode);
279     }
280     break;
281   case 'O':
282     if (movie && movie->m_strMPAARating)
283     {// MPAA Rating
284       value = movie->m_strMPAARating;
285     }
286     break;
287   case 'U':
288     if (movie && movie->m_studio.size() > 0)
289     {// Studios
290       value = StringUtils::Join(movie ->m_studio, g_advancedSettings.m_videoItemSeparator);
291     }
292     break;
293   case 'V': // Playcount
294     if (music)
295       value.Format("%i", music->GetPlayCount());
296     if (movie)
297       value.Format("%i", movie->m_playCount);
298     break;
299   case 'X': // Bitrate
300     if( !item->m_bIsFolder && item->m_dwSize != 0 )
301       value.Format("%i kbps", item->m_dwSize);
302     break;
303    case 'W': // Listeners
304     if( !item->m_bIsFolder && music && music->GetListeners() != 0 )
305      value.Format("%i %s", music->GetListeners(), g_localizeStrings.Get(music->GetListeners() == 1 ? 20454 : 20455));
306     break;
307   case 'a': // Date Added
308     if (movie && movie->m_dateAdded.IsValid())
309       value = movie->m_dateAdded.GetAsLocalizedDate();
310     break;
311   case 'p': // Last played
312     if (movie && movie->m_lastPlayed.IsValid())
313       value = movie->m_lastPlayed.GetAsLocalizedDate();
314     break;
315   }
316   if (!value.IsEmpty())
317     return mask.m_prefix + value + mask.m_postfix;
318   return "";
319 }
320
321 void CLabelFormatter::SplitMask(unsigned int label, const CStdString &mask)
322 {
323   assert(label < 2);
324   CRegExp reg;
325   reg.RegComp("%([" MASK_CHARS "])");
326   CStdString work(mask);
327   int findStart = -1;
328   while ((findStart = reg.RegFind(work.c_str())) >= 0)
329   { // we've found a match
330     m_staticContent[label].push_back(work.Left(findStart));
331     m_dynamicContent[label].push_back(CMaskString("", 
332           reg.GetReplaceString("\\1")[0], ""));
333     work = work.Mid(findStart + reg.GetFindLen());
334   }
335   m_staticContent[label].push_back(work);
336 }
337
338 void CLabelFormatter::AssembleMask(unsigned int label, const CStdString& mask)
339 {
340   assert(label < 2);
341   m_staticContent[label].clear();
342   m_dynamicContent[label].clear();
343
344   // we want to match [<prefix>%A<postfix]
345   // but allow %%, %[, %] to be in the prefix and postfix.  Anything before the first [
346   // could be a mask that's not surrounded with [], so pass to SplitMask.
347   CRegExp reg;
348   reg.RegComp("(^|[^%])\\[(([^%]|%%|%\\]|%\\[)*)%([" MASK_CHARS "])(([^%]|%%|%\\]|%\\[)*)\\]");
349   CStdString work(mask);
350   int findStart = -1;
351   while ((findStart = reg.RegFind(work.c_str())) >= 0)
352   { // we've found a match for a pre/postfixed string
353     // send anything
354     SplitMask(label, work.Left(findStart) + reg.GetReplaceString("\\1").c_str());
355     m_dynamicContent[label].push_back(CMaskString(
356             reg.GetReplaceString("\\2"),
357             reg.GetReplaceString("\\4")[0],
358             reg.GetReplaceString("\\5")));
359     work = work.Mid(findStart + reg.GetFindLen());
360   }
361   SplitMask(label, work);
362   assert(m_staticContent[label].size() == m_dynamicContent[label].size() + 1);
363 }
364
365 bool CLabelFormatter::FillMusicTag(const CStdString &fileName, CMusicInfoTag *tag) const
366 {
367   // run through and find static content to split the string up
368   int pos1 = fileName.Find(m_staticContent[0][0], 0);
369   if (pos1 == (int)CStdString::npos)
370     return false;
371   for (unsigned int i = 1; i < m_staticContent[0].size(); i++)
372   {
373     int pos2 = m_staticContent[0][i].size() ? fileName.Find(m_staticContent[0][i], pos1) : fileName.size();
374     if (pos2 == (int)CStdString::npos)
375       return false;
376     // found static content - thus we have the dynamic content surrounded
377     FillMusicMaskContent(m_dynamicContent[0][i - 1].m_content, fileName.Mid(pos1, pos2 - pos1), tag);
378     pos1 = pos2 + m_staticContent[0][i].size();
379   }
380   return true;
381 }
382
383 void CLabelFormatter::FillMusicMaskContent(const char mask, const CStdString &value, CMusicInfoTag *tag) const
384 {
385   if (!tag) return;
386   switch (mask)
387   {
388   case 'N':
389     tag->SetTrackNumber(atol(value.c_str()));
390     break;
391   case 'S':
392     tag->SetPartOfSet(atol(value.c_str()));
393     break;
394   case 'A':
395     tag->SetArtist(value);
396     break;
397   case 'T':
398     tag->SetTitle(value);
399     break;
400   case 'B':
401     tag->SetAlbum(value);
402     break;
403   case 'G':
404     tag->SetGenre(value);
405     break;
406   case 'Y':
407     tag->SetYear(atol(value.c_str()));
408     break;
409   case 'D':
410     tag->SetDuration(StringUtils::TimeStringToSeconds(value));
411     break;
412   case 'R': // rating
413     tag->SetRating(value[0]);
414     break;
415   }
416 }
417