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 }