3 This file is part of Minitube.
4 Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
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.
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.
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/>.
21 #include "mediaview.h"
22 #include "constants.h"
23 #include "downloadmanager.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"
36 #include "activation.h"
37 #include "activationview.h"
42 #include "channelaggregator.h"
43 #include "iconutils.h"
44 #include "searchparams.h"
45 #include "videosource.h"
46 #include "ytchannel.h"
48 #include "ytsinglevideosource.h"
50 #include "snapshotsettings.h"
52 #include "datautils.h"
54 #include "videodefinition.h"
56 MediaView *MediaView::instance() {
57 static MediaView *i = new MediaView();
61 MediaView::MediaView(QWidget *parent)
62 : View(parent), splitter(nullptr), stopped(false)
65 snapshotSettings(nullptr)
71 void MediaView::initialize() {
72 MainWindow *mainWindow = MainWindow::instance();
74 QBoxLayout *layout = new QVBoxLayout(this);
77 splitter = new MiniSplitter();
78 layout->addWidget(splitter);
80 playlistView = new PlaylistView();
81 playlistView->setParent(this);
82 connect(playlistView, SIGNAL(activated(const QModelIndex &)),
83 SLOT(onItemActivated(const QModelIndex &)));
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);
93 connect(playlistView->selectionModel(),
94 SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
95 SLOT(selectionChanged(const QItemSelection &, const QItemSelection &)));
97 connect(playlistView, SIGNAL(authorPushed(QModelIndex)), SLOT(onAuthorPushed(QModelIndex)));
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);
108 videoAreaWidget = new VideoArea(this);
109 videoAreaWidget->setListModel(playlistModel);
111 loadingWidget = new LoadingWidget(this);
112 videoAreaWidget->setLoadingWidget(loadingWidget);
114 splitter->addWidget(videoAreaWidget);
116 // restore splitter state
118 if (settings.contains("splitter"))
119 splitter->restoreState(settings.value("splitter").toByteArray());
121 int sidebarDefaultWidth = 180;
122 splitter->setSizes(QList<int>() << sidebarDefaultWidth
123 << splitter->size().width() - sidebarDefaultWidth);
125 splitter->setChildrenCollapsible(false);
126 connect(splitter, SIGNAL(splitterMoved(int, int)), SLOT(adjustWindowSize()));
128 errorTimer = new QTimer(this);
129 errorTimer->setSingleShot(true);
130 errorTimer->setInterval(3000);
131 connect(errorTimer, SIGNAL(timeout()), SLOT(skipVideo()));
133 #ifdef APP_ACTIVATION
134 demoTimer = new QTimer(this);
135 demoTimer->setSingleShot(true);
137 demoTimer, &QTimer::timeout, this,
139 if (media->state() != Media::PlayingState) return;
142 ActivationView::instance(), &ActivationView::done, media,
143 [this] { media->play(); }, Qt::UniqueConnection);
144 MainWindow::instance()->showActivationView();
146 Qt::QueuedConnection);
149 connect(videoAreaWidget, SIGNAL(doubleClicked()), mainWindow->getAction("fullscreen"),
152 QAction *refineSearchAction = mainWindow->getAction("refineSearch");
153 connect(refineSearchAction, SIGNAL(toggled(bool)), sidebar, SLOT(toggleRefineSearch(bool)));
155 const QVector<const char *> videoActionNames = {
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));
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);
178 playingVideoActions << action;
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();
187 if (position < 0) position = 0;
188 media->seek(position);
190 addAction(leftAction);
191 playingVideoActions << leftAction;
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();
199 qint64 duration = media->duration();
200 if (position > duration) position = duration;
201 media->seek(position);
203 addAction(rightAction);
204 playingVideoActions << rightAction;
207 void MediaView::setMedia(Media *media) {
210 videoWidget = media->videoWidget();
211 videoAreaWidget->setVideoWidget(videoWidget);
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);
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();
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);
243 YTSearch *ytSearch = new YTSearch(searchParams);
244 ytSearch->setAsyncDetails(true);
245 connect(ytSearch, SIGNAL(gotDetails()), playlistModel, SLOT(emitDataChanged()));
246 setVideoSource(ytSearch);
249 void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool back) {
254 // qDebug() << "Adding VideoSource" << videoSource->getName() << videoSource;
256 YTSearch * ytSearch = qobject_cast<YTSearch *>(videoSource);
257 if (nullptr != ytSearch) {
258 if (!ytSearch->getSearchParams()->channelId().isEmpty()) {
259 updateSubscriptionActionForChannel(ytSearch->getSearchParams()->channelId());
264 int currentIndex = getHistoryIndex();
265 if (currentIndex >= 0 && currentIndex < history.size() - 1) {
266 while (history.size() > currentIndex + 1) {
267 VideoSource *vs = history.takeLast();
269 qDebug() << "Deleting VideoSource" << vs->getName() << vs;
274 history.append(videoSource);
278 if (history.size() > 1)
279 Extra::slideTransition(playlistView->viewport(), playlistView->viewport(), back);
282 playlistModel->setVideoSource(videoSource);
284 if (media->state() == Media::StoppedState) {
286 if (settings.value("manualplay", false).toBool()) {
287 videoAreaWidget->showPickMessage();
291 sidebar->showPlaylist();
292 sidebar->getRefineSearchWidget()->setSearchParams(getSearchParams());
293 sidebar->hideSuggestions();
294 sidebar->getHeader()->updateInfo();
296 SearchParams *searchParams = getSearchParams();
297 bool isChannel = searchParams && !searchParams->channelId().isEmpty();
298 playlistView->setClickableAuthors(!isChannel);
301 void MediaView::searchAgain() {
302 VideoSource *currentVideoSource = playlistModel->getVideoSource();
303 setVideoSource(currentVideoSource, false);
306 bool MediaView::canGoBack() {
307 return getHistoryIndex() > 0;
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);
320 bool MediaView::canGoForward() {
321 int currentIndex = getHistoryIndex();
322 return currentIndex >= 0 && currentIndex < history.size() - 1;
325 void MediaView::goForward() {
326 if (canGoForward()) {
327 int currentIndex = getHistoryIndex();
328 VideoSource *nextVideoSource = history.at(currentIndex + 1);
329 setVideoSource(nextVideoSource, false);
333 int MediaView::getHistoryIndex() {
334 return history.lastIndexOf(playlistModel->getVideoSource());
337 void MediaView::appear() {
338 MainWindow::instance()->showToolbar();
340 Video *currentVideo = playlistModel->activeVideo();
342 MainWindow::instance()->setWindowTitle(currentVideo->getTitle() + " - " + Constants::NAME);
345 playlistView->setFocus();
348 void MediaView::disappear() {
349 MainWindow::instance()->hideToolbar();
352 void MediaView::handleError(const QString &message) {
353 qWarning() << __PRETTY_FUNCTION__ << message;
354 #ifndef QT_NO_DEBUG_OUTPUT
355 MainWindow::instance()->showMessage(message);
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);
365 if (state == Media::PlayingState) {
366 videoAreaWidget->showVideo();
367 } else if (state == Media::ErrorState) {
368 handleError(media->errorString());
371 bool enablePlayingVideoActions = state == Media::PlayingState || state == Media::PausedState;
372 for (QAction *action : qAsConst(playingVideoActions))
373 action->setEnabled(enablePlayingVideoActions);
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();
384 void MediaView::pause() {
385 switch (media->state()) {
386 case Media::PlayingState:
391 if (pauseTimer.hasExpired(60000)) {
392 pauseTimer.invalidate();
393 connect(playlistModel->activeVideo(), &Video::gotStreamUrl, this,
394 &MediaView::resumeWithNewStreamUrl);
395 playlistModel->activeVideo()->loadStreamUrl();
402 QRegExp MediaView::wordRE(const QString &s) {
403 return QRegExp("\\W" + s + "\\W?", Qt::CaseInsensitive);
406 void MediaView::stop() {
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();
417 playlistModel->abortSearch();
418 videoAreaWidget->clear();
419 videoAreaWidget->update();
421 playlistView->selectionModel()->clearSelection();
423 MainWindow::instance()->getAction("refineSearch")->setChecked(false);
424 updateSubscriptionActionForVideo(nullptr, false);
425 #ifdef APP_ACTIVATION
429 for (QAction *action : currentVideoActions)
430 action->setEnabled(false);
432 QAction *a = MainWindow::instance()->getAction("download");
433 a->setEnabled(false);
434 a->setVisible(false);
438 currentVideoId.clear();
441 if (snapshotSettings) {
442 delete snapshotSettings;
443 snapshotSettings = nullptr;
448 const QString &MediaView::getCurrentVideoId() {
449 return currentVideoId;
452 void MediaView::activeVideoChanged(Video *video, Video *previousVideo) {
458 if (previousVideo && previousVideo != video) {
459 if (previousVideo->isLoadingStreamUrl()) previousVideo->abortLoadStreamUrl();
462 // optimize window for 16:9 video
465 videoAreaWidget->showLoading(video);
467 connect(video, &Video::gotStreamUrl, this, &MediaView::gotStreamUrl, Qt::UniqueConnection);
468 connect(video, SIGNAL(errorStreamUrl(QString)), SLOT(skip()), Qt::UniqueConnection);
469 video->loadStreamUrl();
471 // video title in titlebar
472 MainWindow::instance()->setWindowTitle(video->getTitle() + QLatin1String(" - ") +
473 QLatin1String(Constants::NAME));
475 // ensure active item is visible
476 int row = playlistModel->rowForVideo(video);
478 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
479 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
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);
490 bool enableDownload = video->getLicense() == Video::LicenseCC;
491 #ifdef APP_ACTIVATION
492 enableDownload = enableDownload || Activation::instance().isLegacy();
495 enableDownload = true;
497 QAction *a = MainWindow::instance()->getAction("download");
498 a->setEnabled(enableDownload);
499 a->setVisible(enableDownload);
501 updateSubscriptionActionForVideo(video, YTChannel::isSubscribed(video->getChannelId()));
503 for (QAction *action : currentVideoActions)
504 action->setEnabled(true);
507 if (snapshotSettings) {
508 delete snapshotSettings;
509 snapshotSettings = nullptr;
510 MainWindow::instance()->adjustStatusBarVisibility();
514 // see you in gotStreamUrl...
517 void MediaView::gotStreamUrl(const QString &streamUrl, const QString &audioUrl) {
519 if (streamUrl.isEmpty()) {
520 qWarning() << "Empty stream url";
525 Video *video = static_cast<Video *>(sender());
527 qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
530 video->disconnect(this);
532 currentVideoId = video->getId();
534 if (audioUrl.isEmpty()) {
535 qDebug() << "Playing" << streamUrl;
536 media->play(streamUrl);
538 qDebug() << "Playing" << streamUrl << audioUrl;
539 media->playSeparateAudioAndVideo(streamUrl, audioUrl);
542 // ensure we always have videos ahead
543 playlistModel->searchNeeded();
545 // ensure active item is visible
546 int row = playlistModel->activeRow();
548 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
549 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
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);
560 Extra::notify(video->getTitle(), video->getChannelTitle(), video->getFormattedDuration());
563 ChannelAggregator::instance()->videoWatched(video);
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) {
574 playlistModel->setActiveRow(index.row());
576 // the user doubleclicked on the "Search More" item
578 playlistModel->searchMore();
579 playlistView->selectionModel()->clearSelection();
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
588 if (playlistModel->activeVideo() != skippedVideo) {
589 qDebug() << "Skip of video canceled";
592 int nextRow = playlistModel->rowForVideo(skippedVideo);
594 if (nextRow == -1) return;
595 playlistModel->setActiveRow(nextRow);
599 void MediaView::skip() {
600 int nextRow = playlistModel->nextRow();
601 if (nextRow == -1) return;
602 playlistModel->setActiveRow(nextRow);
605 void MediaView::skipBackward() {
606 int prevRow = playlistModel->previousRow();
607 if (prevRow == -1) return;
608 playlistModel->setActiveRow(prevRow);
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);
622 void MediaView::onPlaybackFinished() {
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()));
633 QAction *stopAfterThisAction = MainWindow::instance()->getAction("stopafterthis");
634 if (stopAfterThisAction->isChecked()) {
635 stopAfterThisAction->setChecked(false);
641 void MediaView::resumePlayback() {
643 const qint64 currentTime = media->position();
644 // qDebug() << __PRETTY_FUNCTION__ << currentTime;
645 if (currentTime > 0) media->seek(currentTime);
649 void MediaView::openWebPage() {
650 Video *video = playlistModel->activeVideo();
654 video->getWebpage() + QLatin1String("&t=") + QString::number(media->position() / 1000);
655 QDesktopServices::openUrl(url);
658 void MediaView::copyWebPage() {
659 Video *video = playlistModel->activeVideo();
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);
667 void MediaView::copyVideoLink() {
668 Video *video = playlistModel->activeVideo();
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);
676 void MediaView::openInBrowser() {
677 Video *video = playlistModel->activeVideo();
680 QDesktopServices::openUrl(video->getStreamUrl());
683 void MediaView::removeSelected() {
684 if (!playlistView->selectionModel()->hasSelection()) return;
685 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
686 playlistModel->removeIndexes(indexes);
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);
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);
705 void MediaView::moveUpSelected() {
706 if (!playlistView->selectionModel()->hasSelection()) return;
708 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
709 qStableSort(indexes.begin(), indexes.end());
710 playlistModel->move(indexes, true);
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);
718 void MediaView::moveDownSelected() {
719 if (!playlistView->selectionModel()->hasSelection()) return;
721 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
722 qStableSort(indexes.begin(), indexes.end(), qGreater<QModelIndex>());
723 playlistModel->move(indexes, false);
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);
732 void MediaView::setSidebarVisibility(bool visible) {
733 if (sidebar->isVisible() == visible) return;
734 sidebar->setVisible(visible);
737 sidebar->resize(sidebar->width(), window()->height());
739 playlistView->setFocus();
743 void MediaView::removeSidebar() {
745 sidebar->setParent(window());
748 void MediaView::restoreSidebar() {
750 splitter->insertWidget(0, sidebar);
753 bool MediaView::isSidebarVisible() {
754 return sidebar->isVisible();
757 void MediaView::saveSplitterState() {
759 if (splitter) settings.setValue("splitter", splitter->saveState());
762 void MediaView::downloadVideo() {
763 Video *video = playlistModel->activeVideo();
765 DownloadManager::instance()->addItem(video);
766 MainWindow::instance()->showActionsInStatusBar({MainWindow::instance()->getAction("downloads")},
768 QString message = tr("Downloading %1").arg(video->getTitle());
769 MainWindow::instance()->showMessage(message);
773 void MediaView::snapshot() {
774 qint64 currentTime = media->position() / 1000;
776 QObject *context = new QObject();
777 connect(media, &Media::snapshotReady, context,
778 [this, currentTime, context](const QImage &image) {
779 context->deleteLater();
781 if (image.isNull()) {
782 qWarning() << "Null snapshot";
786 QPixmap pixmap = QPixmap::fromImage(image.scaled(
787 videoWidget->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
788 videoAreaWidget->showSnapshotPreview(pixmap);
790 Video *video = playlistModel->activeVideo();
793 QString location = SnapshotSettings::getCurrentLocation();
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");
804 if (snapshotSettings) delete snapshotSettings;
805 snapshotSettings = new SnapshotSettings(videoWidget);
806 snapshotSettings->setSnapshot(pixmap, filename);
807 QStatusBar *statusBar = MainWindow::instance()->statusBar();
809 Extra::fadeInWidget(statusBar, statusBar);
811 statusBar->insertPermanentWidget(0, snapshotSettings);
812 snapshotSettings->show();
813 MainWindow::instance()->setStatusBarVisibility(true);
821 void MediaView::fullscreen() {
822 videoAreaWidget->setParent(nullptr);
823 videoAreaWidget->showFullScreen();
826 void MediaView::resumeWithNewStreamUrl(const QString &streamUrl, const QString &audioUrl) {
827 pauseTime = media->position();
829 if (audioUrl.isEmpty()) {
830 qDebug() << "Playing" << streamUrl;
831 media->play(streamUrl);
833 qDebug() << "Playing" << streamUrl << audioUrl;
834 media->playSeparateAudioAndVideo(streamUrl, audioUrl);
837 Video *video = static_cast<Video *>(sender());
839 qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
842 video->disconnect(this);
845 void MediaView::findVideoParts() {
846 Video *video = playlistModel->activeVideo();
849 QString query = video->getTitle();
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(")");
857 // numbers from 1 to 15
858 const QLatin1String counterNumber("([1-9]|1[0-5])");
860 // query.remove(QRegExp(counterSeparators + optionalSpace + counterNumber));
861 query.remove(QRegExp(counterNumber + optionalSpace + counterSeparators + optionalSpace +
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("[\\(\\)\\[\\]]"));
873 #define NUMBERS "one|two|three|four|five|six|seven|eight|nine|ten"
875 QRegExp englishNumberRE = QRegExp(QLatin1String(".*(") + NUMBERS + ").*", Qt::CaseInsensitive);
876 // bool numberAsWords = englishNumberRE.exactMatch(query);
877 query.remove(englishNumberRE);
879 QRegExp localizedNumberRE =
880 QRegExp(QLatin1String(".*(") + tr(NUMBERS) + ").*", Qt::CaseInsensitive);
881 // if (!numberAsWords) numberAsWords = localizedNumberRE.exactMatch(query);
882 query.remove(localizedNumberRE);
884 SearchParams *searchParams = new SearchParams();
885 searchParams->setTransient(true);
886 searchParams->setKeywords(query);
887 searchParams->setChannelId(video->getChannelId());
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);
898 search(searchParams);
901 void MediaView::relatedVideos() {
902 Video *video = playlistModel->activeVideo();
904 YTSingleVideoSource *singleVideoSource = new YTSingleVideoSource();
905 singleVideoSource->setVideo(video->clone());
906 singleVideoSource->setAsyncDetails(true);
907 setVideoSource(singleVideoSource);
908 MainWindow::instance()->getAction("relatedVideos")->setEnabled(false);
911 void MediaView::shareViaTwitter() {
912 Video *video = playlistModel->activeVideo();
914 QUrl url("https://twitter.com/intent/tweet");
916 q.addQueryItem("via", "minitubeapp");
917 q.addQueryItem("text", video->getTitle());
918 q.addQueryItem("url", video->getWebpage());
920 QDesktopServices::openUrl(url);
923 void MediaView::shareViaFacebook() {
924 Video *video = playlistModel->activeVideo();
926 QUrl url("https://www.facebook.com/sharer.php");
928 q.addQueryItem("t", video->getTitle());
929 q.addQueryItem("u", video->getWebpage());
931 QDesktopServices::openUrl(url);
934 void MediaView::shareViaEmail() {
935 Video *video = playlistModel->activeVideo();
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);
944 QDesktopServices::openUrl(url);
947 void MediaView::onAuthorPushed(QModelIndex index) {
948 Video *video = playlistModel->videoAt(index.row());
951 QString channelId = video->getChannelId();
952 // if (channelId.isEmpty()) channelId = video->channelTitle();
953 if (channelId.isEmpty()) return;
955 SearchParams *searchParams = new SearchParams();
956 searchParams->setChannelId(channelId);
957 searchParams->setSortBy(SearchParams::SortByNewest);
960 search(searchParams);
964 void MediaView::updateSubscriptionAction(bool subscribed) {
965 QAction *subscribeAction = MainWindow::instance()->getAction("subscribeChannel");
967 QString subscribeTip;
968 QString subscribeText;
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);
978 subscribeText = tr("Subscribe to %1").arg(currentSubscriptionChannelTitle);
979 subscribeTip = subscribeText;
980 subscribeAction->setEnabled(true);
982 subscribeAction->setText(subscribeText);
983 subscribeAction->setStatusTip(subscribeTip);
986 subscribeAction->setIcon(IconUtils::icon("bookmark-remove"));
988 subscribeAction->setIcon(IconUtils::icon("bookmark-new"));
991 MainWindow::instance()->setupAction(subscribeAction);
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();
1001 bool subscribed = YTChannel::isSubscribed(channelId);
1003 currentSubscriptionChannelId = channelId;
1004 currentSubscriptionChannelTitle = channelTitle;
1005 updateSubscriptionAction(subscribed);
1008 void MediaView::updateSubscriptionActionForVideo(Video *video, bool subscribed) {
1010 currentSubscriptionChannelId = "";
1011 currentSubscriptionChannelTitle = "";
1012 updateSubscriptionAction(false);
1014 currentSubscriptionChannelId = video->getChannelId();
1015 currentSubscriptionChannelTitle = video->getChannelTitle();
1016 updateSubscriptionAction(subscribed);
1020 void MediaView::reloadCurrentVideo() {
1021 Video *video = playlistModel->activeVideo();
1024 int oldFormat = video->getDefinitionCode();
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())
1043 MainWindow::instance()->showMessage(msg);
1047 if (audioUrl.isEmpty()) {
1048 media->play(videoUrl);
1050 media->playSeparateAudioAndVideo(videoUrl, audioUrl);
1053 video->loadStreamUrl();
1056 void MediaView::toggleSubscription() {
1057 //Video *video = playlistModel->activeVideo();
1058 if (currentSubscriptionChannelId.isEmpty()) {
1062 bool subscribed = YTChannel::isSubscribed(currentSubscriptionChannelId);
1064 YTChannel::unsubscribe(currentSubscriptionChannelId);
1065 MainWindow::instance()->showMessage(
1066 tr("Unsubscribed from %1").arg(currentSubscriptionChannelTitle));
1068 YTChannel::subscribe(currentSubscriptionChannelId);
1069 MainWindow::instance()->showMessage(tr("Subscribed to %1").arg(currentSubscriptionChannelTitle));
1072 updateSubscriptionAction(!subscribed);
1075 void MediaView::adjustWindowSize() {
1076 qDebug() << "Adjusting window size";
1077 Video *video = playlistModel->activeVideo();
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);