Merge pull request #4594 from FernetMenta/paplayer
[xbmc.git] / xbmc / CueDocument.cpp
blob56190547920cb5069f25987efc95cba0937fa478
1 /*
2 * Copyright (C) 2005-2013 Team XBMC
3 * http://xbmc.org
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.
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.
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/>.
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:
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
47 // etc.
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.
53 ////////////////////////////////////////////////////////////////////////////////////
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"
65 #include <set>
67 using namespace std;
68 using namespace XFILE;
70 CCueDocument::CCueDocument(void)
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;
83 CCueDocument::~CCueDocument(void)
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)
92 if (!m_file.Open(strFile))
93 return false;
95 CStdString strLine;
96 m_iTotalTracks = -1;
97 CStdString strCurrentFile = "";
98 bool bCurrentFileChanged = false;
99 int time;
101 // Run through the .CUE file and extract the tracks...
102 while (true)
104 if (!ReadNextLine(strLine))
105 break;
106 if (StringUtils::StartsWithNoCase(strLine,"INDEX 01"))
108 if (bCurrentFileChanged)
110 OutputDebugString("Track split over multiple files, unsupported ('" + strFile + "')\n");
111 return false;
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;
121 if (m_iTotalTracks > 0) // Set the end time of the last track
122 m_Track[m_iTotalTracks - 1].iEndTime = time;
124 if (m_iTotalTracks >= 0)
125 m_Track[m_iTotalTracks].iStartTime = time; // start time of the next track
127 else if (StringUtils::StartsWithNoCase(strLine,"TITLE"))
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));
134 else if (StringUtils::StartsWithNoCase(strLine,"PERFORMER"))
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));
141 else if (StringUtils::StartsWithNoCase(strLine,"TRACK"))
143 int iTrackNumber = ExtractNumericInfo(strLine.substr(5));
145 m_iTotalTracks++;
147 CCueTrack track;
148 m_Track.push_back(track);
149 m_Track[m_iTotalTracks].strFile = strCurrentFile;
151 if (iTrackNumber > 0)
152 m_Track[m_iTotalTracks].iTrackNumber = iTrackNumber;
153 else
154 m_Track[m_iTotalTracks].iTrackNumber = m_iTotalTracks + 1;
156 bCurrentFileChanged = false;
158 else if (StringUtils::StartsWithNoCase(strLine,"REM DISCNUMBER"))
160 int iDiscNumber = ExtractNumericInfo(strLine.substr(14));
161 if (iDiscNumber > 0)
162 m_iDiscNumber = iDiscNumber;
164 else if (StringUtils::StartsWithNoCase(strLine,"FILE"))
166 // already a file name? then the time computation will be changed
167 if(strCurrentFile.size() > 0)
168 bCurrentFileChanged = true;
170 strCurrentFile = ExtractInfo(strLine.substr(4));
172 // Resolve absolute paths (if needed).
173 if (strCurrentFile.length() > 0)
174 ResolvePath(strCurrentFile, strFile);
176 else if (StringUtils::StartsWithNoCase(strLine,"REM DATE"))
178 int iYear = ExtractNumericInfo(strLine.substr(8));
179 if (iYear > 0)
180 m_iYear = iYear;
182 else if (StringUtils::StartsWithNoCase(strLine,"REM GENRE"))
184 m_strGenre = ExtractInfo(strLine.substr(9));
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());
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)
205 m_iTotalTracks++;
207 return (m_iTotalTracks > 0);
210 //////////////////////////////////////////////////////////////////////////////////
211 // Function:GetNextItem()
212 // Returns the track information from the next item in the cuelist
213 //////////////////////////////////////////////////////////////////////////////////
214 void CCueDocument::GetSongs(VECSONGS &songs)
216 for (int i = 0; i < m_iTotalTracks; i++)
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);
246 void CCueDocument::GetMediaFiles(vector<CStdString>& mediaFiles)
248 set<CStdString> uniqueFiles;
249 for (int i = 0; i < m_iTotalTracks; i++)
250 uniqueFiles.insert(m_Track[i].strFile);
252 for (set<CStdString>::iterator it = uniqueFiles.begin(); it != uniqueFiles.end(); it++)
253 mediaFiles.push_back(*it);
256 CStdString CCueDocument::GetMediaTitle()
258 return m_strAlbum;
261 // Private Functions start here
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)
270 // Read the next line.
271 while (m_file.ReadString(m_szBuffer, 1023)) // Bigger than MAX_PATH_SIZE, for usage with relax!
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
280 return false;
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)
289 size_t left = line.find('\"');
290 if (left != std::string::npos)
292 size_t right = line.find('\"', left + 1);
293 if (right != std::string::npos)
295 CStdString text = line.substr(left + 1, right - left - 1);
296 g_charsetConverter.unknownToUTF8(text);
297 return text;
300 CStdString text = line;
301 StringUtils::Trim(text);
302 g_charsetConverter.unknownToUTF8(text);
303 return text;
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)
315 // Get rid of the index number and any whitespace
316 CStdString numberTime = index.substr(5);
317 StringUtils::TrimLeft(numberTime);
318 while (!numberTime.empty())
320 if (!isdigit(numberTime[0]))
321 break;
322 numberTime.erase(0, 1);
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;
331 int mins = atoi(time[0].c_str());
332 int secs = atoi(time[1].c_str());
333 int frames = atoi(time[2].c_str());
335 return (mins*60 + secs)*75 + frames;
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)
344 CStdString number(info);
345 StringUtils::TrimLeft(number);
346 if (number.empty() || !isdigit(number[0]))
347 return -1;
348 return atoi(number.c_str());
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)
358 CStdString strDirectory = URIUtils::GetDirectory(strBase);
359 CStdString strFilename = URIUtils::GetFileName(strPath);
361 strPath = URIUtils::AddFileToFolder(strDirectory, strFilename);
363 // i *hate* windows
364 if (!CFile::Exists(strPath))
366 CFileItemList items;
367 CDirectory::GetDirectory(strDirectory,items);
368 for (int i=0;i<items.Size();++i)
370 if (items[i]->GetPath().Equals(strPath))
372 strPath = items[i]->GetPath();
373 return true;
376 CLog::Log(LOGERROR,"Could not find '%s' referenced in cue, case sensitivity issue?", strPath.c_str());
377 return false;
380 return true;