]> git.sur5r.net Git - minitube/blob - src/mediaview.cpp
New upstream version 3.8
[minitube] / src / mediaview.cpp
1 /* $BEGIN_LICENSE
2
3 This file is part of Minitube.
4 Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
5
6 Minitube is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 Minitube is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Minitube.  If not, see <http://www.gnu.org/licenses/>.
18
19 $END_LICENSE */
20
21 #include "mediaview.h"
22 #include "constants.h"
23 #include "downloadmanager.h"
24 #include "http.h"
25 #include "loadingwidget.h"
26 #include "mainwindow.h"
27 #include "minisplitter.h"
28 #include "playlistmodel.h"
29 #include "playlistview.h"
30 #include "refinesearchwidget.h"
31 #include "sidebarheader.h"
32 #include "sidebarwidget.h"
33 #include "temporary.h"
34 #include "videoarea.h"
35 #ifdef APP_ACTIVATION
36 #include "activation.h"
37 #include "activationview.h"
38 #endif
39 #ifdef APP_EXTRA
40 #include "extra.h"
41 #endif
42 #include "channelaggregator.h"
43 #include "iconutils.h"
44 #include "searchparams.h"
45 #include "videosource.h"
46 #include "ytchannel.h"
47 #include "ytsearch.h"
48 #ifdef APP_SNAPSHOT
49 #include "snapshotsettings.h"
50 #endif
51 #include "datautils.h"
52 #include "idle.h"
53 #include "videodefinition.h"
54
55 #include "searchvideosource.h"
56 #include "singlevideosource.h"
57 #include "videoapi.h"
58
59 MediaView *MediaView::instance() {
60     static MediaView *i = new MediaView();
61     return i;
62 }
63
64 MediaView::MediaView(QWidget *parent)
65     : View(parent), splitter(nullptr), stopped(false)
66 #ifdef APP_SNAPSHOT
67       ,
68       snapshotSettings(nullptr)
69 #endif
70       ,
71       pauseTime(0) {
72 }
73
74 void MediaView::initialize() {
75     MainWindow *mainWindow = MainWindow::instance();
76
77     QBoxLayout *layout = new QVBoxLayout(this);
78     layout->setMargin(0);
79
80     splitter = new MiniSplitter();
81     layout->addWidget(splitter);
82
83     playlistView = new PlaylistView();
84     playlistView->setParent(this);
85     connect(playlistView, SIGNAL(activated(const QModelIndex &)),
86             SLOT(onItemActivated(const QModelIndex &)));
87
88     playlistModel = new PlaylistModel();
89     connect(playlistModel, &PlaylistModel::activeVideoChanged, this,
90             &MediaView::activeVideoChanged);
91     // needed to restore the selection after dragndrop
92     connect(playlistModel, SIGNAL(needSelectionFor(QVector<Video *>)),
93             SLOT(selectVideos(QVector<Video *>)));
94     playlistView->setModel(playlistModel);
95
96     connect(playlistView->selectionModel(),
97             SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
98             SLOT(selectionChanged(const QItemSelection &, const QItemSelection &)));
99
100     connect(playlistView, SIGNAL(authorPushed(QModelIndex)), SLOT(onAuthorPushed(QModelIndex)));
101
102     sidebar = new SidebarWidget(this);
103     sidebar->setPlaylist(playlistView);
104     sidebar->setMaximumWidth(playlistView->minimumWidth() * 3);
105     connect(sidebar->getRefineSearchWidget(), SIGNAL(searchRefined()), SLOT(searchAgain()));
106     connect(playlistModel, SIGNAL(haveSuggestions(const QStringList &)), sidebar,
107             SLOT(showSuggestions(const QStringList &)));
108     connect(sidebar, SIGNAL(suggestionAccepted(QString)), mainWindow, SLOT(search(QString)));
109     splitter->addWidget(sidebar);
110
111     videoAreaWidget = new VideoArea(this);
112     videoAreaWidget->setListModel(playlistModel);
113
114     loadingWidget = new LoadingWidget(this);
115     videoAreaWidget->setLoadingWidget(loadingWidget);
116
117     splitter->addWidget(videoAreaWidget);
118
119     // restore splitter state
120     QSettings settings;
121     if (settings.contains("splitter"))
122         splitter->restoreState(settings.value("splitter").toByteArray());
123     else {
124         int sidebarDefaultWidth = 180;
125         splitter->setSizes(QList<int>() << sidebarDefaultWidth
126                                         << splitter->size().width() - sidebarDefaultWidth);
127     }
128     splitter->setChildrenCollapsible(false);
129     connect(splitter, SIGNAL(splitterMoved(int, int)), SLOT(adjustWindowSize()));
130
131     errorTimer = new QTimer(this);
132     errorTimer->setSingleShot(true);
133     errorTimer->setInterval(3000);
134     connect(errorTimer, SIGNAL(timeout()), SLOT(skipVideo()));
135
136 #ifdef APP_ACTIVATION
137     demoTimer = new QTimer(this);
138     demoTimer->setSingleShot(true);
139     connect(
140             demoTimer, &QTimer::timeout, this,
141             [this] {
142                 if (media->state() != Media::PlayingState) return;
143                 media->pause();
144                 connect(
145                         ActivationView::instance(), &ActivationView::done, media,
146                         [this] { media->play(); }, Qt::UniqueConnection);
147                 MainWindow::instance()->showActivationView();
148             },
149             Qt::QueuedConnection);
150 #endif
151
152     connect(videoAreaWidget, SIGNAL(doubleClicked()), mainWindow->getAction("fullscreen"),
153             SLOT(trigger()));
154
155     QAction *refineSearchAction = mainWindow->getAction("refineSearch");
156     connect(refineSearchAction, SIGNAL(toggled(bool)), sidebar, SLOT(toggleRefineSearch(bool)));
157
158     const QVector<const char *> videoActionNames = {
159 #ifdef APP_SNAPSHOT
160             "snapshot",
161 #endif
162             "webpage",  "pagelink", "videolink",     "openInBrowser", "findVideoParts",
163             "skip",     "previous", "stopafterthis", "relatedVideos", "refineSearch",
164             "twitter",  "facebook", "email"};
165     currentVideoActions.reserve(videoActionNames.size());
166     for (auto *name : videoActionNames) {
167         currentVideoActions.append(mainWindow->getAction(name));
168     }
169
170     for (int i = 0; i < 10; ++i) {
171         QAction *action = new QAction(QString());
172         action->setShortcut(Qt::Key_0 + i);
173         action->setAutoRepeat(false);
174         connect(action, &QAction::triggered, this, [this, i] {
175             qint64 duration = media->duration();
176             // dur : pos = 100 : i*10
177             qint64 position = (duration * (i * 10)) / 100;
178             media->seek(position);
179         });
180         addAction(action);
181         playingVideoActions << action;
182     }
183
184     QAction *leftAction = new QAction(tr("Rewind %1 seconds").arg(10));
185     leftAction->setShortcut(Qt::Key_Left);
186     leftAction->setAutoRepeat(false);
187     connect(leftAction, &QAction::triggered, this, [this] { media->relativeSeek(-10000); });
188     addAction(leftAction);
189     playingVideoActions << leftAction;
190
191     QAction *rightAction = new QAction(tr("Fast forward %1 seconds").arg(10));
192     rightAction->setShortcut(Qt::Key_Right);
193     rightAction->setAutoRepeat(false);
194     connect(rightAction, &QAction::triggered, this, [this] { media->relativeSeek(10000); });
195     addAction(rightAction);
196     playingVideoActions << rightAction;
197 }
198
199 void MediaView::setMedia(Media *media) {
200     this->media = media;
201
202     videoWidget = media->videoWidget();
203     videoAreaWidget->setVideoWidget(videoWidget);
204
205     connect(media, &Media::finished, this, &MediaView::onPlaybackFinished);
206     connect(media, &Media::stateChanged, this, &MediaView::mediaStateChanged);
207     connect(media, &Media::aboutToFinish, this, &MediaView::onAboutToFinish);
208     connect(media, &Media::bufferStatus, loadingWidget, &LoadingWidget::bufferStatus);
209 }
210
211 SearchParams *MediaView::getSearchParams() {
212     VideoSource *videoSource = playlistModel->getVideoSource();
213     if (!videoSource) return nullptr;
214     auto clazz = videoSource->metaObject()->className();
215     if (clazz == QLatin1String("SearchVideoSource")) {
216         auto search = qobject_cast<SearchVideoSource *>(videoSource);
217         if (search) return search->getSearchParams();
218     }
219     return nullptr;
220 }
221
222 void MediaView::search(SearchParams *searchParams) {
223     if (!searchParams->keywords().isEmpty()) {
224         if (searchParams->keywords().startsWith("http://") ||
225             searchParams->keywords().startsWith("https://")) {
226             QString videoId = YTSearch::videoIdFromUrl(searchParams->keywords());
227             if (!videoId.isEmpty()) {
228                 auto source = new SingleVideoSource(this);
229                 source->setVideoId(videoId);
230                 setVideoSource(source);
231
232                 QTime tstamp = YTSearch::videoTimestampFromUrl(searchParams->keywords());
233                 pauseTime = QTime(0, 0).msecsTo(tstamp);
234                 return;
235             }
236         }
237     }
238
239     VideoSource *search = new SearchVideoSource(searchParams);
240     setVideoSource(search);
241 }
242
243 void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool back) {
244     Q_UNUSED(back);
245     stopped = false;
246     errorTimer->stop();
247
248     // qDebug() << "Adding VideoSource" << videoSource->getName() << videoSource;
249
250     if (addToHistory) {
251         int currentIndex = getHistoryIndex();
252         if (currentIndex >= 0 && currentIndex < history.size() - 1) {
253             while (history.size() > currentIndex + 1) {
254                 VideoSource *vs = history.takeLast();
255                 if (!vs->parent()) {
256                     qDebug() << "Deleting VideoSource" << vs->getName() << vs;
257                     vs->deleteLater();
258                 }
259             }
260         }
261         history.append(videoSource);
262     }
263
264 #ifdef APP_EXTRA
265     if (history.size() > 1)
266         Extra::slideTransition(playlistView->viewport(), playlistView->viewport(), back);
267 #endif
268
269     playlistModel->setVideoSource(videoSource);
270
271     if (media->state() == Media::StoppedState) {
272         QSettings settings;
273         if (settings.value("manualplay", false).toBool()) {
274             videoAreaWidget->showPickMessage();
275         }
276     }
277
278     SearchParams *searchParams = getSearchParams();
279
280     sidebar->showPlaylist();
281     sidebar->getRefineSearchWidget()->setSearchParams(searchParams);
282     sidebar->hideSuggestions();
283     sidebar->getHeader()->updateInfo();
284
285     bool isChannel = searchParams && !searchParams->channelId().isEmpty();
286     if (isChannel) {
287         updateSubscriptionActionForChannel(searchParams->channelId());
288     }
289     playlistView->setClickableAuthors(!isChannel);
290 }
291
292 void MediaView::searchAgain() {
293     VideoSource *currentVideoSource = playlistModel->getVideoSource();
294     setVideoSource(currentVideoSource, false);
295 }
296
297 bool MediaView::canGoBack() {
298     return getHistoryIndex() > 0;
299 }
300
301 void MediaView::goBack() {
302     if (history.size() > 1) {
303         int currentIndex = getHistoryIndex();
304         if (currentIndex > 0) {
305             VideoSource *previousVideoSource = history.at(currentIndex - 1);
306             setVideoSource(previousVideoSource, false, true);
307         }
308     }
309 }
310
311 bool MediaView::canGoForward() {
312     int currentIndex = getHistoryIndex();
313     return currentIndex >= 0 && currentIndex < history.size() - 1;
314 }
315
316 void MediaView::goForward() {
317     if (canGoForward()) {
318         int currentIndex = getHistoryIndex();
319         VideoSource *nextVideoSource = history.at(currentIndex + 1);
320         setVideoSource(nextVideoSource, false);
321     }
322 }
323
324 int MediaView::getHistoryIndex() {
325     return history.lastIndexOf(playlistModel->getVideoSource());
326 }
327
328 void MediaView::appear() {
329     MainWindow::instance()->showToolbar();
330
331     Video *currentVideo = playlistModel->activeVideo();
332     if (currentVideo) {
333         MainWindow::instance()->setWindowTitle(currentVideo->getTitle() + " - " + Constants::NAME);
334     }
335
336     playlistView->setFocus();
337 }
338
339 void MediaView::disappear() {
340     MainWindow::instance()->hideToolbar();
341 }
342
343 void MediaView::handleError(const QString &message) {
344     qWarning() << __PRETTY_FUNCTION__ << message;
345 #ifndef QT_NO_DEBUG_OUTPUT
346     MainWindow::instance()->showMessage(message);
347 #endif
348 }
349
350 void MediaView::mediaStateChanged(Media::State state) {
351     if (pauseTime > 0 && (state == Media::PlayingState || state == Media::BufferingState)) {
352         qDebug() << "Seeking to" << pauseTime;
353         media->seek(pauseTime);
354         media->play();
355         pauseTime = 0;
356     }
357     if (state == Media::PlayingState) {
358         videoAreaWidget->showVideo();
359     } else if (state == Media::ErrorState) {
360         handleError(media->errorString());
361     }
362
363     bool enablePlayingVideoActions = state != Media::StoppedState;
364     for (QAction *action : qAsConst(playingVideoActions))
365         action->setEnabled(enablePlayingVideoActions);
366
367     if (state == Media::PlayingState) {
368         bool res = Idle::preventDisplaySleep(QString("%1 is playing").arg(Constants::NAME));
369         if (!res) qWarning() << "Error disabling idle display sleep" << Idle::displayErrorMessage();
370     } else if (state == Media::PausedState || state == Media::StoppedState) {
371         bool res = Idle::allowDisplaySleep();
372         if (!res) qWarning() << "Error enabling idle display sleep" << Idle::displayErrorMessage();
373     }
374 }
375
376 void MediaView::pause() {
377     switch (media->state()) {
378     case Media::PlayingState:
379         media->pause();
380         pauseTimer.start();
381         break;
382     default:
383         if (pauseTimer.hasExpired(60000)) {
384             pauseTimer.invalidate();
385             auto activeVideo = playlistModel->activeVideo();
386             if (activeVideo) {
387                 connect(activeVideo, &Video::gotStreamUrl, this,
388                         &MediaView::resumeWithNewStreamUrl);
389                 activeVideo->loadStreamUrl();
390             }
391         } else
392             media->play();
393         break;
394     }
395 }
396
397 QRegExp MediaView::wordRE(const QString &s) {
398     return QRegExp("\\W" + s + "\\W?", Qt::CaseInsensitive);
399 }
400
401 void MediaView::stop() {
402     stopped = true;
403
404     while (!history.isEmpty()) {
405         VideoSource *videoSource = history.takeFirst();
406         // Don't delete videoSource in the Browse view
407         if (!videoSource->parent()) {
408             videoSource->deleteLater();
409         }
410     }
411
412     playlistModel->abortSearch();
413     videoAreaWidget->clear();
414     videoAreaWidget->update();
415     errorTimer->stop();
416     playlistView->selectionModel()->clearSelection();
417
418     MainWindow::instance()->getAction("refineSearch")->setChecked(false);
419     updateSubscriptionActionForVideo(nullptr, false);
420 #ifdef APP_ACTIVATION
421     demoTimer->stop();
422 #endif
423
424     for (QAction *action : qAsConst(currentVideoActions))
425         action->setEnabled(false);
426
427     QAction *a = MainWindow::instance()->getAction("download");
428     a->setEnabled(false);
429     a->setVisible(false);
430
431     media->stop();
432     media->clearQueue();
433     currentVideoId.clear();
434
435 #ifdef APP_SNAPSHOT
436     if (snapshotSettings) {
437         delete snapshotSettings;
438         snapshotSettings = nullptr;
439     }
440 #endif
441 }
442
443 const QString &MediaView::getCurrentVideoId() {
444     return currentVideoId;
445 }
446
447 void MediaView::activeVideoChanged(Video *video, Video *previousVideo) {
448     if (stopped) return;
449
450     media->stop();
451     errorTimer->stop();
452
453     if (previousVideo && previousVideo != video) {
454         if (previousVideo->isLoadingStreamUrl()) previousVideo->abortLoadStreamUrl();
455     }
456
457     // optimize window for 16:9 video
458     adjustWindowSize();
459
460     videoAreaWidget->showLoading(video);
461
462     connect(video, &Video::gotStreamUrl, this, &MediaView::gotStreamUrl, Qt::UniqueConnection);
463     connect(video, SIGNAL(errorStreamUrl(QString)), SLOT(skip()), Qt::UniqueConnection);
464     video->loadStreamUrl();
465
466     // video title in titlebar
467     MainWindow::instance()->setWindowTitle(video->getTitle() + QLatin1String(" - ") +
468                                            QLatin1String(Constants::NAME));
469
470     // ensure active item is visible
471     int row = playlistModel->rowForVideo(video);
472     if (row != -1) {
473         QModelIndex index = playlistModel->index(row, 0, QModelIndex());
474         playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
475     }
476
477     // enable/disable actions
478     MainWindow::instance()
479             ->getAction("download")
480             ->setEnabled(DownloadManager::instance()->itemForVideo(video) == nullptr);
481     MainWindow::instance()->getAction("previous")->setEnabled(row > 0);
482     MainWindow::instance()->getAction("stopafterthis")->setEnabled(true);
483     MainWindow::instance()->getAction("relatedVideos")->setEnabled(true);
484
485     bool enableDownload = video->getLicense() == Video::LicenseCC;
486 #ifdef APP_ACTIVATION
487     enableDownload = enableDownload || Activation::instance().isLegacy();
488 #endif
489 #ifdef APP_DOWNLOADS
490     enableDownload = true;
491 #endif
492     QAction *a = MainWindow::instance()->getAction("download");
493     a->setEnabled(enableDownload);
494     a->setVisible(enableDownload);
495
496     updateSubscriptionActionForVideo(video, YTChannel::isSubscribed(video->getChannelId()));
497
498     for (QAction *action : currentVideoActions)
499         action->setEnabled(true);
500
501 #ifdef APP_SNAPSHOT
502     if (snapshotSettings) {
503         delete snapshotSettings;
504         snapshotSettings = nullptr;
505         MainWindow::instance()->adjustStatusBarVisibility();
506     }
507 #endif
508
509     // see you in gotStreamUrl...
510 }
511
512 void MediaView::gotStreamUrl(const QString &streamUrl, const QString &audioUrl) {
513     if (stopped) return;
514     if (streamUrl.isEmpty()) {
515         qWarning() << "Empty stream url";
516         skip();
517         return;
518     }
519
520     Video *video = static_cast<Video *>(sender());
521     if (!video) {
522         qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
523         return;
524     }
525     video->disconnect(this);
526
527     currentVideoId = video->getId();
528
529     if (audioUrl.isEmpty()) {
530         qDebug() << "Playing" << streamUrl;
531         media->play(streamUrl);
532     } else {
533         qDebug() << "Playing" << streamUrl << audioUrl;
534         media->playSeparateAudioAndVideo(streamUrl, audioUrl);
535     }
536
537     // ensure we always have videos ahead
538     playlistModel->searchNeeded();
539
540     // ensure active item is visible
541     int row = playlistModel->activeRow();
542     if (row != -1) {
543         QModelIndex index = playlistModel->index(row, 0, QModelIndex());
544         playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
545     }
546
547 #ifdef APP_ACTIVATION
548     if (!demoTimer->isActive() && !Activation::instance().isActivated()) {
549         int ms = (60000 * 2) + (QRandomGenerator::global()->generate() % (60000 * 2));
550         demoTimer->start(ms);
551     }
552 #endif
553
554 #ifdef APP_EXTRA
555     Extra::notify(video->getTitle(), video->getChannelTitle(), video->getFormattedDuration());
556 #endif
557
558     ChannelAggregator::instance()->videoWatched(video);
559 }
560
561 void MediaView::onItemActivated(const QModelIndex &index) {
562     if (playlistModel->rowExists(index.row())) {
563         // if it's the current video, just rewind and play
564         Video *activeVideo = playlistModel->activeVideo();
565         Video *video = playlistModel->videoAt(index.row());
566         if (activeVideo && video && activeVideo == video) {
567             media->play();
568         } else
569             playlistModel->setActiveRow(index.row());
570
571         // the user doubleclicked on the "Search More" item
572     } else {
573         playlistModel->searchMore();
574         playlistView->selectionModel()->clearSelection();
575     }
576 }
577
578 void MediaView::skipVideo() {
579     // skippedVideo is useful for DELAYED skip operations
580     // in order to be sure that we're skipping the video we wanted
581     // and not another one
582     if (skippedVideo) {
583         if (playlistModel->activeVideo() != skippedVideo) {
584             qDebug() << "Skip of video canceled";
585             return;
586         }
587         int nextRow = playlistModel->rowForVideo(skippedVideo);
588         nextRow++;
589         if (nextRow == -1) return;
590         playlistModel->setActiveRow(nextRow);
591     }
592 }
593
594 void MediaView::skip() {
595     int nextRow = playlistModel->nextRow();
596     if (nextRow == -1) return;
597     playlistModel->setActiveRow(nextRow);
598 }
599
600 void MediaView::skipBackward() {
601     int prevRow = playlistModel->previousRow();
602     if (prevRow == -1) return;
603     playlistModel->setActiveRow(prevRow);
604 }
605
606 void MediaView::onAboutToFinish() {
607
608 }
609
610 void MediaView::onPlaybackFinished() {
611     if (stopped) return;
612
613     const qint64 totalTime = media->duration();
614     const qint64 currentTime = media->position();
615     // qDebug() << __PRETTY_FUNCTION__ << mediaObject->currentTime() << totalTime;
616     // add 10 secs for imprecise Phonon backends (VLC, Xine)
617     if (currentTime > 0 && currentTime + 10000 < totalTime) {
618         // mediaObject->seek(currentTime);
619         QTimer::singleShot(500, this, SLOT(resumePlayback()));
620     } else {
621         QAction *stopAfterThisAction = MainWindow::instance()->getAction("stopafterthis");
622         if (stopAfterThisAction->isChecked()) {
623             stopAfterThisAction->setChecked(false);
624         } else
625             skip();
626     }
627 }
628
629 void MediaView::resumePlayback() {
630     if (stopped) return;
631     const qint64 currentTime = media->position();
632     // qDebug() << __PRETTY_FUNCTION__ << currentTime;
633     if (currentTime > 0) media->seek(currentTime);
634     media->play();
635 }
636
637 void MediaView::openWebPage() {
638     Video *video = playlistModel->activeVideo();
639     if (!video) return;
640     media->pause();
641     QString url =
642             video->getWebpage() + QLatin1String("&t=") + QString::number(media->position() / 1000);
643     QDesktopServices::openUrl(url);
644 }
645
646 void MediaView::copyWebPage() {
647     Video *video = playlistModel->activeVideo();
648     if (!video) return;
649     QString address = video->getWebpage();
650     QApplication::clipboard()->setText(address);
651     QString message = tr("You can now paste the YouTube link into another application");
652     MainWindow::instance()->showMessage(message);
653 }
654
655 void MediaView::copyVideoLink() {
656     Video *video = playlistModel->activeVideo();
657     if (!video) return;
658     QApplication::clipboard()->setText(video->getStreamUrl());
659     QString message = tr("You can now paste the video stream URL into another application") + ". " +
660                       tr("The link will be valid only for a limited time.");
661     MainWindow::instance()->showMessage(message);
662 }
663
664 void MediaView::openInBrowser() {
665     Video *video = playlistModel->activeVideo();
666     if (!video) return;
667     media->pause();
668     QDesktopServices::openUrl(video->getStreamUrl());
669 }
670
671 void MediaView::removeSelected() {
672     if (!playlistView->selectionModel()->hasSelection()) return;
673     QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
674     playlistModel->removeIndexes(indexes);
675 }
676
677 void MediaView::selectVideos(const QVector<Video *> &videos) {
678     for (Video *video : videos) {
679         QModelIndex index = playlistModel->indexForVideo(video);
680         playlistView->selectionModel()->select(index, QItemSelectionModel::Select);
681         playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
682     }
683 }
684
685 void MediaView::selectionChanged(const QItemSelection & /*selected*/,
686                                  const QItemSelection & /*deselected*/) {
687     const bool gotSelection = playlistView->selectionModel()->hasSelection();
688     MainWindow::instance()->getAction("remove")->setEnabled(gotSelection);
689     MainWindow::instance()->getAction("moveUp")->setEnabled(gotSelection);
690     MainWindow::instance()->getAction("moveDown")->setEnabled(gotSelection);
691 }
692
693 void MediaView::moveUpSelected() {
694     if (!playlistView->selectionModel()->hasSelection()) return;
695
696     QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
697     std::stable_sort(indexes.begin(), indexes.end());
698     playlistModel->move(indexes, true);
699
700     // set current index after row moves to something more intuitive
701     int row = indexes.at(0).row();
702     playlistView->selectionModel()->setCurrentIndex(playlistModel->index(row > 1 ? row : 1),
703                                                     QItemSelectionModel::NoUpdate);
704 }
705
706 void MediaView::moveDownSelected() {
707     if (!playlistView->selectionModel()->hasSelection()) return;
708
709     QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
710     std::stable_sort(indexes.begin(), indexes.end(),
711                      [](const QModelIndex &a, const QModelIndex &b) { return b < a; });
712     playlistModel->move(indexes, false);
713
714     // set current index after row moves to something more intuitive
715     // (respect 1 static item on bottom)
716     int row = indexes.at(0).row() + 1, max = playlistModel->rowCount() - 2;
717     playlistView->selectionModel()->setCurrentIndex(playlistModel->index(row > max ? max : row),
718                                                     QItemSelectionModel::NoUpdate);
719 }
720
721 void MediaView::setSidebarVisibility(bool visible) {
722     if (sidebar->isVisible() == visible) return;
723     sidebar->setVisible(visible);
724     if (visible) {
725         sidebar->move(0, 0);
726         sidebar->resize(sidebar->width(), window()->height());
727         sidebar->raise();
728         playlistView->setFocus();
729     }
730 }
731
732 void MediaView::removeSidebar() {
733     sidebar->hide();
734     sidebar->setParent(window());
735 }
736
737 void MediaView::restoreSidebar() {
738     sidebar->show();
739     splitter->insertWidget(0, sidebar);
740 }
741
742 bool MediaView::isSidebarVisible() {
743     return sidebar->isVisible();
744 }
745
746 void MediaView::saveSplitterState() {
747     QSettings settings;
748     if (splitter) settings.setValue("splitter", splitter->saveState());
749 }
750
751 void MediaView::downloadVideo() {
752     Video *video = playlistModel->activeVideo();
753     if (!video) return;
754     DownloadManager::instance()->addItem(video);
755     MainWindow::instance()->showActionsInStatusBar({MainWindow::instance()->getAction("downloads")},
756                                                    true);
757     QString message = tr("Downloading %1").arg(video->getTitle());
758     MainWindow::instance()->showMessage(message);
759 }
760
761 #ifdef APP_SNAPSHOT
762 void MediaView::snapshot() {
763     qint64 currentTime = media->position() / 1000;
764
765     QObject *context = new QObject();
766     connect(media, &Media::snapshotReady, context,
767             [this, currentTime, context](const QImage &image) {
768                 context->deleteLater();
769
770                 if (image.isNull()) {
771                     qWarning() << "Null snapshot";
772                     return;
773                 }
774
775                 QPixmap pixmap = QPixmap::fromImage(image.scaled(
776                         videoWidget->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
777                 videoAreaWidget->showSnapshotPreview(pixmap);
778
779                 Video *video = playlistModel->activeVideo();
780                 if (!video) return;
781
782                 QString location = SnapshotSettings::getCurrentLocation();
783                 QDir dir(location);
784                 if (!dir.exists()) dir.mkpath(location);
785                 QString basename = video->getTitle();
786                 QString format = video->getDuration() > 3600 ? "h_mm_ss" : "m_ss";
787                 basename += " (" + QTime(0, 0, 0).addSecs(currentTime).toString(format) + ")";
788                 basename = DataUtils::stringToFilename(basename);
789                 QString filename = location + "/" + basename + ".png";
790                 qDebug() << filename;
791                 image.save(filename, "PNG");
792
793                 if (snapshotSettings) delete snapshotSettings;
794                 snapshotSettings = new SnapshotSettings(videoWidget);
795                 snapshotSettings->setSnapshot(pixmap, filename);
796                 QStatusBar *statusBar = MainWindow::instance()->statusBar();
797 #ifdef APP_EXTRA
798                 Extra::fadeInWidget(statusBar, statusBar);
799 #endif
800                 statusBar->insertPermanentWidget(0, snapshotSettings);
801                 snapshotSettings->show();
802                 MainWindow::instance()->setStatusBarVisibility(true);
803             }
804 #endif
805     );
806
807     media->snapshot();
808 }
809
810 void MediaView::fullscreen() {
811     videoAreaWidget->setParent(nullptr);
812     videoAreaWidget->showFullScreen();
813 }
814
815 void MediaView::resumeWithNewStreamUrl(const QString &streamUrl, const QString &audioUrl) {
816     pauseTime = media->position();
817
818     if (audioUrl.isEmpty()) {
819         qDebug() << "Playing" << streamUrl;
820         media->play(streamUrl);
821     } else {
822         qDebug() << "Playing" << streamUrl << audioUrl;
823         media->playSeparateAudioAndVideo(streamUrl, audioUrl);
824     }
825
826     Video *video = static_cast<Video *>(sender());
827     if (!video) {
828         qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
829         return;
830     }
831     video->disconnect(this);
832 }
833
834 void MediaView::findVideoParts() {
835     Video *video = playlistModel->activeVideo();
836     if (!video) return;
837
838     QString query = video->getTitle();
839
840     const QLatin1String optionalSpace("\\s*");
841     const QLatin1String staticCounterSeparators("[\\/\\-]");
842     const QString counterSeparators =
843             QLatin1String("( of | ") + tr("of", "Used in video parts, as in '2 of 3'") +
844             QLatin1String(" |") + staticCounterSeparators + QLatin1String(")");
845
846     // numbers from 1 to 15
847     const QLatin1String counterNumber("([1-9]|1[0-5])");
848
849     // query.remove(QRegExp(counterSeparators + optionalSpace + counterNumber));
850     query.remove(QRegExp(counterNumber + optionalSpace + counterSeparators + optionalSpace +
851                          counterNumber));
852     query.remove(wordRE("pr?t\\.?" + optionalSpace + counterNumber));
853     query.remove(wordRE("ep\\.?" + optionalSpace + counterNumber));
854     query.remove(wordRE("part" + optionalSpace + counterNumber));
855     query.remove(wordRE("episode" + optionalSpace + counterNumber));
856     query.remove(wordRE(tr("part", "This is for video parts, as in 'Cool video - part 1'") +
857                         optionalSpace + counterNumber));
858     query.remove(wordRE(tr("episode", "This is for video parts, as in 'Cool series - episode 1'") +
859                         optionalSpace + counterNumber));
860     query.remove(QRegExp("[\\(\\)\\[\\]]"));
861
862 #define NUMBERS "one|two|three|four|five|six|seven|eight|nine|ten"
863
864     QRegExp englishNumberRE = QRegExp(QLatin1String(".*(") + NUMBERS + ").*", Qt::CaseInsensitive);
865     // bool numberAsWords = englishNumberRE.exactMatch(query);
866     query.remove(englishNumberRE);
867
868     QRegExp localizedNumberRE =
869             QRegExp(QLatin1String(".*(") + tr(NUMBERS) + ").*", Qt::CaseInsensitive);
870     // if (!numberAsWords) numberAsWords = localizedNumberRE.exactMatch(query);
871     query.remove(localizedNumberRE);
872
873     SearchParams *searchParams = new SearchParams();
874     searchParams->setTransient(true);
875     searchParams->setKeywords(query);
876     searchParams->setChannelId(video->getChannelId());
877
878     /*
879     if (!numberAsWords) {
880         qDebug() << "We don't have number as words";
881         // searchParams->setSortBy(SearchParams::SortByNewest);
882         // TODO searchParams->setReverseOrder(true);
883         // TODO searchParams->setMax(50);
884     }
885     */
886
887     search(searchParams);
888 }
889
890 void MediaView::relatedVideos() {
891     Video *video = playlistModel->activeVideo();
892     if (!video) return;
893
894     auto source = new SingleVideoSource(this);
895     source->setVideo(video->clone());
896     setVideoSource(source);
897
898     MainWindow::instance()->getAction("relatedVideos")->setEnabled(false);
899 }
900
901 void MediaView::shareViaTwitter() {
902     Video *video = playlistModel->activeVideo();
903     if (!video) return;
904     QUrl url("https://twitter.com/intent/tweet");
905     QUrlQuery q;
906     q.addQueryItem("via", "minitubeapp");
907     q.addQueryItem("text", video->getTitle());
908     q.addQueryItem("url", video->getWebpage());
909     url.setQuery(q);
910     QDesktopServices::openUrl(url);
911 }
912
913 void MediaView::shareViaFacebook() {
914     Video *video = playlistModel->activeVideo();
915     if (!video) return;
916     QUrl url("https://www.facebook.com/sharer.php");
917     QUrlQuery q;
918     q.addQueryItem("t", video->getTitle());
919     q.addQueryItem("u", video->getWebpage());
920     url.setQuery(q);
921     QDesktopServices::openUrl(url);
922 }
923
924 void MediaView::shareViaEmail() {
925     Video *video = playlistModel->activeVideo();
926     if (!video) return;
927     QUrl url("mailto:");
928     QUrlQuery q;
929     q.addQueryItem("subject", video->getTitle());
930     const QString body = video->getTitle() + "\n" + video->getWebpage() + "\n\n" +
931                          tr("Sent from %1").arg(Constants::NAME) + "\n" + Constants::WEBSITE;
932     q.addQueryItem("body", body);
933     url.setQuery(q);
934     QDesktopServices::openUrl(url);
935 }
936
937 void MediaView::onAuthorPushed(QModelIndex index) {
938     Video *video = playlistModel->videoAt(index.row());
939     if (!video) return;
940
941     QString channelId = video->getChannelId();
942     // if (channelId.isEmpty()) channelId = video->channelTitle();
943     if (channelId.isEmpty()) return;
944
945     SearchParams *searchParams = new SearchParams();
946     searchParams->setChannelId(channelId);
947     searchParams->setSortBy(SearchParams::SortByNewest);
948
949     // go!
950     search(searchParams);
951 }
952
953
954 void MediaView::updateSubscriptionAction(bool subscribed) {
955     QAction *subscribeAction = MainWindow::instance()->getAction("subscribeChannel");
956
957     QString subscribeTip;
958     QString subscribeText;
959
960     if (currentSubscriptionChannelId.isEmpty()) {
961         subscribeText = subscribeAction->property("originalText").toString();
962         subscribeAction->setEnabled(false);
963     } else if (subscribed) {
964         subscribeText = tr("Unsubscribe from %1").arg(currentSubscriptionChannelTitle);
965         subscribeTip = subscribeText;
966         subscribeAction->setEnabled(true);
967     } else {
968         subscribeText = tr("Subscribe to %1").arg(currentSubscriptionChannelTitle);
969         subscribeTip = subscribeText;
970         subscribeAction->setEnabled(true);
971     }
972     subscribeAction->setText(subscribeText);
973     subscribeAction->setStatusTip(subscribeTip);
974
975     if (subscribed) {
976         subscribeAction->setIcon(IconUtils::icon("bookmark-remove"));
977     } else {
978         subscribeAction->setIcon(IconUtils::icon("bookmark-new"));
979     }
980
981     MainWindow::instance()->setupAction(subscribeAction);
982 }
983
984 void MediaView::updateSubscriptionActionForChannel(const QString & channelId) {
985     QString channelTitle = tr("channel");
986     YTChannel *channel = YTChannel::forId(channelId);
987     if (nullptr != channel && !channel->getDisplayName().isEmpty()) {
988         channelTitle = channel->getDisplayName();
989     }
990
991     bool subscribed = YTChannel::isSubscribed(channelId);
992
993     currentSubscriptionChannelId = channelId;
994     currentSubscriptionChannelTitle = channelTitle;
995     updateSubscriptionAction(subscribed);
996 }
997
998 void MediaView::updateSubscriptionActionForVideo(Video *video, bool subscribed) {
999     if (!video) {
1000         currentSubscriptionChannelId = "";
1001         currentSubscriptionChannelTitle = "";
1002         updateSubscriptionAction(false);
1003     } else {
1004         currentSubscriptionChannelId = video->getChannelId();
1005         currentSubscriptionChannelTitle = video->getChannelTitle();
1006         updateSubscriptionAction(subscribed);
1007     }
1008 }
1009
1010 void MediaView::reloadCurrentVideo() {
1011     Video *video = playlistModel->activeVideo();
1012     if (!video) return;
1013
1014     int oldFormat = video->getDefinitionCode();
1015
1016     QObject *context = new QObject();
1017     connect(video, &Video::gotStreamUrl, context,
1018             [this, oldFormat, video, context](const QString &videoUrl, const QString &audioUrl) {
1019                 context->deleteLater();
1020                 if (oldFormat == video->getDefinitionCode()) return;
1021                 QObject *context2 = new QObject();
1022                 const qint64 position = media->position();
1023                 connect(media, &Media::stateChanged, context2,
1024                         [position, this, context2](Media::State state) {
1025                             if (state == Media::PlayingState) {
1026                                 media->seek(position);
1027                                 context2->deleteLater();
1028                                 Video *video = playlistModel->activeVideo();
1029                                 QString msg = tr("Switched to %1")
1030                                                       .arg(VideoDefinition::forCode(
1031                                                                    video->getDefinitionCode())
1032                                                                    .getName());
1033                                 MainWindow::instance()->showMessage(msg);
1034                             }
1035                         });
1036
1037                 if (audioUrl.isEmpty()) {
1038                     media->play(videoUrl);
1039                 } else {
1040                     media->playSeparateAudioAndVideo(videoUrl, audioUrl);
1041                 }
1042             });
1043     video->loadStreamUrl();
1044 }
1045
1046 void MediaView::toggleSubscription() {
1047     //Video *video = playlistModel->activeVideo();
1048     if (currentSubscriptionChannelId.isEmpty()) {
1049         return;
1050     }
1051
1052     bool subscribed = YTChannel::isSubscribed(currentSubscriptionChannelId);
1053     if (subscribed) {
1054         YTChannel::unsubscribe(currentSubscriptionChannelId);
1055         MainWindow::instance()->showMessage(
1056                 tr("Unsubscribed from %1").arg(currentSubscriptionChannelTitle));
1057     } else {
1058         YTChannel::subscribe(currentSubscriptionChannelId);
1059         MainWindow::instance()->showMessage(tr("Subscribed to %1").arg(currentSubscriptionChannelTitle));
1060     }
1061
1062     updateSubscriptionAction(!subscribed);
1063 }
1064
1065 void MediaView::adjustWindowSize() {
1066     qDebug() << "Adjusting window size";
1067     Video *video = playlistModel->activeVideo();
1068     if (!video) return;
1069     QWidget *window = this->window();
1070     if (!window->isMaximized() && !window->isFullScreen()) {
1071         const double ratio = 16. / 9.;
1072         const double w = (double)videoAreaWidget->width();
1073         const double h = (double)videoAreaWidget->height();
1074         const double currentVideoRatio = w / h;
1075         if (currentVideoRatio != ratio) {
1076             qDebug() << "Adjust size";
1077             int newHeight = std::round((window->height() - h) + (w / ratio));
1078             window->resize(window->width(), newHeight);
1079         }
1080     }
1081 }