[videodb] remove unused seasons table from episode_view
[xbmc.git] / xbmc / CueDocument.cpp
blob3a5c0fae074f3eaeff3c5a18e102ed5b35260c8a
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 "FileItemList.h"
47 #include "ServiceBroker.h"
48 #include "Util.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"
59 #include <cstdlib>
60 #include <set>
62 using namespace XFILE;
64 // Stuff for read CUE data from different sources.
65 class CueReader
67 public:
68 virtual bool ready() const = 0;
69 virtual bool ReadLine(std::string &line) = 0;
70 virtual ~CueReader() = default;
71 private:
72 std::string m_sourcePath;
75 class FileReader
76 : public CueReader
78 public:
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.
86 line = m_szBuffer;
87 StringUtils::Trim(line);
88 if (!line.empty())
89 return true;
90 // If we are here, we have an empty line so try the next line
92 return false;
94 bool ready() const override
96 return m_opened;
98 ~FileReader() override
100 if (m_opened)
101 m_file.Close();
104 private:
105 CFile m_file;
106 bool m_opened;
107 char m_szBuffer[1024]{};
110 class BufferReader
111 : public CueReader
113 public:
114 explicit BufferReader(const std::string& strContent) : m_data(strContent) {}
115 bool ReadLine(std::string &line) override
117 // Read the next line.
118 line.clear();
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);
125 if (!line.empty())
126 return true;
128 else
130 line.push_back(ch);
134 StringUtils::Trim(line);
135 return !line.empty();
137 bool ready() const override
139 return m_data.size() > 0;
141 private:
142 std::string m_data;
143 size_t m_pos = 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)
178 CSong aSong;
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;
183 else
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);
195 else
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);
203 else
204 aSong.iDuration = 0;
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;
228 TSet uniqueFiles;
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()
250 m_strArtist.clear();
251 m_strAlbum.clear();
252 m_strGenre.clear();
253 m_iYear = 0;
254 m_iTrack = 0;
255 m_iDiscNumber = 0;
256 m_albumReplayGain = ReplayGain::Info();
257 m_tracks.clear();
259 ////////////////////////////////////////////////////////////////////////////////////
260 // Function: Parse()
261 // Constructs the track database information from CUE source
262 ////////////////////////////////////////////////////////////////////////////////////
263 bool CCueDocument::Parse(CueReader& reader, const std::string& strFile)
265 Clear();
266 if (!reader.ready())
267 return false;
269 std::string strLine;
270 std::string strCurrentFile = "";
271 bool bCurrentFileChanged = false;
272 int time;
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.");
284 return false;
287 // find the end of the number section
288 time = ExtractTimeFromIndex(strLine);
289 if (time == -1)
290 { // Error!
291 CLog::Log(LOGERROR, "Mangled Time in INDEX 0x tag in CUE file!");
292 return false;
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));
304 else
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));
318 totalTracks++;
320 CCueTrack track;
321 m_tracks.push_back(track);
322 m_tracks[totalTracks].strFile = strCurrentFile;
323 if (iTrackNumber > 0)
324 m_tracks[totalTracks].iTrackNumber = iTrackNumber;
325 else
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));
333 if (iDiscNumber > 0)
334 m_iDiscNumber = iDiscNumber;
336 else if (StringUtils::StartsWithNoCase(strLine, "FILE"))
338 numberFiles++;
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));
352 if (iYear > 0)
353 m_iYear = iYear;
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
370 m_iTrack = 0;
371 if (totalTracks >= 0)
372 m_tracks[totalTracks].iEndTime = 0;
373 else
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);
396 return text;
399 std::string text = line;
400 StringUtils::Trim(text);
401 g_charsetConverter.unknownToUTF8(text);
402 return text;
405 ////////////////////////////////////////////////////////////////////////////////////
406 // Function: ExtractTimeFromIndex()
407 // Extracts the time information from the index string index, returning it as a value in
408 // milliseconds.
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]))
420 break;
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)
427 return -1;
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]))
445 return -1;
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);
461 // i *hate* windows
462 if (!CFile::Exists(strPath))
464 CFileItemList items;
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();
471 return true;
474 CLog::Log(LOGERROR, "Could not find '{}' referenced in cue, case sensitivity issue?", strPath);
475 return false;
478 return true;
481 bool CCueDocument::LoadTracks(CFileItemList& scannedItems, const CFileItem& item)
483 const auto& tag = *item.GetMusicInfoTag();
485 VECSONGS tracks;
486 this->GetSongs(tracks);
488 bool oneFilePerTrack = this->IsOneFilePerTrack();
490 int tracksFound = 0;
491 for (auto& song : tracks)
493 if (song.strFileName == item.GetPath())
495 if (tag.Loaded())
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();
513 if (tag.GetYear())
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));
530 else
532 scannedItems.Add(std::make_shared<CFileItem>(song));
534 ++tracksFound;
537 return tracksFound != 0;