Merge pull request #4651 from sobkas/fix

Fix the parsing of some strange date formats in podcasts, and add a way to limit the number of podcast episodes shown, and hide listened podcasts.  Fixes #3696, fixes #3475
This commit is contained in:
David Sansome 2014-12-16 12:33:50 +11:00
commit 6851a15dc0
9 changed files with 155 additions and 39 deletions

View File

@ -235,18 +235,15 @@ bool RemoveRecursive(const QString& path) {
QDir dir(path);
for (const QString& child :
dir.entryList(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Hidden)) {
if (!RemoveRecursive(path + "/" + child))
return false;
if (!RemoveRecursive(path + "/" + child)) return false;
}
for (const QString& child :
dir.entryList(QDir::NoDotAndDotDot | QDir::Files | QDir::Hidden)) {
if (!QFile::remove(path + "/" + child))
return false;
if (!QFile::remove(path + "/" + child)) return false;
}
if (!dir.rmdir(path))
return false;
if (!dir.rmdir(path)) return false;
return true;
}
@ -465,19 +462,18 @@ QByteArray HmacSha1(const QByteArray& key, const QByteArray& data) {
}
QByteArray Sha256(const QByteArray& data) {
#ifndef USE_SYSTEM_SHA2
using clementine_sha2::SHA256_CTX;
using clementine_sha2::SHA256_Init;
using clementine_sha2::SHA256_Update;
using clementine_sha2::SHA256_Final;
using clementine_sha2::SHA256_DIGEST_LENGTH;
#endif
#ifndef USE_SYSTEM_SHA2
using clementine_sha2::SHA256_CTX;
using clementine_sha2::SHA256_Init;
using clementine_sha2::SHA256_Update;
using clementine_sha2::SHA256_Final;
using clementine_sha2::SHA256_DIGEST_LENGTH;
#endif
SHA256_CTX context;
SHA256_Init(&context);
SHA256_Update(
&context, reinterpret_cast<const quint8*>(data.constData()),
data.length());
SHA256_Update(&context, reinterpret_cast<const quint8*>(data.constData()),
data.length());
QByteArray ret(SHA256_DIGEST_LENGTH, '\0');
SHA256_Final(reinterpret_cast<quint8*>(ret.data()), &context);
@ -571,17 +567,43 @@ bool ParseUntilElement(QXmlStreamReader* reader, const QString& name) {
QDateTime ParseRFC822DateTime(const QString& text) {
// This sucks but we need it because some podcasts don't quite follow the
// spec properly - they might have 1-digit hour numbers for example.
QDateTime ret;
QRegExp re(
"([a-zA-Z]{3}),? (\\d{1,2}) ([a-zA-Z]{3}) (\\d{4}) "
"(\\d{1,2}):(\\d{1,2}):(\\d{1,2})");
if (re.indexIn(text) == -1) return QDateTime();
if (re.indexIn(text) != -1) {
ret = QDateTime(
QDate::fromString(QString("%1 %2 %3 %4")
.arg(re.cap(1), re.cap(3), re.cap(2), re.cap(4)),
Qt::TextDate),
QTime(re.cap(5).toInt(), re.cap(6).toInt(), re.cap(7).toInt()));
}
if (ret.isValid()) return ret;
// Because http://feeds.feedburner.com/reasonabledoubts/Msxh?format=xml
QRegExp re2(
"(\\d{1,2}) ([a-zA-Z]{3}) (\\d{4}) "
"(\\d{1,2}):(\\d{1,2}):(\\d{1,2})");
return QDateTime(
QDate::fromString(QString("%1 %2 %3 %4")
.arg(re.cap(1), re.cap(3), re.cap(2), re.cap(4)),
Qt::TextDate),
QTime(re.cap(5).toInt(), re.cap(6).toInt(), re.cap(7).toInt()));
QMap<QString, int> monthmap;
monthmap["Jan"] = 1;
monthmap["Feb"] = 2;
monthmap["Mar"] = 3;
monthmap["Apr"] = 4;
monthmap["May"] = 5;
monthmap["Jun"] = 6;
monthmap["Jul"] = 7;
monthmap["Aug"] = 8;
monthmap["Sep"] = 9;
monthmap["Oct"] = 10;
monthmap["Nov"] = 11;
monthmap["Dec"] = 12;
if (re2.indexIn(text) != -1) {
QDate date(re2.cap(3).toInt(), monthmap[re2.cap(2)], re2.cap(1).toInt());
ret = QDateTime(date, QTime(re2.cap(4).toInt(), re2.cap(5).toInt(),
re2.cap(6).toInt()));
}
return ret;
}
const char* EnumToString(const QMetaObject& meta, const char* name, int value) {

View File

@ -143,7 +143,11 @@ void InternetModel::AddService(InternetService* service) {
SIGNAL(ScrollToIndex(QModelIndex)));
connect(service, SIGNAL(destroyed()), SLOT(ServiceDeleted()));
service->ReloadSettings();
if (service->has_initial_load_settings()) {
service->InitialLoadSettings();
} else {
service->ReloadSettings();
}
}
void InternetModel::RemoveService(InternetService* service) {

View File

@ -48,7 +48,8 @@ class InternetService : public QObject {
virtual QStandardItem* CreateRootItem() = 0;
virtual void LazyPopulate(QStandardItem* parent) = 0;
virtual bool has_initial_load_settings() const { return false; }
virtual void InitialLoadSettings() {}
virtual void ShowContextMenu(const QPoint& global_pos) {}
virtual void ItemDoubleClicked(QStandardItem* item) {}
// Create a generator for smart playlists

View File

@ -225,7 +225,8 @@ PodcastEpisodeList PodcastBackend::GetEpisodes(int podcast_id) {
QSqlQuery q("SELECT ROWID, " + PodcastEpisode::kColumnSpec +
" FROM podcast_episodes"
" WHERE podcast_id = :id",
" WHERE podcast_id = :id"
" ORDER BY publication_date DESC",
db);
q.bindValue(":db", podcast_id);
q.exec();

View File

@ -123,8 +123,7 @@ void PodcastParser::ParseChannel(QXmlStreamReader* reader, Podcast* ret) const {
ParseImage(reader, ret);
} else if (name == "copyright") {
ret->set_copyright(reader->readElementText());
} else if (name == "link" &&
lower_namespace == kAtomNamespace &&
} else if (name == "link" && lower_namespace == kAtomNamespace &&
ret->url().isEmpty() &&
reader->attributes().value("rel") == "self") {
ret->set_url(QUrl::fromEncoded(reader->readElementText().toAscii()));
@ -211,8 +210,15 @@ void PodcastParser::ParseItem(QXmlStreamReader* reader, Podcast* ret) const {
} else if (name == "description") {
episode.set_description(reader->readElementText());
} else if (name == "pubDate") {
episode.set_publication_date(
Utilities::ParseRFC822DateTime(reader->readElementText()));
QString date = reader->readElementText();
episode.set_publication_date(Utilities::ParseRFC822DateTime(date));
if (!episode.publication_date().isValid()) {
qLog(Error) << "Unable to parse date:" << date
<< "Please submit it to "
<< QUrl::toPercentEncoding(QString("https://github.com/clementine-player/Clementine/"
"issues/new?title=[podcast]"
" Unable to parse date: %1").arg(date));
}
} else if (name == "duration" && lower_namespace == kItunesNamespace) {
// http://www.apple.com/itunes/podcasts/specs.html
QStringList parts = reader->readElementText().split(':');
@ -238,6 +244,9 @@ void PodcastParser::ParseItem(QXmlStreamReader* reader, Podcast* ret) const {
}
case QXmlStreamReader::EndElement:
if (!episode.publication_date().isValid()) {
episode.set_publication_date(QDateTime::currentDateTime());
}
if (!episode.url().isEmpty()) {
ret->add_episode(episode);
}

View File

@ -58,6 +58,8 @@ class PodcastSortProxyModel : public QSortFilterProxyModel {
PodcastService::PodcastService(Application* app, InternetModel* parent)
: InternetService(kServiceName, app, parent, parent),
use_pretty_covers_(true),
hide_listened_(false),
show_episodes_(0),
icon_loader_(new StandardItemIconLoader(app->album_cover_loader(), this)),
backend_(app->podcast_backend()),
model_(new PodcastServiceModel(this)),
@ -232,6 +234,10 @@ void PodcastService::PopulatePodcastList(QStandardItem* parent) {
}
}
void PodcastService::ClearPodcastList(QStandardItem* parent) {
parent->removeRows(0, parent->rowCount());
}
void PodcastService::UpdatePodcastText(QStandardItem* item,
int unlistened_count) const {
const Podcast podcast = item->data(Role_Podcast).value<Podcast>();
@ -349,13 +355,23 @@ QStandardItem* PodcastService::CreatePodcastItem(const Podcast& podcast) {
// Add the episodes in this podcast and gather aggregate stats.
int unlistened_count = 0;
qint64 number = 0;
for (const PodcastEpisode& episode :
backend_->GetEpisodes(podcast.database_id())) {
if (!episode.listened()) {
unlistened_count++;
}
item->appendRow(CreatePodcastEpisodeItem(episode));
if (episode.listened() && hide_listened_) {
continue;
} else {
item->appendRow(CreatePodcastEpisodeItem(episode));
++number;
}
if ((number >= show_episodes_) && (show_episodes_ != 0)) {
break;
}
}
item->setIcon(default_icon_);
@ -418,9 +434,9 @@ void PodcastService::ShowContextMenu(const QPoint& global_pos) {
copy_to_device_ = context_menu_->addAction(
IconLoader::Load("multimedia-player-ipod-mini-blue"),
tr("Copy to device..."), this, SLOT(CopyToDevice()));
cancel_download_ = context_menu_->addAction(
IconLoader::Load("cancel"),
tr("Cancel download"), this, SLOT(CancelDownload()));
cancel_download_ = context_menu_->addAction(IconLoader::Load("cancel"),
tr("Cancel download"), this,
SLOT(CancelDownload()));
remove_selected_action_ = context_menu_->addAction(
IconLoader::Load("list-remove"), tr("Unsubscribe"), this,
SLOT(RemoveSelectedPodcast()));
@ -535,10 +551,20 @@ void PodcastService::RemoveSelectedPodcast() {
}
void PodcastService::ReloadSettings() {
InitialLoadSettings();
ClearPodcastList(model_->invisibleRootItem());
PopulatePodcastList(model_->invisibleRootItem());
}
void PodcastService::InitialLoadSettings() {
QSettings s;
s.beginGroup(LibraryView::kSettingsGroup);
use_pretty_covers_ = s.value("pretty_covers", true).toBool();
s.endGroup();
s.beginGroup(kSettingsGroup);
hide_listened_ = s.value("hide_listened", false).toBool();
show_episodes_ = s.value("show_episodes", 0).toInt();
s.endGroup();
// TODO(notme): reload the podcast icons that are already loaded?
}
@ -598,7 +624,6 @@ void PodcastService::EpisodesAdded(const PodcastEpisodeList& episodes) {
if (!parent) continue;
parent->appendRow(CreatePodcastEpisodeItem(episode));
if (!seen_podcast_ids.contains(database_id)) {
// Update the unlistened count text once for each podcast
int unlistened_count = 0;
@ -611,6 +636,8 @@ void PodcastService::EpisodesAdded(const PodcastEpisodeList& episodes) {
UpdatePodcastText(parent, unlistened_count);
seen_podcast_ids.insert(database_id);
}
const Podcast podcast = parent->data(Role_Podcast).value<Podcast>();
ReloadPodcast(podcast);
}
}
@ -622,7 +649,6 @@ void PodcastService::EpisodesUpdated(const PodcastEpisodeList& episodes) {
QStandardItem* item = episodes_by_database_id_[episode.database_id()];
QStandardItem* parent = podcasts_by_database_id_[podcast_database_id];
if (!item || !parent) continue;
// Update the episode data on the item, and update the item's text.
item->setData(QVariant::fromValue(episode), Role_Episode);
UpdateEpisodeText(item);
@ -641,6 +667,8 @@ void PodcastService::EpisodesUpdated(const PodcastEpisodeList& episodes) {
UpdatePodcastText(parent, unlistened_count);
seen_podcast_ids.insert(podcast_database_id);
}
const Podcast podcast = parent->data(Role_Podcast).value<Podcast>();
ReloadPodcast(podcast);
}
}
@ -780,3 +808,13 @@ void PodcastService::SubscribeAndShow(const QVariant& podcast_or_opml) {
add_podcast_dialog_->ShowWithOpml(podcast_or_opml.value<OpmlContainer>());
}
}
void PodcastService::ReloadPodcast(const Podcast& podcast) {
if (!(hide_listened_ || (show_episodes_ > 0))) {
return;
}
QStandardItem* item = podcasts_by_database_id_[podcast.database_id()];
model_->invisibleRootItem()->removeRow(item->row());
model_->invisibleRootItem()->appendRow(CreatePodcastItem(podcast));
}

View File

@ -58,10 +58,10 @@ class PodcastService : public InternetService {
QStandardItem* CreateRootItem();
void LazyPopulate(QStandardItem* parent);
bool has_initial_load_settings() const { return true; }
void ShowContextMenu(const QPoint& global_pos);
void ReloadSettings();
void InitialLoadSettings();
// Called by SongLoader when the user adds a Podcast URL directly. Adds a
// subscription to the podcast and displays it in the UI. If the QVariant
// contains an OPML file then this displays it in the Add Podcast dialog.
@ -73,6 +73,7 @@ class PodcastService : public InternetService {
private slots:
void UpdateSelectedPodcast();
void ReloadPodcast(const Podcast& podcast);
void RemoveSelectedPodcast();
void DownloadSelectedEpisode();
void DeleteDownloadedData();
@ -103,6 +104,7 @@ class PodcastService : public InternetService {
void UpdatePodcastListenedStateAsync(const Song& metadata);
void PopulatePodcastList(QStandardItem* parent);
void ClearPodcastList(QStandardItem* parent);
void UpdatePodcastText(QStandardItem* item, int unlistened_count) const;
void UpdateEpisodeText(
QStandardItem* item,
@ -126,6 +128,8 @@ class PodcastService : public InternetService {
private:
bool use_pretty_covers_;
bool hide_listened_;
qint64 show_episodes_;
StandardItemIconLoader* icon_loader_;
// The podcast icon

View File

@ -75,7 +75,9 @@ void PodcastSettingsPage::Load() {
s.value("download_dir", default_download_dir).toString()));
ui_->auto_download->setChecked(s.value("auto_download", false).toBool());
ui_->hide_listened->setChecked(s.value("hide_listened", false).toBool());
ui_->delete_after->setValue(s.value("delete_after", 0).toInt() / kSecsPerDay);
ui_->show_episodes->setValue(s.value("show_episodes", 0).toInt());
ui_->username->setText(s.value("gpodder_username").toString());
ui_->device_name->setText(
s.value("gpodder_device_name", GPodderSync::DefaultDeviceName())
@ -98,7 +100,9 @@ void PodcastSettingsPage::Save() {
s.setValue("download_dir",
QDir::fromNativeSeparators(ui_->download_dir->text()));
s.setValue("auto_download", ui_->auto_download->isChecked());
s.setValue("hide_listened", ui_->hide_listened->isChecked());
s.setValue("delete_after", ui_->delete_after->value() * kSecsPerDay);
s.setValue("show_episodes", ui_->show_episodes->value());
s.setValue("gpodder_device_name", ui_->device_name->text());
}

View File

@ -144,6 +144,39 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Appearance</string>
</property>
<layout class="QFormLayout" name="formLayout_4">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QCheckBox" name="hide_listened">
<property name="text">
<string>Don't show listened episodes</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="show_episodes">
<property name="specialValueText">
<string>All</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Number of episodes to show</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">