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
|
enclosuredownloadjob.cpp
|
||||||
storagemanager.cpp
|
storagemanager.cpp
|
||||||
storagemovejob.cpp
|
storagemovejob.cpp
|
||||||
|
models/chaptermodel.cpp
|
||||||
models/feedsmodel.cpp
|
models/feedsmodel.cpp
|
||||||
models/entriesmodel.cpp
|
models/entriesmodel.cpp
|
||||||
models/queuemodel.cpp
|
models/queuemodel.cpp
|
||||||
|
|
|
@ -47,6 +47,8 @@ bool Database::migrate()
|
||||||
TRUE_OR_RETURN(migrateTo3());
|
TRUE_OR_RETURN(migrateTo3());
|
||||||
if (dbversion < 4)
|
if (dbversion < 4)
|
||||||
TRUE_OR_RETURN(migrateTo4());
|
TRUE_OR_RETURN(migrateTo4());
|
||||||
|
if (dbversion < 5)
|
||||||
|
TRUE_OR_RETURN(migrateTo5());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,6 +112,15 @@ bool Database::migrateTo4()
|
||||||
return true;
|
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)
|
bool Database::execute(const QString &query)
|
||||||
{
|
{
|
||||||
QSqlQuery q;
|
QSqlQuery q;
|
||||||
|
|
|
@ -32,5 +32,6 @@ private:
|
||||||
bool migrateTo2();
|
bool migrateTo2();
|
||||||
bool migrateTo3();
|
bool migrateTo3();
|
||||||
bool migrateTo4();
|
bool migrateTo4();
|
||||||
|
bool migrateTo5();
|
||||||
void cleanup();
|
void cleanup();
|
||||||
};
|
};
|
||||||
|
|
|
@ -243,6 +243,11 @@ void DataManager::removeFeed(const int index)
|
||||||
query.bindValue(QStringLiteral(":feed"), feedurl);
|
query.bindValue(QStringLiteral(":feed"), feedurl);
|
||||||
Database::instance().execute(query);
|
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
|
// Delete Entries
|
||||||
query.prepare(QStringLiteral("DELETE FROM Entries WHERE feed=:feed;"));
|
query.prepare(QStringLiteral("DELETE FROM Entries WHERE feed=:feed;"));
|
||||||
query.bindValue(QStringLiteral(":feed"), feedurl);
|
query.bindValue(QStringLiteral(":feed"), feedurl);
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
#include <QTextDocumentFragment>
|
#include <QTextDocumentFragment>
|
||||||
|
#include <QTime>
|
||||||
#include <Syndication/Syndication>
|
#include <Syndication/Syndication>
|
||||||
|
|
||||||
#include "database.h"
|
#include "database.h"
|
||||||
|
@ -303,6 +303,22 @@ bool Fetcher::processEntry(Syndication::ItemPtr entry, const QString &url, bool
|
||||||
processAuthor(url, entry->id(), authorName, QLatin1String(""), QLatin1String(""));
|
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);
|
// only process first enclosure if there are multiple (e.g. mp3 and ogg);
|
||||||
// the first one is probably the podcast author's preferred version
|
// the first one is probably the podcast author's preferred version
|
||||||
// TODO: handle more than one enclosure?
|
// TODO: handle more than one enclosure?
|
||||||
|
@ -366,6 +382,25 @@ void Fetcher::processEnclosure(Syndication::EnclosurePtr enclosure, Syndication:
|
||||||
Database::instance().execute(query);
|
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
|
QString Fetcher::image(const QString &url) const
|
||||||
{
|
{
|
||||||
if (url.isEmpty()) {
|
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
|
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 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 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;
|
QNetworkReply *head(QNetworkRequest &request) const;
|
||||||
void setHeader(QNetworkRequest &request) const;
|
void setHeader(QNetworkRequest &request) const;
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
#include "feed.h"
|
#include "feed.h"
|
||||||
#include "fetcher.h"
|
#include "fetcher.h"
|
||||||
#include "kasts-version.h"
|
#include "kasts-version.h"
|
||||||
|
#include "models/chaptermodel.h"
|
||||||
#include "models/downloadmodel.h"
|
#include "models/downloadmodel.h"
|
||||||
#include "models/entriesmodel.h"
|
#include "models/entriesmodel.h"
|
||||||
#include "models/episodemodel.h"
|
#include "models/episodemodel.h"
|
||||||
|
@ -124,6 +125,7 @@ int main(int argc, char *argv[])
|
||||||
qmlRegisterType<EpisodeProxyModel>("org.kde.kasts", 1, 0, "EpisodeProxyModel");
|
qmlRegisterType<EpisodeProxyModel>("org.kde.kasts", 1, 0, "EpisodeProxyModel");
|
||||||
qmlRegisterType<Mpris2>("org.kde.kasts", 1, 0, "Mpris2");
|
qmlRegisterType<Mpris2>("org.kde.kasts", 1, 0, "Mpris2");
|
||||||
qmlRegisterType<PodcastSearchModel>("org.kde.kasts", 1, 0, "PodcastSearchModel");
|
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<EntriesModel>("org.kde.kasts", 1, 0, "EntriesModel", QStringLiteral("Get from Feed"));
|
||||||
qmlRegisterUncreatableType<Enclosure>("org.kde.kasts", 1, 0, "Enclosure", QStringLiteral("Only for enums"));
|
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) }
|
onWidthChanged: { text = entry.adjustedContent(width, font.pixelSize) }
|
||||||
font.pointSize: SettingsManager && !(SettingsManager.articleFontUseSystem) ? SettingsManager.articleFontSize : Kirigami.Theme.defaultFont.pointSize
|
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 {
|
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 {
|
Controls.PageIndicator {
|
||||||
|
@ -248,5 +270,4 @@ Kirigami.Page {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
<file alias="PlaybackRateDialog.qml">qml/PlaybackRateDialog.qml</file>
|
<file alias="PlaybackRateDialog.qml">qml/PlaybackRateDialog.qml</file>
|
||||||
<file alias="ErrorNotification.qml">qml/ErrorNotification.qml</file>
|
<file alias="ErrorNotification.qml">qml/ErrorNotification.qml</file>
|
||||||
<file alias="ConnectionCheckAction.qml">qml/ConnectionCheckAction.qml</file>
|
<file alias="ConnectionCheckAction.qml">qml/ConnectionCheckAction.qml</file>
|
||||||
|
<file alias="ChapterListDelegate.qml">qml/ChapterListDelegate.qml</file>
|
||||||
<file>qtquickcontrols2.conf</file>
|
<file>qtquickcontrols2.conf</file>
|
||||||
<file alias="logo.svg">../kasts.svg</file>
|
<file alias="logo.svg">../kasts.svg</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
|
|
Loading…
Reference in New Issue