Add chapter marks

This commit is contained in:
Swapnil Tripathi 2021-09-21 20:36:54 +00:00 committed by Bart De Vries
parent 4c2aa9e3ab
commit a141cda44a
13 changed files with 293 additions and 2 deletions

View File

@ -15,6 +15,7 @@ set(SRCS_base
enclosuredownloadjob.cpp
storagemanager.cpp
storagemovejob.cpp
models/chaptermodel.cpp
models/feedsmodel.cpp
models/entriesmodel.cpp
models/queuemodel.cpp

View File

@ -47,6 +47,8 @@ bool Database::migrate()
TRUE_OR_RETURN(migrateTo3());
if (dbversion < 4)
TRUE_OR_RETURN(migrateTo4());
if (dbversion < 5)
TRUE_OR_RETURN(migrateTo5());
return true;
}
@ -110,6 +112,15 @@ bool Database::migrateTo4()
return true;
}
bool Database::migrateTo5()
{
qDebug() << "Migrating database to version 5";
TRUE_OR_RETURN(execute(QStringLiteral("BEGIN TRANSACTION;")));
TRUE_OR_RETURN(execute(QStringLiteral("CREATE TABLE IF NOT EXISTS Chapters (feed TEXT, id TEXT, start INTEGER, title TEXT, link TEXT, image TEXT);")));
TRUE_OR_RETURN(execute(QStringLiteral("PRAGMA user_version = 5;")));
TRUE_OR_RETURN(execute(QStringLiteral("COMMIT;")));
return true;
}
bool Database::execute(const QString &query)
{
QSqlQuery q;

View File

@ -32,5 +32,6 @@ private:
bool migrateTo2();
bool migrateTo3();
bool migrateTo4();
bool migrateTo5();
void cleanup();
};

View File

@ -243,6 +243,11 @@ void DataManager::removeFeed(const int index)
query.bindValue(QStringLiteral(":feed"), feedurl);
Database::instance().execute(query);
// Delete Chapters
query.prepare(QStringLiteral("DELETE FROM Chapters WHERE feed=:feed;"));
query.bindValue(QStringLiteral(":feed"), feedurl);
Database::instance().execute(query);
// Delete Entries
query.prepare(QStringLiteral("DELETE FROM Entries WHERE feed=:feed;"));
query.bindValue(QStringLiteral(":feed"), feedurl);

View File

@ -19,7 +19,7 @@
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QTextDocumentFragment>
#include <QTime>
#include <Syndication/Syndication>
#include "database.h"
@ -303,6 +303,22 @@ bool Fetcher::processEntry(Syndication::ItemPtr entry, const QString &url, bool
processAuthor(url, entry->id(), authorName, QLatin1String(""), QLatin1String(""));
}
/* Process chapters */
if (otherItems.value(QStringLiteral("http://podlove.org/simple-chapterschapters")).hasChildNodes()) {
QDomNodeList nodelist = otherItems.value(QStringLiteral("http://podlove.org/simple-chapterschapters")).childNodes();
for (int i = 0; i < nodelist.length(); i++) {
if (nodelist.item(i).nodeName() == QStringLiteral("psc:chapter")) {
QDomElement element = nodelist.at(i).toElement();
QString title = element.attribute(QStringLiteral("title"));
QString start = element.attribute(QStringLiteral("start"));
QTime startString = QTime::fromString(start, QStringLiteral("hh:mm:ss.zzz"));
int startInt = startString.hour() * 60 * 60 + startString.minute() * 60 + startString.second();
QString images = element.attribute(QStringLiteral("image"));
processChapter(url, entry->id(), startInt, title, entry->link(), images);
}
}
}
// only process first enclosure if there are multiple (e.g. mp3 and ogg);
// the first one is probably the podcast author's preferred version
// TODO: handle more than one enclosure?
@ -366,6 +382,25 @@ void Fetcher::processEnclosure(Syndication::EnclosurePtr enclosure, Syndication:
Database::instance().execute(query);
}
void Fetcher::processChapter(const QString &url,
const QString &entryId,
const int &start,
const QString &chapterTitle,
const QString &link,
const QString &image)
{
QSqlQuery query;
query.prepare(QStringLiteral("INSERT INTO Chapters VALUES(:feed, :id, :start, :title, :link, :image);"));
query.bindValue(QStringLiteral(":feed"), url);
query.bindValue(QStringLiteral(":id"), entryId);
query.bindValue(QStringLiteral(":start"), start);
query.bindValue(QStringLiteral(":title"), chapterTitle);
query.bindValue(QStringLiteral(":link"), link);
query.bindValue(QStringLiteral(":image"), image);
Database::instance().execute(query);
}
QString Fetcher::image(const QString &url) const
{
if (url.isEmpty()) {

View File

@ -77,6 +77,7 @@ private:
bool processEntry(Syndication::ItemPtr entry, const QString &url, bool isNewFeed); // returns true if this is a new entry; false if it already existed
void processAuthor(const QString &url, const QString &entryId, const QString &authorName, const QString &authorUri, const QString &authorEmail);
void processEnclosure(Syndication::EnclosurePtr enclosure, Syndication::ItemPtr entry, const QString &feedUrl);
void processChapter(const QString &url, const QString &entryId, const int &start, const QString &chapterTitle, const QString &link, const QString &image);
QNetworkReply *head(QNetworkRequest &request) const;
void setHeader(QNetworkRequest &request) const;

View File

@ -39,6 +39,7 @@
#include "feed.h"
#include "fetcher.h"
#include "kasts-version.h"
#include "models/chaptermodel.h"
#include "models/downloadmodel.h"
#include "models/entriesmodel.h"
#include "models/episodemodel.h"
@ -124,6 +125,7 @@ int main(int argc, char *argv[])
qmlRegisterType<EpisodeProxyModel>("org.kde.kasts", 1, 0, "EpisodeProxyModel");
qmlRegisterType<Mpris2>("org.kde.kasts", 1, 0, "Mpris2");
qmlRegisterType<PodcastSearchModel>("org.kde.kasts", 1, 0, "PodcastSearchModel");
qmlRegisterType<ChapterModel>("org.kde.kasts", 1, 0, "ChapterModel");
qmlRegisterUncreatableType<EntriesModel>("org.kde.kasts", 1, 0, "EntriesModel", QStringLiteral("Get from Feed"));
qmlRegisterUncreatableType<Enclosure>("org.kde.kasts", 1, 0, "Enclosure", QStringLiteral("Only for enums"));

View File

@ -0,0 +1,95 @@
/**
* SPDX-FileCopyrightText: 2021 Swapnil Tripathi <swapnil06.st@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include "models/chaptermodel.h"
#include <QDebug>
#include <QObject>
#include <QSqlQuery>
#include "database.h"
ChapterModel::ChapterModel(QObject *parent)
: QAbstractListModel(parent)
{
}
QVariant ChapterModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return QVariant();
}
int row = index.row();
switch (role) {
case Title:
return QVariant::fromValue(m_chapters.at(row).title);
case Link:
return QVariant::fromValue(m_chapters.at(row).link);
case Image:
return QVariant::fromValue(m_chapters.at(row).image);
case StartTime:
return QVariant::fromValue(m_chapters.at(row).start);
case FormattedStartTime:
return QVariant::fromValue(m_kformat.formatDuration(m_chapters.at(row).start * 1000));
default:
return QVariant();
}
}
int ChapterModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return m_chapters.count();
}
QHash<int, QByteArray> ChapterModel::roleNames() const
{
return {
{Title, "title"},
{Link, "link"},
{Image, "image"},
{StartTime, "start"},
{FormattedStartTime, "formattedStart"},
};
}
QString ChapterModel::enclosureId() const
{
return m_enclosureId;
}
void ChapterModel::setEnclosureId(QString newEnclosureId)
{
m_enclosureId = newEnclosureId;
loadFromDatabase();
Q_EMIT enclosureIdChanged();
}
void ChapterModel::loadFromDatabase()
{
beginResetModel();
m_chapters = {};
QSqlQuery query;
query.prepare(QStringLiteral("SELECT * FROM Chapters WHERE id=:id"));
query.bindValue(QStringLiteral(":id"), enclosureId());
Database::instance().execute(query);
while (query.next()) {
ChapterEntry chapter{};
chapter.title = query.value(QStringLiteral("title")).toString();
chapter.link = query.value(QStringLiteral("link")).toString();
chapter.image = query.value(QStringLiteral("image")).toString();
chapter.start = query.value(QStringLiteral("start")).toInt();
m_chapters << chapter;
}
endResetModel();
}

52
src/models/chaptermodel.h Normal file
View File

@ -0,0 +1,52 @@
/**
* SPDX-FileCopyrightText: 2021 Swapnil Tripathi <swapnil06.st@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#pragma once
#include <KFormat>
#include <QAbstractListModel>
struct ChapterEntry {
QString title;
QString link;
QString image;
int start;
};
class ChapterModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(QString enclosureId READ enclosureId WRITE setEnclosureId NOTIFY enclosureIdChanged)
public:
enum RoleNames {
Title = Qt::UserRole,
Link,
Image,
StartTime,
FormattedStartTime,
};
explicit ChapterModel(QObject *parent = nullptr);
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent) const override;
void setEnclosureId(QString newEnclosureId);
QString enclosureId() const;
Q_SIGNALS:
void enclosureIdChanged();
private:
void loadFromDatabase();
QString m_enclosureId;
QVector<ChapterEntry> m_chapters;
KFormat m_kformat;
};

View File

@ -0,0 +1,52 @@
/**
* SPDX-FileCopyrightText: 2021 Bart De Vries <bart@mogwai.be>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
import QtQuick 2.14
import QtQuick.Controls 2.14 as Controls
import QtQuick.Layouts 1.14
import QtMultimedia 5.15
import org.kde.kirigami 2.14 as Kirigami
import org.kde.kasts 1.0
Kirigami.SwipeListItem {
alwaysVisibleActions: true
property var entry: undefined
ColumnLayout {
Controls.Label {
text: title
}
Controls.Label {
opacity: 0.7
font: Kirigami.Theme.smallFont
text: formattedStart
}
}
actions: [
Kirigami.Action {
text: i18n("Play")
icon.name: "media-playback-start"
enabled: entry != undefined && entry.enclosure && entry.enclosure.status === Enclosure.Downloaded
onTriggered: {
if (AudioManager.entry != entry) {
AudioManager.entry = entry;
}
if (AudioManager.playbbackState !== Audio.PlayingState) {
AudioManager.play();
}
AudioManager.position = start * 1000;
}
}
]
//onClicked: {
// AudioManager.position = start * 1000;
//}
}

View File

@ -86,6 +86,20 @@ Kirigami.ScrollablePage {
onWidthChanged: { text = entry.adjustedContent(width, font.pixelSize) }
font.pointSize: SettingsManager && !(SettingsManager.articleFontUseSystem) ? SettingsManager.articleFontSize : Kirigami.Theme.defaultFont.pointSize
}
ListView {
Layout.fillWidth: true
height: contentHeight
interactive: false
Layout.leftMargin: Kirigami.Units.gridUnit
Layout.rightMargin: Kirigami.Units.gridUnit
Layout.bottomMargin: Kirigami.Units.gridUnit
model: ChapterModel {
enclosureId: entry.id
}
delegate: ChapterListDelegate {
entry: page.entry
}
}
}
actions.main: Kirigami.Action {

View File

@ -110,6 +110,28 @@ Kirigami.Page {
}
}
}
Item {
Kirigami.PlaceholderMessage {
visible: chapterList.count === 0
width: parent.width
anchors.centerIn: parent
text: i18n("No chapter marks found.")
}
ListView {
id: chapterList
model: ChapterModel {
enclosureId: AudioManager.entry.id
}
clip: true
visible: chapterList.count !== 0
anchors.fill: parent
delegate: ChapterListDelegate {
entry: AudioManager.entry
}
}
}
}
Controls.PageIndicator {
@ -248,5 +270,4 @@ Kirigami.Page {
}
}
}
}

View File

@ -28,6 +28,7 @@
<file alias="PlaybackRateDialog.qml">qml/PlaybackRateDialog.qml</file>
<file alias="ErrorNotification.qml">qml/ErrorNotification.qml</file>
<file alias="ConnectionCheckAction.qml">qml/ConnectionCheckAction.qml</file>
<file alias="ChapterListDelegate.qml">qml/ChapterListDelegate.qml</file>
<file>qtquickcontrols2.conf</file>
<file alias="logo.svg">../kasts.svg</file>
</qresource>