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
) : m_data(strContent
) {}
116 bool ReadLine(std::string
&line
) override
118 // Read the next line.
120 while (m_pos
< m_data
.size())
122 // Remove the white space at the beginning of the line.
123 char ch
= m_data
.at(m_pos
++);
124 if (ch
== '\r' || ch
== '\n') {
125 StringUtils::Trim(line
);
135 StringUtils::Trim(line
);
136 return !line
.empty();
138 bool ready() const override
140 return m_data
.size() > 0;
147 CCueDocument::~CCueDocument() = default;
149 ////////////////////////////////////////////////////////////////////////////////////
150 // Function: ParseFile()
151 // Opens the CUE file for reading, and constructs the track database information
152 ////////////////////////////////////////////////////////////////////////////////////
153 bool CCueDocument::ParseFile(const std::string
&strFilePath
)
155 FileReader
reader(strFilePath
);
156 return Parse(reader
, strFilePath
);
159 ////////////////////////////////////////////////////////////////////////////////////
160 // Function: ParseTag()
161 // Reads CUE data from string buffer, and constructs the track database information
162 ////////////////////////////////////////////////////////////////////////////////////
163 bool CCueDocument::ParseTag(const std::string
&strContent
)
165 BufferReader
reader(strContent
);
166 return Parse(reader
);
169 //////////////////////////////////////////////////////////////////////////////////
170 // Function:GetSongs()
171 // Store track information into songs list.
172 //////////////////////////////////////////////////////////////////////////////////
173 void CCueDocument::GetSongs(VECSONGS
&songs
)
175 const std::shared_ptr
<CAdvancedSettings
> advancedSettings
= CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
177 for (const auto& track
: m_tracks
)
180 //Pass artist to MusicInfoTag object by setting artist description string only.
181 //Artist credits not used during loading from cue sheet.
182 if (track
.strArtist
.empty() && !m_strArtist
.empty())
183 aSong
.strArtistDesc
= m_strArtist
;
185 aSong
.strArtistDesc
= track
.strArtist
;
186 //Pass album artist to MusicInfoTag object by setting album artist vector.
187 aSong
.SetAlbumArtist(StringUtils::Split(m_strArtist
, advancedSettings
->m_musicItemSeparator
));
188 aSong
.strAlbum
= m_strAlbum
;
189 aSong
.genre
= StringUtils::Split(m_strGenre
, advancedSettings
->m_musicItemSeparator
);
190 aSong
.strReleaseDate
= StringUtils::Format("{:04}", m_iYear
);
191 aSong
.iTrack
= track
.iTrackNumber
;
192 if (m_iDiscNumber
> 0)
193 aSong
.iTrack
|= (m_iDiscNumber
<< 16); // see CMusicInfoTag::GetDiscNumber()
194 if (track
.strTitle
.length() == 0) // No track information for this track!
195 aSong
.strTitle
= StringUtils::Format("Track {:2d}", track
.iTrackNumber
);
197 aSong
.strTitle
= track
.strTitle
;
198 aSong
.strFileName
= track
.strFile
;
199 aSong
.iStartOffset
= track
.iStartTime
;
200 aSong
.iEndOffset
= track
.iEndTime
;
201 if (aSong
.iEndOffset
)
202 // Convert offset in frames (75 per second) to duration in whole seconds with rounding
203 aSong
.iDuration
= CUtil::ConvertMilliSecsToSecsIntRounded(aSong
.iEndOffset
- aSong
.iStartOffset
);
207 if (m_albumReplayGain
.Valid())
208 aSong
.replayGain
.Set(ReplayGain::ALBUM
, m_albumReplayGain
);
210 if (track
.replayGain
.Valid())
211 aSong
.replayGain
.Set(ReplayGain::TRACK
, track
.replayGain
);
213 songs
.push_back(aSong
);
217 void CCueDocument::UpdateMediaFile(const std::string
& oldMediaFile
, const std::string
& mediaFile
)
219 for (Tracks::iterator it
= m_tracks
.begin(); it
!= m_tracks
.end(); ++it
)
221 if (it
->strFile
== oldMediaFile
)
222 it
->strFile
= mediaFile
;
226 void CCueDocument::GetMediaFiles(std::vector
<std::string
>& mediaFiles
)
228 typedef std::set
<std::string
> TSet
;
230 for (Tracks::const_iterator it
= m_tracks
.begin(); it
!= m_tracks
.end(); ++it
)
231 uniqueFiles
.insert(it
->strFile
);
233 for (TSet::const_iterator it
= uniqueFiles
.begin(); it
!= uniqueFiles
.end(); ++it
)
234 mediaFiles
.push_back(*it
);
237 std::string
CCueDocument::GetMediaTitle()
242 bool CCueDocument::IsLoaded() const
244 return !m_tracks
.empty();
247 bool CCueDocument::IsOneFilePerTrack() const
249 return m_bOneFilePerTrack
;
252 // Private Functions start here
254 void CCueDocument::Clear()
262 m_albumReplayGain
= ReplayGain::Info();
265 ////////////////////////////////////////////////////////////////////////////////////
267 // Constructs the track database information from CUE source
268 ////////////////////////////////////////////////////////////////////////////////////
269 bool CCueDocument::Parse(CueReader
& reader
, const std::string
& strFile
)
276 std::string strCurrentFile
= "";
277 bool bCurrentFileChanged
= false;
279 int totalTracks
= -1;
280 int numberFiles
= -1;
282 // Run through the .CUE file and extract the tracks...
283 while (reader
.ReadLine(strLine
))
285 if (StringUtils::StartsWithNoCase(strLine
, "INDEX 01"))
287 if (bCurrentFileChanged
)
289 CLog::Log(LOGERROR
, "Track split over multiple files, unsupported.");
293 // find the end of the number section
294 time
= ExtractTimeFromIndex(strLine
);
297 CLog::Log(LOGERROR
, "Mangled Time in INDEX 0x tag in CUE file!");
300 if (totalTracks
> 0 && m_tracks
[totalTracks
- 1].strFile
== strCurrentFile
) // Set the end time of the last track
301 m_tracks
[totalTracks
- 1].iEndTime
= time
;
303 if (totalTracks
>= 0) // start time of the next track
304 m_tracks
[totalTracks
].iStartTime
= time
;
306 else if (StringUtils::StartsWithNoCase(strLine
, "TITLE"))
308 if (totalTracks
== -1) // No tracks yet
309 m_strAlbum
= ExtractInfo(strLine
.substr(5));
311 m_tracks
[totalTracks
].strTitle
= ExtractInfo(strLine
.substr(5));
313 else if (StringUtils::StartsWithNoCase(strLine
, "PERFORMER"))
315 if (totalTracks
== -1) // No tracks yet
316 m_strArtist
= ExtractInfo(strLine
.substr(9));
317 else // New Artist for this track
318 m_tracks
[totalTracks
].strArtist
= ExtractInfo(strLine
.substr(9));
320 else if (StringUtils::StartsWithNoCase(strLine
, "TRACK"))
322 int iTrackNumber
= ExtractNumericInfo(strLine
.substr(5));
327 m_tracks
.push_back(track
);
328 m_tracks
[totalTracks
].strFile
= strCurrentFile
;
329 if (iTrackNumber
> 0)
330 m_tracks
[totalTracks
].iTrackNumber
= iTrackNumber
;
332 m_tracks
[totalTracks
].iTrackNumber
= totalTracks
+ 1;
334 bCurrentFileChanged
= false;
336 else if (StringUtils::StartsWithNoCase(strLine
, "REM DISCNUMBER"))
338 int iDiscNumber
= ExtractNumericInfo(strLine
.substr(14));
340 m_iDiscNumber
= iDiscNumber
;
342 else if (StringUtils::StartsWithNoCase(strLine
, "FILE"))
345 // already a file name? then the time computation will be changed
346 if (!strCurrentFile
.empty())
347 bCurrentFileChanged
= true;
349 strCurrentFile
= ExtractInfo(strLine
.substr(4));
351 // Resolve absolute paths (if needed).
352 if (!strFile
.empty() && !strCurrentFile
.empty())
353 ResolvePath(strCurrentFile
, strFile
);
355 else if (StringUtils::StartsWithNoCase(strLine
, "REM DATE"))
357 int iYear
= ExtractNumericInfo(strLine
.substr(8));
361 else if (StringUtils::StartsWithNoCase(strLine
, "REM GENRE"))
363 m_strGenre
= ExtractInfo(strLine
.substr(9));
365 else if (StringUtils::StartsWithNoCase(strLine
, "REM REPLAYGAIN_ALBUM_GAIN"))
366 m_albumReplayGain
.SetGain(strLine
.substr(26));
367 else if (StringUtils::StartsWithNoCase(strLine
, "REM REPLAYGAIN_ALBUM_PEAK"))
368 m_albumReplayGain
.SetPeak(strLine
.substr(26));
369 else if (StringUtils::StartsWithNoCase(strLine
, "REM REPLAYGAIN_TRACK_GAIN") && totalTracks
>= 0)
370 m_tracks
[totalTracks
].replayGain
.SetGain(strLine
.substr(26));
371 else if (StringUtils::StartsWithNoCase(strLine
, "REM REPLAYGAIN_TRACK_PEAK") && totalTracks
>= 0)
372 m_tracks
[totalTracks
].replayGain
.SetPeak(strLine
.substr(26));
375 // reset track counter to 0, and fill in the last tracks end time
377 if (totalTracks
>= 0)
378 m_tracks
[totalTracks
].iEndTime
= 0;
380 CLog::Log(LOGERROR
, "No INDEX 01 tags in CUE file!");
382 if ( totalTracks
== numberFiles
)
383 m_bOneFilePerTrack
= true;
385 return (totalTracks
>= 0);
388 ////////////////////////////////////////////////////////////////////////////////////
389 // Function: ExtractInfo()
390 // Extracts the information in quotes from the string line, returning it in quote
391 ////////////////////////////////////////////////////////////////////////////////////
392 std::string
CCueDocument::ExtractInfo(const std::string
&line
)
394 size_t left
= line
.find('\"');
395 if (left
!= std::string::npos
)
397 size_t right
= line
.find('\"', left
+ 1);
398 if (right
!= std::string::npos
)
400 std::string text
= line
.substr(left
+ 1, right
- left
- 1);
401 g_charsetConverter
.unknownToUTF8(text
);
405 std::string text
= line
;
406 StringUtils::Trim(text
);
407 g_charsetConverter
.unknownToUTF8(text
);
411 ////////////////////////////////////////////////////////////////////////////////////
412 // Function: ExtractTimeFromIndex()
413 // Extracts the time information from the index string index, returning it as a value in
415 // Assumed format is:
416 // MM:SS:FF where MM is minutes, SS seconds, and FF frames (75 frames in a second)
417 ////////////////////////////////////////////////////////////////////////////////////
418 int CCueDocument::ExtractTimeFromIndex(const std::string
&index
)
420 // Get rid of the index number and any whitespace
421 std::string numberTime
= index
.substr(5);
422 StringUtils::TrimLeft(numberTime
);
423 while (!numberTime
.empty())
425 if (!StringUtils::isasciidigit(numberTime
[0]))
427 numberTime
.erase(0, 1);
429 StringUtils::TrimLeft(numberTime
);
430 // split the resulting string
431 std::vector
<std::string
> time
= StringUtils::Split(numberTime
, ":");
432 if (time
.size() != 3)
435 int mins
= atoi(time
[0].c_str());
436 int secs
= atoi(time
[1].c_str());
437 int frames
= atoi(time
[2].c_str());
439 return CUtil::ConvertSecsToMilliSecs(mins
*60 + secs
) + frames
* 1000 / 75;
442 ////////////////////////////////////////////////////////////////////////////////////
443 // Function: ExtractNumericInfo()
444 // Extracts the numeric info from the string info, returning it as an integer value
445 ////////////////////////////////////////////////////////////////////////////////////
446 int CCueDocument::ExtractNumericInfo(const std::string
&info
)
448 std::string
number(info
);
449 StringUtils::TrimLeft(number
);
450 if (number
.empty() || !StringUtils::isasciidigit(number
[0]))
452 return atoi(number
.c_str());
455 ////////////////////////////////////////////////////////////////////////////////////
456 // Function: ResolvePath()
457 // Determines whether strPath is a relative path or not, and if so, converts it to an
458 // absolute path using the path information in strBase
459 ////////////////////////////////////////////////////////////////////////////////////
460 bool CCueDocument::ResolvePath(std::string
&strPath
, const std::string
&strBase
)
462 std::string strDirectory
= URIUtils::GetDirectory(strBase
);
463 std::string strFilename
= URIUtils::GetFileName(strPath
);
465 strPath
= URIUtils::AddFileToFolder(strDirectory
, strFilename
);
468 if (!CFile::Exists(strPath
))
471 CDirectory::GetDirectory(strDirectory
, items
, "", DIR_FLAG_DEFAULTS
);
472 for (int i
=0;i
<items
.Size();++i
)
474 if (items
[i
]->IsPath(strPath
))
476 strPath
= items
[i
]->GetPath();
480 CLog::Log(LOGERROR
, "Could not find '{}' referenced in cue, case sensitivity issue?", strPath
);