// SPDX-FileCopyrightText: 2026 Tri Vo Khac // SPDX-License-Identifier: GPL-3.0-or-later #include "playlistmodel.h" #include #include #include #include #include #include using namespace Qt::Literals::StringLiterals; class PlaylistModelTest : public QObject { Q_OBJECT private: static void touch(const QTemporaryDir &dir, const QString &name) { QFile file(dir.filePath(name)); QVERIFY(file.open(QIODevice::WriteOnly)); } static QStringList names(const PlaylistModel &model) { QStringList result; for (int i = 0; i < model.rowCount(); ++i) { result.append(model.data(model.index(i), PlaylistModel::NameRole).toString()); } return result; } private Q_SLOTS: void initTestCase() { // The C locale's collation is case-sensitive, so the sort order would depend on $LANG. QLocale::setDefault(QLocale(QLocale::English, QLocale::UnitedStates)); } void sortsEpisodesNaturallyAndSkipsOtherFiles() { QTemporaryDir dir; for (const QString &name : {u"Show - 10.mkv"_s, u"show - 2.mkv"_s, u"Show - 1.mkv"_s, u"notes.txt"_s, u"Show - 1.srt"_s, u"theme.mp3"_s}) { touch(dir, name); } const QList urls = PlaylistModel::mediaFilesIn(dir.path()); QStringList fileNames; for (const QUrl &url : urls) { fileNames.append(url.fileName()); } QCOMPARE(fileNames, (QStringList{u"Show - 1.mkv"_s, u"show - 2.mkv"_s, u"Show - 10.mkv"_s, u"theme.mp3"_s})); } void openFileLoadsSiblingsAndPlaysTheFile() { QTemporaryDir dir; for (const QString &name : {u"Ep 1.mkv"_s, u"Ep 2.mkv"_s, u"Ep 3.mkv"_s}) { touch(dir, name); } PlaylistModel model; QSignalSpy played(&model, &PlaylistModel::playRequested); const QUrl second = QUrl::fromLocalFile(dir.filePath(u"Ep 2.mkv"_s)); model.openFile(second, true); QCOMPARE(names(model), (QStringList{u"Ep 1.mkv"_s, u"Ep 2.mkv"_s, u"Ep 3.mkv"_s})); QCOMPARE(model.playingIndex(), 1); QCOMPARE(played.count(), 1); QCOMPARE(played.first().first().toUrl(), second); model.openFile(second, false); QCOMPARE(names(model), (QStringList{u"Ep 2.mkv"_s})); } void nextAndPreviousStopAtTheEndsUnlessWrapping() { PlaylistModel model; model.append({QUrl(u"https://example.com/a"_s), QUrl(u"https://example.com/b"_s)}); model.play(1); QVERIFY(!model.playNext()); QCOMPARE(model.playingIndex(), 1); QVERIFY(model.playNext(true)); QCOMPARE(model.playingIndex(), 0); QVERIFY(!model.playPrevious()); QVERIFY(model.playPrevious(true)); QCOMPARE(model.playingIndex(), 1); } void playingIndexFollowsRemoveAndMove() { PlaylistModel model; model.append({QUrl(u"https://example.com/a"_s), QUrl(u"https://example.com/b"_s), QUrl(u"https://example.com/c"_s)}); model.play(1); model.move(1, 2); QCOMPARE(model.playingIndex(), 2); model.move(0, 2); QCOMPARE(model.playingIndex(), 1); model.remove(0); QCOMPARE(model.playingIndex(), 0); model.remove(0); QCOMPARE(model.playingIndex(), -1); QCOMPARE(model.count(), 1); } void titleAndDurationRoles() { const QString ffmpeg = QStandardPaths::findExecutable(u"ffmpeg"_s); if (ffmpeg.isEmpty()) { QSKIP("ffmpeg is needed to make a sample file"); } QTemporaryDir dir; const QString path = dir.filePath(u"Show.S01E02.The.Tone.1080p.WEB.mka"_s); QProcess process; process.start(ffmpeg, {u"-v"_s, u"error"_s, u"-f"_s, u"lavfi"_s, u"-i"_s, u"sine=duration=2"_s, path}); QVERIFY(process.waitForFinished()); QCOMPARE(process.exitCode(), 0); PlaylistModel model; QSignalSpy changed(&model, &QAbstractItemModel::dataChanged); model.append({QUrl::fromLocalFile(path), QUrl(u"https://example.com/a"_s)}); QCOMPARE(model.data(model.index(0), PlaylistModel::TitleRole).toString(), u"S01E02 ยท The Tone"_s); QCOMPARE(model.data(model.index(1), PlaylistModel::TitleRole).toString(), u"https://example.com/a"_s); QCOMPARE(model.data(model.index(1), PlaylistModel::DurationRole).toDouble(), -1.0); QTRY_COMPARE(changed.count(), 1); QCOMPARE(changed.first().at(2).value>(), QList{PlaylistModel::DurationRole}); QVERIFY(qAbs(model.data(model.index(0), PlaylistModel::DurationRole).toDouble() - 2) < 0.1); } }; QTEST_GUILESS_MAIN(PlaylistModelTest) #include "playlistmodeltest.moc"