Merge pull request #22634 from CastagnaIT/webvtt_overllaped_segment
[xbmc.git] / xbmc / CueDocument.cpp
blobc128874bd112c9d58a21ad45ec18dc6ddf33a718
1 /*
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.
7 */
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:
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
35 // etc.
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"
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"
57 #include <cstdlib>
58 #include <set>
60 using namespace XFILE;
62 // Stuff for read CUE data from different sources.
63 class CueReader
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;
73 class FileReader
74 : public CueReader
76 public:
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.
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
93 return false;
95 bool ready() const override
97 return m_opened;
99 ~FileReader() override
101 if (m_opened)
102 m_file.Close();
105 private:
106 CFile m_file;
107 bool m_opened;
108 char m_szBuffer[1024];
111 class BufferReader
112 : public CueReader
114 public:
115 explicit BufferReader(const std::string &strContent)
116 : m_data(strContent)
117 , m_pos(0)
120 bool ReadLine(std::string &line) override
122 // Read the next line.
123 line.clear();
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);
130 if (!line.empty())
131 return true;
133 else
135 line.push_back(ch);
139 StringUtils::Trim(line);
140 return !line.empty();
142 bool ready() const override
144 return m_data.size() > 0;
146 private:
147 std::string m_data;
148 size_t m_pos;
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)
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("{: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);
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;
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;
233 TSet uniqueFiles;
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()
243 return m_strAlbum;
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()
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();
269 ////////////////////////////////////////////////////////////////////////////////////
270 // Function: Parse()
271 // Constructs the track database information from CUE source
272 ////////////////////////////////////////////////////////////////////////////////////
273 bool CCueDocument::Parse(CueReader& reader, const std::string& strFile)
275 Clear();
276 if (!reader.ready())
277 return false;
279 std::string strLine;
280 std::string strCurrentFile = "";
281 bool bCurrentFileChanged = false;
282 int time;
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.");
294 return false;
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;
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));
314 else
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));
328 totalTracks++;
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;
338 bCurrentFileChanged = false;
340 else if (StringUtils::StartsWithNoCase(strLine, "REM DISCNUMBER"))
342 int iDiscNumber = ExtractNumericInfo(strLine.substr(14));
343 if (iDiscNumber > 0)
344 m_iDiscNumber = iDiscNumber;
346 else if (StringUtils::StartsWithNoCase(strLine, "FILE"))
348 numberFiles++;
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));
362 if (iYear > 0)
363 m_iYear = iYear;
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
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!");
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);
406 return text;
409 std::string text = line;
410 StringUtils::Trim(text);
411 g_charsetConverter.unknownToUTF8(text);
412 return text;
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 ////////////////////////////////////////////////////////////////////////////////////
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]))
430 break;
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)
437 return -1;
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]))
455 return -1;
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);
471 // i *hate* windows
472 if (!CFile::Exists(strPath))
474 CFileItemList items;
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();
481 return true;
484 CLog::Log(LOGERROR, "Could not find '{}' referenced in cue, case sensitivity issue?", strPath);
485 return false;
488 return true;