diff options
Diffstat (limited to 'xbmc/CueDocument.cpp')
-rw-r--r-- | xbmc/CueDocument.cpp | 490 |
1 files changed, 490 insertions, 0 deletions
diff --git a/xbmc/CueDocument.cpp b/xbmc/CueDocument.cpp new file mode 100644 index 0000000..c128874 --- /dev/null +++ b/xbmc/CueDocument.cpp @@ -0,0 +1,490 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +//////////////////////////////////////////////////////////////////////////////////// +// Class: CueDocument +// This class handles the .cue file format. This is produced by programs such as +// EAC and CDRwin when one extracts audio data from a CD as a continuous .WAV +// containing all the audio tracks in one big file. The .cue file contains all the +// track and timing information. An example file is: +// +// PERFORMER "Pink Floyd" +// TITLE "The Dark Side Of The Moon" +// FILE "The Dark Side Of The Moon.mp3" WAVE +// TRACK 01 AUDIO +// TITLE "Speak To Me / Breathe" +// PERFORMER "Pink Floyd" +// INDEX 00 00:00:00 +// INDEX 01 00:00:32 +// TRACK 02 AUDIO +// TITLE "On The Run" +// PERFORMER "Pink Floyd" +// INDEX 00 03:58:72 +// INDEX 01 04:00:72 +// TRACK 03 AUDIO +// TITLE "Time" +// PERFORMER "Pink Floyd" +// INDEX 00 07:31:70 +// INDEX 01 07:33:70 +// +// etc. +// +// The CCueDocument class member functions extract this information, and construct +// the playlist items needed to seek to a track directly. This works best on CBR +// compressed files - VBR files do not seek accurately enough for it to work well. +// +//////////////////////////////////////////////////////////////////////////////////// + +#include "CueDocument.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "Util.h" +#include "filesystem/Directory.h" +#include "filesystem/File.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/CharsetConverter.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +#include <cstdlib> +#include <set> + +using namespace XFILE; + +// Stuff for read CUE data from different sources. +class CueReader +{ +public: + virtual bool ready() const = 0; + virtual bool ReadLine(std::string &line) = 0; + virtual ~CueReader() = default; +private: + std::string m_sourcePath; +}; + +class FileReader + : public CueReader +{ +public: + explicit FileReader(const std::string &strFile) : m_szBuffer{} + { + m_opened = m_file.Open(strFile); + } + bool ReadLine(std::string &line) override + { + // Read the next line. + while (m_file.ReadString(m_szBuffer, 1023)) // Bigger than MAX_PATH_SIZE, for usage with relax! + { + // Remove the white space at the beginning and end of the line. + line = m_szBuffer; + StringUtils::Trim(line); + if (!line.empty()) + return true; + // If we are here, we have an empty line so try the next line + } + return false; + } + bool ready() const override + { + return m_opened; + } + ~FileReader() override + { + if (m_opened) + m_file.Close(); + + } +private: + CFile m_file; + bool m_opened; + char m_szBuffer[1024]; +}; + +class BufferReader + : public CueReader +{ +public: + explicit BufferReader(const std::string &strContent) + : m_data(strContent) + , m_pos(0) + { + } + bool ReadLine(std::string &line) override + { + // Read the next line. + line.clear(); + while (m_pos < m_data.size()) + { + // Remove the white space at the beginning of the line. + char ch = m_data.at(m_pos++); + if (ch == '\r' || ch == '\n') { + StringUtils::Trim(line); + if (!line.empty()) + return true; + } + else + { + line.push_back(ch); + } + } + + StringUtils::Trim(line); + return !line.empty(); + } + bool ready() const override + { + return m_data.size() > 0; + } +private: + std::string m_data; + size_t m_pos; +}; + +CCueDocument::~CCueDocument() = default; + +//////////////////////////////////////////////////////////////////////////////////// +// Function: ParseFile() +// Opens the CUE file for reading, and constructs the track database information +//////////////////////////////////////////////////////////////////////////////////// +bool CCueDocument::ParseFile(const std::string &strFilePath) +{ + FileReader reader(strFilePath); + return Parse(reader, strFilePath); +} + +//////////////////////////////////////////////////////////////////////////////////// +// Function: ParseTag() +// Reads CUE data from string buffer, and constructs the track database information +//////////////////////////////////////////////////////////////////////////////////// +bool CCueDocument::ParseTag(const std::string &strContent) +{ + BufferReader reader(strContent); + return Parse(reader); +} + +////////////////////////////////////////////////////////////////////////////////// +// Function:GetSongs() +// Store track information into songs list. +////////////////////////////////////////////////////////////////////////////////// +void CCueDocument::GetSongs(VECSONGS &songs) +{ + const std::shared_ptr<CAdvancedSettings> advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings(); + + for (const auto& track : m_tracks) + { + CSong aSong; + //Pass artist to MusicInfoTag object by setting artist description string only. + //Artist credits not used during loading from cue sheet. + if (track.strArtist.empty() && !m_strArtist.empty()) + aSong.strArtistDesc = m_strArtist; + else + aSong.strArtistDesc = track.strArtist; + //Pass album artist to MusicInfoTag object by setting album artist vector. + aSong.SetAlbumArtist(StringUtils::Split(m_strArtist, advancedSettings->m_musicItemSeparator)); + aSong.strAlbum = m_strAlbum; + aSong.genre = StringUtils::Split(m_strGenre, advancedSettings->m_musicItemSeparator); + aSong.strReleaseDate = StringUtils::Format("{:04}", m_iYear); + aSong.iTrack = track.iTrackNumber; + if (m_iDiscNumber > 0) + aSong.iTrack |= (m_iDiscNumber << 16); // see CMusicInfoTag::GetDiscNumber() + if (track.strTitle.length() == 0) // No track information for this track! + aSong.strTitle = StringUtils::Format("Track {:2d}", track.iTrackNumber); + else + aSong.strTitle = track.strTitle; + aSong.strFileName = track.strFile; + aSong.iStartOffset = track.iStartTime; + aSong.iEndOffset = track.iEndTime; + if (aSong.iEndOffset) + // Convert offset in frames (75 per second) to duration in whole seconds with rounding + aSong.iDuration = CUtil::ConvertMilliSecsToSecsIntRounded(aSong.iEndOffset - aSong.iStartOffset); + else + aSong.iDuration = 0; + + if (m_albumReplayGain.Valid()) + aSong.replayGain.Set(ReplayGain::ALBUM, m_albumReplayGain); + + if (track.replayGain.Valid()) + aSong.replayGain.Set(ReplayGain::TRACK, track.replayGain); + + songs.push_back(aSong); + } +} + +void CCueDocument::UpdateMediaFile(const std::string& oldMediaFile, const std::string& mediaFile) +{ + for (Tracks::iterator it = m_tracks.begin(); it != m_tracks.end(); ++it) + { + if (it->strFile == oldMediaFile) + it->strFile = mediaFile; + } +} + +void CCueDocument::GetMediaFiles(std::vector<std::string>& mediaFiles) +{ + typedef std::set<std::string> TSet; + TSet uniqueFiles; + for (Tracks::const_iterator it = m_tracks.begin(); it != m_tracks.end(); ++it) + uniqueFiles.insert(it->strFile); + + for (TSet::const_iterator it = uniqueFiles.begin(); it != uniqueFiles.end(); ++it) + mediaFiles.push_back(*it); +} + +std::string CCueDocument::GetMediaTitle() +{ + return m_strAlbum; +} + +bool CCueDocument::IsLoaded() const +{ + return !m_tracks.empty(); +} + +bool CCueDocument::IsOneFilePerTrack() const +{ + return m_bOneFilePerTrack; +} + +// Private Functions start here + +void CCueDocument::Clear() +{ + m_strArtist.clear(); + m_strAlbum.clear(); + m_strGenre.clear(); + m_iYear = 0; + m_iTrack = 0; + m_iDiscNumber = 0; + m_albumReplayGain = ReplayGain::Info(); + m_tracks.clear(); +} +//////////////////////////////////////////////////////////////////////////////////// +// Function: Parse() +// Constructs the track database information from CUE source +//////////////////////////////////////////////////////////////////////////////////// +bool CCueDocument::Parse(CueReader& reader, const std::string& strFile) +{ + Clear(); + if (!reader.ready()) + return false; + + std::string strLine; + std::string strCurrentFile = ""; + bool bCurrentFileChanged = false; + int time; + int totalTracks = -1; + int numberFiles = -1; + + // Run through the .CUE file and extract the tracks... + while (reader.ReadLine(strLine)) + { + if (StringUtils::StartsWithNoCase(strLine, "INDEX 01")) + { + if (bCurrentFileChanged) + { + CLog::Log(LOGERROR, "Track split over multiple files, unsupported."); + return false; + } + + // find the end of the number section + time = ExtractTimeFromIndex(strLine); + if (time == -1) + { // Error! + CLog::Log(LOGERROR, "Mangled Time in INDEX 0x tag in CUE file!"); + return false; + } + if (totalTracks > 0 && m_tracks[totalTracks - 1].strFile == strCurrentFile) // Set the end time of the last track + m_tracks[totalTracks - 1].iEndTime = time; + + if (totalTracks >= 0) // start time of the next track + m_tracks[totalTracks].iStartTime = time; + } + else if (StringUtils::StartsWithNoCase(strLine, "TITLE")) + { + if (totalTracks == -1) // No tracks yet + m_strAlbum = ExtractInfo(strLine.substr(5)); + else + m_tracks[totalTracks].strTitle = ExtractInfo(strLine.substr(5)); + } + else if (StringUtils::StartsWithNoCase(strLine, "PERFORMER")) + { + if (totalTracks == -1) // No tracks yet + m_strArtist = ExtractInfo(strLine.substr(9)); + else // New Artist for this track + m_tracks[totalTracks].strArtist = ExtractInfo(strLine.substr(9)); + } + else if (StringUtils::StartsWithNoCase(strLine, "TRACK")) + { + int iTrackNumber = ExtractNumericInfo(strLine.substr(5)); + + totalTracks++; + + CCueTrack track; + m_tracks.push_back(track); + m_tracks[totalTracks].strFile = strCurrentFile; + if (iTrackNumber > 0) + m_tracks[totalTracks].iTrackNumber = iTrackNumber; + else + m_tracks[totalTracks].iTrackNumber = totalTracks + 1; + + bCurrentFileChanged = false; + } + else if (StringUtils::StartsWithNoCase(strLine, "REM DISCNUMBER")) + { + int iDiscNumber = ExtractNumericInfo(strLine.substr(14)); + if (iDiscNumber > 0) + m_iDiscNumber = iDiscNumber; + } + else if (StringUtils::StartsWithNoCase(strLine, "FILE")) + { + numberFiles++; + // already a file name? then the time computation will be changed + if (!strCurrentFile.empty()) + bCurrentFileChanged = true; + + strCurrentFile = ExtractInfo(strLine.substr(4)); + + // Resolve absolute paths (if needed). + if (!strFile.empty() && !strCurrentFile.empty()) + ResolvePath(strCurrentFile, strFile); + } + else if (StringUtils::StartsWithNoCase(strLine, "REM DATE")) + { + int iYear = ExtractNumericInfo(strLine.substr(8)); + if (iYear > 0) + m_iYear = iYear; + } + else if (StringUtils::StartsWithNoCase(strLine, "REM GENRE")) + { + m_strGenre = ExtractInfo(strLine.substr(9)); + } + else if (StringUtils::StartsWithNoCase(strLine, "REM REPLAYGAIN_ALBUM_GAIN")) + m_albumReplayGain.SetGain(strLine.substr(26)); + else if (StringUtils::StartsWithNoCase(strLine, "REM REPLAYGAIN_ALBUM_PEAK")) + m_albumReplayGain.SetPeak(strLine.substr(26)); + else if (StringUtils::StartsWithNoCase(strLine, "REM REPLAYGAIN_TRACK_GAIN") && totalTracks >= 0) + m_tracks[totalTracks].replayGain.SetGain(strLine.substr(26)); + else if (StringUtils::StartsWithNoCase(strLine, "REM REPLAYGAIN_TRACK_PEAK") && totalTracks >= 0) + m_tracks[totalTracks].replayGain.SetPeak(strLine.substr(26)); + } + + // reset track counter to 0, and fill in the last tracks end time + m_iTrack = 0; + if (totalTracks >= 0) + m_tracks[totalTracks].iEndTime = 0; + else + CLog::Log(LOGERROR, "No INDEX 01 tags in CUE file!"); + + if ( totalTracks == numberFiles ) + m_bOneFilePerTrack = true; + + return (totalTracks >= 0); +} + +//////////////////////////////////////////////////////////////////////////////////// +// Function: ExtractInfo() +// Extracts the information in quotes from the string line, returning it in quote +//////////////////////////////////////////////////////////////////////////////////// +std::string CCueDocument::ExtractInfo(const std::string &line) +{ + size_t left = line.find('\"'); + if (left != std::string::npos) + { + size_t right = line.find('\"', left + 1); + if (right != std::string::npos) + { + std::string text = line.substr(left + 1, right - left - 1); + g_charsetConverter.unknownToUTF8(text); + return text; + } + } + std::string text = line; + StringUtils::Trim(text); + g_charsetConverter.unknownToUTF8(text); + return text; +} + +//////////////////////////////////////////////////////////////////////////////////// +// Function: ExtractTimeFromIndex() +// Extracts the time information from the index string index, returning it as a value in +// milliseconds. +// Assumed format is: +// MM:SS:FF where MM is minutes, SS seconds, and FF frames (75 frames in a second) +//////////////////////////////////////////////////////////////////////////////////// +int CCueDocument::ExtractTimeFromIndex(const std::string &index) +{ + // Get rid of the index number and any whitespace + std::string numberTime = index.substr(5); + StringUtils::TrimLeft(numberTime); + while (!numberTime.empty()) + { + if (!StringUtils::isasciidigit(numberTime[0])) + break; + numberTime.erase(0, 1); + } + StringUtils::TrimLeft(numberTime); + // split the resulting string + std::vector<std::string> time = StringUtils::Split(numberTime, ":"); + if (time.size() != 3) + return -1; + + int mins = atoi(time[0].c_str()); + int secs = atoi(time[1].c_str()); + int frames = atoi(time[2].c_str()); + + return CUtil::ConvertSecsToMilliSecs(mins*60 + secs) + frames * 1000 / 75; +} + +//////////////////////////////////////////////////////////////////////////////////// +// Function: ExtractNumericInfo() +// Extracts the numeric info from the string info, returning it as an integer value +//////////////////////////////////////////////////////////////////////////////////// +int CCueDocument::ExtractNumericInfo(const std::string &info) +{ + std::string number(info); + StringUtils::TrimLeft(number); + if (number.empty() || !StringUtils::isasciidigit(number[0])) + return -1; + return atoi(number.c_str()); +} + +//////////////////////////////////////////////////////////////////////////////////// +// Function: ResolvePath() +// Determines whether strPath is a relative path or not, and if so, converts it to an +// absolute path using the path information in strBase +//////////////////////////////////////////////////////////////////////////////////// +bool CCueDocument::ResolvePath(std::string &strPath, const std::string &strBase) +{ + std::string strDirectory = URIUtils::GetDirectory(strBase); + std::string strFilename = URIUtils::GetFileName(strPath); + + strPath = URIUtils::AddFileToFolder(strDirectory, strFilename); + + // i *hate* windows + if (!CFile::Exists(strPath)) + { + CFileItemList items; + CDirectory::GetDirectory(strDirectory, items, "", DIR_FLAG_DEFAULTS); + for (int i=0;i<items.Size();++i) + { + if (items[i]->IsPath(strPath)) + { + strPath = items[i]->GetPath(); + return true; + } + } + CLog::Log(LOGERROR, "Could not find '{}' referenced in cue, case sensitivity issue?", strPath); + return false; + } + + return true; +} + |