Merge pull request #4676 from jmarshallnz/dont_set_scraper_on_tvshow_on_nfo
[vuplus_xbmc] / xbmc / utils / LabelFormatter.cpp
1 /*
2  *      Copyright (C) 2005-2013 Team XBMC
3  *      http://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/AdvancedSettings.h"
23 #include "settings/Settings.h"
24 #include "RegExp.h"
25 #include "Util.h"
26 #include "video/VideoInfoTag.h"
27 #include "music/tags/MusicInfoTag.h"
28 #include "pictures/PictureInfoTag.h"
29 #include "FileItem.h"
30 #include "StringUtils.h"
31 #include "URIUtils.h"
32 #include "guilib/LocalizeStrings.h"
33
34 using namespace MUSIC_INFO;
35
36 /* LabelFormatter
37  * ==============
38  *
39  * The purpose of this class is to parse a mask string of the form
40  *
41  *  [%N. ][%T] - [%A][ (%Y)]
42  *
43  * and provide methods to format up a CFileItem's label(s).
44  *
45  * The %N/%A/%B masks are replaced with the corresponding metadata (if available).
46  *
47  * Square brackets are treated as a metadata block.  Anything inside the block other
48  * than the metadata mask is treated as either a prefix or postfix to the metadata. This
49  * information is only included in the formatted string when the metadata is non-empty.
50  *
51  * Any metadata tags not enclosed with square brackets are treated as if it were immediately
52  * enclosed - i.e. with no prefix or postfix.
53  *
54  * The special characters %, [, and ] can be produced using %%, %[, and %] respectively.
55  *
56  * Any static text outside of the metadata blocks is only shown if the blocks on either side
57  * (or just one side in the case of an end) are both non-empty.
58  *
59  * Examples (using the above expression):
60  *
61  *   Track  Title  Artist  Year     Resulting Label
62  *   -----  -----  ------  ----     ---------------
63  *     10    "40"    U2    1983     10. "40" - U2 (1983)
64  *           "40"    U2    1983     "40" - U2 (1983)
65  *     10            U2    1983     10. U2 (1983)
66  *     10    "40"          1983     "40" (1983)
67  *     10    "40"    U2             10. "40" - U2
68  *     10    "40"                   10. "40"
69  *
70  * Available metadata masks:
71  *
72  *  %A - Artist
73  *  %B - Album
74  *  %C - Programs count
75  *  %D - Duration
76  *  %E - episode number
77  *  %F - FileName
78  *  %G - Genre
79  *  %H - season*100+episode
80  *  %I - Size
81  *  %J - Date
82  *  %K - Movie/Game title
83  *  %L - existing Label
84  *  %M - number of episodes
85  *  %N - Track Number
86  *  %O - mpaa rating
87  *  %P - production code
88  *  %Q - file time
89  *  %R - Movie rating
90  *  %S - Disc Number
91  *  %T - Title
92  *  %U - studio
93  *  %V - Playcount
94  *  %W - Listeners
95  *  %X - Bitrate
96  *  %Y - Year
97  *  %Z - tvshow title
98  *  %a - Date Added
99  *  %p - Last Played
100  *  *t - Date Taken (suitable for Pictures)
101  */
102
103 #define MASK_CHARS "NSATBGYFLDIJRCKMEPHZOQUVXWapt"
104
105 CLabelFormatter::CLabelFormatter(const CStdString &mask, const CStdString &mask2)
106 {
107   // assemble our label masks
108   AssembleMask(0, mask);
109   AssembleMask(1, mask2);
110   // save a bool for faster lookups
111   m_hideFileExtensions = !CSettings::Get().GetBool("filelists.showextensions");
112 }
113
114 CStdString CLabelFormatter::GetContent(unsigned int label, const CFileItem *item) const
115 {
116   assert(label < 2);
117   assert(m_staticContent[label].size() == m_dynamicContent[label].size() + 1);
118
119   if (!item) return "";
120
121   CStdString strLabel, dynamicLeft, dynamicRight;
122   for (unsigned int i = 0; i < m_dynamicContent[label].size(); i++)
123   {
124     dynamicRight = GetMaskContent(m_dynamicContent[label][i], item);
125     if ((i == 0 || !dynamicLeft.empty()) && !dynamicRight.empty())
126       strLabel += m_staticContent[label][i];
127     strLabel += dynamicRight;
128     dynamicLeft = dynamicRight;
129   }
130   if (!dynamicLeft.empty())
131     strLabel += m_staticContent[label][m_dynamicContent[label].size()];
132
133   return strLabel;
134 }
135
136 void CLabelFormatter::FormatLabel(CFileItem *item) const
137 {
138   CStdString maskedLabel = GetContent(0, item);
139   if (!maskedLabel.empty())
140     item->SetLabel(maskedLabel);
141   else if (!item->m_bIsFolder && m_hideFileExtensions)
142     item->RemoveExtension();
143 }
144
145 void CLabelFormatter::FormatLabel2(CFileItem *item) const
146 {
147   item->SetLabel2(GetContent(1, item));
148 }
149
150 CStdString CLabelFormatter::GetMaskContent(const CMaskString &mask, const CFileItem *item) const
151 {
152   if (!item) return "";
153   const CMusicInfoTag *music = item->GetMusicInfoTag();
154   const CVideoInfoTag *movie = item->GetVideoInfoTag();
155   const CPictureInfoTag *pic = item->GetPictureInfoTag();
156   CStdString value;
157   switch (mask.m_content)
158   {
159   case 'N':
160     if (music && music->GetTrackNumber() > 0)
161       value = StringUtils::Format("%02.2i", music->GetTrackNumber());
162     if (movie&& movie->m_iTrack > 0)
163       value = StringUtils::Format("%02.2i", movie->m_iTrack);
164     break;
165   case 'S':
166     if (music && music->GetDiscNumber() > 0)
167       value = StringUtils::Format("%02.2i", music->GetDiscNumber());
168     break;
169   case 'A':
170     if (music && music->GetArtist().size())
171       value = StringUtils::Join(music->GetArtist(), g_advancedSettings.m_musicItemSeparator);
172     if (movie && movie->m_artist.size())
173       value = StringUtils::Join(movie->m_artist, g_advancedSettings.m_videoItemSeparator);
174     break;
175   case 'T':
176     if (music && music->GetTitle().size())
177       value = music->GetTitle();
178     if (movie && movie->m_strTitle.size())
179       value = movie->m_strTitle;
180     break;
181   case 'Z':
182     if (movie && !movie->m_strShowTitle.empty())
183       value = movie->m_strShowTitle;
184     break;
185   case 'B':
186     if (music && music->GetAlbum().size())
187       value = music->GetAlbum();
188     else if (movie)
189       value = movie->m_strAlbum;
190     break;
191   case 'G':
192     if (music && music->GetGenre().size())
193       value = StringUtils::Join(music->GetGenre(), g_advancedSettings.m_musicItemSeparator);
194     if (movie && movie->m_genre.size())
195       value = StringUtils::Join(movie->m_genre, g_advancedSettings.m_videoItemSeparator);
196     break;
197   case 'Y':
198     if (music)
199       value = music->GetYearString();
200     if (movie)
201     {
202       if (movie->m_firstAired.IsValid())
203         value = movie->m_firstAired.GetAsLocalizedDate();
204       else if (movie->m_premiered.IsValid())
205         value = movie->m_premiered.GetAsLocalizedDate();
206       else if (movie->m_iYear > 0)
207         value = StringUtils::Format("%i", movie->m_iYear);
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 )
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')
248       value = music->GetRating();
249     else if (movie && movie->m_fRating != 0.f)
250       value = StringUtils::Format("%.1f", movie->m_fRating);
251     break;
252   case 'C': // programs count
253     value = StringUtils::Format("%i", item->m_iprogramCount);
254     break;
255   case 'K':
256     value = item->m_strTitle;
257     break;
258   case 'M':
259     if (movie && movie->m_iEpisode > 0)
260       value = StringUtils::Format("%i %s",
261                                   movie->m_iEpisode,
262                                   g_localizeStrings.Get(movie->m_iEpisode == 1 ? 20452 : 20453).c_str());
263     break;
264   case 'E':
265     if (movie && movie->m_iEpisode > 0)
266     { // episode number
267       if (movie->m_iSeason == 0)
268         value = StringUtils::Format("S%02.2i", movie->m_iEpisode);
269       else
270         value = StringUtils::Format("%02.2i", movie->m_iEpisode);
271     }
272     break;
273   case 'P':
274     if (movie) // tvshow production code
275       value = movie->m_strProductionCode;
276     break;
277   case 'H':
278     if (movie && movie->m_iEpisode > 0)
279     { // season*100+episode number
280       if (movie->m_iSeason == 0)
281         value = StringUtils::Format("S%02.2i", movie->m_iEpisode);
282       else
283         value = StringUtils::Format("%ix%02.2i", movie->m_iSeason,movie->m_iEpisode);
284     }
285     break;
286   case 'O':
287     if (movie && movie->m_strMPAARating)
288     {// MPAA Rating
289       value = movie->m_strMPAARating;
290     }
291     break;
292   case 'U':
293     if (movie && movie->m_studio.size() > 0)
294     {// Studios
295       value = StringUtils::Join(movie ->m_studio, g_advancedSettings.m_videoItemSeparator);
296     }
297     break;
298   case 'V': // Playcount
299     if (music)
300       value = StringUtils::Format("%i", music->GetPlayCount());
301     if (movie)
302       value = StringUtils::Format("%i", movie->m_playCount);
303     break;
304   case 'X': // Bitrate
305     if( !item->m_bIsFolder && item->m_dwSize != 0 )
306       value = StringUtils::Format("%i kbps", item->m_dwSize);
307     break;
308    case 'W': // Listeners
309     if( !item->m_bIsFolder && music && music->GetListeners() != 0 )
310      value = StringUtils::Format("%i %s",
311                                  music->GetListeners(),
312                                  g_localizeStrings.Get(music->GetListeners() == 1 ? 20454 : 20455).c_str());
313     break;
314   case 'a': // Date Added
315     if (movie && movie->m_dateAdded.IsValid())
316       value = movie->m_dateAdded.GetAsLocalizedDate();
317     break;
318   case 'p': // Last played
319     if (movie && movie->m_lastPlayed.IsValid())
320       value = movie->m_lastPlayed.GetAsLocalizedDate();
321     break;
322   case 't': // Date Taken
323     if (pic && pic->GetDateTimeTaken().IsValid())
324       value = pic->GetDateTimeTaken().GetAsLocalizedDate();
325     break;
326   }
327   if (!value.empty())
328     return mask.m_prefix + value + mask.m_postfix;
329   return "";
330 }
331
332 void CLabelFormatter::SplitMask(unsigned int label, const CStdString &mask)
333 {
334   assert(label < 2);
335   CRegExp reg;
336   reg.RegComp("%([" MASK_CHARS "])");
337   CStdString work(mask);
338   int findStart = -1;
339   while ((findStart = reg.RegFind(work.c_str())) >= 0)
340   { // we've found a match
341     m_staticContent[label].push_back(work.substr(0, findStart));
342     m_dynamicContent[label].push_back(CMaskString("", 
343           reg.GetMatch(1)[0], ""));
344     work = work.substr(findStart + reg.GetFindLen());
345   }
346   m_staticContent[label].push_back(work);
347 }
348
349 void CLabelFormatter::AssembleMask(unsigned int label, const CStdString& mask)
350 {
351   assert(label < 2);
352   m_staticContent[label].clear();
353   m_dynamicContent[label].clear();
354
355   // we want to match [<prefix>%A<postfix]
356   // but allow %%, %[, %] to be in the prefix and postfix.  Anything before the first [
357   // could be a mask that's not surrounded with [], so pass to SplitMask.
358   CRegExp reg;
359   reg.RegComp("(^|[^%])\\[(([^%]|%%|%\\]|%\\[)*)%([" MASK_CHARS "])(([^%]|%%|%\\]|%\\[)*)\\]");
360   CStdString work(mask);
361   int findStart = -1;
362   while ((findStart = reg.RegFind(work.c_str())) >= 0)
363   { // we've found a match for a pre/postfixed string
364     // send anything
365     SplitMask(label, work.substr(0, findStart) + reg.GetMatch(1));
366     m_dynamicContent[label].push_back(CMaskString(
367             reg.GetMatch(2),
368             reg.GetMatch(4)[0],
369             reg.GetMatch(5)));
370     work = work.substr(findStart + reg.GetFindLen());
371   }
372   SplitMask(label, work);
373   assert(m_staticContent[label].size() == m_dynamicContent[label].size() + 1);
374 }
375
376 bool CLabelFormatter::FillMusicTag(const CStdString &fileName, CMusicInfoTag *tag) const
377 {
378   // run through and find static content to split the string up
379   size_t pos1 = fileName.find(m_staticContent[0][0], 0);
380   if (pos1 == std::string::npos)
381     return false;
382   for (unsigned int i = 1; i < m_staticContent[0].size(); i++)
383   {
384     size_t pos2 = m_staticContent[0][i].size() ? fileName.find(m_staticContent[0][i], pos1) : fileName.size();
385     if (pos2 == std::string::npos)
386       return false;
387     // found static content - thus we have the dynamic content surrounded
388     FillMusicMaskContent(m_dynamicContent[0][i - 1].m_content, fileName.substr(pos1, pos2 - pos1), tag);
389     pos1 = pos2 + m_staticContent[0][i].size();
390   }
391   return true;
392 }
393
394 void CLabelFormatter::FillMusicMaskContent(const char mask, const CStdString &value, CMusicInfoTag *tag) const
395 {
396   if (!tag) return;
397   switch (mask)
398   {
399   case 'N':
400     tag->SetTrackNumber(atol(value.c_str()));
401     break;
402   case 'S':
403     tag->SetPartOfSet(atol(value.c_str()));
404     break;
405   case 'A':
406     tag->SetArtist(value);
407     break;
408   case 'T':
409     tag->SetTitle(value);
410     break;
411   case 'B':
412     tag->SetAlbum(value);
413     break;
414   case 'G':
415     tag->SetGenre(value);
416     break;
417   case 'Y':
418     tag->SetYear(atol(value.c_str()));
419     break;
420   case 'D':
421     tag->SetDuration(StringUtils::TimeStringToSeconds(value));
422     break;
423   case 'R': // rating
424     tag->SetRating(value[0]);
425     break;
426   }
427 }
428