CONFIG += release
TEMPLATE = app
-VERSION = 2.3
+VERSION = 2.4
DEFINES += APP_VERSION="$$VERSION"
APP_NAME = Minitube
DEFINES += APP_PHONON
DEFINES += APP_PHONON_SEEK
DEFINES += APP_SNAPSHOT
+DEFINES += APP_YT3
DEFINES *= QT_NO_DEBUG_OUTPUT
DEFINES *= QT_USE_QSTRINGBUILDER
DEFINES *= QT_STRICT_ITERATORS
+!contains(DEFINES, APP_GOOGLE_API_KEY) {
+ warning("You need to specify a Google API Key, refer to the README.md file for details")
+}
+
TARGET = $${APP_UNIX_NAME}
-QT += network xml sql script
+QT += network sql script
qt:greaterThan(QT_MAJOR_VERSION, 4) {
contains(QT, gui): QT *= widgets
}
src/gridwidget.h \
src/painterutils.h \
src/database.h \
- src/ytuser.h \
src/channelaggregator.h \
src/channelmodel.h \
src/aggregatevideosource.h \
src/seekslider.h \
src/snapshotsettings.h \
src/snapshotpreview.h \
- src/datautils.h
+ src/datautils.h \
+ src/yt3listparser.h \
+ src/ytchannel.h \
+ src/yt3.h \
+ src/paginatedvideosource.h
SOURCES += src/main.cpp \
src/searchlineedit.cpp \
src/urllineedit.cpp \
src/gridwidget.cpp \
src/painterutils.cpp \
src/database.cpp \
- src/ytuser.cpp \
src/channelaggregator.cpp \
src/channelmodel.cpp \
src/aggregatevideosource.cpp \
src/seekslider.cpp \
src/snapshotsettings.cpp \
src/snapshotpreview.cpp \
- src/datautils.cpp
+ src/datautils.cpp \
+ src/yt3listparser.cpp \
+ src/ytchannel.cpp \
+ src/yt3.cpp \
+ src/paginatedvideosource.cpp
RESOURCES += resources.qrc
DESTDIR = build/target/
OBJECTS_DIR = build/obj/
AggregateVideoSource::AggregateVideoSource(QObject *parent) :
VideoSource(parent),
- unwatched(false) { }
+ unwatched(false), hasMore(true) { }
-void AggregateVideoSource::loadVideos(int max, int skip) {
+void AggregateVideoSource::loadVideos(int max, int startIndex) {
QSqlDatabase db = Database::instance().getConnection();
QSqlQuery query(db);
QString sql = "select v.video_id,"
sql += " from subscriptions_videos v order by published desc ";
sql += "limit ?,?";
query.prepare(sql);
- query.bindValue(0, skip - 1);
+ query.bindValue(0, startIndex - 1);
query.bindValue(1, max);
bool success = query.exec();
if (!success) qWarning() << query.lastQuery() << query.lastError().text();
video->setId(query.value(0).toString());
video->setPublished(QDateTime::fromTime_t(query.value(1).toUInt()));
video->setTitle(query.value(2).toString());
- video->setAuthor(query.value(3).toString());
- video->setUserId(query.value(4).toString());
+ video->setChannelTitle(query.value(3).toString());
+ video->setChannelId(query.value(4).toString());
video->setDescription(query.value(5).toString());
video->setWebpage(query.value(6).toString());
video->setThumbnailUrl(query.value(7).toString());
video->setDuration(query.value(9).toInt());
videos << video;
}
+
+ hasMore = videos.size() >= max;
+
emit gotVideos(videos);
emit finished(videos.size());
}
+bool AggregateVideoSource::hasMoreVideos() {
+ return hasMore;
+}
+
const QStringList & AggregateVideoSource::getSuggestions() {
QStringList *l = new QStringList();
return *l;
public:
AggregateVideoSource(QObject *parent = 0);
- void loadVideos(int max, int skip);
+ void loadVideos(int max, int startIndex);
+ bool hasMoreVideos();
virtual void abort();
virtual const QStringList & getSuggestions();
QString getName() { return name; }
private:
QString name;
bool unwatched;
+ bool hasMore;
};
$END_LICENSE */
#include "channelaggregator.h"
-#include "ytuser.h"
+#include "ytchannel.h"
#include "ytsearch.h"
#include "searchparams.h"
#include "database.h"
unwatchedCount(-1),
running(false),
stopped(false) {
- QSettings settings;
- checkInterval = settings.value("subscriptionsCheckInterval", 1800).toUInt();
+ checkInterval = 3600;
timer = new QTimer(this);
timer->setInterval(60000 * 5);
}
void ChannelAggregator::start() {
+ stopped = false;
updateUnwatchedCount();
QTimer::singleShot(0, this, SLOT(run()));
- timer->start();
+ if (!timer->isActive()) timer->start();
}
void ChannelAggregator::stop() {
stopped = true;
}
-YTUser* ChannelAggregator::getChannelToCheck() {
+YTChannel* ChannelAggregator::getChannelToCheck() {
if (stopped) return 0;
QSqlDatabase db = Database::instance().getConnection();
QSqlQuery query(db);
query.prepare("select user_id from subscriptions where checked<? "
"order by checked limit 1");
- query.bindValue(0, QDateTime::currentDateTime().toTime_t() - checkInterval);
+ query.bindValue(0, QDateTime::currentDateTimeUtc().toTime_t() - checkInterval);
bool success = query.exec();
if (!success) qWarning() << query.lastQuery() << query.lastError().text();
if (query.next())
- return YTUser::forId(query.value(0).toString());
+ return YTChannel::forId(query.value(0).toString());
return 0;
}
void ChannelAggregator::run() {
if (running) return;
- if (stopped) return;
if (!Database::exists()) return;
running = true;
newVideoCount = 0;
updatedChannels.clear();
+
if (!Database::instance().getConnection().transaction())
qWarning() << "Transaction failed" << __PRETTY_FUNCTION__;
+
processNextChannel();
}
void ChannelAggregator::processNextChannel() {
- if (stopped) return;
+ if (stopped) {
+ running = false;
+ return;
+ }
qApp->processEvents();
- YTUser* user = getChannelToCheck();
- if (user) {
+ YTChannel* channel = getChannelToCheck();
+ if (channel) {
SearchParams *params = new SearchParams();
- params->setAuthor(user->getUserId());
+ params->setChannelId(channel->getChannelId());
params->setSortBy(SearchParams::SortByNewest);
params->setTransient(true);
+ params->setPublishedAfter(channel->getChecked());
YTSearch *videoSource = new YTSearch(params, this);
- connect(videoSource, SIGNAL(gotVideos(QList<Video*>)),
- SLOT(videosLoaded(QList<Video*>)));
- videoSource->loadVideos(10, 1);
- user->updateChecked();
+ connect(videoSource, SIGNAL(gotVideos(QList<Video*>)), SLOT(videosLoaded(QList<Video*>)));
+ videoSource->loadVideos(50, 1);
+ channel->updateChecked();
} else finish();
}
void ChannelAggregator::finish() {
- foreach (YTUser *user, updatedChannels)
- if (user->updateNotifyCount())
- emit channelChanged(user);
-
+ /*
+ foreach (YTChannel *channel, updatedChannels)
+ if (channel->updateNotifyCount())
+ emit channelChanged(channel);
updateUnwatchedCount();
+ */
QSqlDatabase db = Database::instance().getConnection();
if (!db.commit())
qWarning() << "Commit failed" << __PRETTY_FUNCTION__;
+
/*
QByteArray b = db.databaseName().right(20).toLocal8Bit();
const char* s = b.constData();
QString channelNames;
const int total = updatedChannels.size();
for (int i = 0; i < total; ++i) {
- YTUser *user = updatedChannels.at(i);
- channelNames += user->getDisplayName();
+ YTChannel *channel = updatedChannels.at(i);
+ channelNames += channel->getDisplayName();
if (i < total-1) channelNames.append(", ");
}
channelNames = tr("By %1").arg(channelNames);
running = false;
}
-void ChannelAggregator::videosLoaded(QList<Video *> videos) {
+void ChannelAggregator::videosLoaded(const QList<Video*> &videos) {
sender()->deleteLater();
+
foreach (Video* video, videos) {
- qApp->processEvents();
addVideo(video);
- video->deleteLater();
+ qApp->processEvents();
}
- processNextChannel();
+
+ if (!videos.isEmpty()) {
+ YTChannel *channel = YTChannel::forId(videos.first()->channelId());
+ channel->updateNotifyCount();
+ emit channelChanged(channel);
+ updateUnwatchedCount();
+ foreach (Video* video, videos) video->deleteLater();
+ }
+
+ QTimer::singleShot(1000, this, SLOT(processNextChannel()));
}
void ChannelAggregator::updateUnwatchedCount() {
// qDebug() << "Inserting" << video->author() << video->title();
- QString userId = video->userId();
- YTUser *user = YTUser::forId(userId);
- if (!updatedChannels.contains(user))
- updatedChannels << user;
- int channelId = user->getId();
+ YTChannel *channel = YTChannel::forId(video->channelId());
+ if (!channel) {
+ qWarning() << "channelId not present in db" << video->channelId() << video->channelTitle();
+ return;
+ }
+
+ if (!updatedChannels.contains(channel))
+ updatedChannels << channel;
- uint now = QDateTime::currentDateTime().toTime_t();
+ uint now = QDateTime::currentDateTimeUtc().toTime_t();
uint published = video->published().toTime_t();
if (published > now) {
qDebug() << "fixing publish time";
"title,author,user_id,description,url,thumb_url,views,duration) "
"values (?,?,?,?,?,?,?,?,?,?,?,?,?)");
query.bindValue(0, video->id());
- query.bindValue(1, channelId);
+ query.bindValue(1, channel->getId());
query.bindValue(2, published);
query.bindValue(3, now);
query.bindValue(4, 0);
query.bindValue(5, video->title());
- query.bindValue(6, video->author());
- query.bindValue(7, video->userId());
+ query.bindValue(6, video->channelTitle());
+ query.bindValue(7, video->channelId());
query.bindValue(8, video->description());
query.bindValue(9, video->webpage());
query.bindValue(10, video->thumbnailUrl());
query = QSqlQuery(db);
query.prepare("update subscriptions set updated=? where user_id=?");
query.bindValue(0, published);
- query.bindValue(1, userId);
+ query.bindValue(1, channel->getChannelId());
success = query.exec();
if (!success) qWarning() << query.lastQuery() << query.lastError().text();
}
void ChannelAggregator::markAllAsWatched() {
- uint now = QDateTime::currentDateTime().toTime_t();
+ uint now = QDateTime::currentDateTimeUtc().toTime_t();
QSqlDatabase db = Database::instance().getConnection();
QSqlQuery query(db);
if (!success) qWarning() << query.lastQuery() << query.lastError().text();
unwatchedCount = 0;
- foreach (YTUser *user, YTUser::getCachedUsers()) {
- user->setWatched(now);
- user->setNotifyCount(0);
+ foreach (YTChannel *channel, YTChannel::getCachedChannels()) {
+ channel->setWatched(now);
+ channel->setNotifyCount(0);
}
emit unwatchedCountChanged(0);
QSqlDatabase db = Database::instance().getConnection();
QSqlQuery query(db);
query.prepare("update subscriptions_videos set watched=? where video_id=?");
- query.bindValue(0, QDateTime::currentDateTime().toTime_t());
+ query.bindValue(0, QDateTime::currentDateTimeUtc().toTime_t());
query.bindValue(1, video->id());
bool success = query.exec();
if (!success) qWarning() << query.lastQuery() << query.lastError().text();
if (query.numRowsAffected() > 0) {
- YTUser *user = YTUser::forId(video->userId());
- user->updateNotifyCount();
+ YTChannel *channel = YTChannel::forId(video->channelId());
+ channel->updateNotifyCount();
}
}
void ChannelAggregator::cleanup() {
- static const int maxVideos = 1000;
- static const int maxDeletions = 1000;
+ const int maxVideos = 1000;
+ const int maxDeletions = 1000;
if (!Database::exists()) return;
QSqlDatabase db = Database::instance().getConnection();
#include <QtCore>
-class YTUser;
+class YTChannel;
class Video;
class ChannelAggregator : public QObject {
void updateUnwatchedCount();
signals:
- void channelChanged(YTUser*);
+ void channelChanged(YTChannel*);
void unwatchedCountChanged(int count);
private slots:
- void videosLoaded(QList<Video*> videos);
+ void videosLoaded(const QList<Video*> &videos);
+ void processNextChannel();
private:
ChannelAggregator(QObject *parent = 0);
- YTUser* getChannelToCheck();
- void processNextChannel();
+ YTChannel* getChannelToCheck();
void addVideo(Video* video);
void finish();
bool running;
int newVideoCount;
- QList<YTUser*> updatedChannels;
+ QList<YTChannel*> updatedChannels;
QTimer *timer;
bool stopped;
#include "channelitemdelegate.h"
#include "channelmodel.h"
-#include "ytuser.h"
+#include "ytchannel.h"
#include "fontutils.h"
#include "channelaggregator.h"
#include "painterutils.h"
void ChannelItemDelegate::paintChannel(QPainter* painter,
const QStyleOptionViewItem& option,
const QModelIndex& index) const {
- const QVariant dataObject = index.data(ChannelModel::DataObjectRole);
- const YTUserPointer channelPointer = dataObject.value<YTUserPointer>();
- YTUser *user = channelPointer.data();
- if (!user) return;
+ YTChannel *channel = index.data(ChannelModel::DataObjectRole).value<YTChannelPointer>().data();
+ if (!channel) return;
painter->save();
// const bool isHovered = index.data(ChannelsModel::HoveredItemRole ).toBool();
// const bool isSelected = option.state & QStyle::State_Selected;
- QPixmap thumbnail = user->getThumbnail();
+ QPixmap thumbnail = channel->getThumbnail();
if (thumbnail.isNull()) {
- user->loadThumbnail();
+ channel->loadThumbnail();
painter->restore();
return;
}
- QString name = user->getDisplayName();
+ QString name = channel->getDisplayName();
drawItem(painter, line, thumbnail, name);
- int notifyCount = user->getNotifyCount();
+ int notifyCount = channel->getNotifyCount();
if (notifyCount > 0)
paintBadge(painter, line, QString::number(notifyCount));
$END_LICENSE */
#include "channelmodel.h"
-#include "ytuser.h"
+#include "ytchannel.h"
static const int channelOffset = 2;
case ChannelModel::DataObjectRole:
if (typeForIndex(index) == ChannelModel::ItemChannel)
- return QVariant::fromValue(QPointer<YTUser>(userForIndex(index)));
+ return QVariant::fromValue(QPointer<YTChannel>(channelForIndex(index)));
break;
case ChannelModel::HoveredItemRole:
case Qt::StatusTipRole:
if (typeForIndex(index) == ChannelModel::ItemChannel)
- return userForIndex(index)->getDescription();
+ return channelForIndex(index)->getDescription();
}
return QVariant();
}
-YTUser* ChannelModel::userForIndex(const QModelIndex &index) const {
+YTChannel* ChannelModel::channelForIndex(const QModelIndex &index) const {
const int row = index.row();
if (row < channelOffset) return 0;
return channels.at(index.row() - channelOffset);
sqlError = q.lastError();
}
while (q.next()) {
- YTUser *user = YTUser::forId(q.value(0).toString());
- connect(user, SIGNAL(thumbnailLoaded()), SLOT(updateSender()), Qt::UniqueConnection);
- connect(user, SIGNAL(notifyCountChanged()), SLOT(updateSender()), Qt::UniqueConnection);
- connect(user, SIGNAL(destroyed(QObject *)), SLOT(removeChannel(QObject *)), Qt::UniqueConnection);
- channels << user;
+ YTChannel *channel = YTChannel::forId(q.value(0).toString());
+ connect(channel, SIGNAL(thumbnailLoaded()), SLOT(updateSender()), Qt::UniqueConnection);
+ connect(channel, SIGNAL(notifyCountChanged()), SLOT(updateSender()), Qt::UniqueConnection);
+ connect(channel, SIGNAL(destroyed(QObject *)), SLOT(removeChannel(QObject *)), Qt::UniqueConnection);
+ channels << channel;
}
endResetModel();
}
}
void ChannelModel::updateSender() {
- YTUser *user = static_cast<YTUser*>(sender());
- if (!user) {
+ YTChannel *channel = static_cast<YTChannel*>(sender());
+ if (!channel) {
qWarning() << "Cannot get sender" << __PRETTY_FUNCTION__;
return;
}
- updateChannel(user);
+ updateChannel(channel);
}
-void ChannelModel::updateChannel(YTUser *user) {
- int row = channels.indexOf(user);
+void ChannelModel::updateChannel(YTChannel *channel) {
+ int row = channels.indexOf(channel);
if (row == -1) return;
row += channelOffset;
QModelIndex i = createIndex(row, 0);
}
void ChannelModel::removeChannel(QObject *obj) {
- YTUser *user = static_cast<YTUser*>(obj);
- qWarning() << "user is" << user << obj << obj->metaObject()->className();
- if (!user) return;
+ YTChannel *channel = static_cast<YTChannel*>(obj);
+ // qWarning() << "channel" << channel << obj << obj->metaObject()->className();
+ if (!channel) return;
- int row = channels.indexOf(user);
+ int row = channels.indexOf(channel);
if (row == -1) return;
int position = row + channelOffset;
#include <QtCore>
#include <QtSql>
-class YTUser;
+class YTChannel;
class ChannelModel : public QAbstractListModel {
void setQuery(const QString &query, const QSqlDatabase &db);
QSqlError lastError() const;
ItemTypes typeForIndex(const QModelIndex &index) const;
- YTUser* userForIndex(const QModelIndex &index) const;
+ YTChannel* channelForIndex(const QModelIndex &index) const;
void setHoveredRow(int row);
int rowCount(const QModelIndex &parent = QModelIndex()) const;
public slots:
void clearHover();
void updateSender();
- void updateChannel(YTUser *user);
+ void updateChannel(YTChannel *channel);
void updateUnwatched();
void removeChannel(QObject *obj);
private:
- QList<YTUser*> channels;
+ QList<YTChannel*> channels;
int hoveredRow;
QSqlError sqlError;
--- /dev/null
+#include "channelsitemdelegate.h"
+#include "channelmodel.h"
+#include "ytuser.h"
+#include "fontutils.h"
+#include "channelaggregator.h"
+#include "painterutils.h"
+
+static const int ITEM_WIDTH = 128;
+static const int ITEM_HEIGHT = 128;
+static const int THUMB_WIDTH = 88;
+static const int THUMB_HEIGHT = 88;
+
+ChannelsItemDelegate::ChannelsItemDelegate(QObject *parent) : QStyledItemDelegate(parent) {
+
+}
+
+QSize ChannelsItemDelegate::sizeHint(const QStyleOptionViewItem& /*option*/,
+ const QModelIndex& /*index*/ ) const {
+ return QSize(ITEM_WIDTH, ITEM_HEIGHT);
+}
+
+void ChannelsItemDelegate::paint( QPainter* painter,
+ const QStyleOptionViewItem& option,
+ const QModelIndex& index ) const {
+ const int itemType = index.data(ChannelModel::ItemTypeRole).toInt();
+ if (itemType == ChannelModel::ItemChannel)
+ paintChannel(painter, option, index);
+ else if (itemType == ChannelModel::ItemAggregate)
+ paintAggregate(painter, option, index);
+ else if (itemType == ChannelModel::ItemUnwatched)
+ paintUnwatched(painter, option, index);
+ else
+ QStyledItemDelegate::paint(painter, option, index);
+}
+
+void ChannelsItemDelegate::paintAggregate(QPainter* painter,
+ const QStyleOptionViewItem& option,
+ const QModelIndex& index) const {
+ painter->save();
+
+ painter->translate(option.rect.topLeft());
+ const QRect line(0, 0, option.rect.width(), option.rect.height());
+
+ static const QPixmap thumbnail = QPixmap(":/images/channels.png");
+
+ QString name = tr("All Videos");
+
+ drawItem(painter, line, thumbnail, name);
+
+ painter->restore();
+}
+
+void ChannelsItemDelegate::paintUnwatched(QPainter* painter,
+ const QStyleOptionViewItem& option,
+ const QModelIndex& index) const {
+ painter->save();
+
+ painter->translate(option.rect.topLeft());
+ const QRect line(0, 0, option.rect.width(), option.rect.height());
+
+ static const QPixmap thumbnail = QPixmap(":/images/unwatched.png");
+
+ QString name = tr("Unwatched Videos");
+
+ drawItem(painter, line, thumbnail, name);
+
+ int notifyCount = ChannelAggregator::instance()->getUnwatchedCount();
+ QString notifyText = QString::number(notifyCount);
+ if (notifyCount > 0) paintBadge(painter, line, notifyText);
+
+ painter->restore();
+}
+
+void ChannelsItemDelegate::paintChannel(QPainter* painter,
+ const QStyleOptionViewItem& option,
+ const QModelIndex& index) const {
+ const QVariant dataObject = index.data(ChannelModel::DataObjectRole);
+ const YTUserPointer channelPointer = dataObject.value<YTUserPointer>();
+ YTUser *user = channelPointer.data();
+ if (!user) return;
+
+ painter->save();
+
+ painter->translate(option.rect.topLeft());
+ const QRect line(0, 0, option.rect.width(), option.rect.height());
+
+ // const bool isActive = index.data( ActiveItemRole ).toBool();
+ // const bool isHovered = index.data(ChannelsModel::HoveredItemRole ).toBool();
+ // const bool isSelected = option.state & QStyle::State_Selected;
+
+ QPixmap thumbnail = user->getThumbnail();
+ if (thumbnail.isNull()) {
+ user->loadThumbnail();
+ painter->restore();
+ return;
+ }
+
+ QString name = user->getDisplayName();
+ drawItem(painter, line, thumbnail, name);
+
+ int notifyCount = user->getNotifyCount();
+ if (notifyCount > 0)
+ paintBadge(painter, line, QString::number(notifyCount));
+
+ painter->restore();
+}
+
+void ChannelsItemDelegate::drawItem(QPainter *painter,
+ const QRect &line,
+ const QPixmap &thumbnail,
+ const QString &name) const {
+ painter->drawPixmap((line.width() - THUMB_WIDTH) / 2, 8, thumbnail);
+
+ QRect nameBox = line;
+ nameBox.adjust(0, 0, 0, -THUMB_HEIGHT - 16);
+ nameBox.translate(0, line.height() - nameBox.height());
+ bool tooBig = false;
+
+#ifdef APP_MAC_NO
+ QFont f = painter->font();
+ f.setFamily("Helvetica");
+ painter->setFont(f);
+#endif
+
+ QRect textBox = painter->boundingRect(nameBox,
+ Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap,
+ name);
+ if (textBox.height() > nameBox.height() || textBox.width() > nameBox.width()) {
+ painter->setFont(FontUtils::small());
+ textBox = painter->boundingRect(nameBox,
+ Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap,
+ name);
+ if (textBox.height() > nameBox.height()) {
+ painter->setClipRect(nameBox);
+ tooBig = true;
+ }
+ }
+ if (tooBig)
+ painter->drawText(nameBox, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, name);
+ else
+ painter->drawText(textBox, Qt::AlignCenter | Qt::TextWordWrap, name);
+}
+
+void ChannelsItemDelegate::paintBadge(QPainter *painter,
+ const QRect &line,
+ const QString &text) const {
+ const int topLeft = (line.width() + THUMB_WIDTH) / 2;
+ painter->save();
+ painter->translate(topLeft, 0);
+ PainterUtils::paintBadge(painter, text, true);
+ painter->restore();
+}
--- /dev/null
+#ifndef CHANNELSITEMDELEGATE_H
+#define CHANNELSITEMDELEGATE_H
+
+#include <QtGui>
+
+class ChannelsItemDelegate : public QStyledItemDelegate {
+
+ Q_OBJECT
+
+public:
+ ChannelsItemDelegate(QObject* parent = 0);
+ QSize sizeHint(const QStyleOptionViewItem&, const QModelIndex&) const;
+ void paint(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const;
+
+private:
+ void paintChannel(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const;
+ void paintAggregate(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const;
+ void paintUnwatched(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const;
+ void paintBadge(QPainter *painter, const QRect &line, const QString &text) const;
+ void drawItem(QPainter*, const QRect &line, const QPixmap &thumbnail, const QString &name) const;
+
+};
+
+#endif // CHANNELSITEMDELEGATE_H
--- /dev/null
+#include "channelsmodel.h"
+#include "ytuser.h"
+#include "mainwindow.h"
+
+ChannelsModel::ChannelsModel(QObject *parent) : QSqlQueryModel(parent) {
+ hoveredRow = -1;
+}
+
+QVariant ChannelsModel::data(const QModelIndex &index, int role) const {
+
+ YTUser* user = 0;
+
+ switch (role) {
+
+ case ChannelsModel::ItemTypeRole:
+ return ChannelsModel::ItemChannel;
+ break;
+
+ case ChannelsModel::DataObjectRole:
+ user = userForIndex(index);
+ return QVariant::fromValue(QPointer<YTUser>(user));
+ break;
+
+ case ChannelsModel::HoveredItemRole:
+ return hoveredRow == index.row();
+ break;
+
+ case Qt::StatusTipRole:
+ user = userForIndex(index);
+ return user->getDescription();
+
+ }
+
+ return QVariant();
+}
+
+YTUser* ChannelsModel::userForIndex(const QModelIndex &index) const {
+ return YTUser::forId(QSqlQueryModel::data(QSqlQueryModel::index(index.row(), 0)).toString());
+}
+
+void ChannelsModel::setHoveredRow(int row) {
+ int oldRow = hoveredRow;
+ hoveredRow = row;
+ emit dataChanged( createIndex( oldRow, 0 ), createIndex( oldRow, columnCount() - 1 ) );
+ emit dataChanged( createIndex( hoveredRow, 0 ), createIndex( hoveredRow, columnCount() - 1 ) );
+}
+
+void ChannelsModel::clearHover() {
+ emit dataChanged( createIndex( hoveredRow, 0 ), createIndex( hoveredRow, columnCount() - 1 ) );
+ hoveredRow = -1;
+}
+
+// --- Sturm und drang ---
+
+
+Qt::DropActions ChannelsModel::supportedDragActions() const {
+ return Qt::CopyAction;
+}
+
+Qt::DropActions ChannelsModel::supportedDropActions() const {
+ return Qt::CopyAction;
+}
+
+Qt::ItemFlags ChannelsModel::flags(const QModelIndex &index) const {
+ if (index.isValid())
+ return Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled;
+ else return 0;
+}
+
+QStringList ChannelsModel::mimeTypes() const {
+ QStringList types;
+ types << "x-minitube/channel";
+ return types;
+}
+
+QMimeData* ChannelsModel::mimeData( const QModelIndexList &indexes ) const {
+
+ /* TODO
+ UserMimeData* mime = new TrackMimeData();
+
+ foreach( const QModelIndex &index, indexes ) {
+ Item *item = userForIndex(index);
+ if (item) {
+ mime->addTracks(item->getTracks());
+ }
+ }
+
+ return mime;
+ */
+}
--- /dev/null
+#ifndef CHANNELSMODEL_H
+#define CHANNELSMODEL_H
+
+#include <QtSql>
+
+class YTUser;
+
+class ChannelsModel : public QSqlQueryModel {
+
+ Q_OBJECT
+
+public:
+ ChannelsModel(QObject *parent = 0);
+ QVariant data(const QModelIndex &item, int role) const;
+ void setHoveredRow(int row);
+ YTUser* userForIndex(const QModelIndex &index) const;
+
+ enum DataRoles {
+ ItemTypeRole = Qt::UserRole,
+ DataObjectRole,
+ ActiveItemRole,
+ HoveredItemRole
+ };
+
+ enum ItemTypes {
+ ItemChannel = 1,
+ ItemFolder,
+ ItemAggregate
+ };
+
+public slots:
+ void clearHover();
+
+protected:
+ Qt::ItemFlags flags(const QModelIndex &index) const;
+ QStringList mimeTypes() const;
+ Qt::DropActions supportedDropActions() const;
+ Qt::DropActions supportedDragActions() const;
+ QMimeData* mimeData( const QModelIndexList &indexes ) const;
+
+private:
+ int hoveredRow;
+};
+
+#endif // CHANNELSMODEL_H
}
void ChannelSuggest::suggest(const QString &query) {
- QUrl url("http://www.youtube.com/results");
+ QUrl url("https://www.youtube.com/results");
#if QT_VERSION >= 0x050000
{
QUrl &u = url;
QList<Suggestion*> suggestions;
QString html = QString::fromUtf8(data);
- QRegExp re("/user/([a-zA-Z0-9]+)");
+ QRegExp re("/(?:user|channel)/[a-zA-Z0-9]+[^>]+data-ytid=[\"']([^\"']+)[\"'][^>]+>([a-zA-Z0-9 ]+)</a>");
int pos = 0;
while ((pos = re.indexIn(html, pos)) != -1) {
- // qDebug() << re.cap(0) << re.cap(1);
- QString choice = re.cap(1);
+ QString choice = re.cap(2);
if (!choices.contains(choice, Qt::CaseInsensitive)) {
- suggestions << new Suggestion(choice);
+ qDebug() << re.capturedTexts();
+ QString channelId = re.cap(1);
+ suggestions << new Suggestion(choice, "channel", channelId);
choices << choice;
if (choices.size() == 10) break;
}
--- /dev/null
+#include "channelsview.h"
+#include "ytuser.h"
+#include "ytsearch.h"
+#include "searchparams.h"
+#include "channelmodel.h"
+#include "channelsitemdelegate.h"
+#include "database.h"
+#include "ytsearch.h"
+#include "channelaggregator.h"
+#include "aggregatevideosource.h"
+#include "painterutils.h"
+#include "mainwindow.h"
+#include "utils.h"
+#ifndef Q_WS_X11
+#include "extra.h"
+#endif
+
+static const char *sortByKey = "subscriptionsSortBy";
+static const char *showUpdatedKey = "subscriptionsShowUpdated";
+
+ChannelsView::ChannelsView(QWidget *parent) : QListView(parent),
+ showUpdated(false),
+ sortBy(SortByName) {
+
+ setItemDelegate(new ChannelsItemDelegate(this));
+ setSelectionMode(QAbstractItemView::ExtendedSelection);
+
+ // layout
+ setSpacing(15);
+ setFlow(QListView::LeftToRight);
+ setWrapping(true);
+ setResizeMode(QListView::Adjust);
+ setMovement(QListView::Static);
+ setUniformItemSizes(true);
+
+ // cosmetics
+ setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
+ setFrameShape(QFrame::NoFrame);
+ setAttribute(Qt::WA_MacShowFocusRect, false);
+
+ QPalette p = palette();
+ p.setColor(QPalette::Disabled, QPalette::Base, p.base().color());
+ p.setColor(QPalette::Disabled, QPalette::Text, p.text().color());
+ setPalette(p);
+
+ verticalScrollBar()->setPageStep(3);
+ verticalScrollBar()->setSingleStep(1);
+
+ setMouseTracking(true);
+
+ connect(this, SIGNAL(clicked(const QModelIndex &)),
+ SLOT(itemActivated(const QModelIndex &)));
+ connect(this, SIGNAL(entered(const QModelIndex &)),
+ SLOT(itemEntered(const QModelIndex &)));
+
+ channelsModel = new ChannelsModel(this);
+ setModel(channelsModel);
+ connect(this, SIGNAL(viewportEntered()),
+ channelsModel, SLOT(clearHover()));
+
+ setupActions();
+
+ connect(ChannelAggregator::instance(), SIGNAL(channelChanged(YTUser*)),
+ channelsModel, SLOT(updateChannel(YTUser*)));
+ connect(ChannelAggregator::instance(), SIGNAL(unwatchedCountChanged(int)),
+ SLOT(unwatchedCountChanged(int)));
+
+ unwatchedCountChanged(ChannelAggregator::instance()->getUnwatchedCount());
+}
+
+void ChannelsView::setupActions() {
+ QSettings settings;
+
+ markAsWatchedAction = new QAction(
+ Utils::icon("mark-watched"), tr("Mark all as watched"), this);
+ markAsWatchedAction->setEnabled(false);
+ markAsWatchedAction->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_W));
+ connect(markAsWatchedAction, SIGNAL(triggered()), SLOT(markAllAsWatched()));
+ statusActions << markAsWatchedAction;
+
+ showUpdated = settings.value(showUpdatedKey, false).toBool();
+ QAction *showUpdatedAction = new QAction(
+ Utils::icon("show-updated"), tr("Show Updated"), this);
+ showUpdatedAction->setCheckable(true);
+ showUpdatedAction->setChecked(showUpdated);
+ showUpdatedAction->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_U));
+ connect(showUpdatedAction, SIGNAL(toggled(bool)), SLOT(toggleShowUpdated(bool)));
+ statusActions << showUpdatedAction;
+
+ SortBy sortBy = static_cast<SortBy>(settings.value(sortByKey, SortByName).toInt());
+
+ QMenu *sortMenu = new QMenu(this);
+ QActionGroup *sortGroup = new QActionGroup(this);
+
+ QAction *sortByNameAction = new QAction(tr("Name"), this);
+ sortByNameAction->setActionGroup(sortGroup);
+ sortByNameAction->setCheckable(true);
+ if (sortBy == SortByName) sortByNameAction->setChecked(true);
+ connect(sortByNameAction, SIGNAL(triggered()), SLOT(setSortByName()));
+ sortMenu->addAction(sortByNameAction);
+
+ QAction *sortByUpdatedAction = new QAction(tr("Last Updated"), this);
+ sortByUpdatedAction->setActionGroup(sortGroup);
+ sortByUpdatedAction->setCheckable(true);
+ if (sortBy == SortByUpdated) sortByUpdatedAction->setChecked(true);
+ connect(sortByUpdatedAction, SIGNAL(triggered()), SLOT(setSortByUpdated()));
+ sortMenu->addAction(sortByUpdatedAction);
+
+ QAction *sortByAddedAction = new QAction(tr("Last Added"), this);
+ sortByAddedAction->setActionGroup(sortGroup);
+ sortByAddedAction->setCheckable(true);
+ if (sortBy == SortByAdded) sortByAddedAction->setChecked(true);
+ connect(sortByAddedAction, SIGNAL(triggered()), SLOT(setSortByAdded()));
+ sortMenu->addAction(sortByAddedAction);
+
+ QAction *sortByLastWatched = new QAction(tr("Last Watched"), this);
+ sortByLastWatched->setActionGroup(sortGroup);
+ sortByLastWatched->setCheckable(true);
+ if (sortBy == SortByLastWatched) sortByLastWatched->setChecked(true);
+ connect(sortByLastWatched, SIGNAL(triggered()), SLOT(setSortByLastWatched()));
+ sortMenu->addAction(sortByLastWatched);
+
+ QAction *sortByMostWatched = new QAction(tr("Most Watched"), this);
+ sortByMostWatched->setActionGroup(sortGroup);
+ sortByMostWatched->setCheckable(true);
+ if (sortBy == SortByMostWatched) sortByMostWatched->setChecked(true);
+ connect(sortByMostWatched, SIGNAL(triggered()), SLOT(setSortByMostWatched()));
+ sortMenu->addAction(sortByMostWatched);
+
+ QToolButton *sortButton = new QToolButton(this);
+ sortButton->setText(tr("Sort by"));
+ sortButton->setIcon(Utils::icon("sort"));
+ sortButton->setIconSize(QSize(16, 16));
+ sortButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ sortButton->setPopupMode(QToolButton::InstantPopup);
+ sortButton->setMenu(sortMenu);
+ QWidgetAction *widgetAction = new QWidgetAction(this);
+ widgetAction->setDefaultWidget(sortButton);
+ widgetAction->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_O));
+ statusActions << widgetAction;
+
+ foreach (QAction *action, statusActions)
+ Utils::setupAction(action);
+}
+
+void ChannelsView::appear() {
+ updateQuery();
+ foreach (QAction* action, statusActions)
+ MainWindow::instance()->showActionInStatusBar(action, true);
+ setFocus();
+ ChannelAggregator::instance()->run();
+}
+
+void ChannelsView::disappear() {
+ foreach (QAction* action, statusActions)
+ MainWindow::instance()->showActionInStatusBar(action, false);
+}
+
+void ChannelsView::mouseMoveEvent(QMouseEvent *event) {
+ QListView::mouseMoveEvent(event);
+ const QModelIndex index = indexAt(event->pos());
+ if (index.isValid()) setCursor(Qt::PointingHandCursor);
+ else unsetCursor();
+}
+
+void ChannelsView::leaveEvent(QEvent *event) {
+ QListView::leaveEvent(event);
+ channelsModel->clearHover();
+}
+
+void ChannelsView::itemEntered(const QModelIndex &index) {
+ // channelsModel->setHoveredRow(index.row());
+}
+
+void ChannelsView::itemActivated(const QModelIndex &index) {
+ ChannelsModel::ItemTypes itemType = channelsModel->typeForIndex(index);
+ if (itemType == ChannelsModel::ItemChannel) {
+ YTUser *user = channelsModel->userForIndex(index);
+ SearchParams *params = new SearchParams();
+ params->setAuthor(user->getUserId());
+ params->setSortBy(SearchParams::SortByNewest);
+ params->setTransient(true);
+ YTSearch *videoSource = new YTSearch(params, this);
+ emit activated(videoSource);
+ user->updateWatched();
+ } else if (itemType == ChannelsModel::ItemAggregate) {
+ AggregateVideoSource *videoSource = new AggregateVideoSource(this);
+ videoSource->setName(tr("All Videos"));
+ emit activated(videoSource);
+ } else if (itemType == ChannelsModel::ItemUnwatched) {
+ AggregateVideoSource *videoSource = new AggregateVideoSource(this);
+ videoSource->setName(tr("Unwatched Videos"));
+ videoSource->setUnwatched(true);
+ emit activated(videoSource);
+ }
+}
+
+void ChannelsView::paintEvent(QPaintEvent *event) {
+ if (model()->rowCount() < 3) {
+ QString msg;
+ if (!errorMessage.isEmpty())
+ msg = errorMessage;
+ else if (showUpdated)
+ msg = tr("There are no updated subscriptions at this time.");
+ else
+ msg = tr("You have no subscriptions. "
+ "Use the star symbol to subscribe to channels.");
+ PainterUtils::centeredMessage(msg, viewport());
+ } else QListView::paintEvent(event);
+ PainterUtils::topShadow(viewport());
+}
+
+void ChannelsView::toggleShowUpdated(bool enable) {
+ showUpdated = enable;
+ updateQuery(true);
+ QSettings settings;
+ settings.setValue(showUpdatedKey, showUpdated);
+}
+
+void ChannelsView::updateQuery(bool transition) {
+ errorMessage.clear();
+ if (!Database::exists()) return;
+
+ QString sql = "select user_id from subscriptions";
+ if (showUpdated)
+ sql += " where notify_count>0";
+
+ switch (sortBy) {
+ case SortByUpdated:
+ sql += " order by updated desc";
+ break;
+ case SortByAdded:
+ sql += " order by added desc";
+ break;
+ case SortByLastWatched:
+ sql += " order by watched desc";
+ break;
+ case SortByMostWatched:
+ sql += " order by views desc";
+ break;
+ default:
+ sql += " order by name collate nocase";
+ break;
+ }
+
+#ifndef Q_WS_X11
+ if (transition)
+ Extra::fadeInWidget(this, this);
+#endif
+
+ channelsModel->setQuery(sql, Database::instance().getConnection());
+ if (channelsModel->lastError().isValid()) {
+ qWarning() << channelsModel->lastError().text();
+ errorMessage = channelsModel->lastError().text();
+ }
+}
+
+void ChannelsView::setSortBy(SortBy sortBy) {
+ this->sortBy = sortBy;
+ updateQuery(true);
+ QSettings settings;
+ settings.setValue(sortByKey, (int)sortBy);
+}
+
+void ChannelsView::markAllAsWatched() {
+ ChannelAggregator::instance()->markAllAsWatched();
+ updateQuery();
+ markAsWatchedAction->setEnabled(false);
+}
+
+void ChannelsView::unwatchedCountChanged(int count) {
+ markAsWatchedAction->setEnabled(count > 0);
+ channelsModel->updateUnwatched();
+ updateQuery();
+}
--- /dev/null
+#ifndef CHANNELSVIEW_H
+#define CHANNELSVIEW_H
+
+#include <QtGui>
+#include "view.h"
+
+class VideoSource;
+class ChannelsModel;
+
+class ChannelsView : public QListView, public View {
+
+ Q_OBJECT
+
+public:
+ ChannelsView(QWidget *parent = 0);
+
+signals:
+ void activated(VideoSource *videoSource);
+
+public slots:
+ void appear();
+ void disappear();
+
+protected:
+ void mouseMoveEvent(QMouseEvent *event);
+ void leaveEvent(QEvent *event);
+ void paintEvent(QPaintEvent *event);
+
+private:
+ enum SortBy {
+ SortByName = 0,
+ SortByAdded,
+ SortByUpdated,
+ SortByLastWatched,
+ SortByMostWatched
+ };
+
+private slots:
+ void itemEntered(const QModelIndex &index);
+ void itemActivated(const QModelIndex &index);
+ void toggleShowUpdated(bool enable);
+ void setSortBy(SortBy sortBy);
+ void setSortByName() { setSortBy(SortByName); }
+ void setSortByUpdated() { setSortBy(SortByUpdated); }
+ void setSortByAdded() { setSortBy(SortByAdded); }
+ void setSortByLastWatched() { setSortBy(SortByLastWatched); }
+ void setSortByMostWatched() { setSortBy(SortByMostWatched); }
+ void markAllAsWatched();
+ void unwatchedCountChanged(int count);
+
+private:
+ void updateQuery(bool transition = false);
+ void setupActions();
+
+ ChannelsModel *channelsModel;
+ QList<QAction*> statusActions;
+ bool showUpdated;
+ SortBy sortBy;
+ QString errorMessage;
+ QAction *markAsWatchedAction;
+
+};
+
+#endif // CHANNELSVIEW_H
$END_LICENSE */
#include "channelview.h"
-#include "ytuser.h"
+#include "ytchannel.h"
#include "ytsearch.h"
#include "searchparams.h"
#include "channelmodel.h"
setupActions();
- connect(ChannelAggregator::instance(), SIGNAL(channelChanged(YTUser*)),
- channelsModel, SLOT(updateChannel(YTUser*)));
+ connect(ChannelAggregator::instance(), SIGNAL(channelChanged(YTChannel*)),
+ channelsModel, SLOT(updateChannel(YTChannel*)));
connect(ChannelAggregator::instance(), SIGNAL(unwatchedCountChanged(int)),
SLOT(unwatchedCountChanged(int)));
void ChannelView::setupActions() {
QSettings settings;
- SortBy sortBy = static_cast<SortBy>(settings.value(sortByKey, SortByName).toInt());
+ sortBy = static_cast<SortBy>(settings.value(sortByKey, SortByName).toInt());
QMenu *sortMenu = new QMenu(this);
QActionGroup *sortGroup = new QActionGroup(this);
foreach (QAction* action, statusActions)
MainWindow::instance()->showActionInStatusBar(action, true);
setFocus();
- ChannelAggregator::instance()->run();
+ ChannelAggregator::instance()->start();
}
void ChannelView::disappear() {
+ ChannelAggregator::instance()->stop();
foreach (QAction* action, statusActions)
MainWindow::instance()->showActionInStatusBar(action, false);
}
void ChannelView::itemActivated(const QModelIndex &index) {
ChannelModel::ItemTypes itemType = channelsModel->typeForIndex(index);
if (itemType == ChannelModel::ItemChannel) {
- YTUser *user = channelsModel->userForIndex(index);
+ YTChannel *channel = channelsModel->channelForIndex(index);
SearchParams *params = new SearchParams();
- params->setAuthor(user->getUserId());
+ params->setChannelId(channel->getChannelId());
params->setSortBy(SearchParams::SortByNewest);
params->setTransient(true);
YTSearch *videoSource = new YTSearch(params, this);
+ videoSource->setAsyncDetails(true);
emit activated(videoSource);
- user->updateWatched();
+ channel->updateWatched();
} else if (itemType == ChannelModel::ItemAggregate) {
AggregateVideoSource *videoSource = new AggregateVideoSource(this);
videoSource->setName(tr("All Videos"));
const QModelIndex index = indexAt(point);
if (!index.isValid()) return;
- YTUser *user = channelsModel->userForIndex(index);
- if (!user) return;
+ YTChannel *channel = channelsModel->channelForIndex(index);
+ if (!channel) return;
unsetCursor();
QMenu menu;
- if (user->getNotifyCount() > 0) {
- QAction *markAsWatchedAction = menu.addAction(tr("Mark as Watched"), user, SLOT(updateWatched()));
+ if (channel->getNotifyCount() > 0) {
+ QAction *markAsWatchedAction = menu.addAction(tr("Mark as Watched"), channel, SLOT(updateWatched()));
connect(markAsWatchedAction, SIGNAL(triggered()),
ChannelAggregator::instance(), SLOT(updateUnwatchedCount()));
menu.addSeparator();
notificationsAction->setChecked(true);
*/
- QAction *unsubscribeAction = menu.addAction(tr("Unsubscribe"), user, SLOT(unsubscribe()));
+ QAction *unsubscribeAction = menu.addAction(tr("Unsubscribe"), channel, SLOT(unsubscribe()));
connect(unsubscribeAction, SIGNAL(triggered()),
ChannelAggregator::instance(), SLOT(updateUnwatchedCount()));
--- /dev/null
+#include "channelwidget.h"
+#include "videosource.h"
+#include "ytuser.h"
+#include "fontutils.h"
+
+ChannelWidget::ChannelWidget(VideoSource *videoSource, YTUser *user, QWidget *parent) :
+ GridWidget(parent) {
+ this->user = user;
+ this->videoSource = videoSource;
+
+ setMinimumSize(132, 176);
+ setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+
+ connect(user, SIGNAL(infoLoaded()), SLOT(gotUserInfo()));
+ // user->loadfromAPI();
+
+ connect(this, SIGNAL(activated()), SLOT(activate()));
+}
+
+void ChannelWidget::activate() {
+ // YTUser.checked(user->getUserId());
+ emit activated(videoSource);
+}
+
+void ChannelWidget::gotUserInfo() {
+ connect(user, SIGNAL(thumbnailLoaded(QByteArray)), SLOT(gotUserThumbnail(QByteArray)));
+ // user->loadThumbnail();
+ update();
+}
+
+void ChannelWidget::paintEvent(QPaintEvent *) {
+ if (thumbnail.isNull()) return;
+
+ const int w = width();
+ const int h = height();
+
+ QPainter p(this);
+ p.drawPixmap((w - thumbnail.width()) / 2, 0, thumbnail);
+ //(h - thumbnail.height()) / 2
+
+ QRect nameBox = rect();
+ nameBox.adjust(0, 0, 0, -thumbnail.height() - 10);
+ nameBox.translate(0, h - nameBox.height());
+
+ QString name = user->getDisplayName();
+ bool tooBig = false;
+ p.save();
+ /*
+ QFont f = font();
+ f.setFamily("Helvetica");
+ p.setFont(f);
+ */
+ QRect textBox = p.boundingRect(nameBox, Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap, name);
+ if (textBox.height() > nameBox.height()) {
+ p.setFont(font());
+ textBox = p.boundingRect(nameBox, Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap, name);
+ if (textBox.height() > nameBox.height()) {
+ p.setClipRect(nameBox);
+ tooBig = true;
+ }
+ }
+ p.setPen(Qt::black);
+ if (tooBig)
+ p.drawText(nameBox, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, name);
+ else
+ p.drawText(textBox, Qt::AlignCenter | Qt::TextWordWrap, name);
+ p.restore();
+
+ if (hasFocus()) {
+ p.save();
+ QPen pen;
+ pen.setBrush(palette().highlight());
+ pen.setWidth(2);
+ p.setPen(pen);
+ p.drawRect(rect());
+ p.restore();
+ }
+}
+
+void ChannelWidget::gotUserThumbnail(QByteArray bytes) {
+ thumbnail.loadFromData(bytes);
+ if (thumbnail.width() > 88)
+ thumbnail = thumbnail.scaledToWidth(88, Qt::SmoothTransformation);
+ update();
+}
--- /dev/null
+#ifndef CHANNELWIDGET_H
+#define CHANNELWIDGET_H
+
+#include <QtGui>
+#include "gridwidget.h"
+
+class VideoSource;
+class YTUser;
+
+class ChannelWidget : public GridWidget {
+
+ Q_OBJECT
+
+public:
+ ChannelWidget(VideoSource *videoSource, YTUser *user, QWidget *parent = 0);
+
+signals:
+ void activated(VideoSource *videoSource);
+
+protected:
+ void paintEvent(QPaintEvent *event);
+
+private slots:
+ void gotUserInfo();
+ void gotUserThumbnail(QByteArray bytes);
+ void activate();
+
+private:
+ QPixmap thumbnail;
+ YTUser *user;
+ VideoSource *videoSource;
+
+};
+
+#endif // CHANNELWIDGET_H
QMutexLocker locker(&lock);
- if(QFile::exists(dbLocation)) {
+ if (QFile::exists(dbLocation)) {
// check db version
int databaseVersion = getAttribute("version").toInt();
- if (databaseVersion != DATABASE_VERSION)
+ if (databaseVersion > DATABASE_VERSION)
qWarning("Wrong database version: %d", databaseVersion);
+
+ if (!getAttribute("channelIdFix").toBool())
+ fixChannelIds();
+
} else createDatabase();
}
QSqlQuery("create table subscriptions ("
"id integer primary key autoincrement,"
- "user_id varchar,"
- "user_name varchar,"
- "name varchar,"
+ "user_id varchar," // this is really channel_id
+ "user_name varchar," // obsolete yt2 username
+ "name varchar," // this is really channel_title
"description varchar,"
"thumb_url varchar,"
"country varchar,"
QSqlQuery("create table subscriptions_videos ("
"id integer primary key autoincrement,"
"video_id varchar,"
- "channel_id integer,"
+ "channel_id integer," // this is really subscription_id
"published integer,"
"added integer,"
"watched integer,"
"title varchar,"
- "author varchar,"
- "user_id varchar,"
+ "author varchar," // this is really channel_title
+ "user_id varchar," // this is really channel_id
"description varchar,"
"url varchar,"
"thumb_url varchar,"
void Database::setAttribute(QString name, QVariant value) {
QSqlQuery query(getConnection());
- query.prepare("update attributes set value=? where name=?");
- query.bindValue(0, value);
- query.bindValue(1, name);
+ query.prepare("insert or replace into attributes (name, value) values (?,?)");
+ query.bindValue(0, name);
+ query.bindValue(1, value);
bool success = query.exec();
- if (!success) qDebug() << query.lastError().text();
+ if (!success) qWarning() << query.lastError().text();
+}
+
+void Database::fixChannelIds() {
+ if (!getConnection().transaction())
+ qWarning() << "Transaction failed" << __PRETTY_FUNCTION__;
+
+ qWarning() << "Fixing channel ids";
+
+ QSqlQuery query(getConnection());
+ bool success = query.exec("update subscriptions set user_id='UC' || user_id where user_id not like 'UC%'");
+ if (!success) qWarning() << query.lastError().text();
+
+ query = QSqlQuery(getConnection());
+ success = query.exec("update subscriptions_videos set user_id='UC' || user_id where user_id not like 'UC%'");
+ if (!success) qWarning() << query.lastError().text();
+
+ setAttribute("channelIdFix", 1);
+
+ if (!getConnection().commit())
+ qWarning() << "Commit failed" << __PRETTY_FUNCTION__;
}
/**
QVariant getAttribute(QString name);
void setAttribute(QString name, QVariant value);
+ void fixChannelIds();
+
QMutex lock;
QString dbLocation;
QHash<QThread*, QSqlDatabase> connections;
return f;
}
+
+QString DataUtils::regioneCode(const QLocale &locale) {
+ QString name = locale.name();
+ int index = name.indexOf('_');
+ if (index == -1) return QString();
+ return name.right(index);
+}
+
+QString DataUtils::systemRegioneCode() {
+ return regioneCode(QLocale::system());
+}
+
+uint DataUtils::parseIsoPeriod(const QString &isoPeriod) {
+ // QTime time = QTime::fromString("1mm12car00", "PT8M50S");
+ // ^P((\d+Y)?(\d+M)?(\d+W)?(\d+D)?)?(T(\d+H)?(\d+M)?(\d+S)?)?$
+ /*
+ QRegExp isoPeriodRE("^PT(\d+H)?(\d+M)?(\d+S)?)?$");
+ if (!isoPeriodRE.indexIn(isoPeriod)) {
+ qWarning() << "Cannot parse ISO period" << isoPeriod;
+ continue;
+ }
+
+ int totalCaptures = isoPeriodRE.capturedTexts();
+ for (int i = totalCaptures; i > 0; --i) {
+
+ }
+ */
+
+ uint days = 0, hours = 0, minutes = 0, seconds = 0;
+
+ const char *ptr = isoPeriod.toStdString().c_str();
+ while (*ptr) {
+ if(*ptr == 'P' || *ptr == 'T') {
+ ptr++;
+ continue;
+ }
+
+ int value, charsRead;
+ char type;
+ if (sscanf(ptr, "%d%c%n", &value, &type, &charsRead) != 2)
+ continue;
+
+ if (type == 'D')
+ days = value;
+ else if (type == 'H')
+ hours = value;
+ else if (type == 'M')
+ minutes = value;
+ else if (type == 'S')
+ seconds = value;
+
+ ptr += charsRead;
+ }
+
+ uint period = ((days * 24 + hours) * 60 + minutes) * 60 + seconds;
+ return period;
+}
public:
static QString stringToFilename(const QString &s);
+ static QString regioneCode(const QLocale &locale);
+ static QString systemRegioneCode();
+ static uint parseIsoPeriod(const QString &isoPeriod);
private:
DataUtils() { }
QIODevice* DiskCache::prepare(const QNetworkCacheMetaData &metaData) {
QString mime;
- foreach (QNetworkCacheMetaData::RawHeader header, metaData.rawHeaders()) {
+ foreach (const QNetworkCacheMetaData::RawHeader &header, metaData.rawHeaders()) {
// qDebug() << header.first << header.second;
if (header.first.constData() == QLatin1String("Content-Type")) {
mime = header.second;
}
}
- if (mime.startsWith(QLatin1String("image/")) ||
- mime.endsWith(QLatin1String("/javascript")))
+ if (mime == QLatin1String("application/json") || mime.startsWith(QLatin1String("image/"))) {
return QNetworkDiskCache::prepare(metaData);
+ }
return 0;
}
+
+QNetworkCacheMetaData DiskCache::metaData(const QUrl &url) {
+ // Remove "key" from query string in order to reuse cache when key changes
+ static const QString keyQueryItem = "key";
+ if (url.hasQueryItem(keyQueryItem)) {
+ QUrl url2(url);
+ url2.removeQueryItem(keyQueryItem);
+ return QNetworkDiskCache::metaData(url2);
+ }
+ return QNetworkDiskCache::metaData(url);
+}
public:
explicit DiskCache(QObject *parent = 0);
QIODevice* prepare(const QNetworkCacheMetaData &metaData);
+ QNetworkCacheMetaData metaData(const QUrl &url);
signals:
#include "mac_startup.h"
#endif
+void showWindow(QtSingleApplication &app, const QString &dataDir) {
+ MainWindow *mainWin = new MainWindow();
+ mainWin->show();
+
+#ifndef APP_MAC
+ QIcon appIcon;
+ if (QDir(dataDir).exists()) {
+ appIcon = IconUtils::icon(Constants::UNIX_NAME);
+ } else {
+ QString dataDir = qApp->applicationDirPath() + "/data";
+ const int iconSizes [] = { 16, 22, 32, 48, 64, 128, 256, 512 };
+ for (int i = 0; i < 8; i++) {
+ QString size = QString::number(iconSizes[i]);
+ QString png = dataDir + "/" + size + "x" + size + "/" + Constants::UNIX_NAME + ".png";
+ appIcon.addFile(png, QSize(iconSizes[i], iconSizes[i]));
+ }
+ }
+ if (appIcon.isNull()) appIcon.addFile(":/images/app.png");
+ mainWin->setWindowIcon(appIcon);
+#endif
+
+ mainWin->connect(&app, SIGNAL(messageReceived(const QString &)),
+ mainWin, SLOT(messageReceived(const QString &)));
+ app.setActivationWindow(mainWin, true);
+}
+
int main(int argc, char **argv) {
#ifdef Q_OS_MAC
mac::MacMain();
- QFont::insertSubstitution(".Helvetica Neue DeskInterface", "Helvetica Neue");
+ // QFont::insertSubstitution(".Helvetica Neue DeskInterface", "Helvetica Neue");
#endif
QtSingleApplication app(argc, argv);
QTextCodec::setCodecForTr(QTextCodec::codecForName("utf8"));
#endif
- MainWindow mainWin;
- mainWin.show();
-
- // no window icon on Mac
-#ifndef APP_MAC
- QIcon appIcon;
- if (QDir(dataDir).exists()) {
- appIcon = IconUtils::icon(Constants::UNIX_NAME);
- } else {
- dataDir = qApp->applicationDirPath() + "/data";
- const int iconSizes [] = { 16, 22, 32, 48, 64, 128, 256, 512 };
- for (int i = 0; i < 8; i++) {
- QString size = QString::number(iconSizes[i]);
- QString png = dataDir + "/" + size + "x" + size + "/" +
- Constants::UNIX_NAME + ".png";
- appIcon.addFile(png, QSize(iconSizes[i], iconSizes[i]));
- }
- }
- if (appIcon.isNull()) {
- appIcon.addFile(":/images/app.png");
- }
- mainWin.setWindowIcon(appIcon);
-#endif
-
- mainWin.connect(&app, SIGNAL(messageReceived(const QString &)),
- &mainWin, SLOT(messageReceived(const QString &)));
- app.setActivationWindow(&mainWin, true);
+ // Seed random number generator
+ qsrand(QDateTime::currentDateTime().toTime_t());
// all string literals are UTF-8
// QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8"));
- // Seed random number generator
- qsrand(QDateTime::currentDateTime().toTime_t());
+#ifdef APP_INTEGRITY
+ if (Extra::integrityCheck())
+#endif
+ showWindow(app, dataDir);
return app.exec();
}
#include "videoareawidget.h"
#include "jsfunctions.h"
#include "seekslider.h"
+#ifdef APP_YT3
+#include "yt3.h"
+#endif
+namespace {
static MainWindow *singleton = 0;
+}
MainWindow* MainWindow::instance() {
- if (!singleton) singleton = new MainWindow();
return singleton;
}
singleton = this;
+#ifdef APP_EXTRA
+ Extra::windowSetup(this);
+#endif
+
// views mechanism
history = new QStack<QWidget*>();
views = new QStackedWidget();
views->show();
-#ifdef APP_EXTRA
- Extra::windowSetup(this);
-#endif
-
qApp->processEvents();
QTimer::singleShot(50, this, SLOT(lazyInit()));
}
JsFunctions::instance();
checkForUpdate();
-
- ChannelAggregator::instance()->start();
}
void MainWindow::changeEvent(QEvent* event) {
// qDebug() << obj << mouseEvent->pos() << isHoveringVideo << mediaView->isPlaylistVisible();
- if (mediaView->isPlaylistVisible()) {
+ if (mediaView && mediaView->isPlaylistVisible()) {
if (isHoveringVideo && x > 5) mediaView->setPlaylistVisible(false);
} else {
if (isHoveringVideo && x >= 0 && x < 5) mediaView->setPlaylistVisible(true);
QAction *definitionAct = new QAction(this);
#ifdef Q_OS_LINUX
definitionAct->setIcon(IconUtils::tintedIcon("video-display", QColor(0, 0, 0),
- QList<QSize>() << QSize(16, 16)));
+ QList<QSize>() << QSize(16, 16)));
#else
definitionAct->setIcon(IconUtils::icon("video-display"));
#endif
mainToolBar->addWidget(new Spacer());
mainToolBar->addAction(volumeMuteAct);
+#ifdef Q_WS_X11
+ QToolButton *volumeMuteButton = qobject_cast<QToolButton *>(mainToolBar->widgetForAction(volumeMuteAct));
+ volumeMuteButton->setIcon(volumeMuteButton->icon().pixmap(16));
+#endif
#ifdef APP_PHONON
volumeSlider = new Phonon::VolumeSlider(this);
if (!m_fullscreen && !compactViewAct->isChecked()) {
writeSettings();
}
- mediaView->stop();
+ // mediaView->stop();
Temporary::deleteAll();
ChannelAggregator::instance()->stop();
ChannelAggregator::instance()->cleanup();
mediaObject = new Phonon::MediaObject(this);
mediaObject->setTickInterval(100);
connect(mediaObject, SIGNAL(stateChanged(Phonon::State, Phonon::State)),
- this, SLOT(stateChanged(Phonon::State, Phonon::State)));
- connect(mediaObject, SIGNAL(tick(qint64)), this, SLOT(tick(qint64)));
- connect(mediaObject, SIGNAL(totalTimeChanged(qint64)), this, SLOT(totalTimeChanged(qint64)));
+ SLOT(stateChanged(Phonon::State, Phonon::State)));
+ connect(mediaObject, SIGNAL(tick(qint64)), SLOT(tick(qint64)));
+ connect(mediaObject, SIGNAL(totalTimeChanged(qint64)), SLOT(totalTimeChanged(qint64)));
+
+ audioOutput = new Phonon::AudioOutput(Phonon::VideoCategory, this);
+ connect(audioOutput, SIGNAL(volumeChanged(qreal)), SLOT(volumeChanged(qreal)));
+ connect(audioOutput, SIGNAL(mutedChanged(bool)), SLOT(volumeMutedChanged(bool)));
+ Phonon::createPath(mediaObject, audioOutput);
+ volumeSlider->setAudioOutput(audioOutput);
+
#ifdef APP_PHONON_SEEK
seekSlider->setMediaObject(mediaObject);
#endif
- audioOutput = new Phonon::AudioOutput(Phonon::VideoCategory, this);
- connect(audioOutput, SIGNAL(volumeChanged(qreal)), this, SLOT(volumeChanged(qreal)));
- connect(audioOutput, SIGNAL(mutedChanged(bool)), this, SLOT(volumeMutedChanged(bool)));
- volumeSlider->setAudioOutput(audioOutput);
- Phonon::createPath(mediaObject, audioOutput);
+
QSettings settings;
- audioOutput->setVolume(settings.value("volume", 1).toReal());
+ audioOutput->setVolume(settings.value("volume", 1.).toReal());
// audioOutput->setMuted(settings.value("volumeMute").toBool());
+
+ mediaObject->stop();
}
#endif
void MainWindow::volumeMute() {
#ifdef APP_PHONON
- volumeSlider->audioOutput()->setMuted(!volumeSlider->audioOutput()->isMuted());
+ bool muted = volumeSlider->audioOutput()->isMuted();
+ volumeSlider->audioOutput()->setMuted(!muted);
+ qApp->processEvents();
+ if (muted && volumeSlider->audioOutput()->volume() == 0) {
+ volumeSlider->audioOutput()->setVolume(volumeSlider->maximumVolume());
+ }
+ qDebug() << volumeSlider->audioOutput()->isMuted() << volumeSlider->audioOutput()->volume();
#endif
}
void MainWindow::volumeChanged(qreal newVolume) {
#ifdef APP_PHONON
// automatically unmute when volume changes
- if (volumeSlider->audioOutput()->isMuted())
- volumeSlider->audioOutput()->setMuted(false);
+ if (volumeSlider->audioOutput()->isMuted()) volumeSlider->audioOutput()->setMuted(false);
bool isZero = volumeSlider->property("zero").toBool();
bool styleChanged = false;
volumeMuteAct->setIcon(IconUtils::icon("audio-volume-high"));
statusBar()->showMessage(tr("Volume is unmuted"));
}
+#ifdef Q_WS_X11
+ QToolButton *volumeMuteButton = qobject_cast<QToolButton *>(mainToolBar->widgetForAction(volumeMuteAct));
+ volumeMuteButton->setIcon(volumeMuteButton->icon().pixmap(16));
+#endif
}
void MainWindow::setDefinitionMode(QString definitionName) {
#include "ytsinglevideosource.h"
#include "channelaggregator.h"
#include "iconutils.h"
-#include "ytuser.h"
+#include "ytchannel.h"
#ifdef APP_SNAPSHOT
#include "snapshotsettings.h"
#endif
}
}
}
- setVideoSource(new YTSearch(searchParams, this));
+ YTSearch *ytSearch = new YTSearch(searchParams, this);
+ ytSearch->setAsyncDetails(true);
+ connect(ytSearch, SIGNAL(gotDetails()), playlistModel, SLOT(emitDataChanged()));
+ setVideoSource(ytSearch);
}
void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool back) {
sidebar->getHeader()->updateInfo();
SearchParams *searchParams = getSearchParams();
- bool isChannel = searchParams && !searchParams->author().isEmpty();
+ bool isChannel = searchParams && !searchParams->channelId().isEmpty();
playlistView->setClickableAuthors(!isChannel);
if (newState == Phonon::PlayingState)
videoAreaWidget->showVideo();
else if (newState == Phonon::ErrorState) {
- qDebug() << "Phonon error:" << mediaObject->errorString() << mediaObject->errorType();
+ qWarning() << "Phonon error:" << mediaObject->errorString() << mediaObject->errorType();
if (mediaObject->errorType() == Phonon::FatalError)
handleError(mediaObject->errorString());
}
a->setEnabled(enableDownload);
a->setVisible(enableDownload);
- updateSubscriptionAction(video, YTUser::isSubscribed(video->userId()));
+ updateSubscriptionAction(video, YTChannel::isSubscribed(video->channelId()));
foreach (QAction *action, currentVideoActions)
action->setEnabled(true);
startDownloading();
#endif
- // ensure we always have 10 videos ahead
+ // ensure we always have videos ahead
playlistModel->searchNeeded();
// ensure active item is visible
#endif
#ifdef APP_EXTRA
- Extra::notify(video->title(), video->author(), video->formattedDuration());
+ Extra::notify(video->title(), video->channelTitle(), video->formattedDuration());
#endif
ChannelAggregator::instance()->videoWatched(video);
void MediaView::copyWebPage() {
Video* video = playlistModel->activeVideo();
if (!video) return;
- QString address = video->webpage().toString();
+ QString address = video->webpage();
QApplication::clipboard()->setText(address);
QString message = tr("You can now paste the YouTube link into another application");
MainWindow::instance()->showMessage(message);
if (!video) return;
QString location = SnapshotSettings::getCurrentLocation();
+ QDir dir(location);
+ if (!dir.exists()) dir.mkpath(location);
QString basename = video->title();
QString format = video->duration() > 3600 ? "h_mm_ss" : "m_ss";
basename += " (" + QTime().addSecs(currentTime).toString(format) + ")";
SearchParams *searchParams = new SearchParams();
searchParams->setTransient(true);
searchParams->setKeywords(query);
- searchParams->setAuthor(video->author());
+ searchParams->setChannelId(video->channelId());
/*
if (!numberAsWords) {
Video* video = playlistModel->activeVideo();
if (!video) return;
YTSingleVideoSource *singleVideoSource = new YTSingleVideoSource();
- singleVideoSource->setVideoId(video->id());
+ singleVideoSource->setVideo(video->clone());
+ singleVideoSource->setAsyncDetails(true);
setVideoSource(singleVideoSource);
The::globalActions()->value("related-videos")->setEnabled(false);
}
#endif
url.addQueryItem("via", "minitubeapp");
url.addQueryItem("text", video->title());
- url.addQueryItem("url", video->webpage().toString());
+ url.addQueryItem("url", video->webpage());
#if QT_VERSION >= 0x050000
u.setQuery(url);
}
QUrlQuery url;
#endif
url.addQueryItem("t", video->title());
- url.addQueryItem("u", video->webpage().toString());
+ url.addQueryItem("u", video->webpage());
#if QT_VERSION >= 0x050000
u.setQuery(url);
}
#endif
url.addQueryItem("via", "minitubeapp");
url.addQueryItem("text", video->title());
- url.addQueryItem("url", video->webpage().toString());
+ url.addQueryItem("url", video->webpage());
url.addQueryItem("picture", video->thumbnailUrl());
#if QT_VERSION >= 0x050000
u.setQuery(url);
#endif
url.addQueryItem("subject", video->title());
QString body = video->title() + "\n" +
- video->webpage().toString() + "\n\n" +
+ video->webpage() + "\n\n" +
tr("Sent from %1").arg(Constants::NAME) + "\n" +
Constants::WEBSITE;
url.addQueryItem("body", body);
Video* video = playlistModel->videoAt(index.row());
if (!video) return;
- QString channel = video->userId();
- if (channel.isEmpty()) channel = video->author();
- if (channel.isEmpty()) return;
+ QString channelId = video->channelId();
+ // if (channelId.isEmpty()) channelId = video->channelTitle();
+ if (channelId.isEmpty()) return;
SearchParams *searchParams = new SearchParams();
- searchParams->setAuthor(channel);
+ searchParams->setChannelId(channelId);
searchParams->setSortBy(SearchParams::SortByNewest);
// go!
subscribeText = subscribeAction->property("originalText").toString();
subscribeAction->setEnabled(false);
} else if (subscribed) {
- subscribeText = tr("Unsubscribe from %1").arg(video->author());
+ subscribeText = tr("Unsubscribe from %1").arg(video->channelTitle());
subscribeTip = subscribeText;
subscribeAction->setEnabled(true);
} else {
- subscribeText = tr("Subscribe to %1").arg(video->author());
+ subscribeText = tr("Subscribe to %1").arg(video->channelTitle());
subscribeTip = subscribeText;
subscribeAction->setEnabled(true);
}
void MediaView::toggleSubscription() {
Video *video = playlistModel->activeVideo();
if (!video) return;
- QString userId = video->userId();
+ QString userId = video->channelId();
if (userId.isEmpty()) return;
- bool subscribed = YTUser::isSubscribed(userId);
- if (subscribed) YTUser::unsubscribe(userId);
- else YTUser::subscribe(userId);
+ bool subscribed = YTChannel::isSubscribed(userId);
+ if (subscribed) YTChannel::unsubscribe(userId);
+ else YTChannel::subscribe(userId);
updateSubscriptionAction(video, !subscribed);
}
setupReply();
readTimeoutTimer->start();
return;
- } else qWarning() << "Redirection not supported" << networkReply->url().toEncoded();
+ } else qDebug() << "Redirection not supported" << networkReply->url().toEncoded();
}
if (receivers(SIGNAL(data(QByteArray))) > 0)
--- /dev/null
+/* $BEGIN_LICENSE
+
+This file is part of Minitube.
+Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
+
+Minitube is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Minitube is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Minitube. If not, see <http://www.gnu.org/licenses/>.
+
+$END_LICENSE */
+
+#include "paginatedvideosource.h"
+
+#include "yt3.h"
+#include "yt3listparser.h"
+#include "datautils.h"
+
+#include "video.h"
+#include "networkaccess.h"
+
+namespace The {
+NetworkAccess* http();
+QHash<QString, QAction*>* globalActions();
+}
+
+PaginatedVideoSource::PaginatedVideoSource(QObject *parent) : VideoSource(parent)
+ , tokenTimestamp(0)
+ , reloadingToken(false)
+ , currentMax(0)
+ , currentStartIndex(0)
+ , asyncDetails(false) { }
+
+bool PaginatedVideoSource::hasMoreVideos() {
+ qDebug() << __PRETTY_FUNCTION__ << nextPageToken;
+ return !nextPageToken.isEmpty();
+}
+
+bool PaginatedVideoSource::maybeReloadToken(int max, int startIndex) {
+ // kind of hackish. Thank the genius who came up with this stateful stuff
+ // in a supposedly RESTful (aka stateless) API.
+
+ if (nextPageToken.isEmpty()) {
+ // previous request did not return a page token. Game over.
+ // emit gotVideos(QList<Video*>());
+ emit finished(0);
+ return true;
+ }
+
+ if (isPageTokenExpired()) {
+ reloadingToken = true;
+ currentMax = max;
+ currentStartIndex = startIndex;
+ reloadToken();
+ return true;
+ }
+ return false;
+}
+
+bool PaginatedVideoSource::setPageToken(const QString &value) {
+ tokenTimestamp = QDateTime::currentDateTime().toTime_t();
+ nextPageToken = value;
+
+ if (reloadingToken) {
+ reloadingToken = false;
+ loadVideos(currentMax, currentStartIndex);
+ currentMax = currentStartIndex = 0;
+ return true;
+ }
+
+ return false;
+}
+
+bool PaginatedVideoSource::isPageTokenExpired() {
+ uint now = QDateTime::currentDateTime().toTime_t();
+ return now - tokenTimestamp > 1800;
+}
+
+void PaginatedVideoSource::reloadToken() {
+ qDebug() << "Reloading pageToken";
+ QObject *reply = The::http()->get(lastUrl);
+ connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResults(QByteArray)));
+ connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
+}
+
+void PaginatedVideoSource::loadVideoDetails(const QList<Video*> &videos) {
+ QString videoIds;
+ foreach (Video *video, videos) {
+ // TODO get video details from cache
+ if (!videoIds.isEmpty()) videoIds += ",";
+ videoIds += video->id();
+ videoMap.insert(video->id(), video);
+ }
+
+ if (videoIds.isEmpty()) {
+ if (!asyncDetails) {
+ emit gotVideos(videos);
+ emit finished(videos.size());
+ }
+ return;
+ }
+
+ QUrl url = YT3::instance().method("videos");
+
+#if QT_VERSION >= 0x050000
+ {
+ QUrl &u = url;
+ QUrlQuery url;
+#endif
+
+ url.addQueryItem("part", "contentDetails,statistics");
+ url.addQueryItem("id", videoIds);
+
+#if QT_VERSION >= 0x050000
+ u.setQuery(url);
+ }
+#endif
+
+ QObject *reply = The::http()->get(url);
+ connect(reply, SIGNAL(data(QByteArray)), SLOT(parseVideoDetails(QByteArray)));
+ connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
+}
+
+void PaginatedVideoSource::parseVideoDetails(const QByteArray &bytes) {
+
+ QScriptEngine engine;
+ QScriptValue json = engine.evaluate("(" + QString::fromUtf8(bytes) + ")");
+
+ QScriptValue items = json.property("items");
+ if (items.isArray()) {
+ QScriptValueIterator it(items);
+ while (it.hasNext()) {
+ it.next();
+ QScriptValue item = it.value();
+ if (!item.isObject()) continue;
+
+ // qDebug() << item.toString();
+
+ QString id = item.property("id").toString();
+ Video *video = videoMap.value(id);
+ if (!video) {
+ qWarning() << "No video for id" << id;
+ continue;
+ }
+
+ QString isoPeriod = item.property("contentDetails").property("duration").toString();
+ int duration = DataUtils::parseIsoPeriod(isoPeriod);
+ video->setDuration(duration);
+
+ uint viewCount = item.property("statistics").property("viewCount").toUInt32();
+ video->setViewCount(viewCount);
+
+ // TODO cache by etag?
+ }
+ }
+ if (!asyncDetails) {
+ emit gotVideos(videoMap.values());
+ emit finished(videoMap.size());
+ } else {
+ emit gotDetails();
+ }
+}
--- /dev/null
+/* $BEGIN_LICENSE
+
+This file is part of Minitube.
+Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
+
+Minitube is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Minitube is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Minitube. If not, see <http://www.gnu.org/licenses/>.
+
+$END_LICENSE */
+
+#ifndef PAGINATEDVIDEOSOURCE_H
+#define PAGINATEDVIDEOSOURCE_H
+
+#include "videosource.h"
+
+class PaginatedVideoSource : public VideoSource {
+
+ Q_OBJECT
+
+public:
+ PaginatedVideoSource(QObject *parent = 0);
+ virtual bool hasMoreVideos();
+
+ bool maybeReloadToken(int max, int startIndex);
+ bool setPageToken(const QString &value);
+ bool isPageTokenExpired();
+ void reloadToken();
+ void setAsyncDetails(bool value) { asyncDetails = value; }
+ void loadVideoDetails(const QList<Video*> &videos);
+
+signals:
+ void gotDetails();
+
+protected slots:
+ void parseVideoDetails(const QByteArray &bytes);
+
+protected:
+ QString nextPageToken;
+ uint tokenTimestamp;
+ QUrl lastUrl;
+ int currentMax;
+ int currentStartIndex;
+ bool reloadingToken;
+ QHash<QString, Video*> videoMap;
+ bool asyncDetails;
+
+};
+
+#endif // PAGINATEDVIDEOSOURCE_H
painter->drawPixmap(playIcon.rect(), playIcon);
// time
- drawTime(painter, video->formattedDuration(), line);
+ if (video->duration() > 0)
+ drawTime(painter, video->formattedDuration(), line);
// separator
painter->setPen(option.palette.color(QPalette::Midlight));
if (line.width() > THUMB_WIDTH + 60) {
- if (isActive) painter->setFont(boldFont);
+ // if (isActive) painter->setFont(boldFont);
// text color
if (isSelected)
else
painter->setOpacity(.5);
}
- QString authorString = video->author();
+ QString authorString = video->channelTitle();
textLoc.setX(textLoc.x() + stringSize.width() + PADDING);
stringSize = QSize(QFontMetrics(painter->font()).size( Qt::TextSingleLine, authorString ) );
QRect authorTextBox(textLoc , stringSize);
} else {
- bool isHovered = option.state & QStyle::State_MouseOver;
+ const bool isHovered = index.data(HoveredItemRole).toBool();
if (!isActive && isHovered) {
painter->setFont(smallerFont);
painter->setPen(Qt::white);
#include "searchparams.h"
#include "mediaview.h"
-static const int maxItems = 10;
+static const int maxItems = 50;
static const QString recentKeywordsKey = "recentKeywords";
static const QString recentChannelsKey = "recentChannels";
firstSearch = false;
m_activeVideo = 0;
m_activeRow = -1;
- skip = 1;
+ startIndex = 1;
max = 0;
hoveredRow = -1;
authorHovered = false;
case Qt::DisplayRole:
if (!errorMessage.isEmpty()) return errorMessage;
if (searching) return tr("Searching...");
- if (canSearchMore) return tr("Show %1 More").arg(maxItems);
+ if (canSearchMore) return tr("Show %1 More").arg("").simplified();
if (videos.isEmpty()) return tr("No videos");
else return tr("No more videos");
case Qt::TextAlignmentRole:
void PlaylistModel::setVideoSource(VideoSource *videoSource) {
beginResetModel();
- while (!videos.isEmpty())
- delete videos.takeFirst();
+ while (!videos.isEmpty()) delete videos.takeFirst();
+ videos.clear();
m_activeVideo = 0;
m_activeRow = -1;
- skip = 1;
+ startIndex = 1;
endResetModel();
this->videoSource = videoSource;
void PlaylistModel::searchMore(int max) {
if (searching) return;
searching = true;
- firstSearch = skip == 1;
+ firstSearch = startIndex == 1;
this->max = max;
errorMessage.clear();
- videoSource->loadVideos(max, skip);
- skip += max;
+ videoSource->loadVideos(max, startIndex);
+ startIndex += max;
}
void PlaylistModel::searchMore() {
}
void PlaylistModel::searchNeeded() {
+ const int desiredRowsAhead = 10;
int remainingRows = videos.size() - m_activeRow;
- int rowsNeeded = maxItems - remainingRows;
- if (rowsNeeded > 0)
- searchMore(rowsNeeded);
+ if (remainingRows < desiredRowsAhead)
+ searchMore(maxItems);
}
void PlaylistModel::abortSearch() {
+ QMutexLocker locker(&mutex);
beginResetModel();
- while (!videos.isEmpty())
- delete videos.takeFirst();
+ // while (!videos.isEmpty()) delete videos.takeFirst();
// if (videoSource) videoSource->abort();
+ videos.clear();
searching = false;
m_activeRow = -1;
m_activeVideo = 0;
- skip = 1;
+ startIndex = 1;
endResetModel();
}
void PlaylistModel::searchFinished(int total) {
+ qDebug() << __PRETTY_FUNCTION__ << total;
searching = false;
- canSearchMore = total >= max;
+ canSearchMore = videoSource->hasMoreVideos();
// update the message item
emit dataChanged( createIndex( maxItems, 0 ), createIndex( maxItems, columnCount() - 1 ) );
endInsertRows();
foreach (Video* video, newVideos) {
connect(video, SIGNAL(gotThumbnail()),
- SLOT(updateThumbnail()), Qt::UniqueConnection);
+ SLOT(updateVideoSender()), Qt::UniqueConnection);
video->loadThumbnail();
+ qApp->processEvents();
}
}
}
// save channel
- QString channel = searchParams->author();
- if (!channel.isEmpty() && !searchParams->isTransient()) {
+ QString channelId = searchParams->channelId();
+ if (!channelId.isEmpty() && !searchParams->isTransient()) {
QString value;
- if (!video->userId().isEmpty() && video->userId() != video->author())
- value = video->userId() + "|" + video->author();
- else value = video->author();
+ if (!video->channelId().isEmpty() && video->channelId() != video->channelTitle())
+ value = video->channelId() + "|" + video->channelTitle();
+ else value = video->channelTitle();
QStringList channels = settings.value(recentChannelsKey).toStringList();
channels.removeAll(value);
- channels.removeAll(channel);
+ channels.removeAll(channelId);
channels.prepend(value);
while (channels.size() > maxRecentElements)
channels.removeLast();
}
}
-void PlaylistModel::updateThumbnail() {
-
+void PlaylistModel::updateVideoSender() {
Video *video = static_cast<Video *>(sender());
if (!video) {
qDebug() << "Cannot get sender";
return;
}
-
int row = rowForVideo(video);
emit dataChanged( createIndex( row, 0 ), createIndex( row, columnCount() - 1 ) );
+}
+void PlaylistModel::emitDataChanged() {
+ QModelIndex index = createIndex(rowCount()-1, 0);
+ emit dataChanged(index, index);
}
// --- item removal
Qt::ItemFlags PlaylistModel::flags(const QModelIndex &index) const {
if (index.isValid()) {
if (index.row() == videos.size()) {
- // don't drag the "show 10 more" item
+ // don't drag the "show more" item
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
} else return (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
}
}
int PlaylistModel::rowForCloneVideo(const QString &videoId) const {
+ if (videoId.isEmpty()) return -1;
for (int i = 0; i < videos.size(); ++i) {
Video *v = videos.at(i);
+ // qDebug() << "Comparing" << v->id() << videoId;
if (v->id() == videoId) return i;
}
return -1;
}
void PlaylistModel::clearHover() {
- emit dataChanged( createIndex( hoveredRow, 0 ), createIndex( hoveredRow, columnCount() - 1 ) );
+ int oldRow = hoveredRow;
hoveredRow = -1;
+ emit dataChanged( createIndex( oldRow, 0 ), createIndex( oldRow, columnCount() - 1) );
+}
+
+void PlaylistModel::updateHoveredRow() {
+ emit dataChanged( createIndex( hoveredRow, 0 ), createIndex( hoveredRow, columnCount() - 1 ) );
}
/* clickable author */
void PlaylistModel::enterAuthorHover() {
if (authorHovered) return;
authorHovered = true;
- updateAuthor();
+ updateHoveredRow();
}
void PlaylistModel::exitAuthorHover() {
if (!authorHovered) return;
authorHovered = false;
- updateAuthor();
+ updateHoveredRow();
setHoveredRow(hoveredRow);
}
void PlaylistModel::enterAuthorPressed() {
if (authorPressed) return;
authorPressed = true;
- updateAuthor();
+ updateHoveredRow();
}
void PlaylistModel::exitAuthorPressed() {
if (!authorPressed) return;
authorPressed = false;
- updateAuthor();
-}
-
-void PlaylistModel::updateAuthor() {
- emit dataChanged( createIndex( hoveredRow, 0 ), createIndex( hoveredRow, columnCount() - 1 ) );
+ updateHoveredRow();
}
void addVideos(QList<Video*> newVideos);
void searchFinished(int total);
void searchError(QString message);
- void updateThumbnail();
+ void updateVideoSender();
+ void emitDataChanged();
void setHoveredRow(int row);
void clearHover();
+ void updateHoveredRow();
+
void enterAuthorHover();
void exitAuthorHover();
void enterAuthorPressed();
void exitAuthorPressed();
- void updateAuthor();
signals:
void activeRowChanged(int);
bool firstSearch;
QList<Video*> videos;
- int skip;
+ int startIndex;
int max;
int m_activeRow;
int hoveredRow;
bool authorHovered;
bool authorPressed;
+
+ QMutex mutex;
};
#endif
if (listModel) listModel->setHoveredRow(index.row());
}
-void PlaylistView::leaveEvent(QEvent * /* event */) {
+void PlaylistView::leaveEvent(QEvent *event) {
+ QListView::leaveEvent(event);
PlaylistModel *listModel = dynamic_cast<PlaylistModel *>(model());
if (listModel) listModel->clearHover();
}
m_duration = DurationAny;
m_quality = QualityAny;
m_time = TimeAny;
+ m_publishedAfter = 0;
}
void SearchParams::setParam(QString name, QVariant value) {
SearchParams(QObject *parent = 0);
const QString keywords() const { return m_keywords; }
- void setKeywords( QString keywords ) { m_keywords = keywords; }
+ void setKeywords(const QString &keywords) { m_keywords = keywords; }
- const QString author() const { return m_author; }
- void setAuthor( QString author ) { m_author = author; }
+ const QString channelId() const { return m_channelId; }
+ void setChannelId(const QString &value) { m_channelId = value; }
int sortBy() const { return m_sortBy; }
void setSortBy( int sortBy ) { m_sortBy = sortBy; }
int time() const { return m_time; }
void setTime( int time ) { m_time = time; }
+ uint publishedAfter() const { return m_publishedAfter; }
+ void setPublishedAfter(uint value) { m_publishedAfter = value; }
+
bool operator==(const SearchParams &other) const {
return m_keywords == other.keywords() &&
- m_author == other.author();
+ m_channelId == other.channelId();
}
public slots:
private:
QString m_keywords;
- QString m_author;
+ QString m_channelId;
bool m_transient;
int m_sortBy;
int m_duration;
int m_quality;
int m_time;
+ uint m_publishedAfter;
};
#if defined(APP_UBUNTU) || defined(APP_WIN)
"normal"
#else
- "bold"
+ "normal"
#endif
"' ")
.arg(Constants::WEBSITE, Constants::NAME)
}
void SearchView::appear() {
+ setUpdatesEnabled(false);
updateRecentKeywords();
updateRecentChannels();
queryEdit->selectAll();
queryEdit->enableSuggest();
if (!queryEdit->hasFocus())
QTimer::singleShot(10, queryEdit, SLOT(setFocus()));
+ setUpdatesEnabled(true);
}
void SearchView::updateRecentKeywords() {
else {
// remove spaces from channel name
query = query.simplified();
- searchParams->setAuthor(query);
+ query = query.remove(' ');
+ searchParams->setChannelId(query);
searchParams->setSortBy(SearchParams::SortByNewest);
}
emit search(searchParams);
}
-void SearchView::watchChannel(QString channel) {
-
- channel = channel.simplified();
-
- // check for empty query
- if (channel.length() == 0) {
+void SearchView::watchChannel(const QString &channelId) {
+ if (channelId.length() == 0) {
queryEdit->setFocus(Qt::OtherFocusReason);
return;
}
- // remove spaces from channel name
- channel = channel.remove(" ");
+ QString id = channelId;
+
+ // Fix old settings
+ if (!id.startsWith("UC")) id = "UC" + id;
SearchParams *searchParams = new SearchParams();
- searchParams->setAuthor(channel);
+ searchParams->setChannelId(id);
searchParams->setSortBy(SearchParams::SortByNewest);
// go!
}
void SearchView::suggestionAccepted(Suggestion *suggestion) {
- watch(suggestion->value);
+ if (suggestion->type == QLatin1String("channel")) {
+ watchChannel(suggestion->userData);
+ } else watch(suggestion->value);
}
void appear();
void disappear() { }
void watch(QString query);
- void watchChannel(QString channel);
+ void watchChannel(const QString &channelId);
void watchKeywords(QString query);
signals:
}
void StandardFeedsView::load() {
+ setUpdatesEnabled(false);
YTCategories *youTubeCategories = new YTCategories(this);
connect(youTubeCategories, SIGNAL(categoriesLoaded(const QList<YTCategory> &)),
SLOT(layoutCategories(const QList<YTCategory> &)));
feed->setFeedId("most_popular");
addVideoSourceWidget(feed);
}
+ if (categories.size() > 1) setUpdatesEnabled(true);
}
void StandardFeedsView::addVideoSourceWidget(VideoSource *videoSource) {
void StandardFeedsView::appear() {
setFocus();
- if (!layout) load();
+ if (!layout) {
+ update();
+ qApp->processEvents();
+ load();
+ }
QAction *regionAction = MainWindow::instance()->getRegionAction();
regionAction->setVisible(true);
}
void StandardFeedsView::paintEvent(QPaintEvent *event) {
QWidget::paintEvent(event);
- PainterUtils::topShadow(this);
+ // PainterUtils::topShadow(this);
}
public:
Suggestion(QString value = QString(),
- QString type = QString()) :
- value(value), type(type) { }
+ QString type = QString(),
+ QString userData = QString()) :
+ value(value), type(type), userData(userData) { }
QString value;
QString type;
+ QString userData;
bool operator==(const Suggestion &other) const {
return (value == other.value) && (type == other.type);
elIndex(0),
ageGate(false),
loadingStreamUrl(false),
- loadingThumbnail(false)
-{ }
+ loadingThumbnail(false) {
+}
Video* Video::clone() {
Video* cloneVideo = new Video();
cloneVideo->m_title = m_title;
cloneVideo->m_description = m_description;
- cloneVideo->m_author = m_author;
- cloneVideo->m_userId = m_userId;
+ cloneVideo->m_channelTitle = m_channelTitle;
+ cloneVideo->m_channelId = m_channelId;
cloneVideo->m_webpage = m_webpage;
cloneVideo->m_streamUrl = m_streamUrl;
cloneVideo->m_thumbnail = m_thumbnail;
return cloneVideo;
}
-void Video::setWebpage(QUrl webpage) {
- m_webpage = webpage;
+const QString &Video::webpage() {
+ if (m_webpage.isEmpty() && !videoId.isEmpty())
+ m_webpage.append("https://www.youtube.com/watch?v=").append(videoId);
+ return m_webpage;
+}
+
+void Video::setWebpage(const QString &value) {
+ m_webpage = value;
// Get Video ID
- QRegExp re(JsFunctions::instance()->videoIdRE());
- if (re.indexIn(m_webpage.toString()) == -1) {
- qWarning() << QString("Cannot get video id for %1").arg(m_webpage.toString());
- // emit errorStreamUrl(QString("Cannot get video id for %1").arg(m_webpage.toString()));
- // loadingStreamUrl = false;
- return;
+ if (videoId.isEmpty()) {
+ QRegExp re(JsFunctions::instance()->videoIdRE());
+ if (re.indexIn(m_webpage) == -1) {
+ qWarning() << QString("Cannot get video id for %1").arg(m_webpage);
+ // emit errorStreamUrl(QString("Cannot get video id for %1").arg(m_webpage.toString()));
+ // loadingStreamUrl = false;
+ return;
+ }
+ videoId = re.cap(1);
}
- videoId = re.cap(1);
}
void Video::loadThumbnail() {
void Video::setThumbnail(QByteArray bytes) {
loadingThumbnail = false;
+ m_thumbnail = QPixmap();
m_thumbnail.loadFromData(bytes);
if (m_thumbnail.width() > 160)
m_thumbnail = m_thumbnail.scaledToWidth(160, Qt::SmoothTransformation);
if (elIndex == elTypes.size()) {
// qDebug() << "Trying special embedded el param";
- url = QUrl("http://www.youtube.com/get_video_info");
+ url = QUrl("https://www.youtube.com/get_video_info");
#if QT_VERSION >= 0x050000
{
} else {
// qDebug() << "Trying el param:" << elTypes.at(elIndex) << elIndex;
url = QUrl(QString(
- "http://www.youtube.com/get_video_info?video_id=%1%2&ps=default&eurl=&gl=US&hl=en"
+ "https://www.youtube.com/get_video_info?video_id=%1%2&ps=default&eurl=&gl=US&hl=en"
).arg(videoId, elTypes.at(elIndex)));
}
// get video token
QRegExp videoTokeRE(JsFunctions::instance()->videoTokenRE());
if (videoTokeRE.indexIn(videoInfo) == -1) {
- // qWarning() << "Cannot get token. Trying next el param" << videoInfo << videoTokeRE.pattern();
+ qDebug() << "Cannot get token. Trying next el param" << videoInfo << videoTokeRE.pattern();
// Don't panic! We're gonna try another magic "el" param
elIndex++;
getVideoInfo();
}
QString videoToken = videoTokeRE.cap(1);
- // qWarning() << "got token" << videoToken;
+ // qDebug() << "got token" << videoToken;
while (videoToken.contains('%'))
videoToken = QByteArray::fromPercentEncoding(videoToken.toLatin1());
// qDebug() << "videoToken" << videoToken;
// get fmt_url_map
QRegExp fmtMapRE(JsFunctions::instance()->videoInfoFmtMapRE());
if (fmtMapRE.indexIn(videoInfo) == -1) {
- // qWarning() << "Cannot get urlMap. Trying next el param";
+ // qDebug() << "Cannot get urlMap. Trying next el param";
// Don't panic! We're gonna try another magic "el" param
elIndex++;
getVideoInfo();
// qDebug() << "Got token and urlMap" << elIndex;
QString fmtUrlMap = fmtMapRE.cap(1);
- // qWarning() << "got fmtUrlMap" << fmtUrlMap;
+ // qDebug() << "got fmtUrlMap" << fmtUrlMap;
fmtUrlMap = QByteArray::fromPercentEncoding(fmtUrlMap.toUtf8());
parseFmtUrlMap(fmtUrlMap);
}
sig = JsFunctions::instance()->decryptSignature(sig);
}
} else {
- // qDebug() << "Loading webpage";
+
QUrl url("http://www.youtube.com/watch");
#if QT_VERSION >= 0x050000
u.setQuery(url);
}
#endif
+ // qDebug() << "Loading webpage" << url;
QObject *reply = The::http()->get(url);
connect(reply, SIGNAL(data(QByteArray)), SLOT(scrapeWebPage(QByteArray)));
connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorVideoInfo(QNetworkReply*)));
// qWarning() << url;
if (format == definitionCode) {
- qDebug() << "Found format" << definitionCode;
+ // qDebug() << "Found format" << definitionCode;
QUrl videoUrl = QUrl::fromEncoded(url.toUtf8(), QUrl::StrictMode);
m_streamUrl = videoUrl;
this->definitionCode = definitionCode;
if (previousIndex < 0) previousIndex = 0;
int definitionCode = definitionCodes.at(previousIndex);
if (urlMap.contains(definitionCode)) {
- qDebug() << "Found format" << definitionCode;
+ // qDebug() << "Found format" << definitionCode;
QString url = urlMap.value(definitionCode);
QUrl videoUrl = QUrl::fromEncoded(url.toUtf8(), QUrl::StrictMode);
m_streamUrl = videoUrl;
currentIndex--;
}
- emit errorStreamUrl(tr("Cannot get video stream for %1").arg(m_webpage.toString()));
+ emit errorStreamUrl(tr("Cannot get video stream for %1").arg(m_webpage));
}
void Video::errorVideoInfo(QNetworkReply *reply) {
void Video::scrapeWebPage(QByteArray data) {
QString html = QString::fromUtf8(data);
- // qWarning() << html;
QRegExp ageGateRE(JsFunctions::instance()->ageGateRE());
if (ageGateRE.indexIn(html) != -1) {
QRegExp fmtMapRE(JsFunctions::instance()->webPageFmtMapRE());
if (fmtMapRE.indexIn(html) == -1) {
- // qWarning() << "Error parsing video page";
+ qWarning() << "Error parsing video page";
// emit errorStreamUrl("Error parsing video page");
// loadingStreamUrl = false;
elIndex++;
#ifdef APP_DASH
QSettings settings;
QString definitionName = settings.value("definition", "360p").toString();
- if (definitionName == QLatin1String("1080p") {
+ if (definitionName == QLatin1String("1080p")) {
QRegExp dashManifestRe("\"dashmpd\":\\s*\"([^\"]+)\"");
if (dashManifestRe.indexIn(html) != -1) {
dashManifestUrl = dashManifestRe.cap(1);
jsPlayerUrl = "http:" + jsPlayerUrl;
// qDebug() << "jsPlayerUrl" << jsPlayerUrl;
/*
- QRegExp jsPlayerIdRe("-(.+)\\.js");
- jsPlayerIdRe.indexIn(jsPlayerUrl);
- QString jsPlayerId = jsPlayerRe.cap(1);
- */
+ QRegExp jsPlayerIdRe("-(.+)\\.js");
+ jsPlayerIdRe.indexIn(jsPlayerUrl);
+ QString jsPlayerId = jsPlayerRe.cap(1);
+ */
QObject *reply = The::http()->get(jsPlayerUrl);
connect(reply, SIGNAL(data(QByteArray)), SLOT(parseJsPlayer(QByteArray)));
connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorVideoInfo(QNetworkReply*)));
dashManifestUrl.replace(sigRe, "/signature/" + sig);
qDebug() << dashManifestUrl;
- m_streamUrl = dashManifestUrl;
- this->definitionCode = 37;
- emit gotStreamUrl(m_streamUrl);
- loadingStreamUrl = false;
-
- /*
- QObject *reply = The::http()->get(QUrl::fromEncoded(dashManifestUrl.toUtf8()));
- connect(reply, SIGNAL(data(QByteArray)), SLOT(parseDashManifest(QByteArray)));
- connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorVideoInfo(QNetworkReply*)));
- */
+ if (false) {
+ // let phonon play the manifest
+ m_streamUrl = dashManifestUrl;
+ this->definitionCode = 37;
+ emit gotStreamUrl(m_streamUrl);
+ loadingStreamUrl = false;
+ } else {
+ // download the manifest
+ QObject *reply = The::http()->get(QUrl::fromEncoded(dashManifestUrl.toUtf8()));
+ connect(reply, SIGNAL(data(QByteArray)), SLOT(parseDashManifest(QByteArray)));
+ connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorVideoInfo(QNetworkReply*)));
+ }
return;
}
}
void Video::parseDashManifest(QByteArray bytes) {
- QFile file(Temporary::filename());
+ QFile file(Temporary::filename() + ".mpd");
if (!file.open(QIODevice::WriteOnly))
qWarning() << file.errorString() << file.fileName();
QDataStream stream(&file);
};
const QString & title() const { return m_title; }
- void setTitle( QString title ) { m_title = title; }
+ void setTitle(const QString &title) { m_title = title; }
const QString & description() const { return m_description; }
- void setDescription( QString description ) { m_description = description; }
+ void setDescription(const QString &description) { m_description = description; }
- const QString & author() const { return m_author; }
- void setAuthor( QString author ) { m_author = author; }
+ const QString & channelTitle() const { return m_channelTitle; }
+ void setChannelTitle(const QString &value) { m_channelTitle = value; }
- const QString & userId() const { return m_userId; }
- void setUserId( QString userId ) { m_userId = userId; }
+ const QString & channelId() const { return m_channelId; }
+ void setChannelId(const QString &value ) { m_channelId = value; }
- const QUrl & webpage() const { return m_webpage; }
- void setWebpage(QUrl webpage);
+ const QString & webpage();
+ void setWebpage(const QString &value);
void loadThumbnail();
const QPixmap & thumbnail() const { return m_thumbnail; }
const QString & thumbnailUrl() { return m_thumbnailUrl; }
- void setThumbnailUrl(QString url) { m_thumbnailUrl = url; }
+ void setThumbnailUrl(const QString &url) { m_thumbnailUrl = url; }
void loadMediumThumbnail();
const QString & mediumThumbnailUrl() { return m_mediumThumbnailUrl; }
- void setMediumThumbnailUrl(QString url) { m_mediumThumbnailUrl = url; }
+ void setMediumThumbnailUrl(const QString &url) { m_mediumThumbnailUrl = url; }
int duration() const { return m_duration; }
void setDuration( int duration ) { m_duration = duration; }
void setViewCount( int viewCount ) { m_viewCount = viewCount; }
const QDateTime & published() const { return m_published; }
- void setPublished( QDateTime published ) { m_published = published; }
+ void setPublished(const QDateTime &published ) { m_published = published; }
int getDefinitionCode() const { return definitionCode; }
QString m_title;
QString m_description;
- QString m_author;
- QString m_userId;
- QUrl m_webpage;
+ QString m_channelTitle;
+ QString m_channelId;
+ QString m_webpage;
QUrl m_streamUrl;
QPixmap m_thumbnail;
QString m_thumbnailUrl;
public:
VideoSource(QObject *parent = 0) : QObject(parent) { }
- virtual void loadVideos(int max, int skip) = 0;
+ virtual void loadVideos(int max, int startIndex) = 0;
+ virtual bool hasMoreVideos() { return true; }
virtual void abort() = 0;
virtual const QStringList & getSuggestions() = 0;
virtual QString getName() = 0;
--- /dev/null
+#include "yt3.h"
+
+#include <algorithm>
+#include <ctime>
+
+#include "jsfunctions.h"
+#include "networkaccess.h"
+#include "constants.h"
+
+#ifdef APP_EXTRA
+#include "extra.h"
+#endif
+
+#define STR(x) #x
+#define STRINGIFY(x) STR(x)
+
+namespace The {
+NetworkAccess* http();
+}
+
+YT3 &YT3::instance() {
+ static YT3 *i = new YT3();
+ return *i;
+}
+
+YT3::YT3() {
+ QByteArray customApiKey = qgetenv("GOOGLE_API_KEY");
+ if (!customApiKey.isEmpty()) {
+ keys << QString::fromUtf8(customApiKey);
+ qDebug() << "API key from environment" << keys;
+ }
+
+ if (keys.isEmpty()) {
+ QSettings settings;
+ if (settings.contains("googleApiKey")) {
+ keys << settings.value("googleApiKey").toString();
+ qDebug() << "API key from settings" << keys;
+ }
+ }
+
+#ifdef APP_GOOGLE_API_KEY
+ if (keys.isEmpty()) {
+ keys << STRINGIFY(APP_GOOGLE_API_KEY);
+ qDebug() << "built-in API key" << keys;
+ }
+#endif
+
+#ifdef APP_EXTRA
+ if (keys.isEmpty())
+ keys << Extra::apiKeys();
+#endif
+
+ if (keys.isEmpty()) {
+ qWarning() << "No available API keys";
+ } else {
+ key = keys.takeFirst();
+ testApiKey();
+ }
+}
+
+const QString &YT3::baseUrl() {
+ static const QString base = "https://www.googleapis.com/youtube/v3/";
+ return base;
+}
+
+void YT3::testApiKey() {
+ QUrl url = method("videos");
+#if QT_VERSION >= 0x050000
+ {
+ QUrl &u = url;
+ QUrlQuery url;
+#endif
+ url.addQueryItem("part", "id");
+ url.addQueryItem("chart", "mostPopular");
+ url.addQueryItem("maxResults", "1");
+#if QT_VERSION >= 0x050000
+ u.setQuery(url);
+ }
+#endif
+ QObject *reply = The::http()->get(url);
+ connect(reply, SIGNAL(finished(QNetworkReply*)), SLOT(testResponse(QNetworkReply*)));
+}
+
+void YT3::addApiKey(QUrl &url) {
+ if (key.isEmpty()) {
+ qDebug() << __PRETTY_FUNCTION__ << "empty key";
+ return;
+ }
+#if QT_VERSION >= 0x050000
+ {
+ QUrl &u = url;
+ QUrlQuery url(u);
+#endif
+ url.addQueryItem("key", key);
+#if QT_VERSION >= 0x050000
+ u.setQuery(url);
+ }
+#endif
+}
+
+QUrl YT3::method(const QString &name) {
+ QUrl url(baseUrl() + name);
+ addApiKey(url);
+ return url;
+}
+
+void YT3::testResponse(QNetworkReply *reply) {
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ if (status != 200) {
+ if (keys.isEmpty()) {
+ qWarning() << "Fatal error: No working API keys!";
+ return;
+ }
+ key = keys.takeFirst();
+ testApiKey();
+ } else {
+ qDebug() << "Using key" << key;
+ }
+}
--- /dev/null
+#ifndef YT3_H
+#define YT3_H
+
+#include <QtCore>
+#include <QtNetwork>
+
+class YT3 : public QObject {
+
+ Q_OBJECT
+
+public:
+ static YT3 &instance();
+ static const QString &baseUrl();
+
+ void testApiKey();
+ void addApiKey(QUrl &url);
+ QUrl method(const QString &name);
+
+signals:
+ void gotChannelId(QString channelId);
+
+private slots:
+ void testResponse(QNetworkReply *reply);
+
+private:
+ YT3();
+
+ QStringList keys;
+ QString key;
+};
+
+#endif // YT3_H
--- /dev/null
+#include "yt3listparser.h"
+#include "video.h"
+#include "datautils.h"
+
+YT3ListParser::YT3ListParser(const QByteArray &bytes) {
+
+ QScriptEngine engine;
+ QScriptValue json = engine.evaluate("(" + QString::fromUtf8(bytes) + ")");
+
+ nextPageToken = json.property("nextPageToken").toString();
+
+ QScriptValue items = json.property("items");
+ videos.reserve(items.property("length").toInt32() - 1);
+ if (items.isArray()) {
+ QScriptValueIterator it(items);
+ while (it.hasNext()) {
+ it.next();
+ QScriptValue item = it.value();
+ // For some reason the array has an additional element containing its size.
+ if (item.isObject()) parseItem(item);
+ }
+ }
+
+ // TODO suggestions!
+}
+
+void YT3ListParser::parseItem(const QScriptValue &item) {
+ Video *video = new Video();
+
+ QScriptValue id = item.property("id");
+ if (id.isString()) video->setId(id.toString());
+ else {
+ QString videoId = id.property("videoId").toString();
+ video->setId(videoId);
+ }
+
+ QScriptValue snippet = item.property("snippet");
+
+ bool isLiveBroadcastContent = snippet.property("liveBroadcastContent").toString() != QLatin1String("none");
+ if (isLiveBroadcastContent) {
+ delete video;
+ return;
+ }
+
+ QString publishedAt = snippet.property("publishedAt").toString();
+ QDateTime publishedDateTime = QDateTime::fromString(publishedAt, Qt::ISODate);
+ video->setPublished(publishedDateTime);
+
+ video->setChannelId(snippet.property("channelId").toString());
+
+ video->setTitle(snippet.property("title").toString());
+ video->setDescription(snippet.property("description").toString());
+
+ QScriptValue thumbnails = snippet.property("thumbnails");
+ video->setThumbnailUrl(thumbnails.property("medium").property("url").toString());
+ video->setMediumThumbnailUrl(thumbnails.property("high").property("url").toString());
+
+ video->setChannelTitle(snippet.property("channelTitle").toString());
+
+ // These are only for "videos" requests
+
+ QScriptValue contentDetails = item.property("contentDetails");
+ if (contentDetails.isObject()) {
+ QString isoPeriod = contentDetails.property("duration").toString();
+ int duration = DataUtils::parseIsoPeriod(isoPeriod);
+ video->setDuration(duration);
+ }
+
+ QScriptValue statistics = item.property("statistics");
+ if (statistics.isObject()) {
+ uint viewCount = statistics.property("viewCount").toUInt32();
+ video->setViewCount(viewCount);
+ }
+
+ videos.append(video);
+}
--- /dev/null
+#ifndef YT3LISTPARSER_H
+#define YT3LISTPARSER_H
+
+#include <QtCore>
+#include <QtScript>
+
+class Video;
+
+class YT3ListParser : public QObject {
+
+public:
+ YT3ListParser(const QByteArray &bytes);
+ const QList<Video*> &getVideos() { return videos; }
+ const QStringList &getSuggestions() { return suggestions; }
+ const QString &getNextPageToken() { return nextPageToken; }
+
+private:
+ void parseItem(const QScriptValue &item);
+
+ QList<Video*> videos;
+ QStringList suggestions;
+ QString nextPageToken;
+};
+
+#endif // YT3LISTPARSER_H
#include "ytcategories.h"
#include "networkaccess.h"
-#include <QtXml>
+#ifdef APP_YT3
+#include "datautils.h"
+#include "yt3.h"
+#include "ytregions.h"
+#include <QtScript>
+#endif
namespace The {
NetworkAccess* http();
language = QLocale::system().uiLanguages().first();
lastLanguage = language;
+#ifdef APP_YT3
+ QUrl url = YT3::instance().method("videoCategories");
+
+#if QT_VERSION >= 0x050000
+ {
+ QUrl &u = url;
+ QUrlQuery url(u);
+#endif
+
+ url.addQueryItem("part", "snippet");
+ url.addQueryItem("hl", language);
+
+ QString regionCode = YTRegions::currentRegionId();
+ if (regionCode.isEmpty()) regionCode = "us";
+ url.addQueryItem("regionCode", regionCode);
+
+#if QT_VERSION >= 0x050000
+ u.setQuery(url);
+ }
+#endif
+
+#else
QString url = "http://gdata.youtube.com/schemas/2007/categories.cat?hl=" + language;
+#endif
+
+
QObject *reply = The::http()->get(url);
connect(reply, SIGNAL(data(QByteArray)), SLOT(parseCategories(QByteArray)));
connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
}
+#ifdef APP_YT3
+
+void YTCategories::parseCategories(QByteArray bytes) {
+ QList<YTCategory> categories;
+
+ QScriptEngine engine;
+ QScriptValue json = engine.evaluate("(" + QString::fromUtf8(bytes) + ")");
+
+ QScriptValue items = json.property("items");
+
+ if (items.isArray()) {
+ QScriptValueIterator it(items);
+ while (it.hasNext()) {
+ it.next();
+ QScriptValue item = it.value();
+ // For some reason the array has an additional element containing its size.
+ if (!item.isObject()) continue;
+
+ QScriptValue snippet = item.property("snippet");
+
+ bool isAssignable = snippet.property("assignable").toBool();
+ if (!isAssignable) continue;
+
+ YTCategory category;
+ category.term = item.property("id").toString();
+ category.label = snippet.property("title").toString();
+ categories << category;
+ }
+ }
+
+ emit categoriesLoaded(categories);
+}
+
+#else
+
void YTCategories::parseCategories(QByteArray bytes) {
QList<YTCategory> categories;
emit categoriesLoaded(categories);
}
+#endif
+
void YTCategories::requestError(QNetworkReply *reply) {
if (lastLanguage != "en") loadCategories("en");
else emit error(reply->errorString());
--- /dev/null
+/* $BEGIN_LICENSE
+
+This file is part of Minitube.
+Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
+
+Minitube is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Minitube is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Minitube. If not, see <http://www.gnu.org/licenses/>.
+
+$END_LICENSE */
+
+#include "ytchannel.h"
+#include "networkaccess.h"
+#include "database.h"
+#include <QtSql>
+
+#ifdef APP_YT3
+#include "yt3.h"
+#include <QtScript>
+#endif
+
+namespace The {
+NetworkAccess* http();
+}
+
+YTChannel::YTChannel(const QString &channelId, QObject *parent) : QObject(parent),
+ id(0),
+ channelId(channelId),
+ loadingThumbnail(false),
+ notifyCount(0),
+ checked(0),
+ watched(0),
+ loaded(0),
+ loading(false) { }
+
+QHash<QString, YTChannel*> YTChannel::cache;
+
+YTChannel* YTChannel::forId(const QString &channelId) {
+ if (channelId.isEmpty()) return 0;
+
+ if (cache.contains(channelId))
+ return cache.value(channelId);
+
+ QSqlDatabase db = Database::instance().getConnection();
+ QSqlQuery query(db);
+ query.prepare("select id,name,description,thumb_url,notify_count,watched,checked,loaded "
+ "from subscriptions where user_id=?");
+ query.bindValue(0, channelId);
+ bool success = query.exec();
+ if (!success) qWarning() << query.lastQuery() << query.lastError().text();
+
+ YTChannel* channel = 0;
+ if (query.next()) {
+ // Change userId to ChannelId
+
+ channel = new YTChannel(channelId);
+ channel->id = query.value(0).toInt();
+ channel->displayName = query.value(1).toString();
+ channel->description = query.value(2).toString();
+ channel->thumbnailUrl = query.value(3).toString();
+ channel->notifyCount = query.value(4).toInt();
+ channel->watched = query.value(5).toUInt();
+ channel->checked = query.value(6).toUInt();
+ channel->loaded = query.value(7).toUInt();
+ channel->thumbnail = QPixmap(channel->getThumbnailLocation());
+ channel->maybeLoadfromAPI();
+ cache.insert(channelId, channel);
+ }
+
+ return channel;
+}
+
+void YTChannel::maybeLoadfromAPI() {
+ if (loading) return;
+ if (channelId.isEmpty()) return;
+
+ uint now = QDateTime::currentDateTime().toTime_t();
+ static const int refreshInterval = 60 * 60 * 24 * 10;
+ if (loaded > now - refreshInterval) return;
+
+ loading = true;
+
+#ifdef APP_YT3
+
+ QUrl url = YT3::instance().method("channels");
+#if QT_VERSION >= 0x050000
+ {
+ QUrl &u = url;
+ QUrlQuery url;
+#endif
+ url.addQueryItem("id", channelId);
+ url.addQueryItem("part", "snippet");
+#if QT_VERSION >= 0x050000
+ u.setQuery(url);
+ }
+#endif
+
+#else
+
+ QUrl url("http://gdata.youtube.com/feeds/api/users/" + channelId);
+#if QT_VERSION >= 0x050000
+ {
+ QUrl &u = url;
+ QUrlQuery url;
+#endif
+ url.addQueryItem("v", "2");
+#if QT_VERSION >= 0x050000
+ u.setQuery(url);
+ }
+#endif
+
+#endif
+
+ QObject *reply = The::http()->get(url);
+ connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResponse(QByteArray)));
+ connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
+}
+
+#ifdef APP_YT3
+
+void YTChannel::parseResponse(const QByteArray &bytes) {
+ QScriptEngine engine;
+ QScriptValue json = engine.evaluate("(" + QString::fromUtf8(bytes) + ")");
+ QScriptValue items = json.property("items");
+ if (items.isArray()) {
+ QScriptValueIterator it(items);
+ while (it.hasNext()) {
+ it.next();
+ QScriptValue item = it.value();
+ // For some reason the array has an additional element containing its size.
+ if (item.isObject()) {
+ QScriptValue snippet = item.property("snippet");
+ displayName = snippet.property("title").toString();
+ description = snippet.property("description").toString();
+ QScriptValue thumbnails = snippet.property("thumbnails");
+ thumbnailUrl = thumbnails.property("default").property("url").toString();
+ qDebug() << displayName << description << thumbnailUrl;
+ }
+ }
+ }
+
+ emit infoLoaded();
+ storeInfo();
+ loading = false;
+}
+
+#else
+
+void YTChannel::parseResponse(const QByteArray &bytes) {
+ QXmlStreamReader xml(bytes);
+ xml.readNextStartElement();
+ if (xml.name() == QLatin1String("entry"))
+ while(xml.readNextStartElement()) {
+ const QStringRef n = xml.name();
+ if (n == QLatin1String("summary"))
+ description = xml.readElementText().simplified();
+ else if (n == QLatin1String("title"))
+ displayName = xml.readElementText();
+ else if (n == QLatin1String("thumbnail")) {
+ thumbnailUrl = xml.attributes().value("url").toString();
+ xml.skipCurrentElement();
+ } else if (n == QLatin1String("username"))
+ userName = xml.readElementText();
+ else xml.skipCurrentElement();
+ }
+
+ if (xml.hasError()) {
+ emit error(xml.errorString());
+ qWarning() << xml.errorString();
+ }
+
+ emit infoLoaded();
+ storeInfo();
+ loading = false;
+}
+
+#endif
+
+void YTChannel::loadThumbnail() {
+ if (loadingThumbnail) return;
+ if (thumbnailUrl.isEmpty()) return;
+ loadingThumbnail = true;
+
+#ifdef Q_OS_WIN
+ thumbnailUrl.replace(QLatin1String("https://"), QLatin1String("http://"));
+#endif
+
+ QUrl url(thumbnailUrl);
+ QObject *reply = The::http()->get(url);
+ connect(reply, SIGNAL(data(QByteArray)), SLOT(storeThumbnail(QByteArray)));
+ connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
+}
+
+const QString & YTChannel::getThumbnailDir() {
+ static const QString thumbDir =
+ #if QT_VERSION >= 0x050000
+ QStandardPaths::writableLocation(QStandardPaths::DataLocation)
+ #else
+ QDesktopServices::storageLocation(QDesktopServices::DataLocation)
+ #endif
+ + "/channels/";
+ return thumbDir;
+}
+
+QString YTChannel::getThumbnailLocation() {
+ return getThumbnailDir() + channelId;
+}
+
+void YTChannel::unsubscribe() {
+ YTChannel::unsubscribe(channelId);
+}
+
+void YTChannel::storeThumbnail(const QByteArray &bytes) {
+ thumbnail.loadFromData(bytes);
+ static const int maxWidth = 88;
+
+ QDir dir;
+ dir.mkpath(getThumbnailDir());
+
+ if (thumbnail.width() > maxWidth) {
+ thumbnail = thumbnail.scaledToWidth(maxWidth, Qt::SmoothTransformation);
+ thumbnail.save(getThumbnailLocation(), "JPG");
+ } else {
+ QFile file(getThumbnailLocation());
+ if (!file.open(QIODevice::WriteOnly))
+ qWarning() << "Error opening file for writing" << file.fileName();
+ QDataStream stream(&file);
+ stream.writeRawData(bytes.constData(), bytes.size());
+ }
+
+ emit thumbnailLoaded();
+ loadingThumbnail = false;
+}
+
+void YTChannel::requestError(QNetworkReply *reply) {
+ emit error(reply->errorString());
+ qWarning() << reply->errorString();
+ loading = false;
+ loadingThumbnail = false;
+}
+
+void YTChannel::storeInfo() {
+ if (channelId.isEmpty()) return;
+ QSqlDatabase db = Database::instance().getConnection();
+ QSqlQuery query(db);
+ query.prepare("update subscriptions set "
+ "user_name=?, name=?, description=?, thumb_url=?, loaded=? "
+ "where user_id=?");
+ qDebug() << userName;
+ query.bindValue(0, userName);
+ query.bindValue(1, displayName);
+ query.bindValue(2, description);
+ query.bindValue(3, thumbnailUrl);
+ query.bindValue(4, QDateTime::currentDateTime().toTime_t());
+ query.bindValue(5, channelId);
+ bool success = query.exec();
+ if (!success) qWarning() << query.lastQuery() << query.lastError().text();
+
+ loadThumbnail();
+}
+
+void YTChannel::subscribe(const QString &channelId) {
+ if (channelId.isEmpty()) return;
+
+ uint now = QDateTime::currentDateTime().toTime_t();
+
+ QSqlDatabase db = Database::instance().getConnection();
+ QSqlQuery query(db);
+ query.prepare("insert into subscriptions "
+ "(user_id,added,watched,checked,views,notify_count)"
+ " values (?,?,?,0,0,0)");
+ query.bindValue(0, channelId);
+ query.bindValue(1, now);
+ query.bindValue(2, now);
+ bool success = query.exec();
+ if (!success) qWarning() << query.lastQuery() << query.lastError().text();
+
+ // This will call maybeLoadFromApi
+ YTChannel::forId(channelId);
+}
+
+void YTChannel::unsubscribe(const QString &channelId) {
+ if (channelId.isEmpty()) return;
+ QSqlDatabase db = Database::instance().getConnection();
+ QSqlQuery query(db);
+ query.prepare("delete from subscriptions where user_id=?");
+ query.bindValue(0, channelId);
+ bool success = query.exec();
+ if (!success) qWarning() << query.lastQuery() << query.lastError().text();
+
+ query = QSqlQuery(db);
+ query.prepare("delete from subscriptions_videos where user_id=?");
+ query.bindValue(0, channelId);
+ success = query.exec();
+ if (!success) qWarning() << query.lastQuery() << query.lastError().text();
+
+ YTChannel *user = cache.take(channelId);
+ if (user) user->deleteLater();
+}
+
+bool YTChannel::isSubscribed(const QString &channelId) {
+ if (!Database::exists()) return false;
+ if (channelId.isEmpty()) return false;
+ QSqlDatabase db = Database::instance().getConnection();
+ QSqlQuery query(db);
+ query.prepare("select count(*) from subscriptions where user_id=?");
+ query.bindValue(0, channelId);
+ bool success = query.exec();
+ if (!success) qWarning() << query.lastQuery() << query.lastError().text();
+ if (query.next())
+ return query.value(0).toInt() > 0;
+ return false;
+}
+
+void YTChannel::updateChecked() {
+ if (channelId.isEmpty()) return;
+
+ uint now = QDateTime::currentDateTime().toTime_t();
+ checked = now;
+
+ QSqlDatabase db = Database::instance().getConnection();
+ QSqlQuery query(db);
+ query.prepare("update subscriptions set checked=? where user_id=?");
+ query.bindValue(0, now);
+ query.bindValue(1, channelId);
+ bool success = query.exec();
+ if (!success) qWarning() << query.lastQuery() << query.lastError().text();
+}
+
+void YTChannel::updateWatched() {
+ if (channelId.isEmpty()) return;
+
+ uint now = QDateTime::currentDateTime().toTime_t();
+ watched = now;
+ notifyCount = 0;
+ emit notifyCountChanged();
+
+ QSqlDatabase db = Database::instance().getConnection();
+ QSqlQuery query(db);
+ query.prepare("update subscriptions set watched=?, notify_count=0, views=views+1 where user_id=?");
+ query.bindValue(0, now);
+ query.bindValue(1, channelId);
+ bool success = query.exec();
+ if (!success) qWarning() << query.lastQuery() << query.lastError().text();
+}
+
+void YTChannel::storeNotifyCount(int count) {
+ if (notifyCount != count)
+ emit notifyCountChanged();
+ notifyCount = count;
+
+ QSqlDatabase db = Database::instance().getConnection();
+ QSqlQuery query(db);
+ query.prepare("update subscriptions set notify_count=? where user_id=?");
+ query.bindValue(0, count);
+ query.bindValue(1, channelId);
+ bool success = query.exec();
+ if (!success) qWarning() << query.lastQuery() << query.lastError().text();
+}
+
+bool YTChannel::updateNotifyCount() {
+ QSqlDatabase db = Database::instance().getConnection();
+ QSqlQuery query(db);
+ query.prepare("select count(*) from subscriptions_videos "
+ "where channel_id=? and added>? and published>? and watched=0");
+ query.bindValue(0, id);
+ query.bindValue(1, watched);
+ query.bindValue(2, watched);
+ bool success = query.exec();
+ if (!success) qWarning() << query.lastQuery() << query.lastError().text();
+ if (!query.next()) {
+ qWarning() << __PRETTY_FUNCTION__ << "Count failed";
+ return false;
+ }
+ int count = query.value(0).toInt();
+ storeNotifyCount(count);
+ return count != notifyCount;
+}
--- /dev/null
+/* $BEGIN_LICENSE
+
+This file is part of Minitube.
+Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
+
+Minitube is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Minitube is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Minitube. If not, see <http://www.gnu.org/licenses/>.
+
+$END_LICENSE */
+
+#ifndef YTCHANNEL_H
+#define YTCHANNEL_H
+
+#include <QtGui>
+#if QT_VERSION >= 0x050000
+#include <QtWidgets>
+#endif
+#include <QtNetwork>
+
+class YTChannel : public QObject {
+
+ Q_OBJECT
+
+public:
+ static YTChannel* forId(const QString &channelId);
+ static void subscribe(const QString &channelId);
+ static void unsubscribe(const QString &channelId);
+ static bool isSubscribed(const QString &channelId);
+
+ int getId() { return id; }
+ void setId(int id) { this->id = id; }
+
+ uint getChecked() { return checked; }
+ void updateChecked();
+
+ uint getWatched() const { return watched; }
+ void setWatched(uint watched) { this->watched = watched; }
+
+ int getNotifyCount() const { return notifyCount; }
+ void setNotifyCount(int count) { notifyCount = count; }
+ void storeNotifyCount(int count);
+ bool updateNotifyCount();
+
+ QString getChannelId() const { return channelId; }
+ QString getUserName() const { return userName; }
+ QString getDisplayName() const { return displayName; }
+ QString getDescription() const { return description; }
+ QString getCountryCode() const { return countryCode; }
+
+ void loadThumbnail();
+ const QString & getThumbnailDir();
+ QString getThumbnailLocation();
+ const QPixmap & getThumbnail() { return thumbnail; }
+
+ static QList<YTChannel*> getCachedChannels() { return cache.values(); }
+
+public slots:
+ void updateWatched();
+ void unsubscribe();
+
+signals:
+ void infoLoaded();
+ void thumbnailLoaded();
+ void error(QString message);
+ void notifyCountChanged();
+
+private slots:
+ void parseResponse(const QByteArray &bytes);
+ void requestError(QNetworkReply *reply);
+ void storeThumbnail(const QByteArray &bytes);
+
+private:
+ YTChannel(const QString &channelId, QObject *parent = 0);
+ void maybeLoadfromAPI();
+ void storeInfo();
+
+ static QHash<QString, YTChannel*> cache;
+
+ int id;
+ QString channelId;
+ QString userName;
+ QString displayName;
+ QString description;
+ QString countryCode;
+
+ QString thumbnailUrl;
+ QPixmap thumbnail;
+ bool loadingThumbnail;
+
+ int notifyCount;
+ uint checked;
+ uint watched;
+ uint loaded;
+ bool loading;
+};
+
+// This is required in order to use QPointer<YTUser> as a QVariant
+typedef QPointer<YTChannel> YTChannelPointer;
+Q_DECLARE_METATYPE(YTChannelPointer)
+
+#endif // YTCHANNEL_H
) {
QString webpage = attributes().value("href").toString();
webpage.remove("&feature=youtube_gdata");
- video->setWebpage(QUrl(webpage));
+ video->setWebpage(webpage);
} else if (name() == QLatin1String("author")) {
while(readNextStartElement())
if (name() == QLatin1String("name")) {
QString author = readElementText();
- video->setAuthor(author);
+ video->setChannelTitle(author);
} else if (name() == QLatin1String("userId")) {
QString userId = readElementText();
- video->setUserId(userId);
+ video->setChannelId(userId);
} else skipCurrentElement();
} else if (name() == QLatin1String("published")) {
video->setPublished(QDateTime::fromString(readElementText(), Qt::ISODate));
#ifndef YTFEEDREADER_H
#define YTFEEDREADER_H
-#include <QtXml>
+#include <QtCore>
class Video;
$END_LICENSE */
#include "ytsearch.h"
-#include "ytfeedreader.h"
#include "constants.h"
#include "networkaccess.h"
#include "searchparams.h"
#include "video.h"
-#include "ytuser.h"
+#include "ytchannel.h"
+
+#ifdef APP_YT3
+#include "yt3.h"
+#include "yt3listparser.h"
+#include "datautils.h"
+#else
+#include "ytfeedreader.h"
+#endif
namespace The {
NetworkAccess* http();
QHash<QString, QAction*>* globalActions();
}
+namespace {
+
+QDateTime RFC3339fromString(const QString &s) {
+ return QDateTime::fromString(s, "yyyy-MM-ddThh:mm:ssZ");
+}
+
+QString RFC3339toString(const QDateTime &dt) {
+ return dt.toString("yyyy-MM-ddThh:mm:ssZ");
+}
+
+}
+
YTSearch::YTSearch(SearchParams *searchParams, QObject *parent) :
- VideoSource(parent),
+ PaginatedVideoSource(parent),
searchParams(searchParams) {
searchParams->setParent(this);
}
-void YTSearch::loadVideos(int max, int skip) {
+#ifdef APP_YT3
+
+void YTSearch::loadVideos(int max, int startIndex) {
aborted = false;
- QUrl url("http://gdata.youtube.com/feeds/api/videos/");
+ QUrl url = YT3::instance().method("search");
+
#if QT_VERSION >= 0x050000
-{
- QUrl &u = url;
- QUrlQuery url;
+ {
+ QUrl &u = url;
+ QUrlQuery url;
#endif
- url.addQueryItem("v", "2");
- url.addQueryItem("max-results", QString::number(max));
- url.addQueryItem("start-index", QString::number(skip));
+ url.addQueryItem("part", "snippet");
+ url.addQueryItem("type", "video");
- if (!searchParams->keywords().isEmpty()) {
- if (searchParams->keywords().startsWith("http://") ||
- searchParams->keywords().startsWith("https://")) {
- url.addQueryItem("q", YTSearch::videoIdFromUrl(searchParams->keywords()));
- } else url.addQueryItem("q", searchParams->keywords());
- }
+ url.addQueryItem("maxResults", QString::number(max));
- if (!searchParams->author().isEmpty())
- url.addQueryItem("author", searchParams->author());
-
- switch (searchParams->sortBy()) {
- case SearchParams::SortByNewest:
- url.addQueryItem("orderby", "published");
- break;
- case SearchParams::SortByViewCount:
- url.addQueryItem("orderby", "viewCount");
- break;
- case SearchParams::SortByRating:
- url.addQueryItem("orderby", "rating");
- break;
- }
+ if (startIndex > 1) {
+ if (maybeReloadToken(max, startIndex)) return;
+ url.addQueryItem("pageToken", nextPageToken);
+ }
- switch (searchParams->duration()) {
- case SearchParams::DurationShort:
- url.addQueryItem("duration", "short");
- break;
- case SearchParams::DurationMedium:
- url.addQueryItem("duration", "medium");
- break;
- case SearchParams::DurationLong:
- url.addQueryItem("duration", "long");
- break;
- }
+ // TODO interesting params
+ // url.addQueryItem("videoSyndicated", "true");
+ // url.addQueryItem("regionCode", "IT");
+ // url.addQueryItem("videoType", "movie");
- switch (searchParams->time()) {
- case SearchParams::TimeToday:
- url.addQueryItem("time", "today");
- break;
- case SearchParams::TimeWeek:
- url.addQueryItem("time", "this_week");
- break;
- case SearchParams::TimeMonth:
- url.addQueryItem("time", "this_month");
- break;
- }
+ if (!searchParams->keywords().isEmpty()) {
+ if (searchParams->keywords().startsWith("http://") ||
+ searchParams->keywords().startsWith("https://")) {
+ url.addQueryItem("q", YTSearch::videoIdFromUrl(searchParams->keywords()));
+ } else url.addQueryItem("q", searchParams->keywords());
+ }
- switch (searchParams->quality()) {
- case SearchParams::QualityHD:
- url.addQueryItem("hd", "true");
- break;
- }
+ if (!searchParams->channelId().isEmpty())
+ url.addQueryItem("channelId", searchParams->channelId());
+
+ switch (searchParams->sortBy()) {
+ case SearchParams::SortByNewest:
+ url.addQueryItem("order", "date");
+ break;
+ case SearchParams::SortByViewCount:
+ url.addQueryItem("order", "viewCount");
+ break;
+ case SearchParams::SortByRating:
+ url.addQueryItem("order", "rating");
+ break;
+ }
+
+ switch (searchParams->duration()) {
+ case SearchParams::DurationShort:
+ url.addQueryItem("videoDuration", "short");
+ break;
+ case SearchParams::DurationMedium:
+ url.addQueryItem("videoDuration", "medium");
+ break;
+ case SearchParams::DurationLong:
+ url.addQueryItem("videoDuration", "long");
+ break;
+ }
+
+ switch (searchParams->time()) {
+ case SearchParams::TimeToday:
+ url.addQueryItem("publishedAfter", RFC3339toString(QDateTime::currentDateTimeUtc().addSecs(-60*60*24)));
+ break;
+ case SearchParams::TimeWeek:
+ url.addQueryItem("publishedAfter", RFC3339toString(QDateTime::currentDateTimeUtc().addSecs(-60*60*24*7)));
+ break;
+ case SearchParams::TimeMonth:
+ url.addQueryItem("publishedAfter", RFC3339toString(QDateTime::currentDateTimeUtc().addSecs(-60*60*24*30)));
+ break;
+ }
+
+ if (searchParams->publishedAfter()) {
+ url.addQueryItem("publishedAfter", RFC3339toString(QDateTime::fromTime_t(searchParams->publishedAfter()).toUTC()));
+ }
+
+ switch (searchParams->quality()) {
+ case SearchParams::QualityHD:
+ url.addQueryItem("videoDefinition", "high");
+ break;
+ }
#if QT_VERSION >= 0x050000
u.setQuery(url);
}
#endif
+
+ lastUrl = url;
+
+ // qWarning() << "YT3 search" << url.toString();
QObject *reply = The::http()->get(url);
connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResults(QByteArray)));
connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
}
-void YTSearch::abort() {
- aborted = true;
-}
+void YTSearch::parseResults(QByteArray data) {
+ if (aborted) return;
-const QStringList & YTSearch::getSuggestions() {
- return suggestions;
+ YT3ListParser parser(data);
+ QList<Video*> videos = parser.getVideos();
+ suggestions = parser.getSuggestions();
+
+ bool tryingWithNewToken = setPageToken(parser.getNextPageToken());
+ if (tryingWithNewToken) return;
+
+ if (name.isEmpty() && !searchParams->channelId().isEmpty()) {
+ if (!videos.isEmpty()) {
+ name = videos.first()->channelTitle();
+ }
+ emit nameChanged(name);
+ }
+
+ if (asyncDetails) {
+ emit gotVideos(videos);
+ emit finished(videos.size());
+ }
+ loadVideoDetails(videos);
}
-QString YTSearch::getName() {
- if (!name.isEmpty()) return name;
- if (!searchParams->keywords().isEmpty()) return searchParams->keywords();
- return QString();
+#else
+
+void YTSearch::loadVideos(int max, int startIndex) {
+ aborted = false;
+
+ QUrl url("http://gdata.youtube.com/feeds/api/videos/");
+#if QT_VERSION >= 0x050000
+ {
+ QUrl &u = url;
+ QUrlQuery url;
+#endif
+
+ url.addQueryItem("v", "2");
+ url.addQueryItem("max-results", QString::number(max));
+ url.addQueryItem("start-index", QString::number(startIndex));
+
+ if (!searchParams->keywords().isEmpty()) {
+ if (searchParams->keywords().startsWith("http://") ||
+ searchParams->keywords().startsWith("https://")) {
+ url.addQueryItem("q", YTSearch::videoIdFromUrl(searchParams->keywords()));
+ } else url.addQueryItem("q", searchParams->keywords());
+ }
+
+ if (!searchParams->channelId().isEmpty())
+ url.addQueryItem("author", searchParams->channelId());
+
+ switch (searchParams->sortBy()) {
+ case SearchParams::SortByNewest:
+ url.addQueryItem("orderby", "published");
+ break;
+ case SearchParams::SortByViewCount:
+ url.addQueryItem("orderby", "viewCount");
+ break;
+ case SearchParams::SortByRating:
+ url.addQueryItem("orderby", "rating");
+ break;
+ }
+
+ switch (searchParams->duration()) {
+ case SearchParams::DurationShort:
+ url.addQueryItem("duration", "short");
+ break;
+ case SearchParams::DurationMedium:
+ url.addQueryItem("duration", "medium");
+ break;
+ case SearchParams::DurationLong:
+ url.addQueryItem("duration", "long");
+ break;
+ }
+
+ switch (searchParams->time()) {
+ case SearchParams::TimeToday:
+ url.addQueryItem("time", "today");
+ break;
+ case SearchParams::TimeWeek:
+ url.addQueryItem("time", "this_week");
+ break;
+ case SearchParams::TimeMonth:
+ url.addQueryItem("time", "this_month");
+ break;
+ }
+
+ switch (searchParams->quality()) {
+ case SearchParams::QualityHD:
+ url.addQueryItem("hd", "true");
+ break;
+ }
+
+#if QT_VERSION >= 0x050000
+ u.setQuery(url);
+ }
+#endif
+ QObject *reply = The::http()->get(url);
+ connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResults(QByteArray)));
+ connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
}
void YTSearch::parseResults(QByteArray data) {
QList<Video*> videos = reader.getVideos();
suggestions = reader.getSuggestions();
- if (name.isEmpty() && !searchParams->author().isEmpty()) {
- if (videos.isEmpty()) name = searchParams->author();
+ if (name.isEmpty() && !searchParams->channelId().isEmpty()) {
+ if (videos.isEmpty()) name = searchParams->channelId();
else {
- name = videos.first()->author();
+ name = videos.first()->channelTitle();
// also grab the userId
- userId = videos.first()->userId();
+ userId = videos.first()->channelId();
}
emit nameChanged(name);
}
emit finished(videos.size());
}
+#endif
+
+void YTSearch::abort() {
+ aborted = true;
+}
+
+const QStringList & YTSearch::getSuggestions() {
+ return suggestions;
+}
+
+QString YTSearch::getName() {
+ if (!name.isEmpty()) return name;
+ if (!searchParams->keywords().isEmpty()) return searchParams->keywords();
+ return QString();
+}
+
void YTSearch::requestError(QNetworkReply *reply) {
+ qWarning() << reply->errorString();
emit error(reply->errorString());
}
QList<QAction*> YTSearch::getActions() {
QList<QAction*> channelActions;
- if (searchParams->author().isEmpty())
+ if (searchParams->channelId().isEmpty())
return channelActions;
channelActions << The::globalActions()->value("subscribe-channel");
return channelActions;
#define YTSEARCH_H
#include <QtNetwork>
-#include "videosource.h"
+#include "paginatedvideosource.h"
class SearchParams;
class Video;
-class YTSearch : public VideoSource {
+class YTSearch : public PaginatedVideoSource {
Q_OBJECT
public:
YTSearch(SearchParams *params, QObject *parent = 0);
- void loadVideos(int max, int skip);
- virtual void abort();
- virtual const QStringList & getSuggestions();
- static QString videoIdFromUrl(QString url);
+ void loadVideos(int max, int startIndex);
+ void abort();
+ const QStringList & getSuggestions();
QString getName();
+ QList<QAction*> getActions();
SearchParams* getSearchParams() const { return searchParams; }
+ static QString videoIdFromUrl(QString url);
bool operator==(const YTSearch &other) const {
return searchParams == other.getSearchParams();
}
- QList<QAction*> getActions();
-
private slots:
void parseResults(QByteArray data);
void requestError(QNetworkReply *reply);
bool aborted;
QStringList suggestions;
QString name;
-
- QString userId;
};
#endif // YTSEARCH_H
$END_LICENSE */
#include "ytsinglevideosource.h"
-#include <QtXml>
#include "networkaccess.h"
#include "video.h"
+
+#ifdef APP_YT3
+#include "yt3.h"
+#include "yt3listparser.h"
+#else
#include "ytfeedreader.h"
+#endif
namespace The {
NetworkAccess* http();
}
-YTSingleVideoSource::YTSingleVideoSource(QObject *parent) : VideoSource(parent) {
- skip = 0;
- max = 0;
+YTSingleVideoSource::YTSingleVideoSource(QObject *parent) : PaginatedVideoSource(parent),
+ video(0),
+ startIndex(0),
+ max(0) { }
+
+#ifdef APP_YT3
+
+void YTSingleVideoSource::loadVideos(int max, int startIndex) {
+ aborted = false;
+ this->startIndex = startIndex;
+ this->max = max;
+
+ QUrl url;
+
+ if (startIndex == 1) {
+
+ if (video) {
+ QList<Video*> videos;
+ videos << video->clone();
+ if (name.isEmpty()) {
+ name = videos.first()->title();
+ qDebug() << "Emitting name changed" << name;
+ emit nameChanged(name);
+ }
+ emit gotVideos(videos);
+ loadVideos(max - 1, 2);
+ return;
+ }
+
+ url = YT3::instance().method("videos");
+#if QT_VERSION >= 0x050000
+ {
+ QUrl &u = url;
+ QUrlQuery url;
+#endif
+ url.addQueryItem("part", "snippet");
+ url.addQueryItem("id", videoId);
+#if QT_VERSION >= 0x050000
+ u.setQuery(url);
+ }
+#endif
+ } else {
+ url = YT3::instance().method("search");
+#if QT_VERSION >= 0x050000
+ {
+ QUrl &u = url;
+ QUrlQuery url;
+#endif
+ url.addQueryItem("part", "snippet");
+ url.addQueryItem("type", "video");
+ url.addQueryItem("relatedToVideoId", videoId);
+ url.addQueryItem("maxResults", QString::number(max));
+ if (startIndex > 2) {
+ if (maybeReloadToken(max, startIndex)) return;
+ url.addQueryItem("pageToken", nextPageToken);
+ }
+#if QT_VERSION >= 0x050000
+ u.setQuery(url);
+ }
+#endif
+ }
+
+ lastUrl = url;
+
+ QObject *reply = The::http()->get(url);
+ connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResults(QByteArray)));
+ connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
+}
+
+void YTSingleVideoSource::parseResults(QByteArray data) {
+ if (aborted) return;
+
+ YT3ListParser parser(data);
+ QList<Video*> videos = parser.getVideos();
+
+ bool tryingWithNewToken = setPageToken(parser.getNextPageToken());
+ if (tryingWithNewToken) return;
+
+ if (asyncDetails) {
+ emit gotVideos(videos);
+ if (startIndex == 2) emit finished(videos.size() + 1);
+ else emit finished(videos.size());
+ }
+ loadVideoDetails(videos);
}
-void YTSingleVideoSource::loadVideos(int max, int skip) {
+#else
+
+void YTSingleVideoSource::loadVideos(int max, int startIndex) {
aborted = false;
- this->skip = skip;
+ this->startIndex = startIndex;
this->max = max;
QString s;
- if (skip == 1) s = "http://gdata.youtube.com/feeds/api/videos/" + videoId;
+ if (startIndex == 1) s = "http://gdata.youtube.com/feeds/api/videos/" + videoId;
else s = QString("http://gdata.youtube.com/feeds/api/videos/%1/related").arg(videoId);
QUrl url(s);
#if QT_VERSION >= 0x050000
-{
- QUrl &u = url;
- QUrlQuery url;
+ {
+ QUrl &u = url;
+ QUrlQuery url;
#endif
- url.addQueryItem("v", "2");
+ url.addQueryItem("v", "2");
- if (skip != 1) {
- url.addQueryItem("max-results", QString::number(max));
- url.addQueryItem("start-index", QString::number(skip-1));
- }
+ if (startIndex != 1) {
+ url.addQueryItem("max-results", QString::number(max));
+ url.addQueryItem("start-index", QString::number(startIndex-1));
+ }
#if QT_VERSION >= 0x050000
u.setQuery(url);
connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
}
-void YTSingleVideoSource::abort() {
- aborted = true;
-}
-
-const QStringList & YTSingleVideoSource::getSuggestions() {
- QStringList *l = new QStringList();
- return *l;
-}
-
-QString YTSingleVideoSource::getName() {
- return name;
-}
-
void YTSingleVideoSource::parse(QByteArray data) {
if (aborted) return;
YTFeedReader reader(data);
QList<Video*> videos = reader.getVideos();
- if (name.isEmpty() && !videos.isEmpty() && skip == 1) {
+ if (name.isEmpty() && !videos.isEmpty() && startIndex == 1) {
name = videos.first()->title();
emit nameChanged(name);
}
emit gotVideos(videos);
- if (skip == 1) loadVideos(max - 1, 2);
- else if (skip == 2) emit finished(videos.size() + 1);
+ if (startIndex == 1) loadVideos(max - 1, 2);
+ else if (startIndex == 2) emit finished(videos.size() + 1);
else emit finished(videos.size());
}
+#endif
+
+void YTSingleVideoSource::abort() {
+ aborted = true;
+}
+
+const QStringList & YTSingleVideoSource::getSuggestions() {
+ static const QStringList *l = new QStringList();
+ return *l;
+}
+
+QString YTSingleVideoSource::getName() {
+ return name;
+}
+
+void YTSingleVideoSource::setVideo(Video *video) {
+ this->video = video;
+ videoId = video->id();
+}
+
void YTSingleVideoSource::requestError(QNetworkReply *reply) {
emit error(reply->errorString());
}
#define YTSINGLEVIDEOSOURCE_H
#include <QtNetwork>
-#include "videosource.h"
+#include "paginatedvideosource.h"
-class YTSingleVideoSource : public VideoSource {
+class YTSingleVideoSource : public PaginatedVideoSource {
Q_OBJECT
public:
YTSingleVideoSource(QObject *parent = 0);
- void loadVideos(int max, int skip);
+ void loadVideos(int max, int startIndex);
void abort();
const QStringList & getSuggestions();
QString getName();
void setVideoId(QString videoId) { this->videoId = videoId; }
+ void setVideo(Video *video);
private slots:
- void parse(QByteArray data);
+ void parseResults(QByteArray data);
void requestError(QNetworkReply *reply);
private:
+ Video *video;
QString videoId;
bool aborted;
- int skip;
+ int startIndex;
int max;
QString name;
};
$END_LICENSE */
#include "ytstandardfeed.h"
-#include <QtXml>
#include "networkaccess.h"
#include "video.h"
+
+#ifdef APP_YT3
+#include "yt3.h"
+#include "yt3listparser.h"
+#else
#include "ytfeedreader.h"
+#endif
namespace The {
NetworkAccess* http();
}
YTStandardFeed::YTStandardFeed(QObject *parent)
- : VideoSource(parent),
+ : PaginatedVideoSource(parent),
aborted(false) { }
-void YTStandardFeed::loadVideos(int max, int skip) {
+#ifdef APP_YT3
+
+void YTStandardFeed::loadVideos(int max, int startIndex) {
+ aborted = false;
+
+ QUrl url = YT3::instance().method("videos");
+
+#if QT_VERSION >= 0x050000
+ {
+ QUrl &u = url;
+ QUrlQuery url;
+#endif
+
+ if (startIndex > 1) {
+ if (maybeReloadToken(max, startIndex)) return;
+ url.addQueryItem("pageToken", nextPageToken);
+ }
+
+ url.addQueryItem("part", "snippet,contentDetails,statistics");
+ url.addQueryItem("chart", "mostPopular");
+
+ if (!category.isEmpty())
+ url.addQueryItem("videoCategoryId", category);
+
+ if (!regionId.isEmpty())
+ url.addQueryItem("regionCode", regionId);
+
+ url.addQueryItem("maxResults", QString::number(max));
+
+#if QT_VERSION >= 0x050000
+ u.setQuery(url);
+ }
+#endif
+ QObject *reply = The::http()->get(url);
+ connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResults(QByteArray)));
+ connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
+}
+
+void YTStandardFeed::parseResults(QByteArray data) {
+ if (aborted) return;
+
+ YT3ListParser parser(data);
+ QList<Video*> videos = parser.getVideos();
+
+ bool tryingWithNewToken = setPageToken(parser.getNextPageToken());
+ if (tryingWithNewToken) return;
+
+ if (reloadingToken) {
+ reloadingToken = false;
+ loadVideos(currentMax, currentStartIndex);
+ currentMax = currentStartIndex = 0;
+ return;
+ }
+
+ emit gotVideos(videos);
+ emit finished(videos.size());
+}
+
+#else
+
+void YTStandardFeed::loadVideos(int max, int startIndex) {
aborted = false;
QString s = "http://gdata.youtube.com/feeds/api/standardfeeds/";
QUrl url(s);
#if QT_VERSION >= 0x050000
-{
- QUrl &u = url;
- QUrlQuery url;
+ {
+ QUrl &u = url;
+ QUrlQuery url;
#endif
- url.addQueryItem("v", "2");
+ url.addQueryItem("v", "2");
- if (feedId != "most_shared" && feedId != "on_the_web") {
- QString t = time;
- if (t.isEmpty()) t = "today";
- url.addQueryItem("time", t);
- }
+ if (feedId != "most_shared" && feedId != "on_the_web") {
+ QString t = time;
+ if (t.isEmpty()) t = "today";
+ url.addQueryItem("time", t);
+ }
- url.addQueryItem("max-results", QString::number(max));
- url.addQueryItem("start-index", QString::number(skip));
+ url.addQueryItem("max-results", QString::number(max));
+ url.addQueryItem("start-index", QString::number(startIndex));
#if QT_VERSION >= 0x050000
u.setQuery(url);
connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
}
-void YTStandardFeed::abort() {
- aborted = true;
-}
-
-const QStringList & YTStandardFeed::getSuggestions() {
- QStringList *l = new QStringList();
- return *l;
-}
-
-void YTStandardFeed::parse(QByteArray data) {
+void YTStandardFeed::parseResults(QByteArray data) {
if (aborted) return;
YTFeedReader reader(data);
emit finished(videos.size());
}
+#endif
+
+void YTStandardFeed::abort() {
+ aborted = true;
+}
+
+const QStringList & YTStandardFeed::getSuggestions() {
+ QStringList *l = new QStringList();
+ return *l;
+}
+
void YTStandardFeed::requestError(QNetworkReply *reply) {
emit error(reply->errorString());
}
#define YTSTANDARDFEED_H
#include <QtNetwork>
-#include "videosource.h"
+#include "paginatedvideosource.h"
-class YTStandardFeed : public VideoSource {
+class YTStandardFeed : public PaginatedVideoSource {
Q_OBJECT
QString getTime() { return time; }
void setTime(QString time) { this->time = time; }
- void loadVideos(int max, int skip);
+ void loadVideos(int max, int startIndex);
void abort();
const QStringList & getSuggestions();
QString getName() { return label; }
private slots:
- void parse(QByteArray data);
+ void parseResults(QByteArray data);
void requestError(QNetworkReply *reply);
private:
$END_LICENSE */
#include "ytsuggester.h"
-#include <QtXml>
#include "networkaccess.h"
-#define GSUGGEST_URL "http://suggestqueries.google.com/complete/search?ds=yt&output=toolbar&hl=%1&q=%2"
-
namespace The {
- NetworkAccess* http();
+NetworkAccess* http();
}
YTSuggester::YTSuggester(QObject *parent) : Suggester(parent) {
locale = "en-US";
}
- QString url = QString(GSUGGEST_URL).arg(locale, query);
+ QString url =
+ QString("https://suggestqueries.google.com/complete/search?ds=yt&output=toolbar&hl=%1&q=%2")
+ .arg(locale, query);
QObject *reply = The::http()->get(url);
connect(reply, SIGNAL(data(QByteArray)), SLOT(handleNetworkData(QByteArray)));
+++ /dev/null
-/* $BEGIN_LICENSE
-
-This file is part of Minitube.
-Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
-
-Minitube is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Minitube is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Minitube. If not, see <http://www.gnu.org/licenses/>.
-
-$END_LICENSE */
-
-#include "ytuser.h"
-#include "networkaccess.h"
-#include "database.h"
-#include <QtSql>
-
-namespace The {
-NetworkAccess* http();
-}
-
-YTUser::YTUser(QString userId, QObject *parent) : QObject(parent),
- id(0),
- userId(userId),
- loadingThumbnail(false),
- notifyCount(0),
- checked(0),
- watched(0),
- loaded(0),
- loading(false) { }
-
-QHash<QString, YTUser*> YTUser::cache;
-
-YTUser* YTUser::forId(QString userId) {
- if (userId.isEmpty()) return 0;
-
- if (cache.contains(userId))
- return cache.value(userId);
-
- QSqlDatabase db = Database::instance().getConnection();
- QSqlQuery query(db);
- query.prepare("select id,name,description,thumb_url,notify_count,watched,checked,loaded "
- "from subscriptions where user_id=?");
- query.bindValue(0, userId);
- bool success = query.exec();
- if (!success) qWarning() << query.lastQuery() << query.lastError().text();
-
- YTUser* user = 0;
- if (query.next()) {
- user = new YTUser(userId);
- user->id = query.value(0).toInt();
- user->displayName = query.value(1).toString();
- user->description = query.value(2).toString();
- user->thumbnailUrl = query.value(3).toString();
- user->notifyCount = query.value(4).toInt();
- user->watched = query.value(5).toUInt();
- user->checked = query.value(6).toUInt();
- user->loaded = query.value(7).toUInt();
- user->thumbnail = QPixmap(user->getThumbnailLocation());
- user->maybeLoadfromAPI();
- cache.insert(userId, user);
- }
-
- return user;
-}
-
-void YTUser::maybeLoadfromAPI() {
- if (loading) return;
- if (userId.isEmpty()) return;
-
- uint now = QDateTime::currentDateTime().toTime_t();
- static const int refreshInterval = 60 * 60 * 24 * 10;
- if (loaded > now - refreshInterval) return;
-
- loading = true;
-
- QUrl url("http://gdata.youtube.com/feeds/api/users/" + userId);
-#if QT_VERSION >= 0x050000
- {
- QUrl &u = url;
- QUrlQuery url;
-#endif
- url.addQueryItem("v", "2");
-#if QT_VERSION >= 0x050000
- u.setQuery(url);
- }
-#endif
- QObject *reply = The::http()->get(url);
- connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResponse(QByteArray)));
- connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
-}
-
-void YTUser::parseResponse(QByteArray bytes) {
- QXmlStreamReader xml(bytes);
- xml.readNextStartElement();
- if (xml.name() == QLatin1String("entry"))
- while(xml.readNextStartElement()) {
- const QStringRef n = xml.name();
- if (n == QLatin1String("summary"))
- description = xml.readElementText().simplified();
- else if (n == QLatin1String("title"))
- displayName = xml.readElementText();
- else if (n == QLatin1String("thumbnail")) {
- thumbnailUrl = xml.attributes().value("url").toString();
- xml.skipCurrentElement();
- } else if (n == QLatin1String("username"))
- userName = xml.readElementText();
- else xml.skipCurrentElement();
- }
-
- if (xml.hasError()) {
- emit error(xml.errorString());
- qWarning() << xml.errorString();
- }
-
- emit infoLoaded();
- storeInfo();
- loading = false;
-}
-
-void YTUser::loadThumbnail() {
- if (loadingThumbnail) return;
- if (thumbnailUrl.isEmpty()) return;
- loadingThumbnail = true;
-
-#ifdef Q_OS_WIN
- thumbnailUrl.replace(QLatin1String("https://"), QLatin1String("http://"));
-#endif
-
- QUrl url(thumbnailUrl);
- QObject *reply = The::http()->get(url);
- connect(reply, SIGNAL(data(QByteArray)), SLOT(storeThumbnail(QByteArray)));
- connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
-}
-
-const QString & YTUser::getThumbnailDir() {
- static const QString thumbDir =
- #if QT_VERSION >= 0x050000
- QStandardPaths::writableLocation(QStandardPaths::DataLocation)
- #else
- QDesktopServices::storageLocation(QDesktopServices::DataLocation)
- #endif
- + "/channels/";
- return thumbDir;
-}
-
-QString YTUser::getThumbnailLocation() {
- return getThumbnailDir() + userId;
-}
-
-void YTUser::unsubscribe() {
- YTUser::unsubscribe(userId);
-}
-
-void YTUser::storeThumbnail(QByteArray bytes) {
- thumbnail.loadFromData(bytes);
- static const int maxWidth = 88;
-
- QDir dir;
- dir.mkpath(getThumbnailDir());
-
- if (thumbnail.width() > maxWidth) {
- thumbnail = thumbnail.scaledToWidth(maxWidth, Qt::SmoothTransformation);
- thumbnail.save(getThumbnailLocation(), "JPG");
- } else {
- QFile file(getThumbnailLocation());
- if (!file.open(QIODevice::WriteOnly))
- qWarning() << "Error opening file for writing" << file.fileName();
- QDataStream stream(&file);
- stream.writeRawData(bytes.constData(), bytes.size());
- }
-
- emit thumbnailLoaded();
- loadingThumbnail = false;
-}
-
-void YTUser::requestError(QNetworkReply *reply) {
- emit error(reply->errorString());
- qWarning() << reply->errorString();
- loading = false;
- loadingThumbnail = false;
-}
-
-void YTUser::storeInfo() {
- if (userId.isEmpty()) return;
- QSqlDatabase db = Database::instance().getConnection();
- QSqlQuery query(db);
- query.prepare("update subscriptions set "
- "user_name=?, name=?, description=?, thumb_url=?, loaded=? "
- "where user_id=?");
- qDebug() << userName;
- query.bindValue(0, userName);
- query.bindValue(1, displayName);
- query.bindValue(2, description);
- query.bindValue(3, thumbnailUrl);
- query.bindValue(4, QDateTime::currentDateTime().toTime_t());
- query.bindValue(5, userId);
- bool success = query.exec();
- if (!success) qWarning() << query.lastQuery() << query.lastError().text();
-
- loadThumbnail();
-}
-
-void YTUser::subscribe(QString userId) {
- if (userId.isEmpty()) return;
-
- uint now = QDateTime::currentDateTime().toTime_t();
-
- QSqlDatabase db = Database::instance().getConnection();
- QSqlQuery query(db);
- query.prepare("insert into subscriptions "
- "(user_id,added,watched,checked,views,notify_count)"
- " values (?,?,?,0,0,0)");
- query.bindValue(0, userId);
- query.bindValue(1, now);
- query.bindValue(2, now);
- bool success = query.exec();
- if (!success) qWarning() << query.lastQuery() << query.lastError().text();
-
- // This will call maybeLoadFromApi
- YTUser::forId(userId);
-}
-
-void YTUser::unsubscribe(QString userId) {
- if (userId.isEmpty()) return;
- QSqlDatabase db = Database::instance().getConnection();
- QSqlQuery query(db);
- query.prepare("delete from subscriptions where user_id=?");
- query.bindValue(0, userId);
- bool success = query.exec();
- if (!success) qWarning() << query.lastQuery() << query.lastError().text();
-
- query = QSqlQuery(db);
- query.prepare("delete from subscriptions_videos where user_id=?");
- query.bindValue(0, userId);
- success = query.exec();
- if (!success) qWarning() << query.lastQuery() << query.lastError().text();
-
- YTUser *user = cache.take(userId);
- if (user) user->deleteLater();
-}
-
-bool YTUser::isSubscribed(QString userId) {
- if (!Database::exists()) return false;
- if (userId.isEmpty()) return false;
- QSqlDatabase db = Database::instance().getConnection();
- QSqlQuery query(db);
- query.prepare("select count(*) from subscriptions where user_id=?");
- query.bindValue(0, userId);
- bool success = query.exec();
- if (!success) qWarning() << query.lastQuery() << query.lastError().text();
- if (query.next())
- return query.value(0).toInt() > 0;
- return false;
-}
-
-void YTUser::updateChecked() {
- if (userId.isEmpty()) return;
-
- uint now = QDateTime::currentDateTime().toTime_t();
- checked = now;
-
- QSqlDatabase db = Database::instance().getConnection();
- QSqlQuery query(db);
- query.prepare("update subscriptions set checked=? where user_id=?");
- query.bindValue(0, QDateTime::currentDateTime().toTime_t());
- query.bindValue(1, userId);
- bool success = query.exec();
- if (!success) qWarning() << query.lastQuery() << query.lastError().text();
-}
-
-void YTUser::updateWatched() {
- if (userId.isEmpty()) return;
-
- uint now = QDateTime::currentDateTime().toTime_t();
- watched = now;
- notifyCount = 0;
- emit notifyCountChanged();
-
- QSqlDatabase db = Database::instance().getConnection();
- QSqlQuery query(db);
- query.prepare("update subscriptions set watched=?, notify_count=0, views=views+1 where user_id=?");
- query.bindValue(0, now);
- query.bindValue(1, userId);
- bool success = query.exec();
- if (!success) qWarning() << query.lastQuery() << query.lastError().text();
-}
-
-void YTUser::storeNotifyCount(int count) {
- if (notifyCount != count)
- emit notifyCountChanged();
- notifyCount = count;
-
- QSqlDatabase db = Database::instance().getConnection();
- QSqlQuery query(db);
- query.prepare("update subscriptions set notify_count=? where user_id=?");
- query.bindValue(0, count);
- query.bindValue(1, userId);
- bool success = query.exec();
- if (!success) qWarning() << query.lastQuery() << query.lastError().text();
-}
-
-bool YTUser::updateNotifyCount() {
- QSqlDatabase db = Database::instance().getConnection();
- QSqlQuery query(db);
- query.prepare("select count(*) from subscriptions_videos "
- "where channel_id=? and added>? and published>? and watched=0");
- query.bindValue(0, id);
- query.bindValue(1, watched);
- query.bindValue(2, watched);
- bool success = query.exec();
- if (!success) qWarning() << query.lastQuery() << query.lastError().text();
- if (!query.next()) {
- qWarning() << __PRETTY_FUNCTION__ << "Count failed";
- return false;
- }
- int count = query.value(0).toInt();
- storeNotifyCount(count);
- return count != notifyCount;
-}
+++ /dev/null
-/* $BEGIN_LICENSE
-
-This file is part of Minitube.
-Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
-
-Minitube is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Minitube is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Minitube. If not, see <http://www.gnu.org/licenses/>.
-
-$END_LICENSE */
-
-#ifndef YTUSER_H
-#define YTUSER_H
-
-#include <QtGui>
-#if QT_VERSION >= 0x050000
-#include <QtWidgets>
-#endif
-#include <QtNetwork>
-
-class YTUser : public QObject {
-
- Q_OBJECT
-
-public:
- static YTUser* forId(QString userId);
- static void subscribe(QString userId);
- static void unsubscribe(QString userId);
- static bool isSubscribed(QString userId);
-
- int getId() { return id; }
- void setId(int id) { this->id = id; }
-
- uint getChecked() { return checked; }
- void updateChecked();
-
- uint getWatched() const { return watched; }
- void setWatched(uint watched) { this->watched = watched; }
-
- int getNotifyCount() const { return notifyCount; }
- void setNotifyCount(int count) { notifyCount = count; }
- void storeNotifyCount(int count);
- bool updateNotifyCount();
-
- QString getUserId() const { return userId; }
- QString getUserName() const { return userName; }
- QString getDisplayName() const { return displayName; }
- QString getDescription() const { return description; }
- QString getCountryCode() const { return countryCode; }
-
- void loadThumbnail();
- const QString & getThumbnailDir();
- QString getThumbnailLocation();
- const QPixmap & getThumbnail() { return thumbnail; }
-
- static QList<YTUser*> getCachedUsers() { return cache.values(); }
-
-public slots:
- void updateWatched();
- void unsubscribe();
-
-signals:
- void infoLoaded();
- void thumbnailLoaded();
- void error(QString message);
- void notifyCountChanged();
-
-private slots:
- void parseResponse(QByteArray bytes);
- void requestError(QNetworkReply *reply);
- void storeThumbnail(QByteArray bytes);
-
-private:
- YTUser(QString userId, QObject *parent = 0);
- void maybeLoadfromAPI();
- void storeInfo();
-
- static QHash<QString, YTUser*> cache;
-
- int id;
- QString userId;
- QString userName;
- QString displayName;
- QString description;
- QString countryCode;
-
- QString thumbnailUrl;
- QPixmap thumbnail;
- bool loadingThumbnail;
-
- int notifyCount;
- uint checked;
- uint watched;
- uint loaded;
- bool loading;
-};
-
-// This is required in order to use QPointer<YTUser> as a QVariant
-typedef QPointer<YTUser> YTUserPointer;
-Q_DECLARE_METATYPE(YTUserPointer)
-
-#endif // YTUSER_H