Merge pull request #4775 from jmarshallnz/empty_episode_playcount
[vuplus_xbmc] / xbmc / CueDocument.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 ////////////////////////////////////////////////////////////////////////////////////
22 // Class: CueDocument
23 // This class handles the .cue file format.  This is produced by programs such as
24 // EAC and CDRwin when one extracts audio data from a CD as a continuous .WAV
25 // containing all the audio tracks in one big file.  The .cue file contains all the
26 // track and timing information.  An example file is:
27 //
28 // PERFORMER "Pink Floyd"
29 // TITLE "The Dark Side Of The Moon"
30 // FILE "The Dark Side Of The Moon.mp3" WAVE
31 //   TRACK 01 AUDIO
32 //     TITLE "Speak To Me / Breathe"
33 //     PERFORMER "Pink Floyd"
34 //     INDEX 00 00:00:00
35 //     INDEX 01 00:00:32
36 //   TRACK 02 AUDIO
37 //     TITLE "On The Run"
38 //     PERFORMER "Pink Floyd"
39 //     INDEX 00 03:58:72
40 //     INDEX 01 04:00:72
41 //   TRACK 03 AUDIO
42 //     TITLE "Time"
43 //     PERFORMER "Pink Floyd"
44 //     INDEX 00 07:31:70
45 //     INDEX 01 07:33:70
46 //
47 // etc.
48 //
49 // The CCueDocument class member functions extract this information, and construct
50 // the playlist items needed to seek to a track directly.  This works best on CBR
51 // compressed files - VBR files do not seek accurately enough for it to work well.
52 //
53 ////////////////////////////////////////////////////////////////////////////////////
54
55 #include "CueDocument.h"
56 #include "utils/log.h"
57 #include "utils/URIUtils.h"
58 #include "utils/StringUtils.h"
59 #include "utils/CharsetConverter.h"
60 #include "filesystem/File.h"
61 #include "filesystem/Directory.h"
62 #include "FileItem.h"
63 #include "settings/AdvancedSettings.h"
64
65 #include <set>
66
67 using namespace std;
68 using namespace XFILE;
69
70 CCueDocument::CCueDocument(void)
71 {
72   m_strArtist = "";
73   m_strAlbum = "";
74   m_strGenre = "";
75   m_iYear = 0;
76   m_replayGainAlbumPeak = 0.0f;
77   m_replayGainAlbumGain = 0.0f;
78   m_iTotalTracks = 0;
79   m_iTrack = 0;
80   m_iDiscNumber = 0;
81 }
82
83 CCueDocument::~CCueDocument(void)
84 {}
85
86 ////////////////////////////////////////////////////////////////////////////////////
87 // Function: Parse()
88 // Opens the .cue file for reading, and constructs the track database information
89 ////////////////////////////////////////////////////////////////////////////////////
90 bool CCueDocument::Parse(const CStdString &strFile)
91 {
92   if (!m_file.Open(strFile))
93     return false;
94
95   CStdString strLine;
96   m_iTotalTracks = -1;
97   CStdString strCurrentFile = "";
98   bool bCurrentFileChanged = false;
99   int time;
100
101   // Run through the .CUE file and extract the tracks...
102   while (true)
103   {
104     if (!ReadNextLine(strLine))
105       break;
106     if (StringUtils::StartsWithNoCase(strLine,"INDEX 01"))
107     {
108       if (bCurrentFileChanged)
109       {
110         OutputDebugString("Track split over multiple files, unsupported ('" + strFile + "')\n");
111         return false;
112       }
113
114       // find the end of the number section
115       time = ExtractTimeFromIndex(strLine);
116       if (time == -1)
117       { // Error!
118         OutputDebugString("Mangled Time in INDEX 0x tag in CUE file!\n");
119         return false;
120       }
121       if (m_iTotalTracks > 0)  // Set the end time of the last track
122         m_Track[m_iTotalTracks - 1].iEndTime = time;
123
124       if (m_iTotalTracks >= 0)
125         m_Track[m_iTotalTracks].iStartTime = time; // start time of the next track
126     }
127     else if (StringUtils::StartsWithNoCase(strLine,"TITLE"))
128     {
129       if (m_iTotalTracks == -1) // No tracks yet
130         m_strAlbum = ExtractInfo(strLine.substr(5));
131       else
132         m_Track[m_iTotalTracks].strTitle = ExtractInfo(strLine.substr(5));
133     }
134     else if (StringUtils::StartsWithNoCase(strLine,"PERFORMER"))
135     {
136       if (m_iTotalTracks == -1) // No tracks yet
137         m_strArtist = ExtractInfo(strLine.substr(9));
138       else // New Artist for this track
139         m_Track[m_iTotalTracks].strArtist = ExtractInfo(strLine.substr(9));
140     }
141     else if (StringUtils::StartsWithNoCase(strLine,"TRACK"))
142     {
143       int iTrackNumber = ExtractNumericInfo(strLine.substr(5));
144
145       m_iTotalTracks++;
146
147       CCueTrack track;
148       m_Track.push_back(track);
149       m_Track[m_iTotalTracks].strFile = strCurrentFile;
150
151       if (iTrackNumber > 0)
152         m_Track[m_iTotalTracks].iTrackNumber = iTrackNumber;
153       else
154         m_Track[m_iTotalTracks].iTrackNumber = m_iTotalTracks + 1;
155
156       bCurrentFileChanged = false;
157     }
158     else if (StringUtils::StartsWithNoCase(strLine,"REM DISCNUMBER"))
159     {
160       int iDiscNumber = ExtractNumericInfo(strLine.substr(14));
161       if (iDiscNumber > 0)
162         m_iDiscNumber = iDiscNumber;
163     }
164     else if (StringUtils::StartsWithNoCase(strLine,"FILE"))
165     {
166       // already a file name? then the time computation will be changed
167       if(strCurrentFile.size() > 0)
168         bCurrentFileChanged = true;
169
170       strCurrentFile = ExtractInfo(strLine.substr(4));
171
172       // Resolve absolute paths (if needed).
173       if (strCurrentFile.length() > 0)
174         ResolvePath(strCurrentFile, strFile);
175     }
176     else if (StringUtils::StartsWithNoCase(strLine,"REM DATE"))
177     {
178       int iYear = ExtractNumericInfo(strLine.substr(8));
179       if (iYear > 0)
180         m_iYear = iYear;
181     }
182     else if (StringUtils::StartsWithNoCase(strLine,"REM GENRE"))
183     {
184       m_strGenre = ExtractInfo(strLine.substr(9));
185     }
186     else if (StringUtils::StartsWithNoCase(strLine,"REM REPLAYGAIN_ALBUM_GAIN"))
187       m_replayGainAlbumGain = (float)atof(strLine.substr(26).c_str());
188     else if (StringUtils::StartsWithNoCase(strLine,"REM REPLAYGAIN_ALBUM_PEAK"))
189       m_replayGainAlbumPeak = (float)atof(strLine.substr(26).c_str());
190     else if (StringUtils::StartsWithNoCase(strLine,"REM REPLAYGAIN_TRACK_GAIN") && m_iTotalTracks >= 0)
191       m_Track[m_iTotalTracks].replayGainTrackGain = (float)atof(strLine.substr(26).c_str());
192     else if (StringUtils::StartsWithNoCase(strLine,"REM REPLAYGAIN_TRACK_PEAK") && m_iTotalTracks >= 0)
193       m_Track[m_iTotalTracks].replayGainTrackPeak = (float)atof(strLine.substr(26).c_str());
194   }
195
196   // reset track counter to 0, and fill in the last tracks end time
197   m_iTrack = 0;
198   if (m_iTotalTracks >= 0)
199     m_Track[m_iTotalTracks].iEndTime = 0;
200   else
201     OutputDebugString("No INDEX 01 tags in CUE file!\n");
202   m_file.Close();
203   if (m_iTotalTracks >= 0)
204   {
205     m_iTotalTracks++;
206   }
207   return (m_iTotalTracks > 0);
208 }
209
210 //////////////////////////////////////////////////////////////////////////////////
211 // Function:GetNextItem()
212 // Returns the track information from the next item in the cuelist
213 //////////////////////////////////////////////////////////////////////////////////
214 void CCueDocument::GetSongs(VECSONGS &songs)
215 {
216   for (int i = 0; i < m_iTotalTracks; i++)
217   {
218     CSong song;
219     if ((m_Track[i].strArtist.length() == 0) && (m_strArtist.length() > 0))
220       song.artist = StringUtils::Split(m_strArtist, g_advancedSettings.m_musicItemSeparator);
221     else
222       song.artist = StringUtils::Split(m_Track[i].strArtist, g_advancedSettings.m_musicItemSeparator);
223     song.albumArtist = StringUtils::Split(m_strArtist, g_advancedSettings.m_musicItemSeparator);
224     song.strAlbum = m_strAlbum;
225     song.genre = StringUtils::Split(m_strGenre, g_advancedSettings.m_musicItemSeparator);
226     song.iYear = m_iYear;
227     song.iTrack = m_Track[i].iTrackNumber;
228     if ( m_iDiscNumber > 0 )  
229       song.iTrack |= (m_iDiscNumber << 16); // see CMusicInfoTag::GetDiscNumber()
230     if (m_Track[i].strTitle.length() == 0) // No track information for this track!
231       song.strTitle = StringUtils::Format("Track %2d", i + 1);
232     else
233       song.strTitle = m_Track[i].strTitle;
234     song.strFileName =  m_Track[i].strFile;
235     song.iStartOffset = m_Track[i].iStartTime;
236     song.iEndOffset = m_Track[i].iEndTime;
237     if (song.iEndOffset)
238       song.iDuration = (song.iEndOffset - song.iStartOffset + 37) / 75;
239     else
240       song.iDuration = 0;
241     // TODO: replayGain goes here
242     songs.push_back(song);
243   }
244 }
245
246 void CCueDocument::GetMediaFiles(vector<CStdString>& mediaFiles)
247 {
248   set<CStdString> uniqueFiles;
249   for (int i = 0; i < m_iTotalTracks; i++)
250     uniqueFiles.insert(m_Track[i].strFile);
251
252   for (set<CStdString>::iterator it = uniqueFiles.begin(); it != uniqueFiles.end(); it++)
253     mediaFiles.push_back(*it);
254 }
255
256 CStdString CCueDocument::GetMediaTitle()
257 {
258   return m_strAlbum;
259 }
260
261 // Private Functions start here
262
263 ////////////////////////////////////////////////////////////////////////////////////
264 // Function: ReadNextLine()
265 // Returns the next non-blank line of the textfile, stripping any whitespace from
266 // the left.
267 ////////////////////////////////////////////////////////////////////////////////////
268 bool CCueDocument::ReadNextLine(CStdString &szLine)
269 {
270   // Read the next line.
271   while (m_file.ReadString(m_szBuffer, 1023)) // Bigger than MAX_PATH_SIZE, for usage with relax!
272   {
273     // Remove the white space at the beginning and end of the line.
274     szLine = m_szBuffer;
275     StringUtils::Trim(szLine);
276     if (!szLine.empty())
277       return true;
278     // If we are here, we have an empty line so try the next line
279   }
280   return false;
281 }
282
283 ////////////////////////////////////////////////////////////////////////////////////
284 // Function: ExtractInfo()
285 // Extracts the information in quotes from the string line, returning it in quote
286 ////////////////////////////////////////////////////////////////////////////////////
287 CStdString CCueDocument::ExtractInfo(const CStdString &line)
288 {
289   size_t left = line.find('\"');
290   if (left != std::string::npos)
291   {
292     size_t right = line.find('\"', left + 1);
293     if (right != std::string::npos)
294     {
295       CStdString text = line.substr(left + 1, right - left - 1);
296       g_charsetConverter.unknownToUTF8(text);
297       return text;
298     }
299   }
300   CStdString text = line;
301   StringUtils::Trim(text);
302   g_charsetConverter.unknownToUTF8(text);
303   return text;
304 }
305
306 ////////////////////////////////////////////////////////////////////////////////////
307 // Function: ExtractTimeFromIndex()
308 // Extracts the time information from the index string index, returning it as a value in
309 // milliseconds.
310 // Assumed format is:
311 // MM:SS:FF where MM is minutes, SS seconds, and FF frames (75 frames in a second)
312 ////////////////////////////////////////////////////////////////////////////////////
313 int CCueDocument::ExtractTimeFromIndex(const CStdString &index)
314 {
315   // Get rid of the index number and any whitespace
316   CStdString numberTime = index.substr(5);
317   StringUtils::TrimLeft(numberTime);
318   while (!numberTime.empty())
319   {
320     if (!isdigit(numberTime[0]))
321       break;
322     numberTime.erase(0, 1);
323   }
324   StringUtils::TrimLeft(numberTime);
325   // split the resulting string
326   CStdStringArray time;
327   StringUtils::SplitString(numberTime, ":", time);
328   if (time.size() != 3)
329     return -1;
330
331   int mins = atoi(time[0].c_str());
332   int secs = atoi(time[1].c_str());
333   int frames = atoi(time[2].c_str());
334
335   return (mins*60 + secs)*75 + frames;
336 }
337
338 ////////////////////////////////////////////////////////////////////////////////////
339 // Function: ExtractNumericInfo()
340 // Extracts the numeric info from the string info, returning it as an integer value
341 ////////////////////////////////////////////////////////////////////////////////////
342 int CCueDocument::ExtractNumericInfo(const CStdString &info)
343 {
344   CStdString number(info);
345   StringUtils::TrimLeft(number);
346   if (number.empty() || !isdigit(number[0]))
347     return -1;
348   return atoi(number.c_str());
349 }
350
351 ////////////////////////////////////////////////////////////////////////////////////
352 // Function: ResolvePath()
353 // Determines whether strPath is a relative path or not, and if so, converts it to an
354 // absolute path using the path information in strBase
355 ////////////////////////////////////////////////////////////////////////////////////
356 bool CCueDocument::ResolvePath(CStdString &strPath, const CStdString &strBase)
357 {
358   CStdString strDirectory = URIUtils::GetDirectory(strBase);
359   CStdString strFilename = URIUtils::GetFileName(strPath);
360
361   strPath = URIUtils::AddFileToFolder(strDirectory, strFilename);
362
363   // i *hate* windows
364   if (!CFile::Exists(strPath))
365   {
366     CFileItemList items;
367     CDirectory::GetDirectory(strDirectory,items);
368     for (int i=0;i<items.Size();++i)
369     {
370       if (items[i]->GetPath().Equals(strPath))
371       {
372         strPath = items[i]->GetPath();
373         return true;
374       }
375     }
376     CLog::Log(LOGERROR,"Could not find '%s' referenced in cue, case sensitivity issue?", strPath.c_str());
377     return false;
378   }
379
380   return true;
381 }
382