File indexing completed on 2024-05-19 04:55:58
0001 /** 0002 * \file playlistcreator.cpp 0003 * Playlist creator. 0004 * 0005 * \b Project: Kid3 0006 * \author Urs Fleisch 0007 * \date 21 Sep 2009 0008 * 0009 * Copyright (C) 2009-2024 Urs Fleisch 0010 * 0011 * This file is part of Kid3. 0012 * 0013 * Kid3 is free software; you can redistribute it and/or modify 0014 * it under the terms of the GNU General Public License as published by 0015 * the Free Software Foundation; either version 2 of the License, or 0016 * (at your option) any later version. 0017 * 0018 * Kid3 is distributed in the hope that it will be useful, 0019 * but WITHOUT ANY WARRANTY; without even the implied warranty of 0020 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0021 * GNU General Public License for more details. 0022 * 0023 * You should have received a copy of the GNU General Public License 0024 * along with this program. If not, see <http://www.gnu.org/licenses/>. 0025 */ 0026 0027 #include "playlistcreator.h" 0028 #include <QDir> 0029 #include <QUrl> 0030 #include <QFile> 0031 #include <QTextStream> 0032 #if QT_VERSION >= 0x060000 0033 #include <QStringConverter> 0034 #endif 0035 #include "fileconfig.h" 0036 #include "formatconfig.h" 0037 #include "taggedfile.h" 0038 #include "trackdata.h" 0039 #include "fileproxymodel.h" 0040 #include "saferename.h" 0041 #include "config.h" 0042 0043 /** 0044 * Constructor. 0045 * 0046 * @param topLevelDir top-level directory of playlist 0047 * @param cfg playlist configuration 0048 */ 0049 PlaylistCreator::PlaylistCreator(const QString& topLevelDir, 0050 const PlaylistConfig& cfg) : 0051 m_cfg(cfg) 0052 { 0053 if (m_cfg.location() == PlaylistConfig::PL_TopLevelDirectory) { 0054 m_playlistDirName = topLevelDir; 0055 if (!m_playlistDirName.endsWith(QLatin1Char('/'))) { 0056 m_playlistDirName += QLatin1Char('/'); 0057 } 0058 } 0059 } 0060 0061 /** 0062 * Write a playlist from a list of model indexes. 0063 * @param playlistPath file path to be used for playlist 0064 * @param indexes indexes in FileProxyModel 0065 * @return true if ok. 0066 */ 0067 bool PlaylistCreator::write(const QString& playlistPath, 0068 const QList<QPersistentModelIndex>& indexes) 0069 { 0070 QFileInfo fileInfo(playlistPath); 0071 QDir playlistDir = fileInfo.absoluteDir(); 0072 m_playlistDirName = fileInfo.absolutePath(); 0073 if (!m_playlistDirName.endsWith(QLatin1Char('/'))) { 0074 m_playlistDirName += QLatin1Char('/'); 0075 } 0076 m_playlistFileName = fileInfo.fileName(); 0077 0078 QList<Entry> entries; 0079 for (const QPersistentModelIndex& index : indexes) { 0080 if (const auto model = 0081 qobject_cast<const FileProxyModel*>(index.model())) { 0082 QString filePath = model->filePath(index); 0083 PlaylistCreator::Entry entry; 0084 entry.filePath = m_cfg.useFullPath() 0085 ? filePath 0086 : playlistDir.relativeFilePath(filePath); 0087 if (m_cfg.writeInfo()) { 0088 Item(index, *this).getInfo(entry.info, entry.duration); 0089 } 0090 entries.append(entry); 0091 } 0092 } 0093 return write(entries); 0094 } 0095 0096 /** 0097 * Write playlist containing added Entry elements. 0098 * 0099 * @return true if ok. 0100 */ 0101 bool PlaylistCreator::write() 0102 { 0103 if (m_playlistFileName.isEmpty()) { 0104 return true; 0105 } 0106 if (write(m_entries.values())) { 0107 m_entries.clear(); 0108 m_playlistFileName = QLatin1String(""); 0109 return true; 0110 } 0111 return false; 0112 } 0113 0114 /** 0115 * Write a playlist from a list entries. 0116 * @param entries playlist entries 0117 * @return true if ok. 0118 */ 0119 bool PlaylistCreator::write(const QList<Entry>& entries) 0120 { 0121 QFile file(m_playlistDirName + m_playlistFileName); 0122 bool ok = file.open(QIODevice::WriteOnly); 0123 if (ok) { 0124 QTextStream stream(&file); 0125 if (QString codecName = FileConfig::instance().textEncoding(); 0126 codecName != QLatin1String("System")) { 0127 #if QT_VERSION >= 0x060000 0128 if (auto encoding = QStringConverter::encodingForName(codecName.toLatin1())) { 0129 stream.setEncoding(*encoding); 0130 } 0131 #else 0132 stream.setCodec(codecName.toLatin1()); 0133 #endif 0134 } 0135 0136 switch (m_cfg.format()) { 0137 case PlaylistConfig::PF_M3U: 0138 if (m_cfg.writeInfo()) { 0139 stream << "#EXTM3U\n"; 0140 } 0141 if (entries.isEmpty() && m_cfg.useFullPath()) { 0142 stream << "# Kid3: useFullPath\n"; 0143 } 0144 for (auto it = entries.constBegin(); it != entries.constEnd(); ++it) { 0145 if (m_cfg.writeInfo()) { 0146 stream << QString(QLatin1String("#EXTINF:%1,%2\n")) 0147 .arg(it->duration).arg(it->info); 0148 } 0149 stream << it->filePath << "\n"; 0150 } 0151 break; 0152 case PlaylistConfig::PF_PLS: 0153 { 0154 unsigned nr = 1; 0155 stream << "[playlist]\n"; 0156 stream << QString(QLatin1String("NumberOfEntries=%1\n")).arg(entries.size()); 0157 for (auto it = entries.constBegin(); it != entries.constEnd(); ++it) { 0158 stream << QString(QLatin1String("File%1=%2\n")).arg(nr).arg(it->filePath); 0159 if (m_cfg.writeInfo()) { 0160 stream << QString(QLatin1String("Title%1=%2\n")).arg(nr).arg(it->info); 0161 stream << QString(QLatin1String("Length%1=%2\n")).arg(nr).arg(it->duration); 0162 } 0163 ++nr; 0164 } 0165 stream << "Version=2\n"; 0166 if (entries.isEmpty() && (m_cfg.useFullPath() || m_cfg.writeInfo())) { 0167 stream << "; Kid3:"; 0168 if (m_cfg.useFullPath()) { 0169 stream << " useFullPath"; 0170 } 0171 if (m_cfg.writeInfo()) { 0172 stream << " writeInfo"; 0173 } 0174 stream << "\n"; 0175 } 0176 } 0177 break; 0178 case PlaylistConfig::PF_XSPF: 0179 { 0180 stream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"; 0181 // Using a raw string literal here causes clang to issue 0182 // "Unbalanced opening parenthesis in C++ code". 0183 QString line = QLatin1String("<playlist version=\"1\" xmlns=\"http://xspf.org/ns/0/\""); 0184 if (!m_cfg.useFullPath()) { 0185 QUrl url(m_playlistDirName); 0186 url.setScheme(QLatin1String("file")); 0187 line += QString(QLatin1String(" xml:base=\"%1\"")) 0188 .arg(QString::fromLatin1(url.toEncoded().constData())); 0189 } 0190 line += QLatin1String(">\n"); 0191 stream << line; 0192 stream << " <trackList>\n"; 0193 0194 for (auto it = entries.constBegin(); it != entries.constEnd(); ++it) { 0195 stream << " <track>\n"; 0196 QUrl url(it->filePath); 0197 if (m_cfg.useFullPath()) { 0198 url.setScheme(QLatin1String("file")); 0199 } 0200 stream << QString(QLatin1String(" <location>%1</location>\n")) 0201 .arg(QString::fromLatin1(url.toEncoded().constData())); 0202 if (m_cfg.writeInfo()) { 0203 // the info is already formatted in the case of XSPF 0204 stream << it->info; 0205 } 0206 stream << " </track>\n"; 0207 } 0208 0209 stream << " </trackList>\n"; 0210 if (entries.isEmpty() && m_cfg.writeInfo()) { 0211 stream << " <!-- Kid3: writeInfo -->\n"; 0212 } 0213 stream << "</playlist>\n"; 0214 } 0215 break; 0216 } 0217 0218 file.close(); 0219 } 0220 return ok; 0221 } 0222 0223 /** 0224 * Read playlist from file 0225 * @param playlistPath path to playlist file 0226 * @param filePaths absolute paths to the playlist files are returned here 0227 * @param format the playlist format is returned here 0228 * @param hasFullPath true is returned here if the files use absolute paths 0229 * @param hasInfo true is returned here if the playlist contains additional 0230 * information 0231 * @return true if ok. 0232 */ 0233 bool PlaylistCreator::read( 0234 const QString& playlistPath, QStringList& filePaths, 0235 PlaylistConfig::PlaylistFormat& format, 0236 bool& hasFullPath, bool& hasInfo) const 0237 { 0238 QFile file(playlistPath); 0239 if (file.open(QIODevice::ReadOnly)) { 0240 QFileInfo fileInfo(playlistPath); 0241 QDir playlistDir = fileInfo.absoluteDir(); 0242 QString playlistFileName = fileInfo.fileName(); 0243 0244 hasFullPath = false; 0245 hasInfo = false; 0246 format = PlaylistConfig::formatFromFileExtension(playlistFileName); 0247 0248 QTextStream stream(&file); 0249 if (QString codecName = FileConfig::instance().textEncoding(); 0250 codecName != QLatin1String("System")) { 0251 #if QT_VERSION >= 0x060000 0252 if (auto encoding = QStringConverter::encodingForName(codecName.toLatin1())) { 0253 stream.setEncoding(*encoding); 0254 } 0255 #else 0256 stream.setCodec(codecName.toLatin1()); 0257 #endif 0258 } 0259 0260 filePaths.clear(); 0261 0262 QString line; 0263 while (!(line = stream.readLine()).isNull()) { 0264 QString path; 0265 switch (format) { 0266 case PlaylistConfig::PF_M3U: 0267 if (line.startsWith(QLatin1Char('#'))) { 0268 if (line.startsWith(QLatin1String("#EXT"))) { 0269 hasInfo = true; 0270 } else if (line.startsWith(QLatin1String("# Kid3:")) && 0271 line.contains(QLatin1String("useFullPath"))) { 0272 hasFullPath = true; 0273 } 0274 } else { 0275 path = line.trimmed(); 0276 } 0277 break; 0278 case PlaylistConfig::PF_PLS: 0279 if (line.startsWith(QLatin1String("File"))) { 0280 if (int colonPos = line.indexOf(QLatin1Char('='), 4); 0281 colonPos != -1) { 0282 path = line.mid(colonPos + 1).trimmed(); 0283 } 0284 } else if (line.startsWith(QLatin1String("Title")) || 0285 line.startsWith(QLatin1String("Length"))) { 0286 hasInfo = true; 0287 } else if (line.startsWith(QLatin1String("; Kid3:"))) { 0288 if (line.contains(QLatin1String("useFullPath"))) { 0289 hasFullPath = true; 0290 } 0291 if (line.contains(QLatin1String("writeInfo"))) { 0292 hasInfo = true; 0293 } 0294 } 0295 break; 0296 case PlaylistConfig::PF_XSPF: 0297 if (line.contains(QLatin1String("<location>"))) { 0298 if (int startPos = line.indexOf(QLatin1String("<location>")); 0299 startPos != -1) { 0300 startPos += 10; 0301 if (int endPos = line.indexOf(QLatin1String("</location>"), startPos); 0302 endPos != -1) { 0303 QUrl url = QUrl::fromEncoded( 0304 line.mid(startPos, endPos - startPos).toLatin1()); 0305 path = url.toLocalFile(); 0306 if (path.isEmpty()) { 0307 // For relative paths, QUrl::toLocalFile() returns "". 0308 path = url.toString(); 0309 } 0310 } 0311 } 0312 } else if (line.contains(QLatin1String("<title>")) || 0313 line.contains(QLatin1String("<creator>")) || 0314 line.contains(QLatin1String("<album>")) || 0315 line.contains(QLatin1String("<trackNum>")) || 0316 line.contains(QLatin1String("<duration>")) || 0317 line.contains(QLatin1String("<!-- Kid3: writeInfo -->"))) { 0318 hasInfo = true; 0319 } else if (line.startsWith(QLatin1String("<playlist")) && 0320 !line.contains(QLatin1String("xml:base="))) { 0321 hasFullPath = true; 0322 } 0323 break; 0324 } 0325 if (!path.isEmpty()) { 0326 if (QDir::isAbsolutePath(path)) { 0327 hasFullPath = true; 0328 } else { 0329 path = playlistDir.absoluteFilePath(path); 0330 } 0331 filePaths.append(path); 0332 } 0333 } 0334 0335 file.close(); 0336 return true; 0337 } 0338 return false; 0339 } 0340 0341 0342 /** 0343 * Constructor. 0344 * 0345 * @param index model index 0346 * @param ctr associated playlist creator 0347 */ 0348 PlaylistCreator::Item::Item(const QModelIndex& index, PlaylistCreator& ctr) 0349 : m_ctr(ctr), m_taggedFile(FileProxyModel::getTaggedFileOfIndex(index)), 0350 m_isDir(false) 0351 { 0352 if (m_taggedFile) { 0353 m_dirName = m_taggedFile->getDirname(); 0354 } else { 0355 m_dirName = FileProxyModel::getPathIfIndexOfDir(index); 0356 m_isDir = !m_dirName.isNull(); 0357 } 0358 if (!m_dirName.endsWith(QLatin1Char('/'))) { 0359 m_dirName += QLatin1Char('/'); 0360 } 0361 // fix double separators 0362 m_dirName.replace(QLatin1String("//"), QLatin1String("/")); 0363 } 0364 0365 /** 0366 * Get additional information for item. 0367 * @param info additional information is returned here 0368 * @param duration the duration of the track is returned here 0369 */ 0370 void PlaylistCreator::Item::getInfo(QString& info, unsigned long& duration) 0371 { 0372 if (m_ctr.m_cfg.format() != PlaylistConfig::PF_XSPF) { 0373 info = formatString(m_ctr.m_cfg.infoFormat()); 0374 } else { 0375 info = formatString(QLatin1String( 0376 " <title>%{title}</title>\n" 0377 " <creator>%{artist}</creator>\n" 0378 " <album>%{album}</album>\n" 0379 " <trackNum>%{track.1}</trackNum>\n" 0380 " <duration>%{seconds}000</duration>\n")); 0381 } 0382 TaggedFile::DetailInfo detailInfo; 0383 m_taggedFile->getDetailInfo(detailInfo); 0384 duration = detailInfo.duration; 0385 } 0386 0387 /** 0388 * Format string using tags and properties of item. 0389 * 0390 * @param format format string 0391 * 0392 * @return string with percent codes replaced. 0393 */ 0394 QString PlaylistCreator::Item::formatString(const QString& format) 0395 { 0396 if (!m_trackData) { 0397 m_taggedFile = FileProxyModel::readTagsFromTaggedFile(m_taggedFile); 0398 m_trackData.reset(new ImportTrackData(*m_taggedFile, Frame::TagVAll)); 0399 } 0400 return m_trackData->formatString(format); 0401 } 0402 0403 /** 0404 * Add item to playlist. 0405 * This operation will write a playlist if the configuration is set to write 0406 * a playlist in every directory and a new directory is entered. 0407 * 0408 * @return true if ok. 0409 */ 0410 bool PlaylistCreator::Item::add() 0411 { 0412 bool ok = true; 0413 if (m_ctr.m_cfg.location() != PlaylistConfig::PL_TopLevelDirectory) { 0414 if (m_ctr.m_playlistDirName != m_dirName) { 0415 ok = m_ctr.write(); 0416 m_ctr.m_playlistDirName = m_dirName; 0417 } 0418 } 0419 if (m_ctr.m_playlistFileName.isEmpty()) { 0420 if (!m_ctr.m_cfg.useFileNameFormat()) { 0421 m_ctr.m_playlistFileName = QDir(m_ctr.m_playlistDirName).dirName(); 0422 } else { 0423 m_ctr.m_playlistFileName = formatString(m_ctr.m_cfg.fileNameFormat()); 0424 Utils::replaceIllegalFileNameCharacters(m_ctr.m_playlistFileName); 0425 } 0426 FormatConfig& fnCfg = FilenameFormatConfig::instance(); 0427 if (fnCfg.useForOtherFileNames()) { 0428 bool isFilenameFormatter = fnCfg.switchFilenameFormatter(false); 0429 fnCfg.formatString(m_ctr.m_playlistFileName); 0430 fnCfg.switchFilenameFormatter(isFilenameFormatter); 0431 } 0432 m_ctr.m_playlistFileName = fnCfg.joinFileName( 0433 m_ctr.m_playlistFileName, m_ctr.m_cfg.fileExtensionForFormat()); 0434 } 0435 QString filePath = m_dirName + m_taggedFile->getFilename(); 0436 if (!m_ctr.m_cfg.useFullPath() && 0437 filePath.startsWith(m_ctr.m_playlistDirName)) { 0438 filePath = filePath.mid(m_ctr.m_playlistDirName.length()); 0439 } 0440 QString sortKey; 0441 if (m_ctr.m_cfg.useSortTagField()) { 0442 sortKey = formatString(m_ctr.m_cfg.sortTagField()); 0443 } 0444 sortKey += filePath; 0445 PlaylistCreator::Entry entry; 0446 entry.filePath = filePath; 0447 if (m_ctr.m_cfg.writeInfo()) { 0448 getInfo(entry.info, entry.duration); 0449 } 0450 m_ctr.m_entries.insert(sortKey, entry); 0451 return ok; 0452 }