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 "music/tags/MusicInfoTag.h"
52 #include "settings/AdvancedSettings.h"
53 #include "settings/SettingsComponent.h"
54 #include "utils/CharsetConverter.h"
55 #include "utils/StringUtils.h"
56 #include "utils/URIUtils.h"
57 #include "utils/log.h"
62 using namespace XFILE
;
64 // Stuff for read CUE data from different sources.
68 virtual bool ready() const = 0;
69 virtual bool ReadLine(std::string
&line
) = 0;
70 virtual ~CueReader() = default;
72 std::string m_sourcePath
;
79 explicit FileReader(const std::string
& strFile
) { m_opened
= m_file
.Open(strFile
); }
80 bool ReadLine(std::string
&line
) override
82 // Read the next line.
83 while (m_file
.ReadString(m_szBuffer
, 1023)) // Bigger than MAX_PATH_SIZE, for usage with relax!
85 // Remove the white space at the beginning and end of the line.
87 StringUtils::Trim(line
);
90 // If we are here, we have an empty line so try the next line
94 bool ready() const override
98 ~FileReader() override
107 char m_szBuffer
[1024]{};
114 explicit BufferReader(const std::string
& strContent
) : m_data(strContent
) {}
115 bool ReadLine(std::string
&line
) override
117 // Read the next line.
119 while (m_pos
< m_data
.size())
121 // Remove the white space at the beginning of the line.
122 char ch
= m_data
.at(m_pos
++);
123 if (ch
== '\r' || ch
== '\n') {
124 StringUtils::Trim(line
);
134 StringUtils::Trim(line
);
135 return !line
.empty();
137 bool ready() const override
139 return m_data
.size() > 0;
146 CCueDocument::~CCueDocument() = default;
148 ////////////////////////////////////////////////////////////////////////////////////
149 // Function: ParseFile()
150 // Opens the CUE file for reading, and constructs the track database information
151 ////////////////////////////////////////////////////////////////////////////////////
152 bool CCueDocument::ParseFile(const std::string
&strFilePath
)
154 FileReader
reader(strFilePath
);
155 return Parse(reader
, strFilePath
);
158 ////////////////////////////////////////////////////////////////////////////////////
159 // Function: ParseTag()
160 // Reads CUE data from string buffer, and constructs the track database information
161 ////////////////////////////////////////////////////////////////////////////////////
162 bool CCueDocument::ParseTag(const std::string
&strContent
)
164 BufferReader
reader(strContent
);
165 return Parse(reader
);
168 //////////////////////////////////////////////////////////////////////////////////
169 // Function:GetSongs()
170 // Store track information into songs list.
171 //////////////////////////////////////////////////////////////////////////////////
172 void CCueDocument::GetSongs(VECSONGS
&songs
)
174 const std::shared_ptr
<CAdvancedSettings
> advancedSettings
= CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
176 for (const auto& track
: m_tracks
)
179 //Pass artist to MusicInfoTag object by setting artist description string only.
180 //Artist credits not used during loading from cue sheet.
181 if (track
.strArtist
.empty() && !m_strArtist
.empty())
182 aSong
.strArtistDesc
= m_strArtist
;
184 aSong
.strArtistDesc
= track
.strArtist
;
185 //Pass album artist to MusicInfoTag object by setting album artist vector.
186 aSong
.SetAlbumArtist(StringUtils::Split(m_strArtist
, advancedSettings
->m_musicItemSeparator
));
187 aSong
.strAlbum
= m_strAlbum
;
188 aSong
.genre
= StringUtils::Split(m_strGenre
, advancedSettings
->m_musicItemSeparator
);
189 aSong
.strReleaseDate
= StringUtils::Format("{:04}", m_iYear
);
190 aSong
.iTrack
= track
.iTrackNumber
;
191 if (m_iDiscNumber
> 0)
192 aSong
.iTrack
|= (m_iDiscNumber
<< 16); // see CMusicInfoTag::GetDiscNumber()
193 if (track
.strTitle
.length() == 0) // No track information for this track!
194 aSong
.strTitle
= StringUtils::Format("Track {:2d}", track
.iTrackNumber
);
196 aSong
.strTitle
= track
.strTitle
;
197 aSong
.strFileName
= track
.strFile
;
198 aSong
.iStartOffset
= track
.iStartTime
;
199 aSong
.iEndOffset
= track
.iEndTime
;
200 if (aSong
.iEndOffset
)
201 // Convert offset in frames (75 per second) to duration in whole seconds with rounding
202 aSong
.iDuration
= CUtil::ConvertMilliSecsToSecsIntRounded(aSong
.iEndOffset
- aSong
.iStartOffset
);
206 if (m_albumReplayGain
.Valid())
207 aSong
.replayGain
.Set(ReplayGain::ALBUM
, m_albumReplayGain
);
209 if (track
.replayGain
.Valid())
210 aSong
.replayGain
.Set(ReplayGain::TRACK
, track
.replayGain
);
212 songs
.push_back(aSong
);
216 void CCueDocument::UpdateMediaFile(const std::string
& oldMediaFile
, const std::string
& mediaFile
)
218 for (Tracks::iterator it
= m_tracks
.begin(); it
!= m_tracks
.end(); ++it
)
220 if (it
->strFile
== oldMediaFile
)
221 it
->strFile
= mediaFile
;
225 void CCueDocument::GetMediaFiles(std::vector
<std::string
>& mediaFiles
)
227 typedef std::set
<std::string
> TSet
;
229 for (Tracks::const_iterator it
= m_tracks
.begin(); it
!= m_tracks
.end(); ++it
)
230 uniqueFiles
.insert(it
->strFile
);
232 for (TSet::const_iterator it
= uniqueFiles
.begin(); it
!= uniqueFiles
.end(); ++it
)
233 mediaFiles
.push_back(*it
);
236 bool CCueDocument::IsLoaded() const
238 return !m_tracks
.empty();
241 bool CCueDocument::IsOneFilePerTrack() const
243 return m_bOneFilePerTrack
;
246 // Private Functions start here
248 void CCueDocument::Clear()
256 m_albumReplayGain
= ReplayGain::Info();
259 ////////////////////////////////////////////////////////////////////////////////////
261 // Constructs the track database information from CUE source
262 ////////////////////////////////////////////////////////////////////////////////////
263 bool CCueDocument::Parse(CueReader
& reader
, const std::string
& strFile
)
270 std::string strCurrentFile
= "";
271 bool bCurrentFileChanged
= false;
273 int totalTracks
= -1;
274 int numberFiles
= -1;
276 // Run through the .CUE file and extract the tracks...
277 while (reader
.ReadLine(strLine
))
279 if (StringUtils::StartsWithNoCase(strLine
, "INDEX 01"))
281 if (bCurrentFileChanged
)
283 CLog::Log(LOGERROR
, "Track split over multiple files, unsupported.");
287 // find the end of the number section
288 time
= ExtractTimeFromIndex(strLine
);
291 CLog::Log(LOGERROR
, "Mangled Time in INDEX 0x tag in CUE file!");
294 if (totalTracks
> 0 && m_tracks
[totalTracks
- 1].strFile
== strCurrentFile
) // Set the end time of the last track
295 m_tracks
[totalTracks
- 1].iEndTime
= time
;
297 if (totalTracks
>= 0) // start time of the next track
298 m_tracks
[totalTracks
].iStartTime
= time
;
300 else if (StringUtils::StartsWithNoCase(strLine
, "TITLE"))
302 if (totalTracks
== -1) // No tracks yet
303 m_strAlbum
= ExtractInfo(strLine
.substr(5));
305 m_tracks
[totalTracks
].strTitle
= ExtractInfo(strLine
.substr(5));
307 else if (StringUtils::StartsWithNoCase(strLine
, "PERFORMER"))
309 if (totalTracks
== -1) // No tracks yet
310 m_strArtist
= ExtractInfo(strLine
.substr(9));
311 else // New Artist for this track
312 m_tracks
[totalTracks
].strArtist
= ExtractInfo(strLine
.substr(9));
314 else if (StringUtils::StartsWithNoCase(strLine
, "TRACK"))
316 int iTrackNumber
= ExtractNumericInfo(strLine
.substr(5));
321 m_tracks
.push_back(track
);
322 m_tracks
[totalTracks
].strFile
= strCurrentFile
;
323 if (iTrackNumber
> 0)
324 m_tracks
[totalTracks
].iTrackNumber
= iTrackNumber
;
326 m_tracks
[totalTracks
].iTrackNumber
= totalTracks
+ 1;
328 bCurrentFileChanged
= false;
330 else if (StringUtils::StartsWithNoCase(strLine
, "REM DISCNUMBER"))
332 int iDiscNumber
= ExtractNumericInfo(strLine
.substr(14));
334 m_iDiscNumber
= iDiscNumber
;
336 else if (StringUtils::StartsWithNoCase(strLine
, "FILE"))
339 // already a file name? then the time computation will be changed
340 if (!strCurrentFile
.empty())
341 bCurrentFileChanged
= true;
343 strCurrentFile
= ExtractInfo(strLine
.substr(4));
345 // Resolve absolute paths (if needed).
346 if (!strFile
.empty() && !strCurrentFile
.empty())
347 ResolvePath(strCurrentFile
, strFile
);
349 else if (StringUtils::StartsWithNoCase(strLine
, "REM DATE"))
351 int iYear
= ExtractNumericInfo(strLine
.substr(8));
355 else if (StringUtils::StartsWithNoCase(strLine
, "REM GENRE"))
357 m_strGenre
= ExtractInfo(strLine
.substr(9));
359 else if (StringUtils::StartsWithNoCase(strLine
, "REM REPLAYGAIN_ALBUM_GAIN"))
360 m_albumReplayGain
.SetGain(strLine
.substr(26));
361 else if (StringUtils::StartsWithNoCase(strLine
, "REM REPLAYGAIN_ALBUM_PEAK"))
362 m_albumReplayGain
.SetPeak(strLine
.substr(26));
363 else if (StringUtils::StartsWithNoCase(strLine
, "REM REPLAYGAIN_TRACK_GAIN") && totalTracks
>= 0)
364 m_tracks
[totalTracks
].replayGain
.SetGain(strLine
.substr(26));
365 else if (StringUtils::StartsWithNoCase(strLine
, "REM REPLAYGAIN_TRACK_PEAK") && totalTracks
>= 0)
366 m_tracks
[totalTracks
].replayGain
.SetPeak(strLine
.substr(26));
369 // reset track counter to 0, and fill in the last tracks end time
371 if (totalTracks
>= 0)
372 m_tracks
[totalTracks
].iEndTime
= 0;
374 CLog::Log(LOGERROR
, "No INDEX 01 tags in CUE file!");
376 if ( totalTracks
== numberFiles
)
377 m_bOneFilePerTrack
= true;
379 return (totalTracks
>= 0);
382 ////////////////////////////////////////////////////////////////////////////////////
383 // Function: ExtractInfo()
384 // Extracts the information in quotes from the string line, returning it in quote
385 ////////////////////////////////////////////////////////////////////////////////////
386 std::string
CCueDocument::ExtractInfo(const std::string
&line
)
388 size_t left
= line
.find('\"');
389 if (left
!= std::string::npos
)
391 size_t right
= line
.find('\"', left
+ 1);
392 if (right
!= std::string::npos
)
394 std::string text
= line
.substr(left
+ 1, right
- left
- 1);
395 g_charsetConverter
.unknownToUTF8(text
);
399 std::string text
= line
;
400 StringUtils::Trim(text
);
401 g_charsetConverter
.unknownToUTF8(text
);
405 ////////////////////////////////////////////////////////////////////////////////////
406 // Function: ExtractTimeFromIndex()
407 // Extracts the time information from the index string index, returning it as a value in
409 // Assumed format is:
410 // MM:SS:FF where MM is minutes, SS seconds, and FF frames (75 frames in a second)
411 ////////////////////////////////////////////////////////////////////////////////////
412 int CCueDocument::ExtractTimeFromIndex(const std::string
&index
)
414 // Get rid of the index number and any whitespace
415 std::string numberTime
= index
.substr(5);
416 StringUtils::TrimLeft(numberTime
);
417 while (!numberTime
.empty())
419 if (!StringUtils::isasciidigit(numberTime
[0]))
421 numberTime
.erase(0, 1);
423 StringUtils::TrimLeft(numberTime
);
424 // split the resulting string
425 std::vector
<std::string
> time
= StringUtils::Split(numberTime
, ":");
426 if (time
.size() != 3)
429 int mins
= atoi(time
[0].c_str());
430 int secs
= atoi(time
[1].c_str());
431 int frames
= atoi(time
[2].c_str());
433 return CUtil::ConvertSecsToMilliSecs(mins
*60 + secs
) + frames
* 1000 / 75;
436 ////////////////////////////////////////////////////////////////////////////////////
437 // Function: ExtractNumericInfo()
438 // Extracts the numeric info from the string info, returning it as an integer value
439 ////////////////////////////////////////////////////////////////////////////////////
440 int CCueDocument::ExtractNumericInfo(const std::string
&info
)
442 std::string
number(info
);
443 StringUtils::TrimLeft(number
);
444 if (number
.empty() || !StringUtils::isasciidigit(number
[0]))
446 return atoi(number
.c_str());
449 ////////////////////////////////////////////////////////////////////////////////////
450 // Function: ResolvePath()
451 // Determines whether strPath is a relative path or not, and if so, converts it to an
452 // absolute path using the path information in strBase
453 ////////////////////////////////////////////////////////////////////////////////////
454 bool CCueDocument::ResolvePath(std::string
&strPath
, const std::string
&strBase
)
456 std::string strDirectory
= URIUtils::GetDirectory(strBase
);
457 std::string strFilename
= URIUtils::GetFileName(strPath
);
459 strPath
= URIUtils::AddFileToFolder(strDirectory
, strFilename
);
462 if (!CFile::Exists(strPath
))
465 CDirectory::GetDirectory(strDirectory
, items
, "", DIR_FLAG_DEFAULTS
);
466 for (int i
=0;i
<items
.Size();++i
)
468 if (items
[i
]->IsPath(strPath
))
470 strPath
= items
[i
]->GetPath();
474 CLog::Log(LOGERROR
, "Could not find '{}' referenced in cue, case sensitivity issue?", strPath
);
481 bool CCueDocument::LoadTracks(CFileItemList
& scannedItems
, const CFileItem
& item
)
483 const auto& tag
= *item
.GetMusicInfoTag();
486 this->GetSongs(tracks
);
488 bool oneFilePerTrack
= this->IsOneFilePerTrack();
491 for (auto& song
: tracks
)
493 if (song
.strFileName
== item
.GetPath())
497 if (song
.strAlbum
.empty() && !tag
.GetAlbum().empty())
498 song
.strAlbum
= tag
.GetAlbum();
499 //Pass album artist to final MusicInfoTag object via setting song album artist vector.
500 if (song
.GetAlbumArtist().empty() && !tag
.GetAlbumArtist().empty())
501 song
.SetAlbumArtist(tag
.GetAlbumArtist());
502 if (song
.genre
.empty() && !tag
.GetGenre().empty())
503 song
.genre
= tag
.GetGenre();
504 //Pass artist to final MusicInfoTag object via setting song artist description string only.
505 //Artist credits not used during loading from cue sheet.
506 if (song
.strArtistDesc
.empty() && !tag
.GetArtistString().empty())
507 song
.strArtistDesc
= tag
.GetArtistString();
508 if (tag
.GetDiscNumber())
509 song
.iTrack
|= (tag
.GetDiscNumber() << 16); // see CMusicInfoTag::GetDiscNumber()
510 if (!tag
.GetCueSheet().empty())
511 song
.strCueSheet
= tag
.GetCueSheet();
514 song
.strReleaseDate
= tag
.GetReleaseDate();
515 if (song
.embeddedArt
.Empty() && !tag
.GetCoverArtInfo().Empty())
516 song
.embeddedArt
= tag
.GetCoverArtInfo();
519 if (!song
.iDuration
&& tag
.GetDuration() > 0)
520 { // must be the last song
521 song
.iDuration
= CUtil::ConvertMilliSecsToSecsIntRounded(
522 CUtil::ConvertSecsToMilliSecs(tag
.GetDuration()) - song
.iStartOffset
);
524 if (tag
.Loaded() && oneFilePerTrack
&&
525 !(tag
.GetAlbum().empty() || tag
.GetArtist().empty() || tag
.GetTitle().empty()))
527 // If there are multiple files in a cue file, the tags from the files should be preferred if they exist.
528 scannedItems
.Add(std::make_shared
<CFileItem
>(song
, tag
));
532 scannedItems
.Add(std::make_shared
<CFileItem
>(song
));
537 return tracksFound
!= 0;