mirror of https://github.com/KDE/kasts.git
Add chapter marks
This commit is contained in:
parent
4c2aa9e3ab
commit
a141cda44a
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -32,5 +32,6 @@ private:
|
|||
bool migrateTo2();
|
||||
bool migrateTo3();
|
||||
bool migrateTo4();
|
||||
bool migrateTo5();
|
||||
void cleanup();
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
//}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue