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