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