]> git.sur5r.net Git - minitube/commitdiff
YT api v3 support
authorFlavio Tordini <flavio.tordini@gmail.com>
Tue, 7 Apr 2015 19:51:28 +0000 (21:51 +0200)
committerFlavio Tordini <flavio.tordini@gmail.com>
Tue, 7 Apr 2015 19:51:28 +0000 (21:51 +0200)
61 files changed:
minitube.pro
src/aggregatevideosource.cpp
src/aggregatevideosource.h
src/channelaggregator.cpp
src/channelaggregator.h
src/channelitemdelegate.cpp
src/channelmodel.cpp
src/channelmodel.h
src/channelsitemdelegate.cpp [new file with mode: 0644]
src/channelsitemdelegate.h [new file with mode: 0644]
src/channelsmodel.cpp [new file with mode: 0644]
src/channelsmodel.h [new file with mode: 0644]
src/channelsuggest.cpp
src/channelsview.cpp [new file with mode: 0644]
src/channelsview.h [new file with mode: 0644]
src/channelview.cpp
src/channelwidget.cpp [new file with mode: 0644]
src/channelwidget.h [new file with mode: 0644]
src/database.cpp
src/database.h
src/datautils.cpp
src/datautils.h
src/diskcache.cpp
src/diskcache.h
src/main.cpp
src/mainwindow.cpp
src/mediaview.cpp
src/networkaccess.cpp
src/paginatedvideosource.cpp [new file with mode: 0644]
src/paginatedvideosource.h [new file with mode: 0644]
src/playlistitemdelegate.cpp
src/playlistmodel.cpp
src/playlistmodel.h
src/playlistview.cpp
src/searchparams.cpp
src/searchparams.h
src/searchview.cpp
src/searchview.h
src/standardfeedsview.cpp
src/suggester.h
src/video.cpp
src/video.h
src/videosource.h
src/yt3.cpp [new file with mode: 0644]
src/yt3.h [new file with mode: 0644]
src/yt3listparser.cpp [new file with mode: 0644]
src/yt3listparser.h [new file with mode: 0644]
src/ytcategories.cpp
src/ytchannel.cpp [new file with mode: 0644]
src/ytchannel.h [new file with mode: 0644]
src/ytfeedreader.cpp
src/ytfeedreader.h
src/ytsearch.cpp
src/ytsearch.h
src/ytsinglevideosource.cpp
src/ytsinglevideosource.h
src/ytstandardfeed.cpp
src/ytstandardfeed.h
src/ytsuggester.cpp
src/ytuser.cpp [deleted file]
src/ytuser.h [deleted file]

index effa02ed02b9d980f9ccaa1fcaee22cb59f2d20c..2f029bf9c475b3dbeac43c386554d540c1b0d4f9 100644 (file)
@@ -1,6 +1,6 @@
 CONFIG += release
 TEMPLATE = app
-VERSION = 2.3
+VERSION = 2.4
 DEFINES += APP_VERSION="$$VERSION"
 
 APP_NAME = Minitube
@@ -12,14 +12,19 @@ DEFINES += APP_UNIX_NAME="$$APP_UNIX_NAME"
 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
 }
@@ -83,7 +88,6 @@ HEADERS += src/video.h \
     src/gridwidget.h \
     src/painterutils.h \
     src/database.h \
-    src/ytuser.h \
     src/channelaggregator.h \
     src/channelmodel.h \
     src/aggregatevideosource.h \
@@ -93,7 +97,11 @@ HEADERS += src/video.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 \
@@ -149,7 +157,6 @@ SOURCES += src/main.cpp \
     src/gridwidget.cpp \
     src/painterutils.cpp \
     src/database.cpp \
-    src/ytuser.cpp \
     src/channelaggregator.cpp \
     src/channelmodel.cpp \
     src/aggregatevideosource.cpp \
@@ -159,7 +166,11 @@ SOURCES += src/main.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/
index 55e72dcc65e4c54f954ff9f34f176eb177b09d96..2c5f33072188682d5b8fdc67c31473a52c10803f 100644 (file)
@@ -25,9 +25,9 @@ $END_LICENSE */
 
 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,"
@@ -48,7 +48,7 @@ void AggregateVideoSource::loadVideos(int max, int skip) {
         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();
@@ -58,8 +58,8 @@ void AggregateVideoSource::loadVideos(int max, int skip) {
         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());
@@ -67,10 +67,17 @@ void AggregateVideoSource::loadVideos(int max, int skip) {
         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;
index 49de4a0a655af3c2bcc72064bc2032aadaac6caf..55aa730091d02973c353d6255ba8bf38f8d26995 100644 (file)
@@ -30,7 +30,8 @@ class AggregateVideoSource : public VideoSource {
 
 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; }
@@ -40,6 +41,7 @@ public:
 private:
     QString name;
     bool unwatched;
+    bool hasMore;
 
 };
 
index 092a9937fb9c4c0d67f67704c7345da853bf8225..a7eb0553449adb7e0dcd227362c379c346f267ec 100644 (file)
@@ -19,7 +19,7 @@ along with Minitube.  If not, see <http://www.gnu.org/licenses/>.
 $END_LICENSE */
 
 #include "channelaggregator.h"
-#include "ytuser.h"
+#include "ytchannel.h"
 #include "ytsearch.h"
 #include "searchparams.h"
 #include "database.h"
@@ -32,8 +32,7 @@ ChannelAggregator::ChannelAggregator(QObject *parent) : QObject(parent),
     unwatchedCount(-1),
     running(false),
     stopped(false) {
-    QSettings settings;
-    checkInterval = settings.value("subscriptionsCheckInterval", 1800).toUInt();
+    checkInterval = 3600;
 
     timer = new QTimer(this);
     timer->setInterval(60000 * 5);
@@ -46,9 +45,10 @@ ChannelAggregator* ChannelAggregator::instance() {
 }
 
 void ChannelAggregator::start() {
+    stopped = false;
     updateUnwatchedCount();
     QTimer::singleShot(0, this, SLOT(run()));
-    timer->start();
+    if (!timer->isActive()) timer->start();
 }
 
 void ChannelAggregator::stop() {
@@ -56,59 +56,65 @@ 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();
@@ -124,8 +130,8 @@ void ChannelAggregator::finish() {
         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);
@@ -138,14 +144,23 @@ void ChannelAggregator::finish() {
     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() {
@@ -177,13 +192,16 @@ void ChannelAggregator::addVideo(Video *video) {
 
     // 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";
@@ -196,13 +214,13 @@ void ChannelAggregator::addVideo(Video *video) {
                   "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());
@@ -216,13 +234,13 @@ void ChannelAggregator::addVideo(Video *video) {
     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);
@@ -232,9 +250,9 @@ void ChannelAggregator::markAllAsWatched() {
     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);
@@ -245,19 +263,19 @@ void ChannelAggregator::videoWatched(Video *video) {
     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();
 
index 1ef224f7644f900737880a244f02b5870002752d..288ce82d70955883cbc9e1d263bf1b6e223e312f 100644 (file)
@@ -23,7 +23,7 @@ $END_LICENSE */
 
 #include <QtCore>
 
-class YTUser;
+class YTChannel;
 class Video;
 
 class ChannelAggregator : public QObject {
@@ -44,16 +44,16 @@ public slots:
     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();
 
@@ -62,7 +62,7 @@ private:
     bool running;
 
     int newVideoCount;
-    QList<YTUser*> updatedChannels;
+    QList<YTChannel*> updatedChannels;
 
     QTimer *timer;
     bool stopped;
index b06e8612c1ad59e78e2b7db68704653178ebdc84..797b82a539a2ea502776ce60a75e0367c4543e3b 100644 (file)
@@ -20,7 +20,7 @@ $END_LICENSE */
 
 #include "channelitemdelegate.h"
 #include "channelmodel.h"
-#include "ytuser.h"
+#include "ytchannel.h"
 #include "fontutils.h"
 #include "channelaggregator.h"
 #include "painterutils.h"
@@ -94,10 +94,8 @@ void ChannelItemDelegate::paintUnwatched(QPainter* painter,
 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();
 
@@ -108,17 +106,17 @@ void ChannelItemDelegate::paintChannel(QPainter* painter,
     // 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));
 
index e51e4dc2701102715b9ef8723f94f863014b84d6..a2cbd36f4f9b90eea5cfad882fa280fa4e1c073e 100644 (file)
@@ -19,7 +19,7 @@ along with Minitube.  If not, see <http://www.gnu.org/licenses/>.
 $END_LICENSE */
 
 #include "channelmodel.h"
-#include "ytuser.h"
+#include "ytchannel.h"
 
 static const int channelOffset = 2;
 
@@ -40,7 +40,7 @@ QVariant ChannelModel::data(const QModelIndex &index, int role) const {
 
     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:
@@ -48,14 +48,14 @@ QVariant ChannelModel::data(const QModelIndex &index, int role) const {
 
     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);
@@ -85,11 +85,11 @@ void ChannelModel::setQuery(const QString &query, const QSqlDatabase &db) {
         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();
 }
@@ -99,16 +99,16 @@ QSqlError ChannelModel::lastError() const {
 }
 
 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);
@@ -121,11 +121,11 @@ void ChannelModel::updateUnwatched() {
 }
 
 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;
index 6ec5cf1dc7841b42bd680d7c9693ace19d4cb1b6..ee53f7fe1409cac8748a6fc03e71f8c6b1694e71 100644 (file)
@@ -24,7 +24,7 @@ $END_LICENSE */
 #include <QtCore>
 #include <QtSql>
 
-class YTUser;
+class YTChannel;
 
 class ChannelModel : public QAbstractListModel {
 
@@ -48,7 +48,7 @@ public:
     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;
@@ -57,12 +57,12 @@ public:
 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;
 
diff --git a/src/channelsitemdelegate.cpp b/src/channelsitemdelegate.cpp
new file mode 100644 (file)
index 0000000..4f629a8
--- /dev/null
@@ -0,0 +1,152 @@
+#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();
+}
diff --git a/src/channelsitemdelegate.h b/src/channelsitemdelegate.h
new file mode 100644 (file)
index 0000000..afee35d
--- /dev/null
@@ -0,0 +1,24 @@
+#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
diff --git a/src/channelsmodel.cpp b/src/channelsmodel.cpp
new file mode 100644 (file)
index 0000000..4ba3a68
--- /dev/null
@@ -0,0 +1,90 @@
+#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;
+    */
+}
diff --git a/src/channelsmodel.h b/src/channelsmodel.h
new file mode 100644 (file)
index 0000000..02fd5a9
--- /dev/null
@@ -0,0 +1,45 @@
+#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
index c252802f3455e34ce7ba45c0f64da0c1067dd538..95d412abaf6ff67354ec6244eef2fd3cecc3a2a9 100644 (file)
@@ -30,7 +30,7 @@ ChannelSuggest::ChannelSuggest(QObject *parent) : Suggester(parent) {
 }
 
 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;
@@ -51,14 +51,15 @@ void ChannelSuggest::handleNetworkData(QByteArray data) {
     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;
         }
diff --git a/src/channelsview.cpp b/src/channelsview.cpp
new file mode 100644 (file)
index 0000000..11680ca
--- /dev/null
@@ -0,0 +1,275 @@
+#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();
+}
diff --git a/src/channelsview.h b/src/channelsview.h
new file mode 100644 (file)
index 0000000..0988a31
--- /dev/null
@@ -0,0 +1,64 @@
+#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
index af5cb0184f8b57c4caf1f739424209473acbef62..893f891288ee2764f19e98215898023ce0dbd7f5 100644 (file)
@@ -19,7 +19,7 @@ along with Minitube.  If not, see <http://www.gnu.org/licenses/>.
 $END_LICENSE */
 
 #include "channelview.h"
-#include "ytuser.h"
+#include "ytchannel.h"
 #include "ytsearch.h"
 #include "searchparams.h"
 #include "channelmodel.h"
@@ -84,8 +84,8 @@ ChannelView::ChannelView(QWidget *parent) : QListView(parent),
 
     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)));
 
@@ -95,7 +95,7 @@ ChannelView::ChannelView(QWidget *parent) : QListView(parent),
 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);
@@ -174,10 +174,11 @@ void ChannelView::appear() {
     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);
 }
@@ -208,14 +209,15 @@ void ChannelView::itemEntered(const QModelIndex &index) {
 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"));
@@ -232,15 +234,15 @@ void ChannelView::showContextMenu(const QPoint &point) {
     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();
@@ -253,7 +255,7 @@ void ChannelView::showContextMenu(const QPoint &point) {
     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()));
 
diff --git a/src/channelwidget.cpp b/src/channelwidget.cpp
new file mode 100644 (file)
index 0000000..ca227b1
--- /dev/null
@@ -0,0 +1,85 @@
+#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();
+}
diff --git a/src/channelwidget.h b/src/channelwidget.h
new file mode 100644 (file)
index 0000000..7ceb6ae
--- /dev/null
@@ -0,0 +1,35 @@
+#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
index 3e7c537d5970aadeb2c1f607fa804fee03d4926b..4e089ff9cdc895d812aabadf81be0470e0a93f05 100644 (file)
@@ -38,11 +38,15 @@ Database::Database() {
 
     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();
 }
 
@@ -58,9 +62,9 @@ void Database::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,"
@@ -77,13 +81,13 @@ void Database::createDatabase() {
     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,"
@@ -156,11 +160,31 @@ QVariant Database::getAttribute(QString name) {
 
 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__;
 }
 
 /**
index 74ab8b710ba19d2a5e0bb68f7aac265935ebbf78..e5133fca18994e7931fe7d262bd443872617256d 100644 (file)
@@ -45,6 +45,8 @@ private:
     QVariant getAttribute(QString name);
     void setAttribute(QString name, QVariant value);
 
+    void fixChannelIds();
+
     QMutex lock;
     QString dbLocation;
     QHash<QThread*, QSqlDatabase> connections;
index 3490448266e1a13be0df027c841b1c6e1c1b1286..11bb943e231b00c3adde1c7799c9876031a36064 100644 (file)
@@ -20,3 +20,60 @@ QString DataUtils::stringToFilename(const QString &s) {
 
     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;
+}
index eb0a3c0c6035625a665fd439598f96f6ef089435..e5a8de08cbe3ec761876f14df349173d17c18876 100644 (file)
@@ -7,6 +7,9 @@ class DataUtils {
 
 public:
     static QString stringToFilename(const QString &s);
+    static QString regioneCode(const QLocale &locale);
+    static QString systemRegioneCode();
+    static uint parseIsoPeriod(const QString &isoPeriod);
 
 private:
     DataUtils() { }
index f9d0391b45c4b86700a46c268854f8b253ffa6a1..4766b008635f3c5c8dc988914dfd35ae389fe939 100644 (file)
@@ -25,7 +25,7 @@ DiskCache::DiskCache(QObject *parent) : QNetworkDiskCache(parent) { }
 
 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;
@@ -33,9 +33,20 @@ QIODevice* DiskCache::prepare(const QNetworkCacheMetaData &metaData) {
         }
     }
 
-    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);
+}
index 1f3cdc911a59517508a82581d5a5cd640a5d7530..c79a71b146e27242f7da7e6d1fa18dfa63158fcf 100644 (file)
@@ -29,6 +29,7 @@ class DiskCache : public QNetworkDiskCache
 public:
     explicit DiskCache(QObject *parent = 0);
     QIODevice* prepare(const QNetworkCacheMetaData &metaData);
+    QNetworkCacheMetaData metaData(const QUrl &url);
 
 signals:
 
index 91f2ed87e20b887956dfd0203395850c5add7780..131c17cb664a031c7ec0f676ab1a6319d2815fa6 100644 (file)
@@ -34,11 +34,37 @@ $END_LICENSE */
 #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);
@@ -96,39 +122,16 @@ int main(int argc, char **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();
 }
index 27959971075e608e00f190ac0251165fcc37d8d9..08a20a64087bff3f5e8f14c279454a921e03b468 100644 (file)
@@ -70,11 +70,15 @@ $END_LICENSE */
 #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;
 }
 
@@ -92,6 +96,10 @@ MainWindow::MainWindow() :
 
     singleton = this;
 
+#ifdef APP_EXTRA
+    Extra::windowSetup(this);
+#endif
+
     // views mechanism
     history = new QStack<QWidget*>();
     views = new QStackedWidget();
@@ -139,10 +147,6 @@ MainWindow::MainWindow() :
 
     views->show();
 
-#ifdef APP_EXTRA
-    Extra::windowSetup(this);
-#endif
-
     qApp->processEvents();
     QTimer::singleShot(50, this, SLOT(lazyInit()));
 }
@@ -204,8 +208,6 @@ void MainWindow::lazyInit() {
     JsFunctions::instance();
 
     checkForUpdate();
-
-    ChannelAggregator::instance()->start();
 }
 
 void MainWindow::changeEvent(QEvent* event) {
@@ -230,7 +232,7 @@ bool MainWindow::eventFilter(QObject *obj, 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);
@@ -438,7 +440,7 @@ void MainWindow::createActions() {
     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
@@ -750,6 +752,10 @@ void MainWindow::createToolBars() {
 
     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);
@@ -978,7 +984,7 @@ void MainWindow::quit() {
     if (!m_fullscreen && !compactViewAct->isChecked()) {
         writeSettings();
     }
-    mediaView->stop();
+    // mediaView->stop();
     Temporary::deleteAll();
     ChannelAggregator::instance()->stop();
     ChannelAggregator::instance()->cleanup();
@@ -1310,20 +1316,25 @@ void MainWindow::initPhonon() {
     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
 
@@ -1401,15 +1412,20 @@ void MainWindow::volumeDown() {
 
 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;
@@ -1437,6 +1453,10 @@ void MainWindow::volumeMutedChanged(bool muted) {
         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) {
index 1f703181f425f850e451912a19487c3a1c5dda53..c1617f1e060c991ca029c2946f71fde9aae7523c 100644 (file)
@@ -45,7 +45,7 @@ $END_LICENSE */
 #include "ytsinglevideosource.h"
 #include "channelaggregator.h"
 #include "iconutils.h"
-#include "ytuser.h"
+#include "ytchannel.h"
 #ifdef APP_SNAPSHOT
 #include "snapshotsettings.h"
 #endif
@@ -205,7 +205,10 @@ void MediaView::search(SearchParams *searchParams) {
             }
         }
     }
-    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) {
@@ -245,7 +248,7 @@ void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool
     sidebar->getHeader()->updateInfo();
 
     SearchParams *searchParams = getSearchParams();
-    bool isChannel = searchParams && !searchParams->author().isEmpty();
+    bool isChannel = searchParams && !searchParams->channelId().isEmpty();
     playlistView->setClickableAuthors(!isChannel);
 
 
@@ -315,7 +318,7 @@ void MediaView::stateChanged(Phonon::State newState, Phonon::State /*oldState*/)
     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());
     }
@@ -446,7 +449,7 @@ void MediaView::activeRowChanged(int row) {
     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);
@@ -488,7 +491,7 @@ void MediaView::gotStreamUrl(QUrl streamUrl) {
     startDownloading();
 #endif
 
-    // ensure we always have 10 videos ahead
+    // ensure we always have videos ahead
     playlistModel->searchNeeded();
 
     // ensure active item is visible
@@ -504,7 +507,7 @@ void MediaView::gotStreamUrl(QUrl streamUrl) {
 #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);
@@ -676,7 +679,7 @@ void MediaView::openWebPage() {
 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);
@@ -845,6 +848,8 @@ void MediaView::snapshot() {
     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) + ")";
@@ -977,7 +982,7 @@ void MediaView::findVideoParts() {
     SearchParams *searchParams = new SearchParams();
     searchParams->setTransient(true);
     searchParams->setKeywords(query);
-    searchParams->setAuthor(video->author());
+    searchParams->setChannelId(video->channelId());
 
     /*
     if (!numberAsWords) {
@@ -996,7 +1001,8 @@ void MediaView::relatedVideos() {
     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);
 }
@@ -1012,7 +1018,7 @@ void MediaView::shareViaTwitter() {
 #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);
     }
@@ -1030,7 +1036,7 @@ void MediaView::shareViaFacebook() {
         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);
     }
@@ -1049,7 +1055,7 @@ void MediaView::shareViaBuffer() {
 #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);
@@ -1069,7 +1075,7 @@ void MediaView::shareViaEmail() {
 #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);
@@ -1084,12 +1090,12 @@ void MediaView::authorPushed(QModelIndex index) {
     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!
@@ -1105,11 +1111,11 @@ void MediaView::updateSubscriptionAction(Video *video, bool subscribed) {
         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);
     }
@@ -1138,10 +1144,10 @@ void MediaView::updateSubscriptionAction(Video *video, bool subscribed) {
 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);
 }
index 7f2c33414692fee029552da64858312127e6e368..9b99e467781c57adaa0e286c601614289a9577c1 100644 (file)
@@ -74,7 +74,7 @@ void NetworkReply::finished() {
             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)
diff --git a/src/paginatedvideosource.cpp b/src/paginatedvideosource.cpp
new file mode 100644 (file)
index 0000000..396b836
--- /dev/null
@@ -0,0 +1,170 @@
+/* $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();
+    }
+}
diff --git a/src/paginatedvideosource.h b/src/paginatedvideosource.h
new file mode 100644 (file)
index 0000000..1c04b52
--- /dev/null
@@ -0,0 +1,59 @@
+/* $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
index 81bdae47612e9e13789d06d7ce8eb47ce01e395d..717c7e57d64cbe7ee82a8318bd552d645a8bb41d 100644 (file)
@@ -131,7 +131,8 @@ void PlaylistItemDelegate::paintBody( QPainter* painter,
         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));
@@ -142,7 +143,7 @@ void PlaylistItemDelegate::paintBody( QPainter* painter,
 
     if (line.width() > THUMB_WIDTH + 60) {
 
-        if (isActive) painter->setFont(boldFont);
+        // if (isActive) painter->setFont(boldFont);
 
         // text color
         if (isSelected)
@@ -192,7 +193,7 @@ void PlaylistItemDelegate::paintBody( QPainter* painter,
                 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);
@@ -226,7 +227,7 @@ void PlaylistItemDelegate::paintBody( QPainter* painter,
 
     } 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);
index a3cad088bcee9646d686264c1ab6bdd25e1cd457..eb8a2ddb06ffc808244ee45887bb8452894ebdcc 100644 (file)
@@ -26,7 +26,7 @@ $END_LICENSE */
 #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";
 
@@ -37,7 +37,7 @@ PlaylistModel::PlaylistModel(QWidget *parent) : QAbstractListModel(parent) {
     firstSearch = false;
     m_activeVideo = 0;
     m_activeRow = -1;
-    skip = 1;
+    startIndex = 1;
     max = 0;
     hoveredRow = -1;
     authorHovered = false;
@@ -70,7 +70,7 @@ QVariant PlaylistModel::data(const QModelIndex &index, int role) const {
         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:
@@ -165,11 +165,11 @@ Video* PlaylistModel::activeVideo() const {
 
 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;
@@ -186,11 +186,11 @@ void PlaylistModel::setVideoSource(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() {
@@ -198,27 +198,29 @@ 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 ) );
@@ -243,8 +245,9 @@ void PlaylistModel::addVideos(QList<Video*> newVideos) {
     endInsertRows();
     foreach (Video* video, newVideos) {
         connect(video, SIGNAL(gotThumbnail()),
-                SLOT(updateThumbnail()), Qt::UniqueConnection);
+                SLOT(updateVideoSender()), Qt::UniqueConnection);
         video->loadThumbnail();
+        qApp->processEvents();
     }
 }
 
@@ -288,15 +291,15 @@ void PlaylistModel::handleFirstVideo(Video *video) {
         }
 
         // 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();
@@ -305,17 +308,19 @@ void PlaylistModel::handleFirstVideo(Video *video) {
     }
 }
 
-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
@@ -366,7 +371,7 @@ Qt::DropActions PlaylistModel::supportedDragActions() const {
 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);
     }
@@ -439,8 +444,10 @@ bool PlaylistModel::dropMimeData(const QMimeData *data,
 }
 
 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;
@@ -496,8 +503,13 @@ void PlaylistModel::setHoveredRow(int row) {
 }
 
 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 */
@@ -505,28 +517,24 @@ void PlaylistModel::clearHover() {
 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();
 }
index 88d6dcfea7601e24bfa3bb6a17449e9774485f21..2123bd245d603b29c14ddc7f821c076044abee75 100644 (file)
@@ -91,15 +91,17 @@ public slots:
     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);
@@ -116,7 +118,7 @@ private:
     bool firstSearch;
 
     QList<Video*> videos;
-    int skip;
+    int startIndex;
     int max;
 
     int m_activeRow;
@@ -127,6 +129,8 @@ private:
     int hoveredRow;
     bool authorHovered;
     bool authorPressed;
+
+    QMutex mutex;
 };
 
 #endif
index 2391f9fe6258f019f424d497c8846631cb6842e3..c831c3be5007cac9576a1545f84c05c7dd3de2dc 100644 (file)
@@ -54,7 +54,8 @@ void PlaylistView::itemEntered(const QModelIndex &index) {
     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();
 }
index be08e72241a6b6e2a2b46afa7d03b4b73cb13c44..17a8dfa68247dd4b86afc236590c8e9c44aa3c65 100644 (file)
@@ -26,6 +26,7 @@ SearchParams::SearchParams(QObject *parent) : QObject(parent) {
     m_duration = DurationAny;
     m_quality = QualityAny;
     m_time = TimeAny;
+    m_publishedAfter = 0;
 }
 
 void SearchParams::setParam(QString name, QVariant value) {
index f2e9d035ec98839842a12265a6fd3c5694b02d77..f55dcebaf2e1641dd97f9d5a3fa0a5f8e3777f94 100644 (file)
@@ -62,10 +62,10 @@ public:
     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; }
@@ -82,9 +82,12 @@ public:
     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:
@@ -92,12 +95,13 @@ 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;
 
 };
 
index 2e3ac2769a846007bee9ff583a42927c1b2ef8ab..a0037a3d48a0b71690ed0bd59a9e6d073a49d5b5 100644 (file)
@@ -91,7 +91,7 @@ SearchView::SearchView(QWidget *parent) : QWidget(parent) {
                             #if defined(APP_UBUNTU) || defined(APP_WIN)
                                 "normal"
                             #else
-                                "bold"
+                                "normal"
                             #endif
                                 "' ")
                        .arg(Constants::WEBSITE, Constants::NAME)
@@ -198,12 +198,14 @@ SearchView::SearchView(QWidget *parent) : QWidget(parent) {
 }
 
 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() {
@@ -318,7 +320,8 @@ void SearchView::watch(QString query) {
     else {
         // remove spaces from channel name
         query = query.simplified();
-        searchParams->setAuthor(query);
+        query = query.remove(' ');
+        searchParams->setChannelId(query);
         searchParams->setSortBy(SearchParams::SortByNewest);
     }
 
@@ -326,21 +329,19 @@ void SearchView::watch(QString query) {
     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!
@@ -402,5 +403,7 @@ void SearchView::searchTypeChanged(int index) {
 }
 
 void SearchView::suggestionAccepted(Suggestion *suggestion) {
-    watch(suggestion->value);
+    if (suggestion->type == QLatin1String("channel")) {
+        watchChannel(suggestion->userData);
+    } else watch(suggestion->value);
 }
index f513c61e9e9f70fd9ed548eb22cab948d637a51c..a8022b6f724fcfc4ce3d6780d9f1ed313c58e397 100644 (file)
@@ -46,7 +46,7 @@ public slots:
     void appear();
     void disappear() { }
     void watch(QString query);
-    void watchChannel(QString channel);
+    void watchChannel(const QString &channelId);
     void watchKeywords(QString query);
 
 signals:
index 7942d0d77dce2e9084c3d922962a873e292fd9fe..0f0356493d88a3f8cb5dc14046c08cc8aaa0ca1c 100644 (file)
@@ -50,6 +50,7 @@ StandardFeedsView::StandardFeedsView(QWidget *parent) : QWidget(parent),
 }
 
 void StandardFeedsView::load() {
+    setUpdatesEnabled(false);
     YTCategories *youTubeCategories = new YTCategories(this);
     connect(youTubeCategories, SIGNAL(categoriesLoaded(const QList<YTCategory> &)),
             SLOT(layoutCategories(const QList<YTCategory> &)));
@@ -88,6 +89,7 @@ void StandardFeedsView::layoutCategories(const QList<YTCategory> &categories) {
         feed->setFeedId("most_popular");
         addVideoSourceWidget(feed);
     }
+    if (categories.size() > 1) setUpdatesEnabled(true);
 }
 
 void StandardFeedsView::addVideoSourceWidget(VideoSource *videoSource) {
@@ -124,7 +126,11 @@ YTStandardFeed* StandardFeedsView::buildStardardFeed(
 
 void StandardFeedsView::appear() {
     setFocus();
-    if (!layout) load();
+    if (!layout) {
+        update();
+        qApp->processEvents();
+        load();
+    }
     QAction *regionAction = MainWindow::instance()->getRegionAction();
     regionAction->setVisible(true);
 }
@@ -146,6 +152,6 @@ void StandardFeedsView::selectLocalRegion() {
 
 void StandardFeedsView::paintEvent(QPaintEvent *event) {
     QWidget::paintEvent(event);
-    PainterUtils::topShadow(this);
+    // PainterUtils::topShadow(this);
 }
 
index cca7f5f1041e5f11fc874ec3812800ddb4d1b6fe..37b805cc7883c099332f1ba13083231469769a9b 100644 (file)
@@ -27,10 +27,12 @@ class Suggestion {
 
 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);
index e7c90da4464d06b3bbaf85be11e6f8b20d1ad952..29b74b8125731e125886a0de2e4dc9a3beb99c90 100644 (file)
@@ -40,15 +40,15 @@ Video::Video() : m_duration(0),
     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;
@@ -63,18 +63,26 @@ Video* Video::clone() {
     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() {
@@ -86,6 +94,7 @@ 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);
@@ -117,7 +126,7 @@ void  Video::getVideoInfo() {
 
     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
         {
@@ -144,7 +153,7 @@ void  Video::getVideoInfo() {
     } 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)));
     }
 
@@ -162,7 +171,7 @@ void  Video::gotVideoInfo(QByteArray data) {
     // 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();
@@ -170,7 +179,7 @@ void  Video::gotVideoInfo(QByteArray data) {
     }
 
     QString videoToken = videoTokeRE.cap(1);
-    // qWarning() << "got token" << videoToken;
+    // qDebug() << "got token" << videoToken;
     while (videoToken.contains('%'))
         videoToken = QByteArray::fromPercentEncoding(videoToken.toLatin1());
     // qDebug() << "videoToken" << videoToken;
@@ -179,7 +188,7 @@ void  Video::gotVideoInfo(QByteArray data) {
     // 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();
@@ -189,7 +198,7 @@ void  Video::gotVideoInfo(QByteArray data) {
     // 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);
 }
@@ -236,7 +245,7 @@ void Video::parseFmtUrlMap(const QString &fmtUrlMap, bool fromWebPage) {
                             sig = JsFunctions::instance()->decryptSignature(sig);
                     }
                 } else {
-                    // qDebug() << "Loading webpage";
+
                     QUrl url("http://www.youtube.com/watch");
 
 #if QT_VERSION >= 0x050000
@@ -252,6 +261,7 @@ void Video::parseFmtUrlMap(const QString &fmtUrlMap, bool fromWebPage) {
                         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*)));
@@ -270,7 +280,7 @@ void Video::parseFmtUrlMap(const QString &fmtUrlMap, bool fromWebPage) {
         // 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;
@@ -290,7 +300,7 @@ void Video::parseFmtUrlMap(const QString &fmtUrlMap, bool fromWebPage) {
         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;
@@ -302,7 +312,7 @@ void Video::parseFmtUrlMap(const QString &fmtUrlMap, bool fromWebPage) {
         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) {
@@ -312,7 +322,6 @@ 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) {
@@ -325,7 +334,7 @@ void Video::scrapeWebPage(QByteArray data) {
 
     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++;
@@ -339,7 +348,7 @@ void Video::scrapeWebPage(QByteArray data) {
 #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);
@@ -356,10 +365,10 @@ void Video::scrapeWebPage(QByteArray data) {
         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*)));
@@ -391,16 +400,18 @@ void Video::parseJsPlayer(QByteArray bytes) {
             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;
         }
@@ -411,7 +422,7 @@ void Video::parseJsPlayer(QByteArray bytes) {
 }
 
 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);
index 3a85233bb82ad5bb111094d8846afd89dbfbcbf9..16454425a87463b3038b413a4b548ce6e1860281 100644 (file)
@@ -41,29 +41,29 @@ public:
     };
 
     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; }
@@ -73,7 +73,7 @@ public:
     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; }
 
@@ -109,9 +109,9 @@ private:
 
     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;
index 760f512c0063aa280bafb54e622f884ca3cdcbfc..458d120381b111eb588a0820b39730af488e3c69 100644 (file)
@@ -32,7 +32,8 @@ class VideoSource : public QObject {
 
 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;
diff --git a/src/yt3.cpp b/src/yt3.cpp
new file mode 100644 (file)
index 0000000..c341310
--- /dev/null
@@ -0,0 +1,119 @@
+#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;
+    }
+}
diff --git a/src/yt3.h b/src/yt3.h
new file mode 100644 (file)
index 0000000..124409f
--- /dev/null
+++ b/src/yt3.h
@@ -0,0 +1,32 @@
+#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
diff --git a/src/yt3listparser.cpp b/src/yt3listparser.cpp
new file mode 100644 (file)
index 0000000..ed65c72
--- /dev/null
@@ -0,0 +1,76 @@
+#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);
+}
diff --git a/src/yt3listparser.h b/src/yt3listparser.h
new file mode 100644 (file)
index 0000000..6ce915c
--- /dev/null
@@ -0,0 +1,25 @@
+#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
index 89217420c91ca9552977a5134a661d1a63058961..db5a244cddd770c1a2b4605075af0dab18de357f 100644 (file)
@@ -20,7 +20,12 @@ $END_LICENSE */
 
 #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();
@@ -33,12 +38,72 @@ void YTCategories::loadCategories(QString language) {
         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;
 
@@ -66,6 +131,8 @@ void YTCategories::parseCategories(QByteArray bytes) {
     emit categoriesLoaded(categories);
 }
 
+#endif
+
 void YTCategories::requestError(QNetworkReply *reply) {
     if (lastLanguage != "en") loadCategories("en");
     else emit error(reply->errorString());
diff --git a/src/ytchannel.cpp b/src/ytchannel.cpp
new file mode 100644 (file)
index 0000000..ad65241
--- /dev/null
@@ -0,0 +1,387 @@
+/* $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;
+}
diff --git a/src/ytchannel.h b/src/ytchannel.h
new file mode 100644 (file)
index 0000000..25a9fca
--- /dev/null
@@ -0,0 +1,111 @@
+/* $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
index 9aaacf0a1d1209b678dec7d1270aa3d8b7430ac5..c063ce243999643b2b7727a4a7118eda64e5d3c6 100644 (file)
@@ -56,15 +56,15 @@ void YTFeedReader::readEntry() {
                     ) {
                 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));
index 195c2a52b277fb612207a199d9e2e492057bbf50..e7784a1b2f6d58781c39dc02587ae3ff48d6e023 100644 (file)
@@ -21,7 +21,7 @@ $END_LICENSE */
 #ifndef YTFEEDREADER_H
 #define YTFEEDREADER_H
 
-#include <QtXml>
+#include <QtCore>
 
 class Video;
 
index 58f7a021031b0651142da1c174f8b0f7bdbe64e0..331a791d9e595b06e7e1e399fe41bc278b1d4727 100644 (file)
@@ -19,111 +19,239 @@ along with Minitube.  If not, see <http://www.gnu.org/licenses/>.
 $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) {
@@ -133,12 +261,12 @@ 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);
     }
@@ -147,7 +275,24 @@ void YTSearch::parseResults(QByteArray data) {
     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());
 }
 
@@ -161,7 +306,7 @@ QString YTSearch::videoIdFromUrl(QString url) {
 
 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;
index 7ed282cc7ffbaed7ae6e8420947b1ca5a5205e8b..be2a0c162ed742b33335b1c182fe72f59c98756b 100644 (file)
@@ -22,30 +22,29 @@ $END_LICENSE */
 #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);
@@ -55,8 +54,6 @@ private:
     bool aborted;
     QStringList suggestions;
     QString name;
-
-    QString userId;
 };
 
 #endif // YTSEARCH_H
index 05e5a327db238eae33b1654fb10c170898cfee8b..6d9276cb3d9b0c26ed47c3e7b0b6616cefd0dc71 100644 (file)
@@ -19,40 +19,128 @@ along with Minitube.  If not, see <http://www.gnu.org/licenses/>.
 $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);
@@ -63,37 +151,44 @@ void YTSingleVideoSource::loadVideos(int max, int skip) {
     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());
 }
index 53dfd529edb9018eb6e30a2fba025d73e8cd9c41..dc869a6edbe7c2c6f657fb401d206b2c4d38de5d 100644 (file)
@@ -22,29 +22,31 @@ $END_LICENSE */
 #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;
 };
index ee5e0030f141a8982754fd5840eec756f7eed638..59ce7304c857ef111a10c6209ef9bc24545a55c2 100644 (file)
@@ -19,20 +19,85 @@ along with Minitube.  If not, see <http://www.gnu.org/licenses/>.
 $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/";
@@ -42,20 +107,20 @@ void YTStandardFeed::loadVideos(int max, int skip) {
 
     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);
@@ -66,16 +131,7 @@ void YTStandardFeed::loadVideos(int max, int skip) {
     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);
@@ -85,6 +141,17 @@ void YTStandardFeed::parse(QByteArray 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());
 }
index 13979a8dca9d4acc6d78e5d968d6b2e0d8e63745..b1b64e9986e16c082397c18746fa9d997fcb4af3 100644 (file)
@@ -22,9 +22,9 @@ $END_LICENSE */
 #define YTSTANDARDFEED_H
 
 #include <QtNetwork>
-#include "videosource.h"
+#include "paginatedvideosource.h"
 
-class YTStandardFeed : public VideoSource {
+class YTStandardFeed : public PaginatedVideoSource {
 
     Q_OBJECT
 
@@ -46,13 +46,13 @@ public:
     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:
index a11824e7fbec0ea29fe5cea2c4e3177f68391443..86be974049e66aeb912865224e466cc7f4c8f3fe 100644 (file)
@@ -19,13 +19,10 @@ along with Minitube.  If not, see <http://www.gnu.org/licenses/>.
 $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) {
@@ -46,7 +43,9 @@ void YTSuggester::suggest(const QString &query) {
         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)));
diff --git a/src/ytuser.cpp b/src/ytuser.cpp
deleted file mode 100644 (file)
index f51bd18..0000000
+++ /dev/null
@@ -1,328 +0,0 @@
-/* $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;
-}
diff --git a/src/ytuser.h b/src/ytuser.h
deleted file mode 100644 (file)
index 306cd13..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-/* $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