1 /*
2  *  Copyright (C) 2005-2018 Team Kodi
3  *  This file is part of Kodi - https://kodi.tv
4  *
5  *  SPDX-License-Identifier: GPL-2.0-or-later
6  *  See LICENSES/README.md for more information.
7  */
8 
9 ////////////////////////////////////////////////////////////////////////////////////
10 // Class: CueDocument
11 // This class handles the .cue file format.  This is produced by programs such as
12 // EAC and CDRwin when one extracts audio data from a CD as a continuous .WAV
13 // containing all the audio tracks in one big file.  The .cue file contains all the
14 // track and timing information.  An example file is:
15 //
16 // PERFORMER "Pink Floyd"
17 // TITLE "The Dark Side Of The Moon"
18 // FILE "The Dark Side Of The Moon.mp3" WAVE
19 //   TRACK 01 AUDIO
20 //     TITLE "Speak To Me / Breathe"
21 //     PERFORMER "Pink Floyd"
22 //     INDEX 00 00:00:00
23 //     INDEX 01 00:00:32
24 //   TRACK 02 AUDIO
25 //     TITLE "On The Run"
26 //     PERFORMER "Pink Floyd"
27 //     INDEX 00 03:58:72
28 //     INDEX 01 04:00:72
29 //   TRACK 03 AUDIO
30 //     TITLE "Time"
31 //     PERFORMER "Pink Floyd"
32 //     INDEX 00 07:31:70
33 //     INDEX 01 07:33:70
34 //
35 // etc.
36 //
37 // The CCueDocument class member functions extract this information, and construct
38 // the playlist items needed to seek to a track directly.  This works best on CBR
39 // compressed files - VBR files do not seek accurately enough for it to work well.
40 //
41 ////////////////////////////////////////////////////////////////////////////////////
42 
43 #include "CueDocument.h"
44 
45 #include "FileItem.h"
46 #include "ServiceBroker.h"
47 #include "Util.h"
48 #include "filesystem/Directory.h"
49 #include "filesystem/File.h"
50 #include "settings/AdvancedSettings.h"
51 #include "settings/SettingsComponent.h"
52 #include "utils/CharsetConverter.h"
53 #include "utils/StringUtils.h"
54 #include "utils/URIUtils.h"
55 #include "utils/log.h"
56 
57 #include <cstdlib>
58 #include <set>
59 
60 using namespace XFILE;
61 
62 // Stuff for read CUE data from different sources.
63 class CueReader
64 {
65 public:
66   virtual bool ready() const = 0;
67   virtual bool ReadLine(std::string &line) = 0;
68   virtual ~CueReader() = default;
69 private:
70   std::string m_sourcePath;
71 };
72 
73 class FileReader
74   : public CueReader
75 {
76 public:
FileReader(const std::string & strFile)77   explicit FileReader(const std::string &strFile) : m_szBuffer{}
78   {
79     m_opened = m_file.Open(strFile);
80   }
ReadLine(std::string & line)81   bool ReadLine(std::string &line) override
82   {
83     // Read the next line.
84     while (m_file.ReadString(m_szBuffer, 1023)) // Bigger than MAX_PATH_SIZE, for usage with relax!
85     {
86       // Remove the white space at the beginning and end of the line.
87       line = m_szBuffer;
88       StringUtils::Trim(line);
89       if (!line.empty())
90         return true;
91       // If we are here, we have an empty line so try the next line
92     }
93     return false;
94   }
ready() const95   bool ready() const override
96   {
97     return m_opened;
98   }
~FileReader()99   ~FileReader() override
100   {
101     if (m_opened)
102       m_file.Close();
103 
104   }
105 private:
106   CFile m_file;
107   bool m_opened;
108   char m_szBuffer[1024];
109 };
110 
111 class BufferReader
112   : public CueReader
113 {
114 public:
BufferReader(const std::string & strContent)115   explicit BufferReader(const std::string &strContent)
116     : m_data(strContent)
117     , m_pos(0)
118   {
119   }
ReadLine(std::string & line)120   bool ReadLine(std::string &line) override
121   {
122     // Read the next line.
123     line.clear();
124     while (m_pos < m_data.size())
125     {
126       // Remove the white space at the beginning of the line.
127       char ch = m_data.at(m_pos++);
128       if (ch == '\r' || ch == '\n') {
129         StringUtils::Trim(line);
130         if (!line.empty())
131           return true;
132       }
133       else
134       {
135         line.push_back(ch);
136       }
137     }
138 
139     StringUtils::Trim(line);
140     return !line.empty();
141   }
ready() const142   bool ready() const override
143   {
144     return m_data.size() > 0;
145   }
146 private:
147   std::string m_data;
148   size_t m_pos;
149 };
150 
151 CCueDocument::~CCueDocument() = default;
152 
153 ////////////////////////////////////////////////////////////////////////////////////
154 // Function: ParseFile()
155 // Opens the CUE file for reading, and constructs the track database information
156 ////////////////////////////////////////////////////////////////////////////////////
ParseFile(const std::string & strFilePath)157 bool CCueDocument::ParseFile(const std::string &strFilePath)
158 {
159   FileReader reader(strFilePath);
160   return Parse(reader, strFilePath);
161 }
162 
163 ////////////////////////////////////////////////////////////////////////////////////
164 // Function: ParseTag()
165 // Reads CUE data from string buffer, and constructs the track database information
166 ////////////////////////////////////////////////////////////////////////////////////
ParseTag(const std::string & strContent)167 bool CCueDocument::ParseTag(const std::string &strContent)
168 {
169   BufferReader reader(strContent);
170   return Parse(reader);
171 }
172 
173 //////////////////////////////////////////////////////////////////////////////////
174 // Function:GetSongs()
175 // Store track information into songs list.
176 //////////////////////////////////////////////////////////////////////////////////
GetSongs(VECSONGS & songs)177 void CCueDocument::GetSongs(VECSONGS &songs)
178 {
179   const std::shared_ptr<CAdvancedSettings> advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
180 
181   for (const auto& track : m_tracks)
182   {
183     CSong aSong;
184     //Pass artist to MusicInfoTag object by setting artist description string only.
185     //Artist credits not used during loading from cue sheet.
186     if (track.strArtist.empty() && !m_strArtist.empty())
187       aSong.strArtistDesc = m_strArtist;
188     else
189       aSong.strArtistDesc = track.strArtist;
190     //Pass album artist to MusicInfoTag object by setting album artist vector.
191     aSong.SetAlbumArtist(StringUtils::Split(m_strArtist, advancedSettings->m_musicItemSeparator));
192     aSong.strAlbum = m_strAlbum;
193     aSong.genre = StringUtils::Split(m_strGenre, advancedSettings->m_musicItemSeparator);
194     aSong.strReleaseDate = StringUtils::Format("%04i", m_iYear);
195     aSong.iTrack = track.iTrackNumber;
196     if (m_iDiscNumber > 0)
197       aSong.iTrack |= (m_iDiscNumber << 16); // see CMusicInfoTag::GetDiscNumber()
198     if (track.strTitle.length() == 0) // No track information for this track!
199       aSong.strTitle = StringUtils::Format("Track {:2d}", track.iTrackNumber);
200     else
201       aSong.strTitle = track.strTitle;
202     aSong.strFileName = track.strFile;
203     aSong.iStartOffset = track.iStartTime;
204     aSong.iEndOffset = track.iEndTime;
205     if (aSong.iEndOffset)
206       // Convert offset in frames (75 per second) to duration in whole seconds with rounding
207       aSong.iDuration = CUtil::ConvertMilliSecsToSecsIntRounded(aSong.iEndOffset - aSong.iStartOffset);
208     else
209       aSong.iDuration = 0;
210 
211     if (m_albumReplayGain.Valid())
212       aSong.replayGain.Set(ReplayGain::ALBUM, m_albumReplayGain);
213 
214     if (track.replayGain.Valid())
215       aSong.replayGain.Set(ReplayGain::TRACK, track.replayGain);
216 
217     songs.push_back(aSong);
218   }
219 }
220 
UpdateMediaFile(const std::string & oldMediaFile,const std::string & mediaFile)221 void CCueDocument::UpdateMediaFile(const std::string& oldMediaFile, const std::string& mediaFile)
222 {
223   for (Tracks::iterator it = m_tracks.begin(); it != m_tracks.end(); ++it)
224   {
225     if (it->strFile == oldMediaFile)
226       it->strFile = mediaFile;
227   }
228 }
229 
GetMediaFiles(std::vector<std::string> & mediaFiles)230 void CCueDocument::GetMediaFiles(std::vector<std::string>& mediaFiles)
231 {
232   typedef std::set<std::string> TSet;
233   TSet uniqueFiles;
234   for (Tracks::const_iterator it = m_tracks.begin(); it != m_tracks.end(); ++it)
235     uniqueFiles.insert(it->strFile);
236 
237   for (TSet::const_iterator it = uniqueFiles.begin(); it != uniqueFiles.end(); ++it)
238     mediaFiles.push_back(*it);
239 }
240 
GetMediaTitle()241 std::string CCueDocument::GetMediaTitle()
242 {
243   return m_strAlbum;
244 }
245 
IsLoaded() const246 bool CCueDocument::IsLoaded() const
247 {
248   return !m_tracks.empty();
249 }
250 
IsOneFilePerTrack() const251 bool CCueDocument::IsOneFilePerTrack() const
252 {
253   return m_bOneFilePerTrack;
254 }
255 
256 // Private Functions start here
257 
Clear()258 void CCueDocument::Clear()
259 {
260   m_strArtist.clear();
261   m_strAlbum.clear();
262   m_strGenre.clear();
263   m_iYear = 0;
264   m_iTrack = 0;
265   m_iDiscNumber = 0;
266   m_albumReplayGain = ReplayGain::Info();
267   m_tracks.clear();
268 }
269 ////////////////////////////////////////////////////////////////////////////////////
270 // Function: Parse()
271 // Constructs the track database information from CUE source
272 ////////////////////////////////////////////////////////////////////////////////////
Parse(CueReader & reader,const std::string & strFile)273 bool CCueDocument::Parse(CueReader& reader, const std::string& strFile)
274 {
275   Clear();
276   if (!reader.ready())
277     return false;
278 
279   std::string strLine;
280   std::string strCurrentFile = "";
281   bool bCurrentFileChanged = false;
282   int time;
283   int totalTracks = -1;
284   int numberFiles = -1;
285 
286   // Run through the .CUE file and extract the tracks...
287   while (reader.ReadLine(strLine))
288   {
289     if (StringUtils::StartsWithNoCase(strLine, "INDEX 01"))
290     {
291       if (bCurrentFileChanged)
292       {
293         CLog::Log(LOGERROR, "Track split over multiple files, unsupported.");
294         return false;
295       }
296 
297       // find the end of the number section
298       time = ExtractTimeFromIndex(strLine);
299       if (time == -1)
300       { // Error!
301         CLog::Log(LOGERROR, "Mangled Time in INDEX 0x tag in CUE file!");
302         return false;
303       }
304       if (totalTracks > 0 && m_tracks[totalTracks - 1].strFile == strCurrentFile) // Set the end time of the last track
305         m_tracks[totalTracks - 1].iEndTime = time;
306 
307       if (totalTracks >= 0) // start time of the next track
308         m_tracks[totalTracks].iStartTime = time;
309     }
310     else if (StringUtils::StartsWithNoCase(strLine, "TITLE"))
311     {
312       if (totalTracks == -1) // No tracks yet
313         m_strAlbum = ExtractInfo(strLine.substr(5));
314       else
315         m_tracks[totalTracks].strTitle = ExtractInfo(strLine.substr(5));
316     }
317     else if (StringUtils::StartsWithNoCase(strLine, "PERFORMER"))
318     {
319       if (totalTracks == -1) // No tracks yet
320         m_strArtist = ExtractInfo(strLine.substr(9));
321       else // New Artist for this track
322         m_tracks[totalTracks].strArtist = ExtractInfo(strLine.substr(9));
323     }
324     else if (StringUtils::StartsWithNoCase(strLine, "TRACK"))
325     {
326       int iTrackNumber = ExtractNumericInfo(strLine.substr(5));
327 
328       totalTracks++;
329 
330       CCueTrack track;
331       m_tracks.push_back(track);
332       m_tracks[totalTracks].strFile = strCurrentFile;
333       if (iTrackNumber > 0)
334         m_tracks[totalTracks].iTrackNumber = iTrackNumber;
335       else
336         m_tracks[totalTracks].iTrackNumber = totalTracks + 1;
337 
338       bCurrentFileChanged = false;
339     }
340     else if (StringUtils::StartsWithNoCase(strLine, "REM DISCNUMBER"))
341     {
342       int iDiscNumber = ExtractNumericInfo(strLine.substr(14));
343       if (iDiscNumber > 0)
344         m_iDiscNumber = iDiscNumber;
345     }
346     else if (StringUtils::StartsWithNoCase(strLine, "FILE"))
347     {
348       numberFiles++;
349       // already a file name? then the time computation will be changed
350       if (!strCurrentFile.empty())
351         bCurrentFileChanged = true;
352 
353       strCurrentFile = ExtractInfo(strLine.substr(4));
354 
355       // Resolve absolute paths (if needed).
356       if (!strFile.empty() && !strCurrentFile.empty())
357         ResolvePath(strCurrentFile, strFile);
358     }
359     else if (StringUtils::StartsWithNoCase(strLine, "REM DATE"))
360     {
361       int iYear = ExtractNumericInfo(strLine.substr(8));
362       if (iYear > 0)
363         m_iYear = iYear;
364     }
365     else if (StringUtils::StartsWithNoCase(strLine, "REM GENRE"))
366     {
367       m_strGenre = ExtractInfo(strLine.substr(9));
368     }
369     else if (StringUtils::StartsWithNoCase(strLine, "REM REPLAYGAIN_ALBUM_GAIN"))
370       m_albumReplayGain.SetGain(strLine.substr(26));
371     else if (StringUtils::StartsWithNoCase(strLine, "REM REPLAYGAIN_ALBUM_PEAK"))
372       m_albumReplayGain.SetPeak(strLine.substr(26));
373     else if (StringUtils::StartsWithNoCase(strLine, "REM REPLAYGAIN_TRACK_GAIN") && totalTracks >= 0)
374       m_tracks[totalTracks].replayGain.SetGain(strLine.substr(26));
375     else if (StringUtils::StartsWithNoCase(strLine, "REM REPLAYGAIN_TRACK_PEAK") && totalTracks >= 0)
376       m_tracks[totalTracks].replayGain.SetPeak(strLine.substr(26));
377   }
378 
379   // reset track counter to 0, and fill in the last tracks end time
380   m_iTrack = 0;
381   if (totalTracks >= 0)
382     m_tracks[totalTracks].iEndTime = 0;
383   else
384     CLog::Log(LOGERROR, "No INDEX 01 tags in CUE file!");
385 
386   if ( totalTracks == numberFiles )
387     m_bOneFilePerTrack = true;
388 
389   return (totalTracks >= 0);
390 }
391 
392 ////////////////////////////////////////////////////////////////////////////////////
393 // Function: ExtractInfo()
394 // Extracts the information in quotes from the string line, returning it in quote
395 ////////////////////////////////////////////////////////////////////////////////////
ExtractInfo(const std::string & line)396 std::string CCueDocument::ExtractInfo(const std::string &line)
397 {
398   size_t left = line.find('\"');
399   if (left != std::string::npos)
400   {
401     size_t right = line.find('\"', left + 1);
402     if (right != std::string::npos)
403     {
404       std::string text = line.substr(left + 1, right - left - 1);
405       g_charsetConverter.unknownToUTF8(text);
406       return text;
407     }
408   }
409   std::string text = line;
410   StringUtils::Trim(text);
411   g_charsetConverter.unknownToUTF8(text);
412   return text;
413 }
414 
415 ////////////////////////////////////////////////////////////////////////////////////
416 // Function: ExtractTimeFromIndex()
417 // Extracts the time information from the index string index, returning it as a value in
418 // milliseconds.
419 // Assumed format is:
420 // MM:SS:FF where MM is minutes, SS seconds, and FF frames (75 frames in a second)
421 ////////////////////////////////////////////////////////////////////////////////////
ExtractTimeFromIndex(const std::string & index)422 int CCueDocument::ExtractTimeFromIndex(const std::string &index)
423 {
424   // Get rid of the index number and any whitespace
425   std::string numberTime = index.substr(5);
426   StringUtils::TrimLeft(numberTime);
427   while (!numberTime.empty())
428   {
429     if (!StringUtils::isasciidigit(numberTime[0]))
430       break;
431     numberTime.erase(0, 1);
432   }
433   StringUtils::TrimLeft(numberTime);
434   // split the resulting string
435   std::vector<std::string> time = StringUtils::Split(numberTime, ":");
436   if (time.size() != 3)
437     return -1;
438 
439   int mins = atoi(time[0].c_str());
440   int secs = atoi(time[1].c_str());
441   int frames = atoi(time[2].c_str());
442 
443   return CUtil::ConvertSecsToMilliSecs(mins*60 + secs) + frames * 1000 / 75;
444 }
445 
446 ////////////////////////////////////////////////////////////////////////////////////
447 // Function: ExtractNumericInfo()
448 // Extracts the numeric info from the string info, returning it as an integer value
449 ////////////////////////////////////////////////////////////////////////////////////
ExtractNumericInfo(const std::string & info)450 int CCueDocument::ExtractNumericInfo(const std::string &info)
451 {
452   std::string number(info);
453   StringUtils::TrimLeft(number);
454   if (number.empty() || !StringUtils::isasciidigit(number[0]))
455     return -1;
456   return atoi(number.c_str());
457 }
458 
459 ////////////////////////////////////////////////////////////////////////////////////
460 // Function: ResolvePath()
461 // Determines whether strPath is a relative path or not, and if so, converts it to an
462 // absolute path using the path information in strBase
463 ////////////////////////////////////////////////////////////////////////////////////
ResolvePath(std::string & strPath,const std::string & strBase)464 bool CCueDocument::ResolvePath(std::string &strPath, const std::string &strBase)
465 {
466   std::string strDirectory = URIUtils::GetDirectory(strBase);
467   std::string strFilename = URIUtils::GetFileName(strPath);
468 
469   strPath = URIUtils::AddFileToFolder(strDirectory, strFilename);
470 
471   // i *hate* windows
472   if (!CFile::Exists(strPath))
473   {
474     CFileItemList items;
475     CDirectory::GetDirectory(strDirectory, items, "", DIR_FLAG_DEFAULTS);
476     for (int i=0;i<items.Size();++i)
477     {
478       if (items[i]->IsPath(strPath))
479       {
480         strPath = items[i]->GetPath();
481         return true;
482       }
483     }
484     CLog::Log(LOGERROR,"Could not find '%s' referenced in cue, case sensitivity issue?", strPath.c_str());
485     return false;
486   }
487 
488   return true;
489 }
490 
491