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