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 "FileItemList.h"
47 #include "ServiceBroker.h"
49 #include "filesystem/Directory.h"
50 #include "filesystem/File.h"
51 #include "settings/AdvancedSettings.h"
52 #include "settings/SettingsComponent.h"
53 #include "utils/CharsetConverter.h"
54 #include "utils/StringUtils.h"
55 #include "utils/URIUtils.h"
56 #include "utils/log.h"
61 using namespace XFILE
;
63 // Stuff for read CUE data from different sources.
67 virtual bool ready() const = 0;
68 virtual bool ReadLine(std::string
&line
) = 0;
69 virtual ~CueReader() = default;
71 std::string m_sourcePath
;
78 explicit FileReader(const std::string
&strFile
) : m_szBuffer
{}
80 m_opened
= m_file
.Open(strFile
);
82 bool ReadLine(std::string
&line
) override
84 // Read the next line.
85 while (m_file
.ReadString(m_szBuffer
, 1023)) // Bigger than MAX_PATH_SIZE, for usage with relax!
87 // Remove the white space at the beginning and end of the line.
89 StringUtils::Trim(line
);
92 // If we are here, we have an empty line so try the next line
96 bool ready() const override
100 ~FileReader() override
109 char m_szBuffer
[1024];
116 explicit BufferReader(const std::string
& strContent
) : m_data(strContent
) {}
117 bool ReadLine(std::string
&line
) override
119 // Read the next line.
121 while (m_pos
< m_data
.size())
123 // Remove the white space at the beginning of the line.
124 char ch
= m_data
.at(m_pos
++);
125 if (ch
== '\r' || ch
== '\n') {
126 StringUtils::Trim(line
);
136 StringUtils::Trim(line
);
137 return !line
.empty();
139 bool ready() const override
141 return m_data
.size() > 0;
148 CCueDocument::~CCueDocument() = default;
150 ////////////////////////////////////////////////////////////////////////////////////
151 // Function: ParseFile()
152 // Opens the CUE file for reading, and constructs the track database information
153 ////////////////////////////////////////////////////////////////////////////////////
154 bool CCueDocument::ParseFile(const std::string
&strFilePath
)
156 FileReader
reader(strFilePath
);
157 return Parse(reader
, strFilePath
);
160 ////////////////////////////////////////////////////////////////////////////////////
161 // Function: ParseTag()
162 // Reads CUE data from string buffer, and constructs the track database information
163 ////////////////////////////////////////////////////////////////////////////////////
164 bool CCueDocument::ParseTag(const std::string
&strContent
)
166 BufferReader
reader(strContent
);
167 return Parse(reader
);
170 //////////////////////////////////////////////////////////////////////////////////
171 // Function:GetSongs()
172 // Store track information into songs list.
173 //////////////////////////////////////////////////////////////////////////////////
174 void CCueDocument::GetSongs(VECSONGS
&songs
)
176 const std::shared_ptr
<CAdvancedSettings
> advancedSettings
= CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
178 for (const auto& track
: m_tracks
)
181 //Pass artist to MusicInfoTag object by setting artist description string only.
182 //Artist credits not used during loading from cue sheet.
183 if (track
.strArtist
.empty() && !m_strArtist
.empty())
184 aSong
.strArtistDesc
= m_strArtist
;
186 aSong
.strArtistDesc
= track
.strArtist
;
187 //Pass album artist to MusicInfoTag object by setting album artist vector.
188 aSong
.SetAlbumArtist(StringUtils::Split(m_strArtist
, advancedSettings
->m_musicItemSeparator
));
189 aSong
.strAlbum
= m_strAlbum
;
190 aSong
.genre
= StringUtils::Split(m_strGenre
, advancedSettings
->m_musicItemSeparator
);
191 aSong
.strReleaseDate
= StringUtils::Format("{:04}", m_iYear
);
192 aSong
.iTrack
= track
.iTrackNumber
;
193 if (m_iDiscNumber
> 0)
194 aSong
.iTrack
|= (m_iDiscNumber
<< 16); // see CMusicInfoTag::GetDiscNumber()
195 if (track
.strTitle
.length() == 0) // No track information for this track!
196 aSong
.strTitle
= StringUtils::Format("Track {:2d}", track
.iTrackNumber
);
198 aSong
.strTitle
= track
.strTitle
;
199 aSong
.strFileName
= track
.strFile
;
200 aSong
.iStartOffset
= track
.iStartTime
;
201 aSong
.iEndOffset
= track
.iEndTime
;
202 if (aSong
.iEndOffset
)
203 // Convert offset in frames (75 per second) to duration in whole seconds with rounding
204 aSong
.iDuration
= CUtil::ConvertMilliSecsToSecsIntRounded(aSong
.iEndOffset
- aSong
.iStartOffset
);
208 if (m_albumReplayGain
.Valid())
209 aSong
.replayGain
.Set(ReplayGain::ALBUM
, m_albumReplayGain
);
211 if (track
.replayGain
.Valid())
212 aSong
.replayGain
.Set(ReplayGain::TRACK
, track
.replayGain
);
214 songs
.push_back(aSong
);
218 void CCueDocument::UpdateMediaFile(const std::string
& oldMediaFile
, const std::string
& mediaFile
)
220 for (Tracks::iterator it
= m_tracks
.begin(); it
!= m_tracks
.end(); ++it
)
222 if (it
->strFile
== oldMediaFile
)
223 it
->strFile
= mediaFile
;
227 void CCueDocument::GetMediaFiles(std::vector
<std::string
>& mediaFiles
)
229 typedef std::set
<std::string
> TSet
;
231 for (Tracks::const_iterator it
= m_tracks
.begin(); it
!= m_tracks
.end(); ++it
)
232 uniqueFiles
.insert(it
->strFile
);
234 for (TSet::const_iterator it
= uniqueFiles
.begin(); it
!= uniqueFiles
.end(); ++it
)
235 mediaFiles
.push_back(*it
);
238 std::string
CCueDocument::GetMediaTitle()
243 bool CCueDocument::IsLoaded() const
245 return !m_tracks
.empty();
248 bool CCueDocument::IsOneFilePerTrack() const
250 return m_bOneFilePerTrack
;
253 // Private Functions start here
255 void CCueDocument::Clear()
263 m_albumReplayGain
= ReplayGain::Info();
266 ////////////////////////////////////////////////////////////////////////////////////
268 // Constructs the track database information from CUE source
269 ////////////////////////////////////////////////////////////////////////////////////
270 bool CCueDocument::Parse(CueReader
& reader
, const std::string
& strFile
)
277 std::string strCurrentFile
= "";
278 bool bCurrentFileChanged
= false;
280 int totalTracks
= -1;
281 int numberFiles
= -1;
283 // Run through the .CUE file and extract the tracks...
284 while (reader
.ReadLine(strLine
))
286 if (StringUtils::StartsWithNoCase(strLine
, "INDEX 01"))
288 if (bCurrentFileChanged
)
290 CLog::Log(LOGERROR
, "Track split over multiple files, unsupported.");
294 // find the end of the number section
295 time
= ExtractTimeFromIndex(strLine
);
298 CLog::Log(LOGERROR
, "Mangled Time in INDEX 0x tag in CUE file!");
301 if (totalTracks
> 0 && m_tracks
[totalTracks
- 1].strFile
== strCurrentFile
) // Set the end time of the last track
302 m_tracks
[totalTracks
- 1].iEndTime
= time
;
304 if (totalTracks
>= 0) // start time of the next track
305 m_tracks
[totalTracks
].iStartTime
= time
;
307 else if (StringUtils::StartsWithNoCase(strLine
, "TITLE"))
309 if (totalTracks
== -1) // No tracks yet
310 m_strAlbum
= ExtractInfo(strLine
.substr(5));
312 m_tracks
[totalTracks
].strTitle
= ExtractInfo(strLine
.substr(5));
314 else if (StringUtils::StartsWithNoCase(strLine
, "PERFORMER"))
316 if (totalTracks
== -1) // No tracks yet
317 m_strArtist
= ExtractInfo(strLine
.substr(9));
318 else // New Artist for this track
319 m_tracks
[totalTracks
].strArtist
= ExtractInfo(strLine
.substr(9));
321 else if (StringUtils::StartsWithNoCase(strLine
, "TRACK"))
323 int iTrackNumber
= ExtractNumericInfo(strLine
.substr(5));
328 m_tracks
.push_back(track
);
329 m_tracks
[totalTracks
].strFile
= strCurrentFile
;
330 if (iTrackNumber
> 0)
331 m_tracks
[totalTracks
].iTrackNumber
= iTrackNumber
;
333 m_tracks
[totalTracks
].iTrackNumber
= totalTracks
+ 1;
335 bCurrentFileChanged
= false;
337 else if (StringUtils::StartsWithNoCase(strLine
, "REM DISCNUMBER"))
339 int iDiscNumber
= ExtractNumericInfo(strLine
.substr(14));
341 m_iDiscNumber
= iDiscNumber
;
343 else if (StringUtils::StartsWithNoCase(strLine
, "FILE"))
346 // already a file name? then the time computation will be changed
347 if (!strCurrentFile
.empty())
348 bCurrentFileChanged
= true;
350 strCurrentFile
= ExtractInfo(strLine
.substr(4));
352 // Resolve absolute paths (if needed).
353 if (!strFile
.empty() && !strCurrentFile
.empty())
354 ResolvePath(strCurrentFile
, strFile
);
356 else if (StringUtils::StartsWithNoCase(strLine
, "REM DATE"))
358 int iYear
= ExtractNumericInfo(strLine
.substr(8));
362 else if (StringUtils::StartsWithNoCase(strLine
, "REM GENRE"))
364 m_strGenre
= ExtractInfo(strLine
.substr(9));
366 else if (StringUtils::StartsWithNoCase(strLine
, "REM REPLAYGAIN_ALBUM_GAIN"))
367 m_albumReplayGain
.SetGain(strLine
.substr(26));
368 else if (StringUtils::StartsWithNoCase(strLine
, "REM REPLAYGAIN_ALBUM_PEAK"))
369 m_albumReplayGain
.SetPeak(strLine
.substr(26));
370 else if (StringUtils::StartsWithNoCase(strLine
, "REM REPLAYGAIN_TRACK_GAIN") && totalTracks
>= 0)
371 m_tracks
[totalTracks
].replayGain
.SetGain(strLine
.substr(26));
372 else if (StringUtils::StartsWithNoCase(strLine
, "REM REPLAYGAIN_TRACK_PEAK") && totalTracks
>= 0)
373 m_tracks
[totalTracks
].replayGain
.SetPeak(strLine
.substr(26));
376 // reset track counter to 0, and fill in the last tracks end time
378 if (totalTracks
>= 0)
379 m_tracks
[totalTracks
].iEndTime
= 0;
381 CLog::Log(LOGERROR
, "No INDEX 01 tags in CUE file!");
383 if ( totalTracks
== numberFiles
)
384 m_bOneFilePerTrack
= true;
386 return (totalTracks
>= 0);
389 ////////////////////////////////////////////////////////////////////////////////////
390 // Function: ExtractInfo()
391 // Extracts the information in quotes from the string line, returning it in quote
392 ////////////////////////////////////////////////////////////////////////////////////
393 std::string
CCueDocument::ExtractInfo(const std::string
&line
)
395 size_t left
= line
.find('\"');
396 if (left
!= std::string::npos
)
398 size_t right
= line
.find('\"', left
+ 1);
399 if (right
!= std::string::npos
)
401 std::string text
= line
.substr(left
+ 1, right
- left
- 1);
402 g_charsetConverter
.unknownToUTF8(text
);
406 std::string text
= line
;
407 StringUtils::Trim(text
);
408 g_charsetConverter
.unknownToUTF8(text
);
412 ////////////////////////////////////////////////////////////////////////////////////
413 // Function: ExtractTimeFromIndex()
414 // Extracts the time information from the index string index, returning it as a value in
416 // Assumed format is:
417 // MM:SS:FF where MM is minutes, SS seconds, and FF frames (75 frames in a second)
418 ////////////////////////////////////////////////////////////////////////////////////
419 int CCueDocument::ExtractTimeFromIndex(const std::string
&index
)
421 // Get rid of the index number and any whitespace
422 std::string numberTime
= index
.substr(5);
423 StringUtils::TrimLeft(numberTime
);
424 while (!numberTime
.empty())
426 if (!StringUtils::isasciidigit(numberTime
[0]))
428 numberTime
.erase(0, 1);
430 StringUtils::TrimLeft(numberTime
);
431 // split the resulting string
432 std::vector
<std::string
> time
= StringUtils::Split(numberTime
, ":");
433 if (time
.size() != 3)
436 int mins
= atoi(time
[0].c_str());
437 int secs
= atoi(time
[1].c_str());
438 int frames
= atoi(time
[2].c_str());
440 return CUtil::ConvertSecsToMilliSecs(mins
*60 + secs
) + frames
* 1000 / 75;
443 ////////////////////////////////////////////////////////////////////////////////////
444 // Function: ExtractNumericInfo()
445 // Extracts the numeric info from the string info, returning it as an integer value
446 ////////////////////////////////////////////////////////////////////////////////////
447 int CCueDocument::ExtractNumericInfo(const std::string
&info
)
449 std::string
number(info
);
450 StringUtils::TrimLeft(number
);
451 if (number
.empty() || !StringUtils::isasciidigit(number
[0]))
453 return atoi(number
.c_str());
456 ////////////////////////////////////////////////////////////////////////////////////
457 // Function: ResolvePath()
458 // Determines whether strPath is a relative path or not, and if so, converts it to an
459 // absolute path using the path information in strBase
460 ////////////////////////////////////////////////////////////////////////////////////
461 bool CCueDocument::ResolvePath(std::string
&strPath
, const std::string
&strBase
)
463 std::string strDirectory
= URIUtils::GetDirectory(strBase
);
464 std::string strFilename
= URIUtils::GetFileName(strPath
);
466 strPath
= URIUtils::AddFileToFolder(strDirectory
, strFilename
);
469 if (!CFile::Exists(strPath
))
472 CDirectory::GetDirectory(strDirectory
, items
, "", DIR_FLAG_DEFAULTS
);
473 for (int i
=0;i
<items
.Size();++i
)
475 if (items
[i
]->IsPath(strPath
))
477 strPath
= items
[i
]->GetPath();
481 CLog::Log(LOGERROR
, "Could not find '{}' referenced in cue, case sensitivity issue?", strPath
);