2 * Copyright (C) 2005-2018 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
9 ////////////////////////////////////////////////////////////////////////////////////
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:
16 // PERFORMER "Pink Floyd"
17 // TITLE "The Dark Side Of The Moon"
18 // FILE "The Dark Side Of The Moon.mp3" WAVE
20 // TITLE "Speak To Me / Breathe"
21 // PERFORMER "Pink Floyd"
26 // PERFORMER "Pink Floyd"
31 // PERFORMER "Pink Floyd"
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.
41 ////////////////////////////////////////////////////////////////////////////////////
43 #include "CueDocument.h"
46 #include "ServiceBroker.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"
60 using namespace XFILE
;
62 // Stuff for read CUE data from different sources.
66 virtual bool ready() const = 0;
67 virtual bool ReadLine(std::string
&line
) = 0;
68 virtual ~CueReader() = default;
70 std::string m_sourcePath
;
77 explicit FileReader(const std::string
&strFile
) : m_szBuffer
{}
79 m_opened
= m_file
.Open(strFile
);
81 bool ReadLine(std::string
&line
) override
83 // Read the next line.
84 while (m_file
.ReadString(m_szBuffer
, 1023)) // Bigger than MAX_PATH_SIZE, for usage with relax!
86 // Remove the white space at the beginning and end of the line.
88 StringUtils::Trim(line
);
91 // If we are here, we have an empty line so try the next line
95 bool ready() const override
99 ~FileReader() override
108 char m_szBuffer
[1024];
115 explicit BufferReader(const std::string
&strContent
)
120 bool ReadLine(std::string
&line
) override
122 // Read the next line.
124 while (m_pos
< m_data
.size())
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
);
139 StringUtils::Trim(line
);
140 return !line
.empty();
142 bool ready() const override
144 return m_data
.size() > 0;
151 CCueDocument::~CCueDocument() = default;
153 ////////////////////////////////////////////////////////////////////////////////////
154 // Function: ParseFile()
155 // Opens the CUE file for reading, and constructs the track database information
156 ////////////////////////////////////////////////////////////////////////////////////
157 bool CCueDocument::ParseFile(const std::string
&strFilePath
)
159 FileReader
reader(strFilePath
);
160 return Parse(reader
, strFilePath
);
163 ////////////////////////////////////////////////////////////////////////////////////
164 // Function: ParseTag()
165 // Reads CUE data from string buffer, and constructs the track database information
166 ////////////////////////////////////////////////////////////////////////////////////
167 bool CCueDocument::ParseTag(const std::string
&strContent
)
169 BufferReader
reader(strContent
);
170 return Parse(reader
);
173 //////////////////////////////////////////////////////////////////////////////////
174 // Function:GetSongs()
175 // Store track information into songs list.
176 //////////////////////////////////////////////////////////////////////////////////
177 void CCueDocument::GetSongs(VECSONGS
&songs
)
179 const std::shared_ptr
<CAdvancedSettings
> advancedSettings
= CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
181 for (const auto& track
: m_tracks
)
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
;
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("{:04}", 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
);
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
);
211 if (m_albumReplayGain
.Valid())
212 aSong
.replayGain
.Set(ReplayGain::ALBUM
, m_albumReplayGain
);
214 if (track
.replayGain
.Valid())
215 aSong
.replayGain
.Set(ReplayGain::TRACK
, track
.replayGain
);
217 songs
.push_back(aSong
);
221 void CCueDocument::UpdateMediaFile(const std::string
& oldMediaFile
, const std::string
& mediaFile
)
223 for (Tracks::iterator it
= m_tracks
.begin(); it
!= m_tracks
.end(); ++it
)
225 if (it
->strFile
== oldMediaFile
)
226 it
->strFile
= mediaFile
;
230 void CCueDocument::GetMediaFiles(std::vector
<std::string
>& mediaFiles
)
232 typedef std::set
<std::string
> TSet
;
234 for (Tracks::const_iterator it
= m_tracks
.begin(); it
!= m_tracks
.end(); ++it
)
235 uniqueFiles
.insert(it
->strFile
);
237 for (TSet::const_iterator it
= uniqueFiles
.begin(); it
!= uniqueFiles
.end(); ++it
)
238 mediaFiles
.push_back(*it
);
241 std::string
CCueDocument::GetMediaTitle()
246 bool CCueDocument::IsLoaded() const
248 return !m_tracks
.empty();
251 bool CCueDocument::IsOneFilePerTrack() const
253 return m_bOneFilePerTrack
;
256 // Private Functions start here
258 void CCueDocument::Clear()
266 m_albumReplayGain
= ReplayGain::Info();
269 ////////////////////////////////////////////////////////////////////////////////////
271 // Constructs the track database information from CUE source
272 ////////////////////////////////////////////////////////////////////////////////////
273 bool CCueDocument::Parse(CueReader
& reader
, const std::string
& strFile
)
280 std::string strCurrentFile
= "";
281 bool bCurrentFileChanged
= false;
283 int totalTracks
= -1;
284 int numberFiles
= -1;
286 // Run through the .CUE file and extract the tracks...
287 while (reader
.ReadLine(strLine
))
289 if (StringUtils::StartsWithNoCase(strLine
, "INDEX 01"))
291 if (bCurrentFileChanged
)
293 CLog::Log(LOGERROR
, "Track split over multiple files, unsupported.");
297 // find the end of the number section
298 time
= ExtractTimeFromIndex(strLine
);
301 CLog::Log(LOGERROR
, "Mangled Time in INDEX 0x tag in CUE file!");
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
;
307 if (totalTracks
>= 0) // start time of the next track
308 m_tracks
[totalTracks
].iStartTime
= time
;
310 else if (StringUtils::StartsWithNoCase(strLine
, "TITLE"))
312 if (totalTracks
== -1) // No tracks yet
313 m_strAlbum
= ExtractInfo(strLine
.substr(5));
315 m_tracks
[totalTracks
].strTitle
= ExtractInfo(strLine
.substr(5));
317 else if (StringUtils::StartsWithNoCase(strLine
, "PERFORMER"))
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));
324 else if (StringUtils::StartsWithNoCase(strLine
, "TRACK"))
326 int iTrackNumber
= ExtractNumericInfo(strLine
.substr(5));
331 m_tracks
.push_back(track
);
332 m_tracks
[totalTracks
].strFile
= strCurrentFile
;
333 if (iTrackNumber
> 0)
334 m_tracks
[totalTracks
].iTrackNumber
= iTrackNumber
;
336 m_tracks
[totalTracks
].iTrackNumber
= totalTracks
+ 1;
338 bCurrentFileChanged
= false;
340 else if (StringUtils::StartsWithNoCase(strLine
, "REM DISCNUMBER"))
342 int iDiscNumber
= ExtractNumericInfo(strLine
.substr(14));
344 m_iDiscNumber
= iDiscNumber
;
346 else if (StringUtils::StartsWithNoCase(strLine
, "FILE"))
349 // already a file name? then the time computation will be changed
350 if (!strCurrentFile
.empty())
351 bCurrentFileChanged
= true;
353 strCurrentFile
= ExtractInfo(strLine
.substr(4));
355 // Resolve absolute paths (if needed).
356 if (!strFile
.empty() && !strCurrentFile
.empty())
357 ResolvePath(strCurrentFile
, strFile
);
359 else if (StringUtils::StartsWithNoCase(strLine
, "REM DATE"))
361 int iYear
= ExtractNumericInfo(strLine
.substr(8));
365 else if (StringUtils::StartsWithNoCase(strLine
, "REM GENRE"))
367 m_strGenre
= ExtractInfo(strLine
.substr(9));
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));
379 // reset track counter to 0, and fill in the last tracks end time
381 if (totalTracks
>= 0)
382 m_tracks
[totalTracks
].iEndTime
= 0;
384 CLog::Log(LOGERROR
, "No INDEX 01 tags in CUE file!");
386 if ( totalTracks
== numberFiles
)
387 m_bOneFilePerTrack
= true;
389 return (totalTracks
>= 0);
392 ////////////////////////////////////////////////////////////////////////////////////
393 // Function: ExtractInfo()
394 // Extracts the information in quotes from the string line, returning it in quote
395 ////////////////////////////////////////////////////////////////////////////////////
396 std::string
CCueDocument::ExtractInfo(const std::string
&line
)
398 size_t left
= line
.find('\"');
399 if (left
!= std::string::npos
)
401 size_t right
= line
.find('\"', left
+ 1);
402 if (right
!= std::string::npos
)
404 std::string text
= line
.substr(left
+ 1, right
- left
- 1);
405 g_charsetConverter
.unknownToUTF8(text
);
409 std::string text
= line
;
410 StringUtils::Trim(text
);
411 g_charsetConverter
.unknownToUTF8(text
);
415 ////////////////////////////////////////////////////////////////////////////////////
416 // Function: ExtractTimeFromIndex()
417 // Extracts the time information from the index string index, returning it as a value in
419 // Assumed format is:
420 // MM:SS:FF where MM is minutes, SS seconds, and FF frames (75 frames in a second)
421 ////////////////////////////////////////////////////////////////////////////////////
422 int CCueDocument::ExtractTimeFromIndex(const std::string
&index
)
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())
429 if (!StringUtils::isasciidigit(numberTime
[0]))
431 numberTime
.erase(0, 1);
433 StringUtils::TrimLeft(numberTime
);
434 // split the resulting string
435 std::vector
<std::string
> time
= StringUtils::Split(numberTime
, ":");
436 if (time
.size() != 3)
439 int mins
= atoi(time
[0].c_str());
440 int secs
= atoi(time
[1].c_str());
441 int frames
= atoi(time
[2].c_str());
443 return CUtil::ConvertSecsToMilliSecs(mins
*60 + secs
) + frames
* 1000 / 75;
446 ////////////////////////////////////////////////////////////////////////////////////
447 // Function: ExtractNumericInfo()
448 // Extracts the numeric info from the string info, returning it as an integer value
449 ////////////////////////////////////////////////////////////////////////////////////
450 int CCueDocument::ExtractNumericInfo(const std::string
&info
)
452 std::string
number(info
);
453 StringUtils::TrimLeft(number
);
454 if (number
.empty() || !StringUtils::isasciidigit(number
[0]))
456 return atoi(number
.c_str());
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 ////////////////////////////////////////////////////////////////////////////////////
464 bool CCueDocument::ResolvePath(std::string
&strPath
, const std::string
&strBase
)
466 std::string strDirectory
= URIUtils::GetDirectory(strBase
);
467 std::string strFilename
= URIUtils::GetFileName(strPath
);
469 strPath
= URIUtils::AddFileToFolder(strDirectory
, strFilename
);
472 if (!CFile::Exists(strPath
))
475 CDirectory::GetDirectory(strDirectory
, items
, "", DIR_FLAG_DEFAULTS
);
476 for (int i
=0;i
<items
.Size();++i
)
478 if (items
[i
]->IsPath(strPath
))
480 strPath
= items
[i
]->GetPath();
484 CLog::Log(LOGERROR
, "Could not find '{}' referenced in cue, case sensitivity issue?", strPath
);