Merge pull request #3194 from arnova/slow_picture_job_fix
[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         ExtractQuoteInfo(strLine, m_strAlbum);
131       else if (!ExtractQuoteInfo(strLine, m_Track[m_iTotalTracks].strTitle))
132       {
133         // lets manage tracks titles without quotes
134         CStdString titleNoQuote = strLine.Mid(5);
135         titleNoQuote.TrimLeft();
136         if (!titleNoQuote.IsEmpty())
137         {
138           g_charsetConverter.unknownToUTF8(titleNoQuote);
139           m_Track[m_iTotalTracks].strTitle = titleNoQuote;
140         }
141       }
142     }
143     else if (StringUtils::StartsWithNoCase(strLine,"PERFORMER"))
144     {
145       if (m_iTotalTracks == -1) // No tracks yet
146         ExtractQuoteInfo(strLine, m_strArtist);
147       else // New Artist for this track
148         ExtractQuoteInfo(strLine, m_Track[m_iTotalTracks].strArtist);
149     }
150     else if (StringUtils::StartsWithNoCase(strLine,"TRACK"))
151     {
152       int iTrackNumber = ExtractNumericInfo(strLine.Mid(5));
153
154       m_iTotalTracks++;
155
156       CCueTrack track;
157       m_Track.push_back(track);
158       m_Track[m_iTotalTracks].strFile = strCurrentFile;
159
160       if (iTrackNumber > 0)
161         m_Track[m_iTotalTracks].iTrackNumber = iTrackNumber;
162       else
163         m_Track[m_iTotalTracks].iTrackNumber = m_iTotalTracks + 1;
164
165       bCurrentFileChanged = false;
166     }
167     else if (StringUtils::StartsWithNoCase(strLine,"REM DISCNUMBER"))
168     {
169       int iDiscNumber = ExtractNumericInfo(strLine.Mid(14));
170       if (iDiscNumber > 0)
171         m_iDiscNumber = iDiscNumber;
172     }
173     else if (StringUtils::StartsWithNoCase(strLine,"FILE"))
174     {
175       // already a file name? then the time computation will be changed
176       if(strCurrentFile.size() > 0)
177         bCurrentFileChanged = true;
178
179       ExtractQuoteInfo(strLine, strCurrentFile);
180
181       // Resolve absolute paths (if needed).
182       if (strCurrentFile.length() > 0)
183         ResolvePath(strCurrentFile, strFile);
184     }
185     else if (StringUtils::StartsWithNoCase(strLine,"REM DATE"))
186     {
187       int iYear = ExtractNumericInfo(strLine.Mid(8));
188       if (iYear > 0)
189         m_iYear = iYear;
190     }
191     else if (StringUtils::StartsWithNoCase(strLine,"REM GENRE"))
192     {
193       if (!ExtractQuoteInfo(strLine, m_strGenre))
194       {
195         CStdString genreNoQuote = strLine.Mid(9);
196         genreNoQuote.TrimLeft();
197         if (!genreNoQuote.IsEmpty())
198         {
199           g_charsetConverter.unknownToUTF8(genreNoQuote);
200           m_strGenre = genreNoQuote;
201         }
202       }
203     }
204     else if (StringUtils::StartsWithNoCase(strLine,"REM REPLAYGAIN_ALBUM_GAIN"))
205       m_replayGainAlbumGain = (float)atof(strLine.Mid(26));
206     else if (StringUtils::StartsWithNoCase(strLine,"REM REPLAYGAIN_ALBUM_PEAK"))
207       m_replayGainAlbumPeak = (float)atof(strLine.Mid(26));
208     else if (StringUtils::StartsWithNoCase(strLine,"REM REPLAYGAIN_TRACK_GAIN") && m_iTotalTracks >= 0)
209       m_Track[m_iTotalTracks].replayGainTrackGain = (float)atof(strLine.Mid(26));
210     else if (StringUtils::StartsWithNoCase(strLine,"REM REPLAYGAIN_TRACK_PEAK") && m_iTotalTracks >= 0)
211       m_Track[m_iTotalTracks].replayGainTrackPeak = (float)atof(strLine.Mid(26));
212   }
213
214   // reset track counter to 0, and fill in the last tracks end time
215   m_iTrack = 0;
216   if (m_iTotalTracks >= 0)
217     m_Track[m_iTotalTracks].iEndTime = 0;
218   else
219     OutputDebugString("No INDEX 01 tags in CUE file!\n");
220   m_file.Close();
221   if (m_iTotalTracks >= 0)
222   {
223     m_iTotalTracks++;
224   }
225   return (m_iTotalTracks > 0);
226 }
227
228 //////////////////////////////////////////////////////////////////////////////////
229 // Function:GetNextItem()
230 // Returns the track information from the next item in the cuelist
231 //////////////////////////////////////////////////////////////////////////////////
232 void CCueDocument::GetSongs(VECSONGS &songs)
233 {
234   for (int i = 0; i < m_iTotalTracks; i++)
235   {
236     CSong song;
237     if ((m_Track[i].strArtist.length() == 0) && (m_strArtist.length() > 0))
238       song.artist = StringUtils::Split(m_strArtist, g_advancedSettings.m_musicItemSeparator);
239     else
240       song.artist = StringUtils::Split(m_Track[i].strArtist, g_advancedSettings.m_musicItemSeparator);
241     song.albumArtist = StringUtils::Split(m_strArtist, g_advancedSettings.m_musicItemSeparator);
242     song.strAlbum = m_strAlbum;
243     song.genre = StringUtils::Split(m_strGenre, g_advancedSettings.m_musicItemSeparator);
244     song.iYear = m_iYear;
245     song.iTrack = m_Track[i].iTrackNumber;
246     if ( m_iDiscNumber > 0 )  
247       song.iTrack |= (m_iDiscNumber << 16); // see CMusicInfoTag::GetDiscNumber()
248     if (m_Track[i].strTitle.length() == 0) // No track information for this track!
249       song.strTitle.Format("Track %2d", i + 1);
250     else
251       song.strTitle = m_Track[i].strTitle;
252     song.strFileName =  m_Track[i].strFile;
253     song.iStartOffset = m_Track[i].iStartTime;
254     song.iEndOffset = m_Track[i].iEndTime;
255     if (song.iEndOffset)
256       song.iDuration = (song.iEndOffset - song.iStartOffset + 37) / 75;
257     else
258       song.iDuration = 0;
259     // TODO: replayGain goes here
260     songs.push_back(song);
261   }
262 }
263
264 void CCueDocument::GetMediaFiles(vector<CStdString>& mediaFiles)
265 {
266   set<CStdString> uniqueFiles;
267   for (int i = 0; i < m_iTotalTracks; i++)
268     uniqueFiles.insert(m_Track[i].strFile);
269
270   for (set<CStdString>::iterator it = uniqueFiles.begin(); it != uniqueFiles.end(); it++)
271     mediaFiles.push_back(*it);
272 }
273
274 CStdString CCueDocument::GetMediaTitle()
275 {
276   return m_strAlbum;
277 }
278
279 // Private Functions start here
280
281 ////////////////////////////////////////////////////////////////////////////////////
282 // Function: ReadNextLine()
283 // Returns the next non-blank line of the textfile, stripping any whitespace from
284 // the left.
285 ////////////////////////////////////////////////////////////////////////////////////
286 bool CCueDocument::ReadNextLine(CStdString &szLine)
287 {
288   // Read the next line.
289   while (m_file.ReadString(m_szBuffer, 1023)) // Bigger than MAX_PATH_SIZE, for usage with relax!
290   {
291     // Remove the white space at the beginning and end of the line.
292     szLine = m_szBuffer;
293     szLine.Trim();
294     if (!szLine.empty())
295       return true;
296     // If we are here, we have an empty line so try the next line
297   }
298   return false;
299 }
300
301 ////////////////////////////////////////////////////////////////////////////////////
302 // Function: ExtractQuoteInfo()
303 // Extracts the information in quotes from the string line, returning it in quote
304 ////////////////////////////////////////////////////////////////////////////////////
305 bool CCueDocument::ExtractQuoteInfo(const CStdString &line, CStdString &quote)
306 {
307   quote.Empty();
308   int left = line.Find('\"');
309   if (left < 0) return false;
310   int right = line.Find('\"', left + 1);
311   if (right < 0) return false;
312   quote = line.Mid(left + 1, right - left - 1);
313   g_charsetConverter.unknownToUTF8(quote);
314   return true;
315 }
316
317 ////////////////////////////////////////////////////////////////////////////////////
318 // Function: ExtractTimeFromIndex()
319 // Extracts the time information from the index string index, returning it as a value in
320 // milliseconds.
321 // Assumed format is:
322 // MM:SS:FF where MM is minutes, SS seconds, and FF frames (75 frames in a second)
323 ////////////////////////////////////////////////////////////////////////////////////
324 int CCueDocument::ExtractTimeFromIndex(const CStdString &index)
325 {
326   // Get rid of the index number and any whitespace
327   CStdString numberTime = index.Mid(5);
328   numberTime.TrimLeft();
329   while (!numberTime.IsEmpty())
330   {
331     if (!isdigit(numberTime[0]))
332       break;
333     numberTime.erase(0, 1);
334   }
335   numberTime.TrimLeft();
336   // split the resulting string
337   CStdStringArray time;
338   StringUtils::SplitString(numberTime, ":", time);
339   if (time.size() != 3)
340     return -1;
341
342   int mins = atoi(time[0].c_str());
343   int secs = atoi(time[1].c_str());
344   int frames = atoi(time[2].c_str());
345
346   return (mins*60 + secs)*75 + frames;
347 }
348
349 ////////////////////////////////////////////////////////////////////////////////////
350 // Function: ExtractNumericInfo()
351 // Extracts the numeric info from the string info, returning it as an integer value
352 ////////////////////////////////////////////////////////////////////////////////////
353 int CCueDocument::ExtractNumericInfo(const CStdString &info)
354 {
355   CStdString number(info);
356   number.TrimLeft();
357   if (number.IsEmpty() || !isdigit(number[0]))
358     return -1;
359   return atoi(number.c_str());
360 }
361
362 ////////////////////////////////////////////////////////////////////////////////////
363 // Function: ResolvePath()
364 // Determines whether strPath is a relative path or not, and if so, converts it to an
365 // absolute path using the path information in strBase
366 ////////////////////////////////////////////////////////////////////////////////////
367 bool CCueDocument::ResolvePath(CStdString &strPath, const CStdString &strBase)
368 {
369   CStdString strDirectory = URIUtils::GetDirectory(strBase);
370   CStdString strFilename = URIUtils::GetFileName(strPath);
371
372   strPath = URIUtils::AddFileToFolder(strDirectory, strFilename);
373
374   // i *hate* windows
375   if (!CFile::Exists(strPath))
376   {
377     CFileItemList items;
378     CDirectory::GetDirectory(strDirectory,items);
379     for (int i=0;i<items.Size();++i)
380     {
381       if (items[i]->GetPath().Equals(strPath))
382       {
383         strPath = items[i]->GetPath();
384         return true;
385       }
386     }
387     CLog::Log(LOGERROR,"Could not find '%s' referenced in cue, case sensitivity issue?", strPath.c_str());
388     return false;
389   }
390
391   return true;
392 }
393