[video] fix selection after changing video or extra art
[xbmc.git] / xbmc / CueDocument.cpp
blob674a94aa51fbb906b5078ae27aa9d2c3a8ea6982
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) : m_data(strContent) {}
116 bool ReadLine(std::string &line) override
118 // Read the next line.
119 line.clear();
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);
126 if (!line.empty())
127 return true;
129 else
131 line.push_back(ch);
135 StringUtils::Trim(line);
136 return !line.empty();
138 bool ready() const override
140 return m_data.size() > 0;
142 private:
143 std::string m_data;
144 size_t m_pos = 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)
179 CSong aSong;
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;
184 else
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);
196 else
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);
204 else
205 aSong.iDuration = 0;
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;
229 TSet uniqueFiles;
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()
239 return m_strAlbum;
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()
256 m_strArtist.clear();
257 m_strAlbum.clear();
258 m_strGenre.clear();
259 m_iYear = 0;
260 m_iTrack = 0;
261 m_iDiscNumber = 0;
262 m_albumReplayGain = ReplayGain::Info();
263 m_tracks.clear();
265 ////////////////////////////////////////////////////////////////////////////////////
266 // Function: Parse()
267 // Constructs the track database information from CUE source
268 ////////////////////////////////////////////////////////////////////////////////////
269 bool CCueDocument::Parse(CueReader& reader, const std::string& strFile)
271 Clear();
272 if (!reader.ready())
273 return false;
275 std::string strLine;
276 std::string strCurrentFile = "";
277 bool bCurrentFileChanged = false;
278 int time;
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.");
290 return false;
293 // find the end of the number section
294 time = ExtractTimeFromIndex(strLine);
295 if (time == -1)
296 { // Error!
297 CLog::Log(LOGERROR, "Mangled Time in INDEX 0x tag in CUE file!");
298 return false;
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));
310 else
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));
324 totalTracks++;
326 CCueTrack track;
327 m_tracks.push_back(track);
328 m_tracks[totalTracks].strFile = strCurrentFile;
329 if (iTrackNumber > 0)
330 m_tracks[totalTracks].iTrackNumber = iTrackNumber;
331 else
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));
339 if (iDiscNumber > 0)
340 m_iDiscNumber = iDiscNumber;
342 else if (StringUtils::StartsWithNoCase(strLine, "FILE"))
344 numberFiles++;
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));
358 if (iYear > 0)
359 m_iYear = iYear;
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
376 m_iTrack = 0;
377 if (totalTracks >= 0)
378 m_tracks[totalTracks].iEndTime = 0;
379 else
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);
402 return text;
405 std::string text = line;
406 StringUtils::Trim(text);
407 g_charsetConverter.unknownToUTF8(text);
408 return text;
411 ////////////////////////////////////////////////////////////////////////////////////
412 // Function: ExtractTimeFromIndex()
413 // Extracts the time information from the index string index, returning it as a value in
414 // milliseconds.
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]))
426 break;
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)
433 return -1;
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]))
451 return -1;
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);
467 // i *hate* windows
468 if (!CFile::Exists(strPath))
470 CFileItemList items;
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();
477 return true;
480 CLog::Log(LOGERROR, "Could not find '{}' referenced in cue, case sensitivity issue?", strPath);
481 return false;
484 return true;