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