From ad9452f62e59f30d52e1b0e6180c5047d382f54d Mon Sep 17 00:00:00 2001 From: psyton Date: Sat, 31 Jan 2015 05:58:36 +0600 Subject: [PATCH] Support for reading embedded CUE from tags. --- xbmc/CueDocument.cpp | 284 +++++++++++++------- xbmc/CueDocument.h | 14 +- xbmc/FileItem.cpp | 157 ++++++----- xbmc/FileItem.h | 10 + xbmc/music/MusicDatabase.cpp | 118 +++++++- xbmc/music/MusicDatabase.h | 8 + xbmc/music/MusicInfoLoader.cpp | 1 + xbmc/music/Song.cpp | 1 + xbmc/music/Song.h | 1 + xbmc/music/infoscanner/MusicInfoScanner.cpp | 16 +- xbmc/music/tags/MusicInfoTag.cpp | 15 ++ xbmc/music/tags/MusicInfoTag.h | 3 + xbmc/music/tags/TagLoaderTagLib.cpp | 2 + xbmc/music/windows/GUIWindowMusicBase.cpp | 34 +++ 14 files changed, 496 insertions(+), 168 deletions(-) diff --git a/xbmc/CueDocument.cpp b/xbmc/CueDocument.cpp index c7afad718c92f..58a225ed301be 100644 --- a/xbmc/CueDocument.cpp +++ b/xbmc/CueDocument.cpp @@ -69,6 +69,94 @@ using namespace std; 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() {} +private: + std::string m_sourcePath; +}; + +class FileReader + : public CueReader +{ +public: + FileReader(const std::string &strFile) + { + m_opened = m_file.Open(strFile); + } + virtual bool ReadLine(std::string &line) + { + // 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; + } + virtual bool ready() const + { + return m_opened; + } + virtual ~FileReader() + { + if (m_opened) + m_file.Close(); + + } +private: + CFile m_file; + bool m_opened; + char m_szBuffer[1024]; +}; + +class BufferReader + : public CueReader +{ +public: + BufferReader(const std::string &strContent) + : m_data(strContent) + , m_pos(0) + { + } + virtual bool ReadLine(std::string &line) + { + // Read the next line. + line.clear(); + bool stop = false; + 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); + } + } + return false; + } + virtual bool ready() const + { + return m_data.size() > 0; + } +private: + std::string m_data; + size_t m_pos; +}; + CCueDocument::CCueDocument(void) { m_iYear = 0; @@ -82,13 +170,97 @@ CCueDocument::CCueDocument(void) CCueDocument::~CCueDocument(void) {} +//////////////////////////////////////////////////////////////////////////////////// +// 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() +// Returns the track information from the next item in the cuelist +////////////////////////////////////////////////////////////////////////////////// +void CCueDocument::GetSongs(VECSONGS &songs) +{ + for (int i = 0; i < m_iTotalTracks; i++) + { + CSong song; + if ((m_Track[i].strArtist.length() == 0) && (m_strArtist.length() > 0)) + song.artist = StringUtils::Split(m_strArtist, g_advancedSettings.m_musicItemSeparator); + else + song.artist = StringUtils::Split(m_Track[i].strArtist, g_advancedSettings.m_musicItemSeparator); + song.albumArtist = StringUtils::Split(m_strArtist, g_advancedSettings.m_musicItemSeparator); + song.strAlbum = m_strAlbum; + song.genre = StringUtils::Split(m_strGenre, g_advancedSettings.m_musicItemSeparator); + song.iYear = m_iYear; + song.iTrack = m_Track[i].iTrackNumber; + if ( m_iDiscNumber > 0 ) + song.iTrack |= (m_iDiscNumber << 16); // see CMusicInfoTag::GetDiscNumber() + if (m_Track[i].strTitle.length() == 0) // No track information for this track! + song.strTitle = StringUtils::Format("Track %2d", i + 1); + else + song.strTitle = m_Track[i].strTitle; + song.strFileName = m_Track[i].strFile; + song.iStartOffset = m_Track[i].iStartTime; + song.iEndOffset = m_Track[i].iEndTime; + if (song.iEndOffset) + song.iDuration = (song.iEndOffset - song.iStartOffset + 37) / 75; + else + song.iDuration = 0; + // TODO: replayGain goes here + songs.push_back(song); + } +} + +void CCueDocument::UpdateMediaFile(const std::string& oldMediaFile, const std::string& mediaFile) +{ + for (int i = 0; i < m_iTotalTracks; i++) + { + if (m_Track[i].strFile == oldMediaFile) + { + m_Track[i].strFile = mediaFile; + } + } +} + +void CCueDocument::GetMediaFiles(vector& mediaFiles) +{ + set uniqueFiles; + for (int i = 0; i < m_iTotalTracks; i++) + uniqueFiles.insert(m_Track[i].strFile); + + for (set::iterator it = uniqueFiles.begin(); it != uniqueFiles.end(); it++) + mediaFiles.push_back(*it); +} + +std::string CCueDocument::GetMediaTitle() +{ + return m_strAlbum; +} + +// Private Functions start here + //////////////////////////////////////////////////////////////////////////////////// // Function: Parse() -// Opens the .cue file for reading, and constructs the track database information +// Constructs the track database information from CUE source //////////////////////////////////////////////////////////////////////////////////// -bool CCueDocument::Parse(const std::string &strFile) +bool CCueDocument::Parse(CueReader& reader, const std::string& strFile) { - if (!m_file.Open(strFile)) + if (!reader.ready()) return false; std::string strLine; @@ -100,13 +272,13 @@ bool CCueDocument::Parse(const std::string &strFile) // Run through the .CUE file and extract the tracks... while (true) { - if (!ReadNextLine(strLine)) + if (!reader.ReadLine(strLine)) break; - if (StringUtils::StartsWithNoCase(strLine,"INDEX 01")) + if (StringUtils::StartsWithNoCase(strLine, "INDEX 01")) { if (bCurrentFileChanged) { - CLog::Log(LOGERROR, "Track split over multiple files, unsupported ('%s')", strFile.c_str()); + CLog::Log(LOGERROR, "Track split over multiple files, unsupported."); return false; } @@ -123,21 +295,21 @@ bool CCueDocument::Parse(const std::string &strFile) if (m_iTotalTracks >= 0) m_Track[m_iTotalTracks].iStartTime = time; // start time of the next track } - else if (StringUtils::StartsWithNoCase(strLine,"TITLE")) + else if (StringUtils::StartsWithNoCase(strLine, "TITLE")) { if (m_iTotalTracks == -1) // No tracks yet m_strAlbum = ExtractInfo(strLine.substr(5)); else m_Track[m_iTotalTracks].strTitle = ExtractInfo(strLine.substr(5)); } - else if (StringUtils::StartsWithNoCase(strLine,"PERFORMER")) + else if (StringUtils::StartsWithNoCase(strLine, "PERFORMER")) { if (m_iTotalTracks == -1) // No tracks yet m_strArtist = ExtractInfo(strLine.substr(9)); else // New Artist for this track m_Track[m_iTotalTracks].strArtist = ExtractInfo(strLine.substr(9)); } - else if (StringUtils::StartsWithNoCase(strLine,"TRACK")) + else if (StringUtils::StartsWithNoCase(strLine, "TRACK")) { int iTrackNumber = ExtractNumericInfo(strLine.substr(5)); @@ -154,41 +326,41 @@ bool CCueDocument::Parse(const std::string &strFile) bCurrentFileChanged = false; } - else if (StringUtils::StartsWithNoCase(strLine,"REM DISCNUMBER")) + 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")) + else if (StringUtils::StartsWithNoCase(strLine, "FILE")) { // already a file name? then the time computation will be changed - if(strCurrentFile.size() > 0) + if (!strCurrentFile.empty()) bCurrentFileChanged = true; strCurrentFile = ExtractInfo(strLine.substr(4)); // Resolve absolute paths (if needed). - if (strCurrentFile.length() > 0) + if (!strFile.empty() && !strCurrentFile.empty()) ResolvePath(strCurrentFile, strFile); } - else if (StringUtils::StartsWithNoCase(strLine,"REM DATE")) + 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")) + else if (StringUtils::StartsWithNoCase(strLine, "REM GENRE")) { m_strGenre = ExtractInfo(strLine.substr(9)); } - else if (StringUtils::StartsWithNoCase(strLine,"REM REPLAYGAIN_ALBUM_GAIN")) + else if (StringUtils::StartsWithNoCase(strLine, "REM REPLAYGAIN_ALBUM_GAIN")) m_replayGainAlbumGain = (float)atof(strLine.substr(26).c_str()); - else if (StringUtils::StartsWithNoCase(strLine,"REM REPLAYGAIN_ALBUM_PEAK")) + else if (StringUtils::StartsWithNoCase(strLine, "REM REPLAYGAIN_ALBUM_PEAK")) m_replayGainAlbumPeak = (float)atof(strLine.substr(26).c_str()); - else if (StringUtils::StartsWithNoCase(strLine,"REM REPLAYGAIN_TRACK_GAIN") && m_iTotalTracks >= 0) + else if (StringUtils::StartsWithNoCase(strLine, "REM REPLAYGAIN_TRACK_GAIN") && m_iTotalTracks >= 0) m_Track[m_iTotalTracks].replayGainTrackGain = (float)atof(strLine.substr(26).c_str()); - else if (StringUtils::StartsWithNoCase(strLine,"REM REPLAYGAIN_TRACK_PEAK") && m_iTotalTracks >= 0) + else if (StringUtils::StartsWithNoCase(strLine, "REM REPLAYGAIN_TRACK_PEAK") && m_iTotalTracks >= 0) m_Track[m_iTotalTracks].replayGainTrackPeak = (float)atof(strLine.substr(26).c_str()); } @@ -198,7 +370,6 @@ bool CCueDocument::Parse(const std::string &strFile) m_Track[m_iTotalTracks].iEndTime = 0; else CLog::Log(LOGERROR, "No INDEX 01 tags in CUE file!"); - m_file.Close(); if (m_iTotalTracks >= 0) { m_iTotalTracks++; @@ -206,79 +377,6 @@ bool CCueDocument::Parse(const std::string &strFile) return (m_iTotalTracks > 0); } -////////////////////////////////////////////////////////////////////////////////// -// Function:GetNextItem() -// Returns the track information from the next item in the cuelist -////////////////////////////////////////////////////////////////////////////////// -void CCueDocument::GetSongs(VECSONGS &songs) -{ - for (int i = 0; i < m_iTotalTracks; i++) - { - CSong song; - if ((m_Track[i].strArtist.length() == 0) && (m_strArtist.length() > 0)) - song.artist = StringUtils::Split(m_strArtist, g_advancedSettings.m_musicItemSeparator); - else - song.artist = StringUtils::Split(m_Track[i].strArtist, g_advancedSettings.m_musicItemSeparator); - song.albumArtist = StringUtils::Split(m_strArtist, g_advancedSettings.m_musicItemSeparator); - song.strAlbum = m_strAlbum; - song.genre = StringUtils::Split(m_strGenre, g_advancedSettings.m_musicItemSeparator); - song.iYear = m_iYear; - song.iTrack = m_Track[i].iTrackNumber; - if ( m_iDiscNumber > 0 ) - song.iTrack |= (m_iDiscNumber << 16); // see CMusicInfoTag::GetDiscNumber() - if (m_Track[i].strTitle.length() == 0) // No track information for this track! - song.strTitle = StringUtils::Format("Track %2d", i + 1); - else - song.strTitle = m_Track[i].strTitle; - song.strFileName = m_Track[i].strFile; - song.iStartOffset = m_Track[i].iStartTime; - song.iEndOffset = m_Track[i].iEndTime; - if (song.iEndOffset) - song.iDuration = (song.iEndOffset - song.iStartOffset + 37) / 75; - else - song.iDuration = 0; - // TODO: replayGain goes here - songs.push_back(song); - } -} - -void CCueDocument::GetMediaFiles(vector& mediaFiles) -{ - set uniqueFiles; - for (int i = 0; i < m_iTotalTracks; i++) - uniqueFiles.insert(m_Track[i].strFile); - - for (set::iterator it = uniqueFiles.begin(); it != uniqueFiles.end(); ++it) - mediaFiles.push_back(*it); -} - -std::string CCueDocument::GetMediaTitle() -{ - return m_strAlbum; -} - -// Private Functions start here - -//////////////////////////////////////////////////////////////////////////////////// -// Function: ReadNextLine() -// Returns the next non-blank line of the textfile, stripping any whitespace from -// the left. -//////////////////////////////////////////////////////////////////////////////////// -bool CCueDocument::ReadNextLine(std::string &szLine) -{ - // 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. - szLine = m_szBuffer; - StringUtils::Trim(szLine); - if (!szLine.empty()) - return true; - // If we are here, we have an empty line so try the next line - } - return false; -} - //////////////////////////////////////////////////////////////////////////////////// // Function: ExtractInfo() // Extracts the information in quotes from the string line, returning it in quote diff --git a/xbmc/CueDocument.h b/xbmc/CueDocument.h index 0040f6f59bff5..92654e6257ba1 100644 --- a/xbmc/CueDocument.h +++ b/xbmc/CueDocument.h @@ -25,6 +25,8 @@ #define MAX_PATH_SIZE 1024 +class CueReader; + class CCueDocument { class CCueTrack @@ -47,22 +49,19 @@ class CCueDocument float replayGainTrackGain; float replayGainTrackPeak; }; - public: CCueDocument(void); ~CCueDocument(void); // USED - bool Parse(const std::string &strFile); + bool ParseFile(const std::string &strFilePath); + bool ParseTag(const std::string &strContent); void GetSongs(VECSONGS &songs); std::string GetMediaPath(); std::string GetMediaTitle(); void GetMediaFiles(std::vector& mediaFiles); - + void UpdateMediaFile(const std::string& oldMediaFile, const std::string& mediaFile); private: - - // USED for file access - XFILE::CFile m_file; - char m_szBuffer[1024]; + bool Parse(CueReader& reader, const std::string& strFile = std::string()); // Member variables std::string m_strArtist; // album artist @@ -78,7 +77,6 @@ class CCueDocument // cuetrack array std::vector m_Track; - bool ReadNextLine(std::string &strLine); std::string ExtractInfo(const std::string &line); int ExtractTimeFromIndex(const std::string &index); int ExtractNumericInfo(const std::string &info); diff --git a/xbmc/FileItem.cpp b/xbmc/FileItem.cpp index 755cf467f8630..b45131d736e40 100644 --- a/xbmc/FileItem.cpp +++ b/xbmc/FileItem.cpp @@ -51,7 +51,6 @@ #include "pictures/PictureInfoTag.h" #include "music/Artist.h" #include "music/Album.h" -#include "music/Song.h" #include "URL.h" #include "settings/AdvancedSettings.h" #include "settings/Settings.h" @@ -1530,6 +1529,89 @@ bool CFileItem::IsPath(const std::string& path) const return URIUtils::PathEquals(m_strPath, path); } +void CFileItem::SetCueDocument(const CCueDocumentPtr& cuePtr) +{ + m_cueDocument = cuePtr; +} + +void CFileItem::LoadEmbeddedCue() +{ + CMusicInfoTag& tag = *GetMusicInfoTag(); + if (!tag.Loaded()) + return; + + const std::string embeddedCue = tag.GetCueSheet(); + if (!embeddedCue.empty()) + { + CCueDocumentPtr cuesheet(new CCueDocument); + if (cuesheet->ParseTag(embeddedCue)) + { + std::vector MediaFileVec; + cuesheet->GetMediaFiles(MediaFileVec); + for (std::vector::iterator itMedia = MediaFileVec.begin(); itMedia != MediaFileVec.end(); itMedia++) + cuesheet->UpdateMediaFile(*itMedia, GetPath()); + SetCueDocument(cuesheet); + } + } +} + +bool CFileItem::HasCueDocument() const +{ + return (nullptr != m_cueDocument.get()); +} + +bool CFileItem::LoadTracksFromCueDocument(CFileItemList& scannedItems) +{ + if (!m_cueDocument) + return false; + + CMusicInfoTag& tag = *GetMusicInfoTag(); + + VECSONGS tracks; + m_cueDocument->GetSongs(tracks); + m_cueDocument.reset(); + + int tracksFound = 0; + for (VECSONGS::iterator it = tracks.begin(); it != tracks.end(); ++it) + { + CSong& song = *it; + if (song.strFileName == GetPath()) + { + if (tag.Loaded()) + { + if (song.strAlbum.empty() && !tag.GetAlbum().empty()) + song.strAlbum = tag.GetAlbum(); + if (song.albumArtist.empty() && !tag.GetAlbumArtist().empty()) + song.albumArtist = tag.GetAlbumArtist(); + if (song.genre.empty() && !tag.GetGenre().empty()) + song.genre = tag.GetGenre(); + if (song.artist.empty() && !tag.GetArtist().empty()) + song.artist = tag.GetArtist(); + if (tag.GetDiscNumber()) + song.iTrack |= (tag.GetDiscNumber() << 16); // see CMusicInfoTag::GetDiscNumber() + if (!tag.GetCueSheet().empty()) + song.strCueSheet = tag.GetCueSheet(); + + SYSTEMTIME dateTime; + tag.GetReleaseDate(dateTime); + if (dateTime.wYear) + song.iYear = dateTime.wYear; + if (song.embeddedArt.empty() && !tag.GetCoverArtInfo().empty()) + song.embeddedArt = tag.GetCoverArtInfo(); + } + + if (!song.iDuration && tag.GetDuration() > 0) + { // must be the last song + song.iDuration = (tag.GetDuration() * 75 - song.iStartOffset + 37) / 75; + } + scannedItems.Add(CFileItemPtr(new CFileItem(song))); + ++tracksFound; + } + } + return 0 != tracksFound; +} + + ///////////////////////////////////////////////////////////////////////////////// ///// ///// CFileItemList @@ -2106,7 +2188,6 @@ void CFileItemList::FilterCueItems() { CSingleLock lock(m_lock); // Handle .CUE sheet files... - VECSONGS itemstoadd; vector itemstodelete; for (int i = 0; i < (int)m_items.size(); i++) { @@ -2115,14 +2196,11 @@ void CFileItemList::FilterCueItems() { // see if it's a .CUE sheet if (pItem->IsCUESheet()) { - CCueDocument cuesheet; - if (cuesheet.Parse(pItem->GetPath())) + CCueDocumentPtr cuesheet(new CCueDocument); + if (cuesheet->ParseFile(pItem->GetPath())) { - VECSONGS newitems; - cuesheet.GetSongs(newitems); - std::vector MediaFileVec; - cuesheet.GetMediaFiles(MediaFileVec); + cuesheet->GetMediaFiles(MediaFileVec); // queue the cue sheet and the underlying media file for deletion for(std::vector::iterator itMedia = MediaFileVec.begin(); itMedia != MediaFileVec.end(); itMedia++) @@ -2131,7 +2209,6 @@ void CFileItemList::FilterCueItems() std::string fileFromCue = strMediaFile; // save the file from the cue we're matching against, // as we're going to search for others here... bool bFoundMediaFile = CFile::Exists(strMediaFile); - // queue the cue sheet and the underlying media file for deletion if (!bFoundMediaFile) { // try file in same dir, not matching case... @@ -2167,61 +2244,22 @@ void CFileItemList::FilterCueItems() } if (bFoundMediaFile) { - itemstodelete.push_back(pItem->GetPath()); - itemstodelete.push_back(strMediaFile); - // get the additional stuff (year, genre etc.) from the underlying media files tag. - CMusicInfoTag tag; - unique_ptr pLoader (CMusicInfoTagLoaderFactory::CreateLoader(strMediaFile)); - if (NULL != pLoader.get()) - { - // get id3tag - pLoader->Load(strMediaFile, tag); - } - // fill in any missing entries from underlying media file - for (int j = 0; j < (int)newitems.size(); j++) + cuesheet->UpdateMediaFile(fileFromCue, strMediaFile); + // apply CUE for later processing + for (int j = 0; j < (int)m_items.size(); j++) { - CSong song = newitems[j]; - // only for songs that actually match the current media file - if (song.strFileName == fileFromCue) - { - // we might have a new media file from the above matching code - song.strFileName = strMediaFile; - if (tag.Loaded()) - { - if (song.strAlbum.empty() && !tag.GetAlbum().empty()) song.strAlbum = tag.GetAlbum(); - if (song.albumArtist.empty() && !tag.GetAlbumArtist().empty()) song.albumArtist = tag.GetAlbumArtist(); - if (song.genre.empty() && !tag.GetGenre().empty()) song.genre = tag.GetGenre(); - if (song.artist.empty() && !tag.GetArtist().empty()) song.artist = tag.GetArtist(); - if (tag.GetDiscNumber()) song.iTrack |= (tag.GetDiscNumber() << 16); // see CMusicInfoTag::GetDiscNumber() - SYSTEMTIME dateTime; - tag.GetReleaseDate(dateTime); - if (dateTime.wYear) song.iYear = dateTime.wYear; - if (song.embeddedArt.empty() && !tag.GetCoverArtInfo().empty()) - song.embeddedArt = tag.GetCoverArtInfo(); - } - if (!song.iDuration && tag.GetDuration() > 0) - { // must be the last song - song.iDuration = (tag.GetDuration() * 75 - song.iStartOffset + 37) / 75; - } - // add this item to the list - itemstoadd.push_back(song); - } + CFileItemPtr pItem = m_items[j]; + if (stricmp(pItem->GetPath().c_str(), strMediaFile.c_str()) == 0) + pItem->SetCueDocument(cuesheet); } } - else - { // remove the .cue sheet from the directory - itemstodelete.push_back(pItem->GetPath()); - } } } - else - { // remove the .cue sheet from the directory (can't parse it - no point listing it) - itemstodelete.push_back(pItem->GetPath()); - } + itemstodelete.push_back(pItem->GetPath()); } } } - // now delete the .CUE files and underlying media files. + // now delete the .CUE files. for (int i = 0; i < (int)itemstodelete.size(); i++) { for (int j = 0; j < (int)m_items.size(); j++) @@ -2234,13 +2272,6 @@ void CFileItemList::FilterCueItems() } } } - // and add the files from the .CUE sheet - for (int i = 0; i < (int)itemstoadd.size(); i++) - { - // now create the file item, and add to the item list. - CFileItemPtr pItem(new CFileItem(itemstoadd[i])); - m_items.push_back(pItem); - } } // Remove the extensions from the filenames diff --git a/xbmc/FileItem.h b/xbmc/FileItem.h index e7066652df276..f62c908c21334 100644 --- a/xbmc/FileItem.h +++ b/xbmc/FileItem.h @@ -62,6 +62,10 @@ class CGenre; class CURL; +class CFileItemList; +class CCueDocument; +typedef std::shared_ptr CCueDocumentPtr; + /* special startoffset used to indicate that we wish to resume */ #define STARTOFFSET_RESUME (-1) @@ -466,6 +470,10 @@ class CFileItem : int m_iHasLock; // 0 - no lock 1 - lock, but unlocked 2 - locked int m_iBadPwdCount; + void SetCueDocument(const CCueDocumentPtr& cuePtr); + void LoadEmbeddedCue(); + bool HasCueDocument() const; + bool LoadTracksFromCueDocument(CFileItemList& scannedItems); private: /*! \brief initialize all members of this class (not CGUIListItem members) to default values. Called from constructors, and from Reset() @@ -489,6 +497,8 @@ class CFileItem : PVR::CPVRTimerInfoTag * m_pvrTimerInfoTag; CPictureInfoTag* m_pictureInfoTag; bool m_bIsAlbum; + + CCueDocumentPtr m_cueDocument; }; /*! diff --git a/xbmc/music/MusicDatabase.cpp b/xbmc/music/MusicDatabase.cpp index 98d0ad99138f0..6b80ccd8eae6d 100644 --- a/xbmc/music/MusicDatabase.cpp +++ b/xbmc/music/MusicDatabase.cpp @@ -178,6 +178,9 @@ void CMusicDatabase::CreateTables() CLog::Log(LOGINFO, "create art table"); m_pDS->exec("CREATE TABLE art(art_id INTEGER PRIMARY KEY, media_id INTEGER, media_type TEXT, type TEXT, url TEXT)"); + CLog::Log(LOGINFO, "create cue table"); + m_pDS->exec("CREATE TABLE cue (idPath integer, strFileName text, strCuesheet text)"); + // Add 'Karaoke' genre AddGenre( "Karaoke" ); } @@ -226,6 +229,8 @@ void CMusicDatabase::CreateAnalytics() m_pDS->exec("CREATE INDEX ix_art ON art(media_id, media_type(20), type(20))"); + m_pDS->exec("CREATE UNIQUE INDEX idxCue ON cue(idPath, strFileName(255))"); + CLog::Log(LOGINFO, "create triggers"); m_pDS->exec("CREATE TRIGGER tgrDeleteAlbum AFTER delete ON album FOR EACH ROW BEGIN" " DELETE FROM song WHERE song.idAlbum = old.idAlbum;" @@ -246,6 +251,9 @@ void CMusicDatabase::CreateAnalytics() " DELETE FROM karaokedata WHERE karaokedata.idSong = old.idSong;" " DELETE FROM art WHERE media_id=old.idSong AND media_type='song';" " END"); + m_pDS->exec("CREATE TRIGGER tgrDeletePath AFTER delete ON path FOR EACH ROW BEGIN" + " DELETE FROM cue WHERE cue.idPath = old.idPath;" + " END"); // we create views last to ensure all indexes are rolled in CreateViews(); @@ -367,6 +375,105 @@ std::string GetArtistString(const VECARTISTCREDITS &credits) return artistString; } +void CMusicDatabase::SaveCuesheet(const std::string& fullSongPath, const std::string& strCuesheet) +{ + std::string strPath, strFileName; + URIUtils::Split(fullSongPath, strPath, strFileName); + + int idPath = AddPath(strPath); + + if (idPath == -1) + return; + + std::string strSQL; + try + { + CueCache::const_iterator it; + + it = m_cueCache.find(fullSongPath); + if (it != m_cueCache.end() && it->second == strCuesheet) + return; + + if (NULL == m_pDB.get()) + return; + + if (NULL == m_pDS.get()) + return; + + strSQL = PrepareSQL("SELECT * FROM cue WHERE idPath=%i AND strFileName='%s'", idPath, strFileName.c_str()); + m_pDS->query(strSQL.c_str()); + if (m_pDS->num_rows() == 0) + { + if (strCuesheet.empty()) + { + m_pDS->close(); + m_cueCache.insert(CueCache::value_type(fullSongPath, strCuesheet)); + return; + } + strSQL = PrepareSQL("INSERT INTO cue (idPath, strFileName, strCuesheet) VALUES(%i, '%s', '%s')", + idPath, strFileName.c_str(), strCuesheet.c_str()); + } + else + { + if (strCuesheet.empty()) + { + strSQL = PrepareSQL("DELETE FROM cue SET WHERE idPath=%i AND strFileName='%s'", idPath, strFileName.c_str()); + } + else + { + strSQL = PrepareSQL("UPDATE cue SET strCuesheet='%s') WHERE idPath=%i AND strFileName='%s'", + strCuesheet.c_str(), idPath, strFileName.c_str()); + } + } + m_pDS->close(); + m_pDS->exec(strSQL.c_str()); + m_cueCache.insert(CueCache::value_type(fullSongPath, strCuesheet)); + } + catch (...) + { + CLog::Log(LOGERROR, "musicdatabase:unable to addcue (%s)", strSQL.c_str()); + } +} + +std::string CMusicDatabase::LoadCuesheet(const std::string& fullSongPath) +{ + CueCache::const_iterator it; + it = m_cueCache.find(fullSongPath); + if (it != m_cueCache.end()) + return it->second; + + std::string strCuesheet; + + std::string strPath, strFileName; + URIUtils::Split(fullSongPath, strPath, strFileName); + + int idPath = AddPath(strPath); + if (idPath == -1) + return strCuesheet; + + std::string strSQL; + try + { + if (NULL == m_pDB.get()) + return strCuesheet; + + if (NULL == m_pDS.get()) + return strCuesheet; + + strSQL = PrepareSQL("select strCuesheet from cue where idPath=%i AND strFileName='%s'", idPath, strFileName.c_str()); + m_pDS->query(strSQL.c_str()); + + if (0 < m_pDS->num_rows()) + strCuesheet = m_pDS->get_sql_record()->at(0).get_asString(); + m_pDS->close(); + } + catch (...) + { + CLog::Log(LOGERROR, "musicdatabase:unable to loadcue (%s)", strSQL.c_str()); + } + return strCuesheet; +} + bool CMusicDatabase::AddAlbum(CAlbum& album) { BeginTransaction(); @@ -404,6 +511,7 @@ bool CMusicDatabase::AddAlbum(CAlbum& album) song->lastPlayed, song->rating, song->iKaraokeNumber); + for (VECARTISTCREDITS::iterator artistCredit = song->artistCredits.begin(); artistCredit != song->artistCredits.end(); ++artistCredit) { artistCredit->idArtist = AddArtist(artistCredit->GetArtist(), @@ -415,6 +523,8 @@ bool CMusicDatabase::AddAlbum(CAlbum& album) artistCredit == song->artistCredits.begin() ? false : true, std::distance(song->artistCredits.begin(), artistCredit)); } + + SaveCuesheet(song->strFileName, song->strCueSheet); } for (VECSONGS::const_iterator infoSong = album.infoSongs.begin(); infoSong != album.infoSongs.end(); ++infoSong) AddAlbumInfoSong(album.idAlbum, *infoSong); @@ -488,6 +598,8 @@ bool CMusicDatabase::UpdateAlbum(CAlbum& album) artistCredit == song->artistCredits.begin() ? false : true, std::distance(song->artistCredits.begin(), artistCredit)); } + + SaveCuesheet(song->strFileName, song->strCueSheet); } for (VECSONGS::const_iterator infoSong = album.infoSongs.begin(); infoSong != album.infoSongs.end(); ++infoSong) AddAlbumInfoSong(album.idAlbum, *infoSong); @@ -3921,11 +4033,15 @@ void CMusicDatabase::UpdateTables(int version) m_pDS->exec("UPDATE karaokedata SET strKaraLyrFileCRC=NULL"); m_pDS->exec("UPDATE album SET idThumb=NULL"); } + if (version < 49) + { + m_pDS->exec("CREATE TABLE cue (idPath integer, strFileName text, strCuesheet text)"); + } } int CMusicDatabase::GetSchemaVersion() const { - return 48; + return 49; } unsigned int CMusicDatabase::GetSongIDs(const Filter &filter, vector > &songIDs) diff --git a/xbmc/music/MusicDatabase.h b/xbmc/music/MusicDatabase.h index 52d08b69e7e53..5e69b3b5a078e 100644 --- a/xbmc/music/MusicDatabase.h +++ b/xbmc/music/MusicDatabase.h @@ -262,6 +262,12 @@ class CMusicDatabase : public CDatabase std::string GetArtistById(int id); int GetArtistByName(const std::string& strArtist); + ///////////////////////////////////////////////// + // Cuesheets + ///////////////////////////////////////////////// + void SaveCuesheet(const std::string& fullSongPath, const std::string& strCuesheet); + std::string LoadCuesheet(const std::string& fullSongPath); + ///////////////////////////////////////////////// // Paths ///////////////////////////////////////////////// @@ -465,6 +471,8 @@ class CMusicDatabase : public CDatabase std::map m_pathCache; std::map m_thumbCache; std::map m_albumCache; + typedef std::map CueCache; + CueCache m_cueCache; virtual void CreateTables(); virtual void CreateAnalytics(); diff --git a/xbmc/music/MusicInfoLoader.cpp b/xbmc/music/MusicInfoLoader.cpp index 7b51fd7c965cf..c0140f9f494a0 100644 --- a/xbmc/music/MusicInfoLoader.cpp +++ b/xbmc/music/MusicInfoLoader.cpp @@ -177,6 +177,7 @@ bool CMusicInfoLoader::LoadItemLookup(CFileItem* pItem) if (it != m_songsMap.end()) { // Have we loaded this item from database before pItem->GetMusicInfoTag()->SetSong(it->second); + pItem->GetMusicInfoTag()->SetCueSheet(m_musicDatabase.LoadCuesheet(it->second.strFileName)); if (!it->second.strThumb.empty()) pItem->SetArt("thumb", it->second.strThumb); } diff --git a/xbmc/music/Song.cpp b/xbmc/music/Song.cpp index fe88466e1d1a7..dc9f41011ecb0 100644 --- a/xbmc/music/Song.cpp +++ b/xbmc/music/Song.cpp @@ -68,6 +68,7 @@ CSong::CSong(CFileItem& item) albumArtist = tag.GetAlbumArtist(); strMusicBrainzTrackID = tag.GetMusicBrainzTrackID(); strComment = tag.GetComment(); + strCueSheet = tag.GetCueSheet(); rating = tag.GetRating(); iYear = stTime.wYear; iTrack = tag.GetTrackAndDiscNumber(); diff --git a/xbmc/music/Song.h b/xbmc/music/Song.h index 594af187e0def..c80e1f3133bd9 100644 --- a/xbmc/music/Song.h +++ b/xbmc/music/Song.h @@ -92,6 +92,7 @@ class CSong: public ISerializable MUSIC_INFO::EmbeddedArtInfo embeddedArt; std::string strMusicBrainzTrackID; std::string strComment; + std::string strCueSheet; char rating; int iTrack; int iDuration; diff --git a/xbmc/music/infoscanner/MusicInfoScanner.cpp b/xbmc/music/infoscanner/MusicInfoScanner.cpp index cb17456a6ebca..8f2a8913128df 100644 --- a/xbmc/music/infoscanner/MusicInfoScanner.cpp +++ b/xbmc/music/infoscanner/MusicInfoScanner.cpp @@ -52,6 +52,7 @@ #include "GUIUserMessages.h" #include "addons/AddonManager.h" #include "addons/Scraper.h" +#include "CueDocument.h" #include @@ -538,14 +539,23 @@ INFO_RET CMusicInfoScanner::ScanTags(const CFileItemList& items, CFileItemList& } if (m_handle && m_itemCount>0) - m_handle->SetPercentage(m_currentItem/(float)m_itemCount*100); + m_handle->SetPercentage(m_currentItem / (float)m_itemCount * 100); - if (!tag.Loaded()) + if (!tag.Loaded() && !pItem->HasCueDocument()) { CLog::Log(LOGDEBUG, "%s - No tag found for: %s", __FUNCTION__, pItem->GetPath().c_str()); continue; } - scannedItems.Add(pItem); + else + { + if (!tag.GetCueSheet().empty()) + pItem->LoadEmbeddedCue(); + } + + if (pItem->HasCueDocument()) + pItem->LoadTracksFromCueDocument(scannedItems); + else + scannedItems.Add(pItem); } return INFO_ADDED; } diff --git a/xbmc/music/tags/MusicInfoTag.cpp b/xbmc/music/tags/MusicInfoTag.cpp index 1b4f19f7ebd27..fa9c110c00aa1 100644 --- a/xbmc/music/tags/MusicInfoTag.cpp +++ b/xbmc/music/tags/MusicInfoTag.cpp @@ -112,6 +112,7 @@ const CMusicInfoTag& CMusicInfoTag::operator =(const CMusicInfoTag& tag) m_strMusicBrainzTRMID = tag.m_strMusicBrainzTRMID; m_strComment = tag.m_strComment; m_strLyrics = tag.m_strLyrics; + m_cuesheet = tag.m_cuesheet; m_lastPlayed = tag.m_lastPlayed; m_bCompilation = tag.m_bCompilation; m_iDuration = tag.m_iDuration; @@ -238,6 +239,11 @@ const std::string &CMusicInfoTag::GetLyrics() const return m_strLyrics; } +const std::string &CMusicInfoTag::GetCueSheet() const +{ + return m_cuesheet; +} + char CMusicInfoTag::GetRating() const { return m_rating; @@ -394,6 +400,11 @@ void CMusicInfoTag::SetComment(const std::string& comment) m_strComment = comment; } +void CMusicInfoTag::SetCueSheet(const std::string& cueSheet) +{ + m_cuesheet = cueSheet; +} + void CMusicInfoTag::SetLyrics(const std::string& lyrics) { m_strLyrics = lyrics; @@ -556,6 +567,7 @@ void CMusicInfoTag::SetSong(const CSong& song) SetAlbumArtist(song.albumArtist); SetMusicBrainzTrackID(song.strMusicBrainzTrackID); SetComment(song.strComment); + SetCueSheet(song.strCueSheet); SetPlayCount(song.iTimesPlayed); SetLastPlayed(song.lastPlayed); SetCoverArtInfo(song.embeddedArt.size, song.embeddedArt.mime); @@ -663,6 +675,7 @@ void CMusicInfoTag::Archive(CArchive& ar) ar << m_bCompilation; ar << m_listeners; ar << m_coverArt; + ar << m_cuesheet; } else { @@ -692,6 +705,7 @@ void CMusicInfoTag::Archive(CArchive& ar) ar >> m_bCompilation; ar >> m_listeners; ar >> m_coverArt; + ar >> m_cuesheet; } } @@ -714,6 +728,7 @@ void CMusicInfoTag::Clear() m_lastPlayed.Reset(); m_bCompilation = false; m_strComment.clear(); + m_cuesheet.clear(); m_rating = '0'; m_iDbId = -1; m_type.clear(); diff --git a/xbmc/music/tags/MusicInfoTag.h b/xbmc/music/tags/MusicInfoTag.h index c2ff04b38ec72..4e031c3a3db2a 100644 --- a/xbmc/music/tags/MusicInfoTag.h +++ b/xbmc/music/tags/MusicInfoTag.h @@ -102,6 +102,7 @@ class CMusicInfoTag : public IArchivable, public ISerializable, public ISortable const std::string& GetMusicBrainzTRMID() const; const std::string& GetComment() const; const std::string& GetLyrics() const; + const std::string& GetCueSheet() const; const CDateTime& GetLastPlayed() const; bool GetCompilation() const; char GetRating() const; @@ -142,6 +143,7 @@ class CMusicInfoTag : public IArchivable, public ISerializable, public ISortable void SetMusicBrainzTRMID(const std::string& strTRMID); void SetComment(const std::string& comment); void SetLyrics(const std::string& lyrics); + void SetCueSheet(const std::string& cueSheet); void SetRating(char rating); void SetListeners(int listeners); void SetPlayCount(int playcount); @@ -197,6 +199,7 @@ class CMusicInfoTag : public IArchivable, public ISerializable, public ISortable std::string m_strMusicBrainzTRMID; std::string m_strComment; std::string m_strLyrics; + std::string m_cuesheet; CDateTime m_lastPlayed; bool m_bCompilation; int m_iDuration; diff --git a/xbmc/music/tags/TagLoaderTagLib.cpp b/xbmc/music/tags/TagLoaderTagLib.cpp index 7058e278be340..5e1ef3348bffc 100644 --- a/xbmc/music/tags/TagLoaderTagLib.cpp +++ b/xbmc/music/tags/TagLoaderTagLib.cpp @@ -495,6 +495,7 @@ bool CTagLoaderTagLib::ParseAPETag(APE::Tag *ape, EmbeddedArt *art, CMusicInfoTa else if (it->first == "YEAR") tag.SetYear(it->second.toString().toInt()); else if (it->first == "GENRE") SetGenre(tag, StringListToVectorString(it->second.toStringList())); else if (it->first == "COMMENT") tag.SetComment(it->second.toString().to8Bit(true)); + else if (it->first == "CUESHEET") tag.SetCueSheet(it->second.toString().to8Bit(true)); else if (it->first == "ENCODEDBY") {} else if (it->first == "COMPILATION") tag.SetCompilation(it->second.toString().toInt() == 1); else if (it->first == "LYRICS") tag.SetLyrics(it->second.toString().to8Bit(true)); @@ -535,6 +536,7 @@ bool CTagLoaderTagLib::ParseXiphComment(Ogg::XiphComment *xiph, EmbeddedArt *art else if (it->first == "DATE") tag.SetYear(it->second.front().toInt()); else if (it->first == "GENRE") SetGenre(tag, StringListToVectorString(it->second)); else if (it->first == "COMMENT") tag.SetComment(it->second.front().to8Bit(true)); + else if (it->first == "CUESHEET") tag.SetCueSheet(it->second.front().to8Bit(true)); else if (it->first == "ENCODEDBY") {} else if (it->first == "ENSEMBLE") {} else if (it->first == "COMPILATION") tag.SetCompilation(it->second.front().toInt() == 1); diff --git a/xbmc/music/windows/GUIWindowMusicBase.cpp b/xbmc/music/windows/GUIWindowMusicBase.cpp index 2e6f6329d0d75..5766a60532f76 100644 --- a/xbmc/music/windows/GUIWindowMusicBase.cpp +++ b/xbmc/music/windows/GUIWindowMusicBase.cpp @@ -67,6 +67,8 @@ #include "URL.h" #include "music/infoscanner/MusicInfoScanner.h" #include "cores/IPlayer.h" +#include "CueDocument.h" + using namespace std; using namespace XFILE; @@ -519,6 +521,38 @@ void CGUIWindowMusicBase::RetrieveMusicInfo() OnRetrieveMusicInfo(*m_vecItems); + // \todo Scan for multitrack items here... + vector itemsForRemove; + CFileItemList itemsForAdd; + for (int i = 0; i < m_vecItems->Size(); ++i) + { + CFileItemPtr pItem = (*m_vecItems)[i]; + if (pItem->m_bIsFolder || pItem->IsPlayList() || pItem->IsPicture() || pItem->IsLyrics()) + continue; + + CMusicInfoTag& tag = *pItem->GetMusicInfoTag(); + if (tag.Loaded() && !tag.GetCueSheet().empty()) + pItem->LoadEmbeddedCue(); + + if (pItem->HasCueDocument() + && pItem->LoadTracksFromCueDocument(itemsForAdd)) + { + itemsForRemove.push_back(pItem->GetPath()); + } + } + for (size_t i = 0; i < itemsForRemove.size(); ++i) + { + for (int j = 0; j < m_vecItems->Size(); ++j) + { + if ((*m_vecItems)[j]->GetPath() == itemsForRemove[i]) + { + m_vecItems->Remove(j); + break; + } + } + } + m_vecItems->Append(itemsForAdd); + CLog::Log(LOGDEBUG, "RetrieveMusicInfo() took %u msec", XbmcThreads::SystemClockMillis() - startTick); }