/* * 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. */ #include "LabelFormatter.h" #include "FileItem.h" #include "RegExp.h" #include "ServiceBroker.h" #include "StringUtils.h" #include "URIUtils.h" #include "Util.h" #include "Variant.h" #include "addons/IAddon.h" #include "guilib/LocalizeStrings.h" #include "music/tags/MusicInfoTag.h" #include "pictures/PictureInfoTag.h" #include "settings/AdvancedSettings.h" #include "settings/Settings.h" #include "settings/SettingsComponent.h" #include "video/VideoInfoTag.h" #include #include #include using namespace MUSIC_INFO; /* LabelFormatter * ============== * * The purpose of this class is to parse a mask string of the form * * [%N. ][%T] - [%A][ (%Y)] * * and provide methods to format up a CFileItem's label(s). * * The %N/%A/%B masks are replaced with the corresponding metadata (if available). * * Square brackets are treated as a metadata block. Anything inside the block other * than the metadata mask is treated as either a prefix or postfix to the metadata. This * information is only included in the formatted string when the metadata is non-empty. * * Any metadata tags not enclosed with square brackets are treated as if it were immediately * enclosed - i.e. with no prefix or postfix. * * The special characters %, [, and ] can be produced using %%, %[, and %] respectively. * * Any static text outside of the metadata blocks is only shown if the blocks on either side * (or just one side in the case of an end) are both non-empty. * * Examples (using the above expression): * * Track Title Artist Year Resulting Label * ----- ----- ------ ---- --------------- * 10 "40" U2 1983 10. "40" - U2 (1983) * "40" U2 1983 "40" - U2 (1983) * 10 U2 1983 10. U2 (1983) * 10 "40" 1983 "40" (1983) * 10 "40" U2 10. "40" - U2 * 10 "40" 10. "40" * * Available metadata masks: * * %A - Artist * %B - Album * %C - Programs count * %D - Duration * %E - episode number * %F - FileName * %G - Genre * %H - season*100+episode * %I - Size * %J - Date * %K - Movie/Game title * %L - existing Label * %M - number of episodes * %N - Track Number * %O - mpaa rating * %P - production code * %Q - file time * %R - Movie rating * %S - Disc Number * %T - Title * %U - studio * %V - Playcount * %W - Listeners * %X - Bitrate * %Y - Year * %Z - tvshow title * %a - Date Added * %b - Total number of discs * %c - Relevance - Used for actors' appearances * %d - Date and Time * %e - Original release date * %f - bpm * %p - Last Played * %r - User Rating * *t - Date Taken (suitable for Pictures) */ #define MASK_CHARS "NSATBGYFLDIJRCKMEPHZOQUVXWabcdefiprstuv" CLabelFormatter::CLabelFormatter(const std::string &mask, const std::string &mask2) { // assemble our label masks AssembleMask(0, mask); AssembleMask(1, mask2); // save a bool for faster lookups m_hideFileExtensions = !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_SHOWEXTENSIONS); } std::string CLabelFormatter::GetContent(unsigned int label, const CFileItem *item) const { assert(label < 2); assert(m_staticContent[label].size() == m_dynamicContent[label].size() + 1); if (!item) return ""; std::string strLabel, dynamicLeft, dynamicRight; for (unsigned int i = 0; i < m_dynamicContent[label].size(); i++) { dynamicRight = GetMaskContent(m_dynamicContent[label][i], item); if ((i == 0 || !dynamicLeft.empty()) && !dynamicRight.empty()) strLabel += m_staticContent[label][i]; strLabel += dynamicRight; dynamicLeft = dynamicRight; } if (!dynamicLeft.empty()) strLabel += m_staticContent[label][m_dynamicContent[label].size()]; return strLabel; } void CLabelFormatter::FormatLabel(CFileItem *item) const { std::string maskedLabel = GetContent(0, item); if (!maskedLabel.empty()) item->SetLabel(maskedLabel); else if (!item->m_bIsFolder && m_hideFileExtensions) item->RemoveExtension(); } void CLabelFormatter::FormatLabel2(CFileItem *item) const { item->SetLabel2(GetContent(1, item)); } std::string CLabelFormatter::GetMaskContent(const CMaskString &mask, const CFileItem *item) const { if (!item) return ""; const CMusicInfoTag *music = item->GetMusicInfoTag(); const CVideoInfoTag *movie = item->GetVideoInfoTag(); const CPictureInfoTag *pic = item->GetPictureInfoTag(); std::string value; switch (mask.m_content) { case 'N': if (music && music->GetTrackNumber() > 0) value = StringUtils::Format("{:02}", music->GetTrackNumber()); if (movie&& movie->m_iTrack > 0) value = StringUtils::Format("{:02}", movie->m_iTrack); break; case 'S': if (music && music->GetDiscNumber() > 0) value = StringUtils::Format("{:02}", music->GetDiscNumber()); break; case 'A': if (music && music->GetArtistString().size()) value = music->GetArtistString(); if (movie && movie->m_artist.size()) value = StringUtils::Join(movie->m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator); break; case 'T': if (music && music->GetTitle().size()) value = music->GetTitle(); if (movie && movie->m_strTitle.size()) value = movie->m_strTitle; break; case 'Z': if (movie && !movie->m_strShowTitle.empty()) value = movie->m_strShowTitle; break; case 'B': if (music && music->GetAlbum().size()) value = music->GetAlbum(); else if (movie) value = movie->m_strAlbum; break; case 'G': if (music && music->GetGenre().size()) value = StringUtils::Join(music->GetGenre(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator); if (movie && movie->m_genre.size()) value = StringUtils::Join(movie->m_genre, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator); break; case 'Y': if (music) value = music->GetYearString(); if (movie) { if (movie->m_firstAired.IsValid()) value = movie->m_firstAired.GetAsLocalizedDate(); else if (movie->HasYear()) value = std::to_string(movie->GetYear()); } break; case 'F': // filename value = CUtil::GetTitleFromPath(item->GetPath(), item->m_bIsFolder && !item->IsFileFolder()); break; case 'L': value = item->GetLabel(); // is the label the actual file or folder name? if (value == URIUtils::GetFileName(item->GetPath())) { // label is the same as filename, clean it up as appropriate value = CUtil::GetTitleFromPath(item->GetPath(), item->m_bIsFolder && !item->IsFileFolder()); } break; case 'D': { // duration int nDuration=0; if (music) nDuration = music->GetDuration(); if (movie) nDuration = movie->GetDuration(); if (nDuration > 0) value = StringUtils::SecondsToTimeString(nDuration, (nDuration >= 3600) ? TIME_FORMAT_H_MM_SS : TIME_FORMAT_MM_SS); else if (item->m_dwSize > 0) value = StringUtils::SizeToString(item->m_dwSize); } break; case 'I': // size if( (item->m_bIsFolder && item->m_dwSize != 0) || item->m_dwSize >= 0 ) value = StringUtils::SizeToString(item->m_dwSize); break; case 'J': // date if (item->m_dateTime.IsValid()) value = item->m_dateTime.GetAsLocalizedDate(); break; case 'Q': // time if (item->m_dateTime.IsValid()) value = item->m_dateTime.GetAsLocalizedTime("", false); break; case 'R': // rating if (music && music->GetRating() != 0.f) value = StringUtils::Format("{:.1f}", music->GetRating()); else if (movie && movie->GetRating().rating != 0.f) value = StringUtils::Format("{:.1f}", movie->GetRating().rating); break; case 'C': // programs count value = std::to_string(item->m_iprogramCount); break; case 'c': // relevance value = std::to_string(movie->m_relevance); break; case 'K': value = item->m_strTitle; break; case 'M': if (movie && movie->m_iEpisode > 0) value = StringUtils::Format("{} {}", movie->m_iEpisode, g_localizeStrings.Get(movie->m_iEpisode == 1 ? 20452 : 20453)); break; case 'E': if (movie && movie->m_iEpisode > 0) { // episode number if (movie->m_iSeason == 0) value = StringUtils::Format("S{:02}", movie->m_iEpisode); else value = StringUtils::Format("{:02}", movie->m_iEpisode); } break; case 'P': if (movie) // tvshow production code value = movie->m_strProductionCode; break; case 'H': if (movie && movie->m_iEpisode > 0) { // season*100+episode number if (movie->m_iSeason == 0) value = StringUtils::Format("S{:02}", movie->m_iEpisode); else value = StringUtils::Format("{}x{:02}", movie->m_iSeason, movie->m_iEpisode); } break; case 'O': if (movie) {// MPAA Rating value = movie->m_strMPAARating; } break; case 'U': if (movie && !movie->m_studio.empty()) {// Studios value = StringUtils::Join(movie ->m_studio, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator); } break; case 'V': // Playcount if (music) value = std::to_string(music->GetPlayCount()); if (movie) value = std::to_string(movie->GetPlayCount()); break; case 'X': // Bitrate if( !item->m_bIsFolder && item->m_dwSize != 0 ) value = StringUtils::Format("{} kbps", item->m_dwSize); break; case 'W': // Listeners if( !item->m_bIsFolder && music && music->GetListeners() != 0 ) value = StringUtils::Format("{} {}", music->GetListeners(), g_localizeStrings.Get(music->GetListeners() == 1 ? 20454 : 20455)); break; case 'a': // Date Added if (movie && movie->m_dateAdded.IsValid()) value = movie->m_dateAdded.GetAsLocalizedDate(); if (music && music->GetDateAdded().IsValid()) value = music->GetDateAdded().GetAsLocalizedDate(); break; case 'b': // Total number of discs if (music) value = std::to_string(music->GetTotalDiscs()); break; case 'e': // Original release date if (music) { value = music->GetOriginalDate(); if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bMusicLibraryUseISODates) value = StringUtils::ISODateToLocalizedDate(value); } break; case 'd': // date and time if (item->m_dateTime.IsValid()) value = item->m_dateTime.GetAsLocalizedDateTime(); break; case 'p': // Last played if (movie && movie->m_lastPlayed.IsValid()) value = movie->m_lastPlayed.GetAsLocalizedDate(); if (music && music->GetLastPlayed().IsValid()) value = music->GetLastPlayed().GetAsLocalizedDate(); break; case 'r': // userrating if (movie && movie->m_iUserRating != 0) value = std::to_string(movie->m_iUserRating); if (music && music->GetUserrating() != 0) value = std::to_string(music->GetUserrating()); break; case 't': // Date Taken if (pic && pic->GetDateTimeTaken().IsValid()) value = pic->GetDateTimeTaken().GetAsLocalizedDate(); break; case 's': // Addon status if (item->HasProperty("Addon.Status")) value = item->GetProperty("Addon.Status").asString(); break; case 'i': // Install date if (item->HasAddonInfo() && item->GetAddonInfo()->InstallDate().IsValid()) value = item->GetAddonInfo()->InstallDate().GetAsLocalizedDate(); break; case 'u': // Last used if (item->HasAddonInfo() && item->GetAddonInfo()->LastUsed().IsValid()) value = item->GetAddonInfo()->LastUsed().GetAsLocalizedDate(); break; case 'v': // Last updated if (item->HasAddonInfo() && item->GetAddonInfo()->LastUpdated().IsValid()) value = item->GetAddonInfo()->LastUpdated().GetAsLocalizedDate(); break; case 'f': // BPM if (music) value = std::to_string(music->GetBPM()); break; } if (!value.empty()) return mask.m_prefix + value + mask.m_postfix; return ""; } void CLabelFormatter::SplitMask(unsigned int label, const std::string &mask) { assert(label < 2); CRegExp reg; reg.RegComp("%([" MASK_CHARS "])"); std::string work(mask); int findStart = -1; while ((findStart = reg.RegFind(work.c_str())) >= 0) { // we've found a match m_staticContent[label].push_back(work.substr(0, findStart)); m_dynamicContent[label].emplace_back("", reg.GetMatch(1)[0], ""); work = work.substr(findStart + reg.GetFindLen()); } m_staticContent[label].push_back(work); } void CLabelFormatter::AssembleMask(unsigned int label, const std::string& mask) { assert(label < 2); m_staticContent[label].clear(); m_dynamicContent[label].clear(); // we want to match [%A= 0) { // we've found a match for a pre/postfixed string // send anything SplitMask(label, work.substr(0, findStart) + reg.GetMatch(1)); m_dynamicContent[label].emplace_back(reg.GetMatch(2), reg.GetMatch(4)[0], reg.GetMatch(5)); work = work.substr(findStart + reg.GetFindLen()); } SplitMask(label, work); assert(m_staticContent[label].size() == m_dynamicContent[label].size() + 1); } bool CLabelFormatter::FillMusicTag(const std::string &fileName, CMusicInfoTag *tag) const { // run through and find static content to split the string up size_t pos1 = fileName.find(m_staticContent[0][0], 0); if (pos1 == std::string::npos) return false; for (unsigned int i = 1; i < m_staticContent[0].size(); i++) { size_t pos2 = m_staticContent[0][i].size() ? fileName.find(m_staticContent[0][i], pos1) : fileName.size(); if (pos2 == std::string::npos) return false; // found static content - thus we have the dynamic content surrounded FillMusicMaskContent(m_dynamicContent[0][i - 1].m_content, fileName.substr(pos1, pos2 - pos1), tag); pos1 = pos2 + m_staticContent[0][i].size(); } return true; } void CLabelFormatter::FillMusicMaskContent(const char mask, const std::string &value, CMusicInfoTag *tag) const { if (!tag) return; switch (mask) { case 'N': tag->SetTrackNumber(atol(value.c_str())); break; case 'S': tag->SetDiscNumber(atol(value.c_str())); break; case 'A': tag->SetArtist(value); break; case 'T': tag->SetTitle(value); break; case 'B': tag->SetAlbum(value); break; case 'G': tag->SetGenre(value); break; case 'Y': tag->SetYear(atol(value.c_str())); break; case 'D': tag->SetDuration(StringUtils::TimeStringToSeconds(value)); break; case 'R': // rating tag->SetRating(value[0]); break; case 'r': // userrating tag->SetUserrating(value[0]); break; case 'b': // total discs tag->SetTotalDiscs(atol(value.c_str())); break; } }