2 * Copyright (C) 2005-2013 Team XBMC
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)
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 ////////////////////////////////////////////////////////////////////////////////////
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
32 // TITLE "Speak To Me / Breathe"
33 // PERFORMER "Pink Floyd"
38 // PERFORMER "Pink Floyd"
43 // PERFORMER "Pink Floyd"
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"
63 #include "settings/AdvancedSettings.h"
68 using namespace XFILE;
70 CCueDocument::CCueDocument(void)
76 m_replayGainAlbumPeak = 0.0f;
77 m_replayGainAlbumGain = 0.0f;
83 CCueDocument::~CCueDocument(void)
86 ////////////////////////////////////////////////////////////////////////////////////
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))
97 CStdString strCurrentFile = "";
98 bool bCurrentFileChanged = false;
101 // Run through the .CUE file and extract the tracks...
104 if (!ReadNextLine(strLine))
106 if (StringUtils::StartsWith(strLine,"INDEX 01"))
108 if (bCurrentFileChanged)
110 OutputDebugString("Track split over multiple files, unsupported ('" + strFile + "')\n");
114 // find the end of the number section
115 time = ExtractTimeFromIndex(strLine);
118 OutputDebugString("Mangled Time in INDEX 0x tag in CUE file!\n");
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::StartsWith(strLine,"TITLE"))
129 if (m_iTotalTracks == -1) // No tracks yet
130 ExtractQuoteInfo(strLine, m_strAlbum);
131 else if (!ExtractQuoteInfo(strLine, m_Track[m_iTotalTracks].strTitle))
133 // lets manage tracks titles without quotes
134 CStdString titleNoQuote = strLine.Mid(5);
135 titleNoQuote.TrimLeft();
136 if (!titleNoQuote.IsEmpty())
138 g_charsetConverter.unknownToUTF8(titleNoQuote);
139 m_Track[m_iTotalTracks].strTitle = titleNoQuote;
143 else if (StringUtils::StartsWith(strLine,"PERFORMER"))
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);
150 else if (StringUtils::StartsWith(strLine,"TRACK"))
152 int iTrackNumber = ExtractNumericInfo(strLine.Mid(5));
157 m_Track.push_back(track);
158 m_Track[m_iTotalTracks].strFile = strCurrentFile;
160 if (iTrackNumber > 0)
161 m_Track[m_iTotalTracks].iTrackNumber = iTrackNumber;
163 m_Track[m_iTotalTracks].iTrackNumber = m_iTotalTracks + 1;
165 bCurrentFileChanged = false;
167 else if (StringUtils::StartsWith(strLine,"REM DISCNUMBER"))
169 int iDiscNumber = ExtractNumericInfo(strLine.Mid(14));
171 m_iDiscNumber = iDiscNumber;
173 else if (StringUtils::StartsWith(strLine,"FILE"))
175 // already a file name? then the time computation will be changed
176 if(strCurrentFile.size() > 0)
177 bCurrentFileChanged = true;
179 ExtractQuoteInfo(strLine, strCurrentFile);
181 // Resolve absolute paths (if needed).
182 if (strCurrentFile.length() > 0)
183 ResolvePath(strCurrentFile, strFile);
185 else if (StringUtils::StartsWith(strLine,"REM DATE"))
187 int iYear = ExtractNumericInfo(strLine.Mid(8));
191 else if (StringUtils::StartsWith(strLine,"REM GENRE"))
193 if (!ExtractQuoteInfo(strLine, m_strGenre))
195 CStdString genreNoQuote = strLine.Mid(9);
196 genreNoQuote.TrimLeft();
197 if (!genreNoQuote.IsEmpty())
199 g_charsetConverter.unknownToUTF8(genreNoQuote);
200 m_strGenre = genreNoQuote;
204 else if (StringUtils::StartsWith(strLine,"REM REPLAYGAIN_ALBUM_GAIN"))
205 m_replayGainAlbumGain = (float)atof(strLine.Mid(26));
206 else if (StringUtils::StartsWith(strLine,"REM REPLAYGAIN_ALBUM_PEAK"))
207 m_replayGainAlbumPeak = (float)atof(strLine.Mid(26));
208 else if (StringUtils::StartsWith(strLine,"REM REPLAYGAIN_TRACK_GAIN") && m_iTotalTracks >= 0)
209 m_Track[m_iTotalTracks].replayGainTrackGain = (float)atof(strLine.Mid(26));
210 else if (StringUtils::StartsWith(strLine,"REM REPLAYGAIN_TRACK_PEAK") && m_iTotalTracks >= 0)
211 m_Track[m_iTotalTracks].replayGainTrackPeak = (float)atof(strLine.Mid(26));
214 // reset track counter to 0, and fill in the last tracks end time
216 if (m_iTotalTracks >= 0)
217 m_Track[m_iTotalTracks].iEndTime = 0;
219 OutputDebugString("No INDEX 01 tags in CUE file!\n");
221 if (m_iTotalTracks >= 0)
225 return (m_iTotalTracks > 0);
228 //////////////////////////////////////////////////////////////////////////////////
229 // Function:GetNextItem()
230 // Returns the track information from the next item in the cuelist
231 //////////////////////////////////////////////////////////////////////////////////
232 void CCueDocument::GetSongs(VECSONGS &songs)
234 for (int i = 0; i < m_iTotalTracks; i++)
237 if ((m_Track[i].strArtist.length() == 0) && (m_strArtist.length() > 0))
238 song.artist = StringUtils::Split(m_strArtist, g_advancedSettings.m_musicItemSeparator);
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);
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;
256 song.iDuration = (song.iEndOffset - song.iStartOffset + 37) / 75;
259 // TODO: replayGain goes here
260 songs.push_back(song);
264 void CCueDocument::GetMediaFiles(vector<CStdString>& mediaFiles)
266 set<CStdString> uniqueFiles;
267 for (int i = 0; i < m_iTotalTracks; i++)
268 uniqueFiles.insert(m_Track[i].strFile);
270 for (set<CStdString>::iterator it = uniqueFiles.begin(); it != uniqueFiles.end(); it++)
271 mediaFiles.push_back(*it);
274 CStdString CCueDocument::GetMediaTitle()
279 // Private Functions start here
281 ////////////////////////////////////////////////////////////////////////////////////
282 // Function: ReadNextLine()
283 // Returns the next non-blank line of the textfile, stripping any whitespace from
285 ////////////////////////////////////////////////////////////////////////////////////
286 bool CCueDocument::ReadNextLine(CStdString &szLine)
288 // Read the next line.
289 while (m_file.ReadString(m_szBuffer, 1023)) // Bigger than MAX_PATH_SIZE, for usage with relax!
291 // Remove the white space at the beginning and end of the line.
296 // If we are here, we have an empty line so try the next line
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 "e)
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);
317 ////////////////////////////////////////////////////////////////////////////////////
318 // Function: ExtractTimeFromIndex()
319 // Extracts the time information from the index string index, returning it as a value in
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)
326 // Get rid of the index number and any whitespace
327 CStdString numberTime = index.Mid(5);
328 numberTime.TrimLeft();
329 while (!numberTime.IsEmpty())
331 if (!isdigit(numberTime[0]))
333 numberTime.erase(0, 1);
335 numberTime.TrimLeft();
336 // split the resulting string
337 CStdStringArray time;
338 StringUtils::SplitString(numberTime, ":", time);
339 if (time.size() != 3)
342 int mins = atoi(time[0].c_str());
343 int secs = atoi(time[1].c_str());
344 int frames = atoi(time[2].c_str());
346 return (mins*60 + secs)*75 + frames;
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)
355 CStdString number(info);
357 if (number.IsEmpty() || !isdigit(number[0]))
359 return atoi(number.c_str());
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)
369 CStdString strDirectory;
370 URIUtils::GetDirectory(strBase, strDirectory);
372 CStdString strFilename = URIUtils::GetFileName(strPath);
374 strPath = URIUtils::AddFileToFolder(strDirectory, strFilename);
377 if (!CFile::Exists(strPath))
380 CDirectory::GetDirectory(strDirectory,items);
381 for (int i=0;i<items.Size();++i)
383 if (items[i]->GetPath().Equals(strPath))
385 strPath = items[i]->GetPath();
389 CLog::Log(LOGERROR,"Could not find '%s' referenced in cue, case sensitivity issue?", strPath.c_str());