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