File indexing completed on 2024-04-28 08:40:21

0001 /* AUDEX CDDA EXTRACTOR
0002  * SPDX-FileCopyrightText: Copyright (C) 2007 Marco Nelles
0003  * <https://userbase.kde.org/Audex>
0004  *
0005  * SPDX-License-Identifier: GPL-3.0-or-later
0006  */
0007 
0008 #include "playlist.h"
0009 #include <QRegularExpression>
0010 
0011 Playlist::Playlist()
0012 {
0013 }
0014 
0015 Playlist::Playlist(const QByteArray &playlist)
0016 {
0017     addPlaylist(playlist);
0018 }
0019 
0020 Playlist::~Playlist()
0021 {
0022 }
0023 
0024 void Playlist::addPlaylist(const QByteArray &playlist)
0025 {
0026     QString format = p_playlist_format(playlist);
0027 
0028     if (format == "m3u") {
0029         p_add_M3U(playlist);
0030     } else if (format == "pls") {
0031         p_add_PLS(playlist);
0032     } else if (format == "xspf") {
0033         p_add_XSPF(playlist);
0034     }
0035 }
0036 
0037 void Playlist::clear()
0038 {
0039     p_playlist.clear();
0040 }
0041 
0042 void Playlist::appendItem(const PlaylistItem &item)
0043 {
0044     p_playlist.append(item);
0045 }
0046 
0047 QByteArray Playlist::toM3U(const QString &playlistPath, const bool utf8) const
0048 {
0049     QStringList playlist;
0050     playlist.append("#EXTM3U");
0051 
0052     for (int i = 0; i < p_playlist.count(); ++i) {
0053         PlaylistItem pi = p_playlist[i];
0054         if (pi.filename().isEmpty())
0055             continue;
0056 
0057         if (!pi.artist().isEmpty()) {
0058             playlist.append(QString("#EXTINF:%1,%2 - %3").arg(QString::number(pi.length()), pi.artist(), pi.title()));
0059         } else {
0060             playlist.append(QString("#EXTINF:%1,%2").arg(QString::number(pi.length()), pi.title()));
0061         }
0062         if (!playlistPath.isEmpty()) {
0063             QDir dir(playlistPath);
0064             playlist.append(dir.relativeFilePath(pi.filename()));
0065         } else {
0066             playlist.append(pi.filename());
0067         }
0068     }
0069 
0070     if (utf8)
0071         return playlist.join("\n").append("\n").toUtf8();
0072     else
0073         return playlist.join("\n").append("\n").toLatin1();
0074 }
0075 
0076 QByteArray Playlist::toPLS(const QString &playlistPath, const bool utf8) const
0077 {
0078     QStringList playlist;
0079     playlist.append("[Playlist]");
0080 
0081     int j = 0;
0082     for (int i = 0; i < p_playlist.count(); ++i) {
0083         PlaylistItem pi = p_playlist[i];
0084 
0085         if (pi.filename().isEmpty())
0086             continue;
0087         ++j;
0088 
0089         if (!playlistPath.isEmpty()) {
0090             QDir dir(playlistPath);
0091             playlist.append(QString("File%1=%2").arg(i + 1).arg(dir.relativeFilePath(pi.filename())));
0092         } else {
0093             playlist.append(QString("File%1=%2").arg(i + 1).arg(pi.filename()));
0094         }
0095 
0096         if (!pi.artist().isEmpty()) {
0097             playlist.append(QString("Title%1=%2 - %3").arg(QString::number(i + 1), pi.artist(), pi.title()));
0098         } else {
0099             playlist.append(QString("Title%1=%2").arg(i + 1).arg(pi.title()));
0100         }
0101 
0102         playlist.append(QString("Length%1=%2").arg(i + 1).arg(pi.length()));
0103     }
0104 
0105     playlist.append(QString("NumberOfEntries=%1").arg(j));
0106     playlist.append(QString("Version=2"));
0107 
0108     if (utf8)
0109         return playlist.join("\n").append("\n").toUtf8();
0110     else
0111         return playlist.join("\n").append("\n").toLatin1();
0112 }
0113 
0114 QByteArray Playlist::toXSPF() const
0115 {
0116     QDomDocument doc;
0117     QDomElement root = doc.createElement("playlist");
0118     root.setAttribute("version", "1");
0119     root.setAttribute("xmlns", "http://xspf.org/ns/0");
0120 
0121     QDomElement creator = doc.createElement("creator");
0122     QDomText text = doc.createTextNode("audex");
0123     creator.appendChild(text);
0124     root.appendChild(creator);
0125 
0126     QDomElement tracklist = doc.createElement("trackList");
0127 
0128     int j = 0;
0129     for (int i = 0; i < p_playlist.count(); ++i) {
0130         PlaylistItem pi = p_playlist[i];
0131 
0132         if (pi.filename().isEmpty())
0133             continue;
0134         ++j;
0135 
0136         QDomElement track = doc.createElement("track");
0137 
0138         QDomElement ch = doc.createElement("location");
0139         QDomText text = doc.createTextNode(pi.filename());
0140 
0141         ch.appendChild(text);
0142         track.appendChild(ch);
0143 
0144         if (!pi.artist().isEmpty()) {
0145             ch = doc.createElement("creator");
0146             text = doc.createTextNode(pi.artist());
0147             ch.appendChild(text);
0148             track.appendChild(ch);
0149         }
0150 
0151         ch = doc.createElement("title");
0152         text = doc.createTextNode(pi.title());
0153         ch.appendChild(text);
0154         track.appendChild(ch);
0155 
0156         ch = doc.createElement("trackNum");
0157         text = doc.createTextNode(QString::number(j));
0158         ch.appendChild(text);
0159         track.appendChild(ch);
0160 
0161         if (pi.length() > 0) {
0162             ch = doc.createElement("duration");
0163             text = doc.createTextNode(QString::number(pi.length() * 1000));
0164             ch.appendChild(text);
0165             track.appendChild(ch);
0166         }
0167 
0168         tracklist.appendChild(track);
0169     }
0170 
0171     root.appendChild(tracklist);
0172     doc.appendChild(root);
0173     QByteArray xml_header("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
0174     return doc.toByteArray().prepend(xml_header);
0175 }
0176 
0177 const QString Playlist::p_playlist_format(const QByteArray &playlist)
0178 {
0179     if (playlist.contains("#EXTM3U") || playlist.contains("#EXTINF"))
0180         return "m3u";
0181     if (playlist.toLower().contains("[playlist]"))
0182         return "pls";
0183     if (playlist.contains("<playlist") && playlist.contains("<trackList"))
0184         return "xspf";
0185 
0186     return QString();
0187 }
0188 
0189 void Playlist::p_add_M3U(const QByteArray &playlist)
0190 {
0191     QTextStream stream(playlist, QIODevice::ReadOnly);
0192 
0193     QString line = stream.readLine().trimmed();
0194 
0195     bool extended = false;
0196     if (line.startsWith(QLatin1String("#EXTM3U"))) {
0197         extended = true;
0198         line = stream.readLine().trimmed();
0199     }
0200 
0201     PlaylistItem pi;
0202     while (!line.isNull()) {
0203         if (line.startsWith('#')) {
0204             if (extended && line.startsWith(QLatin1String("#EXT"))) {
0205                 pi = p_parse_m3u_metadata_line(line);
0206             }
0207         } else if (!line.isEmpty()) {
0208             pi.setFilename(line);
0209             if (!pi.filename().isEmpty()) {
0210                 p_playlist.append(pi);
0211                 pi.clear();
0212             }
0213         }
0214 
0215         line = stream.readLine().trimmed();
0216     }
0217 }
0218 
0219 void Playlist::p_add_PLS(const QByteArray &playlist)
0220 {
0221     QTextStream stream(playlist, QIODevice::ReadOnly);
0222 
0223     QString line = stream.readLine().trimmed();
0224 
0225     QMap<int, PlaylistItem> items;
0226     while (!line.isNull()) {
0227         int equals = line.indexOf('=');
0228         QString key = line.left(equals).toLower();
0229         QString value = line.mid(equals + 1);
0230 
0231         static QRegularExpression n_re("\\d+$");
0232         auto match = n_re.match(key);
0233         int n = match.captured(0).toInt();
0234 
0235         if (key.startsWith(QLatin1String("file"))) {
0236             items[n].setFilename(value);
0237         } else if (key.startsWith(QLatin1String("title"))) {
0238             items[n].setTitle(value);
0239         } else if (key.startsWith(QLatin1String("length"))) {
0240             bool ok;
0241             int seconds = value.toInt(&ok);
0242             if (ok)
0243                 items[n].setLength(seconds);
0244         }
0245 
0246         line = stream.readLine().trimmed();
0247     }
0248 
0249     QMap<int, PlaylistItem>::const_iterator i = items.constBegin();
0250     while (i != items.constEnd()) {
0251         p_playlist.append(i.value());
0252         ++i;
0253     }
0254 }
0255 
0256 void Playlist::p_add_XSPF(const QByteArray &playlist)
0257 {
0258     QDomDocument doc;
0259     QString errorMsg;
0260     int errorCol;
0261     int errorRow;
0262 
0263     if (!doc.setContent(QString(playlist), &errorMsg, &errorRow, &errorCol))
0264         return;
0265 
0266     QDomElement rootElement = doc.firstChildElement("playlist");
0267     if (rootElement.isNull())
0268         return;
0269 
0270     QDomElement tracklistElement = rootElement.firstChildElement("trackList");
0271     if (tracklistElement.isNull())
0272         return;
0273 
0274     QMap<int, PlaylistItem> items;
0275     QMap<int, PlaylistItem> items_no_tracknum;
0276     QDomElement child = tracklistElement.firstChildElement("track");
0277     int m = 0;
0278     while (!child.isNull()) {
0279         bool ok;
0280         int n = child.firstChildElement("trackNum").text().toInt(&ok);
0281         if (!ok) {
0282             items_no_tracknum[m].setFilename(child.firstChildElement("location").text());
0283             items_no_tracknum[m].setArtist(child.firstChildElement("creator").text());
0284             items_no_tracknum[m].setTitle(child.firstChildElement("title").text());
0285             items_no_tracknum[m].setLength(child.firstChildElement("duration").text().toInt() / 1000);
0286             ++m;
0287         } else {
0288             items[n].setFilename(child.firstChildElement("location").text());
0289             items[n].setArtist(child.firstChildElement("creator").text());
0290             items[n].setTitle(child.firstChildElement("title").text());
0291             items[n].setLength(child.firstChildElement("duration").text().toInt() / 1000);
0292         }
0293 
0294         child = child.nextSiblingElement();
0295     }
0296 
0297     QMap<int, PlaylistItem>::const_iterator i = items.constBegin();
0298     while (i != items.constEnd()) {
0299         p_playlist.append(i.value());
0300         ++i;
0301     }
0302     QMap<int, PlaylistItem>::const_iterator j = items_no_tracknum.constBegin();
0303     while (j != items_no_tracknum.constEnd()) {
0304         p_playlist.append(j.value());
0305         ++j;
0306     }
0307 }
0308 
0309 const PlaylistItem Playlist::p_parse_m3u_metadata_line(const QString &line)
0310 {
0311     PlaylistItem pi;
0312 
0313     QString info = line.section(':', 1);
0314     QString l = info.section(',', 0, 0);
0315     bool ok;
0316     int length = l.toInt(&ok);
0317     if (!ok)
0318         return pi;
0319     pi.setLength(length);
0320 
0321     QString track_info = info.section(',', 1);
0322     QStringList list = track_info.split('-');
0323     if (list.length() <= 1) {
0324         pi.setTitle(track_info);
0325         return pi;
0326     }
0327     pi.setArtist(list[0].trimmed());
0328     pi.setTitle(list[1].trimmed());
0329     return pi;
0330 }