Add and use Chapter class

This commit is contained in:
Bart De Vries 2022-12-08 20:47:16 +01:00
parent 6a6962e2d3
commit 6217dbcc86
10 changed files with 254 additions and 80 deletions

View File

@ -11,6 +11,7 @@ set(SRCS_base
feed.cpp feed.cpp
author.cpp author.cpp
enclosure.cpp enclosure.cpp
chapter.cpp
datamanager.cpp datamanager.cpp
audiomanager.cpp audiomanager.cpp
error.cpp error.cpp

117
src/chapter.cpp Normal file
View File

@ -0,0 +1,117 @@
/**
* SPDX-FileCopyrightText: 2022 Bart De Vries <bart@mogwai.be>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include "chapter.h"
#include "fetcher.h"
Chapter::Chapter(Entry *entry, const QString &title, const QString &link, const QString &image, const int &start, QObject *parent)
: QObject(parent)
, m_entry(entry)
, m_title(title)
, m_link(link)
, m_image(image)
, m_start(start)
{
connect(&Fetcher::instance(), &Fetcher::downloadFinished, this, [this](QString url) {
if (url == m_image) {
Q_EMIT imageChanged(url);
Q_EMIT cachedImageChanged(cachedImage());
}
});
}
Chapter::~Chapter()
{
}
Entry *Chapter::entry() const
{
return m_entry;
}
QString Chapter::title() const
{
return m_title;
}
QString Chapter::link() const
{
return m_link;
}
QString Chapter::image() const
{
if (!m_image.isEmpty()) {
return m_image;
} else if (m_entry) {
// fall back to entry image
return m_entry->image();
} else {
return QStringLiteral("no-image");
}
}
QString Chapter::cachedImage() const
{
// First check for the feed image, fall back if needed
QString image = m_image;
if (image.isEmpty()) {
if (m_entry) {
return m_entry->cachedImage();
} else {
return QStringLiteral("no-image");
}
}
return Fetcher::instance().image(image);
}
int Chapter::start() const
{
return m_start;
}
void Chapter::setTitle(const QString &title, bool emitSignal)
{
if (m_title != title) {
m_title = title;
if (emitSignal) {
Q_EMIT titleChanged(m_title);
}
}
}
void Chapter::setLink(const QString &link, bool emitSignal)
{
if (m_link != link) {
m_link = link;
if (emitSignal) {
Q_EMIT linkChanged(m_link);
}
}
}
void Chapter::setImage(const QString &image, bool emitSignal)
{
if (m_image != image) {
m_image = image;
if (emitSignal) {
Q_EMIT imageChanged(m_image);
Q_EMIT cachedImageChanged(cachedImage());
}
}
}
void Chapter::setStart(const int &start, bool emitSignal)
{
if (m_start != start) {
m_start = start;
if (emitSignal) {
Q_EMIT startChanged(m_start);
}
}
}

55
src/chapter.h Normal file
View File

@ -0,0 +1,55 @@
/**
* SPDX-FileCopyrightText: 2022 Bart De Vries <bart@mogwai.be>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#pragma once
#include <QDebug>
#include <QObject>
#include <QString>
#include "entry.h"
class Chapter : public QObject
{
Q_OBJECT
Q_PROPERTY(Entry *entry READ entry CONSTANT)
Q_PROPERTY(QString title READ title NOTIFY titleChanged)
Q_PROPERTY(QString link READ link NOTIFY linkChanged)
Q_PROPERTY(QString image READ image NOTIFY imageChanged)
Q_PROPERTY(QString cachedImage READ cachedImage NOTIFY cachedImageChanged)
Q_PROPERTY(int start READ start NOTIFY startChanged)
public:
Chapter(Entry *entry, const QString &title, const QString &link, const QString &image, const int &start, QObject *parent = nullptr);
~Chapter();
Entry *entry() const;
QString title() const;
QString link() const;
QString image() const;
QString cachedImage() const;
int start() const;
void setTitle(const QString &title, bool emitSignal = true);
void setLink(const QString &link, bool emitSignal = true);
void setImage(const QString &image, bool emitSignal = true);
void setStart(const int &start, bool emitSignal = true);
Q_SIGNALS:
void titleChanged(const QString &title);
void linkChanged(const QString &link);
void imageChanged(const QString &url);
void cachedImageChanged(const QString &imagePath);
void startChanged(const int &start);
private:
Entry *m_entry = nullptr;
QString m_title;
QString m_link;
QString m_image;
int m_start;
};

View File

@ -115,10 +115,6 @@ QString Fetcher::image(const QString &url)
return QLatin1String("no-image"); return QLatin1String("no-image");
} }
if (url.startsWith(QStringLiteral("/"))) {
return url;
}
// if image is already cached, then return the path // if image is already cached, then return the path
QString path = StorageManager::instance().imagePath(url); QString path = StorageManager::instance().imagePath(url);
if (QFileInfo::exists(path)) { if (QFileInfo::exists(path)) {

View File

@ -29,11 +29,13 @@ ChapterModel::ChapterModel(QObject *parent)
if (!m_entry || m_entry != AudioManager::instance().entry() || m_chapters.isEmpty()) { if (!m_entry || m_entry != AudioManager::instance().entry() || m_chapters.isEmpty()) {
return; return;
} }
if (m_chapters[m_currentChapter].start > AudioManager::instance().position() / 1000 if (m_chapters[m_currentChapter]
|| (m_currentChapter < m_chapters.size() - 1 && m_chapters[m_currentChapter + 1].start < AudioManager::instance().position() / 1000)) { && (m_chapters[m_currentChapter]->start() > AudioManager::instance().position() / 1000
|| (m_currentChapter < m_chapters.size() - 1 && m_chapters[m_currentChapter + 1]
&& m_chapters[m_currentChapter + 1]->start() < AudioManager::instance().position() / 1000))) {
for (int i = 0; i < m_chapters.size(); i++) { for (int i = 0; i < m_chapters.size(); i++) {
if (m_chapters[i].start < AudioManager::instance().position() / 1000 if (m_chapters[i]->start() < AudioManager::instance().position() / 1000
&& (i == m_chapters.size() - 1 || m_chapters[i + 1].start > AudioManager::instance().position() / 1000)) { && (i == m_chapters.size() - 1 || m_chapters[i + 1]->start() > AudioManager::instance().position() / 1000)) {
m_currentChapter = i; m_currentChapter = i;
Q_EMIT currentChapterChanged(); Q_EMIT currentChapterChanged();
} }
@ -42,6 +44,11 @@ ChapterModel::ChapterModel(QObject *parent)
}); });
} }
ChapterModel::~ChapterModel()
{
qDeleteAll(m_chapters.begin(), m_chapters.end());
}
QVariant ChapterModel::data(const QModelIndex &index, int role) const QVariant ChapterModel::data(const QModelIndex &index, int role) const
{ {
if (!index.isValid()) { if (!index.isValid()) {
@ -49,19 +56,24 @@ QVariant ChapterModel::data(const QModelIndex &index, int role) const
} }
int row = index.row(); int row = index.row();
if (m_chapters.at(row)) {
switch (role) { switch (role) {
case Title: case TitleRole:
return QVariant::fromValue(m_chapters.at(row).title); return QVariant::fromValue(m_chapters.at(row)->title());
case Link: case LinkRole:
return QVariant::fromValue(m_chapters.at(row).link); return QVariant::fromValue(m_chapters.at(row)->link());
case Image: case ImageRole:
return QVariant::fromValue(m_chapters.at(row).image); return QVariant::fromValue(m_chapters.at(row)->image());
case StartTime: case StartTimeRole:
return QVariant::fromValue(m_chapters.at(row).start); return QVariant::fromValue(m_chapters.at(row)->start());
case FormattedStartTime: case FormattedStartTimeRole:
return QVariant::fromValue(m_kformat.formatDuration(m_chapters.at(row).start * 1000)); return QVariant::fromValue(m_kformat.formatDuration(m_chapters.at(row)->start() * 1000));
default: case ChapterRole:
return QVariant::fromValue(m_chapters.at(row));
default:
return QVariant();
}
} else {
return QVariant(); return QVariant();
} }
} }
@ -78,11 +90,12 @@ int ChapterModel::rowCount(const QModelIndex &parent) const
QHash<int, QByteArray> ChapterModel::roleNames() const QHash<int, QByteArray> ChapterModel::roleNames() const
{ {
return { return {
{Title, "title"}, {TitleRole, "title"},
{Link, "link"}, {LinkRole, "link"},
{Image, "image"}, {ImageRole, "image"},
{StartTime, "start"}, {StartTimeRole, "start"},
{FormattedStartTime, "formattedStart"}, {FormattedStartTimeRole, "formattedStart"},
{ChapterRole, "chapter"},
}; };
} }
@ -96,6 +109,8 @@ void ChapterModel::setEntry(Entry *entry)
if (entry) { if (entry) {
m_entry = entry; m_entry = entry;
} else { } else {
qDeleteAll(m_chapters.begin(), m_chapters.end());
m_chapters.clear();
m_entry = nullptr; m_entry = nullptr;
} }
load(); load();
@ -105,11 +120,14 @@ void ChapterModel::setEntry(Entry *entry)
void ChapterModel::load() void ChapterModel::load()
{ {
beginResetModel(); beginResetModel();
qDeleteAll(m_chapters.begin(), m_chapters.end());
m_chapters = {}; m_chapters = {};
m_currentChapter = 0; m_currentChapter = 0;
if (m_entry) { if (m_entry) {
loadFromDatabase();
loadChaptersFromFile(); loadChaptersFromFile();
if (m_chapters.isEmpty()) {
loadFromDatabase();
}
} }
endResetModel(); endResetModel();
Q_EMIT currentChapterChanged(); Q_EMIT currentChapterChanged();
@ -123,11 +141,12 @@ void ChapterModel::loadFromDatabase()
query.bindValue(QStringLiteral(":id"), m_entry->id()); query.bindValue(QStringLiteral(":id"), m_entry->id());
Database::instance().execute(query); Database::instance().execute(query);
while (query.next()) { while (query.next()) {
ChapterEntry chapter{}; Chapter *chapter = new Chapter(m_entry,
chapter.title = query.value(QStringLiteral("title")).toString(); query.value(QStringLiteral("title")).toString(),
chapter.link = query.value(QStringLiteral("link")).toString(); query.value(QStringLiteral("link")).toString(),
chapter.image = query.value(QStringLiteral("image")).toString(); query.value(QStringLiteral("image")).toString(),
chapter.start = query.value(QStringLiteral("start")).toInt(); query.value(QStringLiteral("start")).toInt(),
this);
m_chapters << chapter; m_chapters << chapter;
} }
} }
@ -141,10 +160,10 @@ void ChapterModel::loadMPEGChapters(TagLib::MPEG::File &f)
for (const auto &frame : f.ID3v2Tag()->frameListMap()["CHAP"]) { for (const auto &frame : f.ID3v2Tag()->frameListMap()["CHAP"]) {
auto chapterFrame = dynamic_cast<TagLib::ID3v2::ChapterFrame *>(frame); auto chapterFrame = dynamic_cast<TagLib::ID3v2::ChapterFrame *>(frame);
ChapterEntry chapter{};
const auto &apicList = chapterFrame->embeddedFrameListMap()["APIC"]; const auto &apicList = chapterFrame->embeddedFrameListMap()["APIC"];
auto hash = QString::fromLatin1( QString image = QStringLiteral("%1,%2").arg(m_entry->id()).arg(chapterFrame->startTime());
QCryptographicHash::hash(QStringLiteral("%1,%2").arg(m_entry->id(), chapterFrame->startTime()).toLatin1(), QCryptographicHash::Md5).toHex()); // TODO: get hashed filename from a method in Fetcher
auto hash = QString::fromLatin1(QCryptographicHash::hash(image.toLatin1(), QCryptographicHash::Md5).toHex());
auto path = QStringLiteral("%1/images/%2").arg(StorageManager::instance().storagePath(), hash); auto path = QStringLiteral("%1/images/%2").arg(StorageManager::instance().storagePath(), hash);
if (!apicList.isEmpty()) { if (!apicList.isEmpty()) {
if (!QFileInfo(path).exists()) { if (!QFileInfo(path).exists()) {
@ -154,24 +173,23 @@ void ChapterModel::loadMPEGChapters(TagLib::MPEG::File &f)
file.write(QByteArray(apic.data(), apic.size())); file.write(QByteArray(apic.data(), apic.size()));
file.close(); file.close();
} }
chapter.image = path;
} else { } else {
chapter.image = QString(); image = QString();
} }
chapter.title = QString::fromStdString(chapterFrame->embeddedFrameListMap()["TIT2"].front()->toString().to8Bit(true)); QString title = QString::fromStdString(chapterFrame->embeddedFrameListMap()["TIT2"].front()->toString().to8Bit(true));
chapter.link = QString(); int start = chapterFrame->startTime() / 1000;
chapter.start = chapterFrame->startTime() / 1000; Chapter *chapter = new Chapter(m_entry, title, QString(), image, start, this);
auto originalChapter = std::find_if(m_chapters.begin(), m_chapters.end(), [chapter](auto it) { auto originalChapter = std::find_if(m_chapters.begin(), m_chapters.end(), [chapter](auto it) {
return chapter.start == it.start; return chapter->start() == it->start();
}); });
if (originalChapter != m_chapters.end()) { if (originalChapter != m_chapters.end()) {
originalChapter->image = chapter.image; (*originalChapter)->image() = chapter->image();
} else { } else {
m_chapters << chapter; m_chapters << chapter;
} }
} }
std::sort(m_chapters.begin(), m_chapters.end(), [](const ChapterEntry &a, const ChapterEntry &b) { std::sort(m_chapters.begin(), m_chapters.end(), [](const Chapter *a, const Chapter *b) {
return a.start < b.start; return a->start() < b->start();
}); });
} }
@ -187,21 +205,13 @@ void ChapterModel::loadChaptersFromFile()
} // TODO else... } // TODO else...
} }
int ChapterModel::currentChapter() const Chapter *ChapterModel::currentChapter() const
{ {
for (int i = 0; i < m_chapters.size(); i++) { for (int i = 0; i < m_chapters.size(); i++) {
if (m_chapters[i].start < AudioManager::instance().position() / 1000 if (m_chapters[i] && m_chapters[i]->start() < AudioManager::instance().position() / 1000
&& (i == m_chapters.size() - 1 || m_chapters[i + 1].start > AudioManager::instance().position() / 1000)) { && (i == m_chapters.size() - 1 || m_chapters[i + 1]->start() > AudioManager::instance().position() / 1000)) {
return i; return m_chapters[i];
} }
} }
return 0; return nullptr;
}
QString ChapterModel::currentChapterImage() const
{
if (m_chapters.size() <= m_currentChapter) {
return {};
}
return m_chapters[m_currentChapter].image;
} }

View File

@ -11,34 +11,29 @@
#include <mpegfile.h> #include <mpegfile.h>
#include "chapter.h"
#include "entry.h" #include "entry.h"
struct ChapterEntry {
QString title;
QString link;
QString image;
int start;
};
class ChapterModel : public QAbstractListModel class ChapterModel : public QAbstractListModel
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(Entry *entry READ entry WRITE setEntry NOTIFY entryChanged) Q_PROPERTY(Entry *entry READ entry WRITE setEntry NOTIFY entryChanged)
Q_PROPERTY(int currentChapter READ currentChapter NOTIFY currentChapterChanged) Q_PROPERTY(Chapter *currentChapter READ currentChapter NOTIFY currentChapterChanged)
Q_PROPERTY(QString currentChapterImage READ currentChapterImage NOTIFY currentChapterChanged)
public: public:
enum RoleNames { enum RoleNames {
Title = Qt::DisplayRole, TitleRole = Qt::DisplayRole,
Link = Qt::UserRole + 1, LinkRole = Qt::UserRole + 1,
Image, ImageRole,
StartTime, StartTimeRole,
FormattedStartTime, FormattedStartTimeRole,
ChapterRole,
}; };
Q_ENUM(RoleNames); Q_ENUM(RoleNames);
explicit ChapterModel(QObject *parent = nullptr); explicit ChapterModel(QObject *parent = nullptr);
~ChapterModel();
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
@ -47,8 +42,7 @@ public:
void setEntry(Entry *entry); void setEntry(Entry *entry);
Entry *entry() const; Entry *entry() const;
int currentChapter() const; Chapter *currentChapter() const;
QString currentChapterImage() const;
Q_SIGNALS: Q_SIGNALS:
void entryChanged(); void entryChanged();
@ -61,7 +55,7 @@ private:
void loadMPEGChapters(TagLib::MPEG::File &f); void loadMPEGChapters(TagLib::MPEG::File &f);
Entry *m_entry = nullptr; Entry *m_entry = nullptr;
QVector<ChapterEntry> m_chapters; QVector<Chapter *> m_chapters;
KFormat m_kformat; KFormat m_kformat;
int m_currentChapter = 0; int m_currentChapter = 0;
}; };

View File

@ -26,10 +26,11 @@ Kirigami.BasicListItem {
text: model.title text: model.title
subtitle: model.formattedStart subtitle: model.formattedStart
leading: Kirigami.Icon { leading: ImageWithFallback {
width: height imageSource: model.chapter.cachedImage
height: parent.height height: parent.height
source: Fetcher.image(model.image.length > 0 ? model.image : root.entry.image) width: height
fractionalRadius: 1.0 / 8.0
} }
trailing: Controls.ToolButton { trailing: Controls.ToolButton {

View File

@ -50,7 +50,7 @@ Rectangle {
spacing: Kirigami.Units.largeSpacing spacing: Kirigami.Units.largeSpacing
ImageWithFallback { ImageWithFallback {
id: mainImage id: mainImage
imageSource: AudioManager.entry ? ((chapterModel.currentChapterImage && chapterModel.currentChapterImage !== "") ? "file://" + Fetcher.image(chapterModel.currentChapterImage) : AudioManager.entry.cachedImage) : "no-image" imageSource: AudioManager.entry ? ((chapterModel.currentChapter && chapterModel.currentChapter !== undefined) ? chapterModel.currentChapter.cachedImage : AudioManager.entry.cachedImage) : "no-image"
height: controlsLayout.height height: controlsLayout.height
width: height width: height
absoluteRadius: 5 absoluteRadius: 5

View File

@ -56,7 +56,7 @@ Item {
anchors.fill: parent anchors.fill: parent
ImageWithFallback { ImageWithFallback {
imageSource: AudioManager.entry ? ((chapterModel.currentChapterImage && chapterModel.currentChapterImage !== "") ? "file://" + Fetcher.image(chapterModel.currentChapterImage) : AudioManager.entry.cachedImage) : "no-image" imageSource: AudioManager.entry ? ((chapterModel.currentChapter && chapterModel.currentChapter !== undefined) ? chapterModel.currentChapter.cachedImage : AudioManager.entry.cachedImage) : "no-image"
Layout.fillHeight: true Layout.fillHeight: true
Layout.preferredWidth: height Layout.preferredWidth: height
} }

View File

@ -117,7 +117,7 @@ Kirigami.Page {
width: root.isWidescreen ? Math.min(parent.height, parent.width / 2) : Math.min(parent.width, height) width: root.isWidescreen ? Math.min(parent.height, parent.width / 2) : Math.min(parent.width, height)
ImageWithFallback { ImageWithFallback {
imageSource: AudioManager.entry ? ((chapterModel.currentChapterImage && chapterModel.currentChapterImage !== "") ? "file://" + Fetcher.image(chapterModel.currentChapterImage) : AudioManager.entry.cachedImage) : "no-image" imageSource: AudioManager.entry ? ((chapterModel.currentChapter && chapterModel.currentChapter !== undefined) ? chapterModel.currentChapter.cachedImage : AudioManager.entry.cachedImage) : "no-image"
imageFillMode: Image.PreserveAspectCrop imageFillMode: Image.PreserveAspectCrop
anchors.centerIn: parent anchors.centerIn: parent
anchors.margins: 0 anchors.margins: 0