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"
49 #include "snapshotsettings.h"
51 #include "datautils.h"
53 #include "videodefinition.h"
55 #include "searchvideosource.h"
56 #include "singlevideosource.h"
59 MediaView *MediaView::instance() {
60 static MediaView *i = new MediaView();
64 MediaView::MediaView(QWidget *parent)
65 : View(parent), splitter(nullptr), stopped(false)
68 snapshotSettings(nullptr)
74 void MediaView::initialize() {
75 MainWindow *mainWindow = MainWindow::instance();
77 QBoxLayout *layout = new QVBoxLayout(this);
80 splitter = new MiniSplitter();
81 layout->addWidget(splitter);
83 playlistView = new PlaylistView();
84 playlistView->setParent(this);
85 connect(playlistView, SIGNAL(activated(const QModelIndex &)),
86 SLOT(onItemActivated(const QModelIndex &)));
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);
96 connect(playlistView->selectionModel(),
97 SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
98 SLOT(selectionChanged(const QItemSelection &, const QItemSelection &)));
100 connect(playlistView, SIGNAL(authorPushed(QModelIndex)), SLOT(onAuthorPushed(QModelIndex)));
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);
111 videoAreaWidget = new VideoArea(this);
112 videoAreaWidget->setListModel(playlistModel);
114 loadingWidget = new LoadingWidget(this);
115 videoAreaWidget->setLoadingWidget(loadingWidget);
117 splitter->addWidget(videoAreaWidget);
119 // restore splitter state
121 if (settings.contains("splitter"))
122 splitter->restoreState(settings.value("splitter").toByteArray());
124 int sidebarDefaultWidth = 180;
125 splitter->setSizes(QList<int>() << sidebarDefaultWidth
126 << splitter->size().width() - sidebarDefaultWidth);
128 splitter->setChildrenCollapsible(false);
129 connect(splitter, SIGNAL(splitterMoved(int, int)), SLOT(adjustWindowSize()));
131 errorTimer = new QTimer(this);
132 errorTimer->setSingleShot(true);
133 errorTimer->setInterval(3000);
134 connect(errorTimer, SIGNAL(timeout()), SLOT(skipVideo()));
136 #ifdef APP_ACTIVATION
137 demoTimer = new QTimer(this);
138 demoTimer->setSingleShot(true);
140 demoTimer, &QTimer::timeout, this,
142 if (media->state() != Media::PlayingState) return;
145 ActivationView::instance(), &ActivationView::done, media,
146 [this] { media->play(); }, Qt::UniqueConnection);
147 MainWindow::instance()->showActivationView();
149 Qt::QueuedConnection);
152 connect(videoAreaWidget, SIGNAL(doubleClicked()), mainWindow->getAction("fullscreen"),
155 QAction *refineSearchAction = mainWindow->getAction("refineSearch");
156 connect(refineSearchAction, SIGNAL(toggled(bool)), sidebar, SLOT(toggleRefineSearch(bool)));
158 const QVector<const char *> videoActionNames = {
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));
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);
181 playingVideoActions << action;
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;
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;
199 void MediaView::setMedia(Media *media) {
202 videoWidget = media->videoWidget();
203 videoAreaWidget->setVideoWidget(videoWidget);
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);
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();
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);
232 QTime tstamp = YTSearch::videoTimestampFromUrl(searchParams->keywords());
233 pauseTime = QTime(0, 0).msecsTo(tstamp);
239 VideoSource *search = new SearchVideoSource(searchParams);
240 setVideoSource(search);
243 void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool back) {
248 // qDebug() << "Adding VideoSource" << videoSource->getName() << videoSource;
251 int currentIndex = getHistoryIndex();
252 if (currentIndex >= 0 && currentIndex < history.size() - 1) {
253 while (history.size() > currentIndex + 1) {
254 VideoSource *vs = history.takeLast();
256 qDebug() << "Deleting VideoSource" << vs->getName() << vs;
261 history.append(videoSource);
265 if (history.size() > 1)
266 Extra::slideTransition(playlistView->viewport(), playlistView->viewport(), back);
269 playlistModel->setVideoSource(videoSource);
271 if (media->state() == Media::StoppedState) {
273 if (settings.value("manualplay", false).toBool()) {
274 videoAreaWidget->showPickMessage();
278 SearchParams *searchParams = getSearchParams();
280 sidebar->showPlaylist();
281 sidebar->getRefineSearchWidget()->setSearchParams(searchParams);
282 sidebar->hideSuggestions();
283 sidebar->getHeader()->updateInfo();
285 bool isChannel = searchParams && !searchParams->channelId().isEmpty();
287 updateSubscriptionActionForChannel(searchParams->channelId());
289 playlistView->setClickableAuthors(!isChannel);
292 void MediaView::searchAgain() {
293 VideoSource *currentVideoSource = playlistModel->getVideoSource();
294 setVideoSource(currentVideoSource, false);
297 bool MediaView::canGoBack() {
298 return getHistoryIndex() > 0;
301 void MediaView::goBack() {
302 if (history.size() > 1) {
303 int currentIndex = getHistoryIndex();
304 if (currentIndex > 0) {
305 VideoSource *previousVideoSource = history.at(currentIndex - 1);
306 setVideoSource(previousVideoSource, false, true);
311 bool MediaView::canGoForward() {
312 int currentIndex = getHistoryIndex();
313 return currentIndex >= 0 && currentIndex < history.size() - 1;
316 void MediaView::goForward() {
317 if (canGoForward()) {
318 int currentIndex = getHistoryIndex();
319 VideoSource *nextVideoSource = history.at(currentIndex + 1);
320 setVideoSource(nextVideoSource, false);
324 int MediaView::getHistoryIndex() {
325 return history.lastIndexOf(playlistModel->getVideoSource());
328 void MediaView::appear() {
329 MainWindow::instance()->showToolbar();
331 Video *currentVideo = playlistModel->activeVideo();
333 MainWindow::instance()->setWindowTitle(currentVideo->getTitle() + " - " + Constants::NAME);
336 playlistView->setFocus();
339 void MediaView::disappear() {
340 MainWindow::instance()->hideToolbar();
343 void MediaView::handleError(const QString &message) {
344 qWarning() << __PRETTY_FUNCTION__ << message;
345 #ifndef QT_NO_DEBUG_OUTPUT
346 MainWindow::instance()->showMessage(message);
350 void MediaView::mediaStateChanged(Media::State state) {
351 if (pauseTime > 0 && (state == Media::PlayingState || state == Media::BufferingState)) {
352 qDebug() << "Seeking to" << pauseTime;
353 media->seek(pauseTime);
357 if (state == Media::PlayingState) {
358 videoAreaWidget->showVideo();
359 } else if (state == Media::ErrorState) {
360 handleError(media->errorString());
363 bool enablePlayingVideoActions = state != Media::StoppedState;
364 for (QAction *action : qAsConst(playingVideoActions))
365 action->setEnabled(enablePlayingVideoActions);
367 if (state == Media::PlayingState) {
368 bool res = Idle::preventDisplaySleep(QString("%1 is playing").arg(Constants::NAME));
369 if (!res) qWarning() << "Error disabling idle display sleep" << Idle::displayErrorMessage();
370 } else if (state == Media::PausedState || state == Media::StoppedState) {
371 bool res = Idle::allowDisplaySleep();
372 if (!res) qWarning() << "Error enabling idle display sleep" << Idle::displayErrorMessage();
376 void MediaView::pause() {
377 switch (media->state()) {
378 case Media::PlayingState:
383 if (pauseTimer.hasExpired(60000)) {
384 pauseTimer.invalidate();
385 auto activeVideo = playlistModel->activeVideo();
387 connect(activeVideo, &Video::gotStreamUrl, this,
388 &MediaView::resumeWithNewStreamUrl);
389 activeVideo->loadStreamUrl();
397 QRegExp MediaView::wordRE(const QString &s) {
398 return QRegExp("\\W" + s + "\\W?", Qt::CaseInsensitive);
401 void MediaView::stop() {
404 while (!history.isEmpty()) {
405 VideoSource *videoSource = history.takeFirst();
406 // Don't delete videoSource in the Browse view
407 if (!videoSource->parent()) {
408 videoSource->deleteLater();
412 playlistModel->abortSearch();
413 videoAreaWidget->clear();
414 videoAreaWidget->update();
416 playlistView->selectionModel()->clearSelection();
418 MainWindow::instance()->getAction("refineSearch")->setChecked(false);
419 updateSubscriptionActionForVideo(nullptr, false);
420 #ifdef APP_ACTIVATION
424 for (QAction *action : qAsConst(currentVideoActions))
425 action->setEnabled(false);
427 QAction *a = MainWindow::instance()->getAction("download");
428 a->setEnabled(false);
429 a->setVisible(false);
433 currentVideoId.clear();
436 if (snapshotSettings) {
437 delete snapshotSettings;
438 snapshotSettings = nullptr;
443 const QString &MediaView::getCurrentVideoId() {
444 return currentVideoId;
447 void MediaView::activeVideoChanged(Video *video, Video *previousVideo) {
453 if (previousVideo && previousVideo != video) {
454 if (previousVideo->isLoadingStreamUrl()) previousVideo->abortLoadStreamUrl();
457 // optimize window for 16:9 video
460 videoAreaWidget->showLoading(video);
462 connect(video, &Video::gotStreamUrl, this, &MediaView::gotStreamUrl, Qt::UniqueConnection);
463 connect(video, SIGNAL(errorStreamUrl(QString)), SLOT(skip()), Qt::UniqueConnection);
464 video->loadStreamUrl();
466 // video title in titlebar
467 MainWindow::instance()->setWindowTitle(video->getTitle() + QLatin1String(" - ") +
468 QLatin1String(Constants::NAME));
470 // ensure active item is visible
471 int row = playlistModel->rowForVideo(video);
473 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
474 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
477 // enable/disable actions
478 MainWindow::instance()
479 ->getAction("download")
480 ->setEnabled(DownloadManager::instance()->itemForVideo(video) == nullptr);
481 MainWindow::instance()->getAction("previous")->setEnabled(row > 0);
482 MainWindow::instance()->getAction("stopafterthis")->setEnabled(true);
483 MainWindow::instance()->getAction("relatedVideos")->setEnabled(true);
485 bool enableDownload = video->getLicense() == Video::LicenseCC;
486 #ifdef APP_ACTIVATION
487 enableDownload = enableDownload || Activation::instance().isLegacy();
490 enableDownload = true;
492 QAction *a = MainWindow::instance()->getAction("download");
493 a->setEnabled(enableDownload);
494 a->setVisible(enableDownload);
496 updateSubscriptionActionForVideo(video, YTChannel::isSubscribed(video->getChannelId()));
498 for (QAction *action : currentVideoActions)
499 action->setEnabled(true);
502 if (snapshotSettings) {
503 delete snapshotSettings;
504 snapshotSettings = nullptr;
505 MainWindow::instance()->adjustStatusBarVisibility();
509 // see you in gotStreamUrl...
512 void MediaView::gotStreamUrl(const QString &streamUrl, const QString &audioUrl) {
514 if (streamUrl.isEmpty()) {
515 qWarning() << "Empty stream url";
520 Video *video = static_cast<Video *>(sender());
522 qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
525 video->disconnect(this);
527 currentVideoId = video->getId();
529 if (audioUrl.isEmpty()) {
530 qDebug() << "Playing" << streamUrl;
531 media->play(streamUrl);
533 qDebug() << "Playing" << streamUrl << audioUrl;
534 media->playSeparateAudioAndVideo(streamUrl, audioUrl);
537 // ensure we always have videos ahead
538 playlistModel->searchNeeded();
540 // ensure active item is visible
541 int row = playlistModel->activeRow();
543 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
544 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
547 #ifdef APP_ACTIVATION
548 if (!demoTimer->isActive() && !Activation::instance().isActivated()) {
549 int ms = (60000 * 2) + (QRandomGenerator::global()->generate() % (60000 * 2));
550 demoTimer->start(ms);
555 Extra::notify(video->getTitle(), video->getChannelTitle(), video->getFormattedDuration());
558 ChannelAggregator::instance()->videoWatched(video);
561 void MediaView::onItemActivated(const QModelIndex &index) {
562 if (playlistModel->rowExists(index.row())) {
563 // if it's the current video, just rewind and play
564 Video *activeVideo = playlistModel->activeVideo();
565 Video *video = playlistModel->videoAt(index.row());
566 if (activeVideo && video && activeVideo == video) {
569 playlistModel->setActiveRow(index.row());
571 // the user doubleclicked on the "Search More" item
573 playlistModel->searchMore();
574 playlistView->selectionModel()->clearSelection();
578 void MediaView::skipVideo() {
579 // skippedVideo is useful for DELAYED skip operations
580 // in order to be sure that we're skipping the video we wanted
581 // and not another one
583 if (playlistModel->activeVideo() != skippedVideo) {
584 qDebug() << "Skip of video canceled";
587 int nextRow = playlistModel->rowForVideo(skippedVideo);
589 if (nextRow == -1) return;
590 playlistModel->setActiveRow(nextRow);
594 void MediaView::skip() {
595 int nextRow = playlistModel->nextRow();
596 if (nextRow == -1) return;
597 playlistModel->setActiveRow(nextRow);
600 void MediaView::skipBackward() {
601 int prevRow = playlistModel->previousRow();
602 if (prevRow == -1) return;
603 playlistModel->setActiveRow(prevRow);
606 void MediaView::onAboutToFinish() {
610 void MediaView::onPlaybackFinished() {
613 const qint64 totalTime = media->duration();
614 const qint64 currentTime = media->position();
615 // qDebug() << __PRETTY_FUNCTION__ << mediaObject->currentTime() << totalTime;
616 // add 10 secs for imprecise Phonon backends (VLC, Xine)
617 if (currentTime > 0 && currentTime + 10000 < totalTime) {
618 // mediaObject->seek(currentTime);
619 QTimer::singleShot(500, this, SLOT(resumePlayback()));
621 QAction *stopAfterThisAction = MainWindow::instance()->getAction("stopafterthis");
622 if (stopAfterThisAction->isChecked()) {
623 stopAfterThisAction->setChecked(false);
629 void MediaView::resumePlayback() {
631 const qint64 currentTime = media->position();
632 // qDebug() << __PRETTY_FUNCTION__ << currentTime;
633 if (currentTime > 0) media->seek(currentTime);
637 void MediaView::openWebPage() {
638 Video *video = playlistModel->activeVideo();
642 video->getWebpage() + QLatin1String("&t=") + QString::number(media->position() / 1000);
643 QDesktopServices::openUrl(url);
646 void MediaView::copyWebPage() {
647 Video *video = playlistModel->activeVideo();
649 QString address = video->getWebpage();
650 QApplication::clipboard()->setText(address);
651 QString message = tr("You can now paste the YouTube link into another application");
652 MainWindow::instance()->showMessage(message);
655 void MediaView::copyVideoLink() {
656 Video *video = playlistModel->activeVideo();
658 QApplication::clipboard()->setText(video->getStreamUrl());
659 QString message = tr("You can now paste the video stream URL into another application") + ". " +
660 tr("The link will be valid only for a limited time.");
661 MainWindow::instance()->showMessage(message);
664 void MediaView::openInBrowser() {
665 Video *video = playlistModel->activeVideo();
668 QDesktopServices::openUrl(video->getStreamUrl());
671 void MediaView::removeSelected() {
672 if (!playlistView->selectionModel()->hasSelection()) return;
673 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
674 playlistModel->removeIndexes(indexes);
677 void MediaView::selectVideos(const QVector<Video *> &videos) {
678 for (Video *video : videos) {
679 QModelIndex index = playlistModel->indexForVideo(video);
680 playlistView->selectionModel()->select(index, QItemSelectionModel::Select);
681 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
685 void MediaView::selectionChanged(const QItemSelection & /*selected*/,
686 const QItemSelection & /*deselected*/) {
687 const bool gotSelection = playlistView->selectionModel()->hasSelection();
688 MainWindow::instance()->getAction("remove")->setEnabled(gotSelection);
689 MainWindow::instance()->getAction("moveUp")->setEnabled(gotSelection);
690 MainWindow::instance()->getAction("moveDown")->setEnabled(gotSelection);
693 void MediaView::moveUpSelected() {
694 if (!playlistView->selectionModel()->hasSelection()) return;
696 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
697 std::stable_sort(indexes.begin(), indexes.end());
698 playlistModel->move(indexes, true);
700 // set current index after row moves to something more intuitive
701 int row = indexes.at(0).row();
702 playlistView->selectionModel()->setCurrentIndex(playlistModel->index(row > 1 ? row : 1),
703 QItemSelectionModel::NoUpdate);
706 void MediaView::moveDownSelected() {
707 if (!playlistView->selectionModel()->hasSelection()) return;
709 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
710 std::stable_sort(indexes.begin(), indexes.end(),
711 [](const QModelIndex &a, const QModelIndex &b) { return b < a; });
712 playlistModel->move(indexes, false);
714 // set current index after row moves to something more intuitive
715 // (respect 1 static item on bottom)
716 int row = indexes.at(0).row() + 1, max = playlistModel->rowCount() - 2;
717 playlistView->selectionModel()->setCurrentIndex(playlistModel->index(row > max ? max : row),
718 QItemSelectionModel::NoUpdate);
721 void MediaView::setSidebarVisibility(bool visible) {
722 if (sidebar->isVisible() == visible) return;
723 sidebar->setVisible(visible);
726 sidebar->resize(sidebar->width(), window()->height());
728 playlistView->setFocus();
732 void MediaView::removeSidebar() {
734 sidebar->setParent(window());
737 void MediaView::restoreSidebar() {
739 splitter->insertWidget(0, sidebar);
742 bool MediaView::isSidebarVisible() {
743 return sidebar->isVisible();
746 void MediaView::saveSplitterState() {
748 if (splitter) settings.setValue("splitter", splitter->saveState());
751 void MediaView::downloadVideo() {
752 Video *video = playlistModel->activeVideo();
754 DownloadManager::instance()->addItem(video);
755 MainWindow::instance()->showActionsInStatusBar({MainWindow::instance()->getAction("downloads")},
757 QString message = tr("Downloading %1").arg(video->getTitle());
758 MainWindow::instance()->showMessage(message);
762 void MediaView::snapshot() {
763 qint64 currentTime = media->position() / 1000;
765 QObject *context = new QObject();
766 connect(media, &Media::snapshotReady, context,
767 [this, currentTime, context](const QImage &image) {
768 context->deleteLater();
770 if (image.isNull()) {
771 qWarning() << "Null snapshot";
775 QPixmap pixmap = QPixmap::fromImage(image.scaled(
776 videoWidget->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
777 videoAreaWidget->showSnapshotPreview(pixmap);
779 Video *video = playlistModel->activeVideo();
782 QString location = SnapshotSettings::getCurrentLocation();
784 if (!dir.exists()) dir.mkpath(location);
785 QString basename = video->getTitle();
786 QString format = video->getDuration() > 3600 ? "h_mm_ss" : "m_ss";
787 basename += " (" + QTime(0, 0, 0).addSecs(currentTime).toString(format) + ")";
788 basename = DataUtils::stringToFilename(basename);
789 QString filename = location + "/" + basename + ".png";
790 qDebug() << filename;
791 image.save(filename, "PNG");
793 if (snapshotSettings) delete snapshotSettings;
794 snapshotSettings = new SnapshotSettings(videoWidget);
795 snapshotSettings->setSnapshot(pixmap, filename);
796 QStatusBar *statusBar = MainWindow::instance()->statusBar();
798 Extra::fadeInWidget(statusBar, statusBar);
800 statusBar->insertPermanentWidget(0, snapshotSettings);
801 snapshotSettings->show();
802 MainWindow::instance()->setStatusBarVisibility(true);
810 void MediaView::fullscreen() {
811 videoAreaWidget->setParent(nullptr);
812 videoAreaWidget->showFullScreen();
815 void MediaView::resumeWithNewStreamUrl(const QString &streamUrl, const QString &audioUrl) {
816 pauseTime = media->position();
818 if (audioUrl.isEmpty()) {
819 qDebug() << "Playing" << streamUrl;
820 media->play(streamUrl);
822 qDebug() << "Playing" << streamUrl << audioUrl;
823 media->playSeparateAudioAndVideo(streamUrl, audioUrl);
826 Video *video = static_cast<Video *>(sender());
828 qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
831 video->disconnect(this);
834 void MediaView::findVideoParts() {
835 Video *video = playlistModel->activeVideo();
838 QString query = video->getTitle();
840 const QLatin1String optionalSpace("\\s*");
841 const QLatin1String staticCounterSeparators("[\\/\\-]");
842 const QString counterSeparators =
843 QLatin1String("( of | ") + tr("of", "Used in video parts, as in '2 of 3'") +
844 QLatin1String(" |") + staticCounterSeparators + QLatin1String(")");
846 // numbers from 1 to 15
847 const QLatin1String counterNumber("([1-9]|1[0-5])");
849 // query.remove(QRegExp(counterSeparators + optionalSpace + counterNumber));
850 query.remove(QRegExp(counterNumber + optionalSpace + counterSeparators + optionalSpace +
852 query.remove(wordRE("pr?t\\.?" + optionalSpace + counterNumber));
853 query.remove(wordRE("ep\\.?" + optionalSpace + counterNumber));
854 query.remove(wordRE("part" + optionalSpace + counterNumber));
855 query.remove(wordRE("episode" + optionalSpace + counterNumber));
856 query.remove(wordRE(tr("part", "This is for video parts, as in 'Cool video - part 1'") +
857 optionalSpace + counterNumber));
858 query.remove(wordRE(tr("episode", "This is for video parts, as in 'Cool series - episode 1'") +
859 optionalSpace + counterNumber));
860 query.remove(QRegExp("[\\(\\)\\[\\]]"));
862 #define NUMBERS "one|two|three|four|five|six|seven|eight|nine|ten"
864 QRegExp englishNumberRE = QRegExp(QLatin1String(".*(") + NUMBERS + ").*", Qt::CaseInsensitive);
865 // bool numberAsWords = englishNumberRE.exactMatch(query);
866 query.remove(englishNumberRE);
868 QRegExp localizedNumberRE =
869 QRegExp(QLatin1String(".*(") + tr(NUMBERS) + ").*", Qt::CaseInsensitive);
870 // if (!numberAsWords) numberAsWords = localizedNumberRE.exactMatch(query);
871 query.remove(localizedNumberRE);
873 SearchParams *searchParams = new SearchParams();
874 searchParams->setTransient(true);
875 searchParams->setKeywords(query);
876 searchParams->setChannelId(video->getChannelId());
879 if (!numberAsWords) {
880 qDebug() << "We don't have number as words";
881 // searchParams->setSortBy(SearchParams::SortByNewest);
882 // TODO searchParams->setReverseOrder(true);
883 // TODO searchParams->setMax(50);
887 search(searchParams);
890 void MediaView::relatedVideos() {
891 Video *video = playlistModel->activeVideo();
894 auto source = new SingleVideoSource(this);
895 source->setVideo(video->clone());
896 setVideoSource(source);
898 MainWindow::instance()->getAction("relatedVideos")->setEnabled(false);
901 void MediaView::shareViaTwitter() {
902 Video *video = playlistModel->activeVideo();
904 QUrl url("https://twitter.com/intent/tweet");
906 q.addQueryItem("via", "minitubeapp");
907 q.addQueryItem("text", video->getTitle());
908 q.addQueryItem("url", video->getWebpage());
910 QDesktopServices::openUrl(url);
913 void MediaView::shareViaFacebook() {
914 Video *video = playlistModel->activeVideo();
916 QUrl url("https://www.facebook.com/sharer.php");
918 q.addQueryItem("t", video->getTitle());
919 q.addQueryItem("u", video->getWebpage());
921 QDesktopServices::openUrl(url);
924 void MediaView::shareViaEmail() {
925 Video *video = playlistModel->activeVideo();
929 q.addQueryItem("subject", video->getTitle());
930 const QString body = video->getTitle() + "\n" + video->getWebpage() + "\n\n" +
931 tr("Sent from %1").arg(Constants::NAME) + "\n" + Constants::WEBSITE;
932 q.addQueryItem("body", body);
934 QDesktopServices::openUrl(url);
937 void MediaView::onAuthorPushed(QModelIndex index) {
938 Video *video = playlistModel->videoAt(index.row());
941 QString channelId = video->getChannelId();
942 // if (channelId.isEmpty()) channelId = video->channelTitle();
943 if (channelId.isEmpty()) return;
945 SearchParams *searchParams = new SearchParams();
946 searchParams->setChannelId(channelId);
947 searchParams->setSortBy(SearchParams::SortByNewest);
950 search(searchParams);
954 void MediaView::updateSubscriptionAction(bool subscribed) {
955 QAction *subscribeAction = MainWindow::instance()->getAction("subscribeChannel");
957 QString subscribeTip;
958 QString subscribeText;
960 if (currentSubscriptionChannelId.isEmpty()) {
961 subscribeText = subscribeAction->property("originalText").toString();
962 subscribeAction->setEnabled(false);
963 } else if (subscribed) {
964 subscribeText = tr("Unsubscribe from %1").arg(currentSubscriptionChannelTitle);
965 subscribeTip = subscribeText;
966 subscribeAction->setEnabled(true);
968 subscribeText = tr("Subscribe to %1").arg(currentSubscriptionChannelTitle);
969 subscribeTip = subscribeText;
970 subscribeAction->setEnabled(true);
972 subscribeAction->setText(subscribeText);
973 subscribeAction->setStatusTip(subscribeTip);
976 subscribeAction->setIcon(IconUtils::icon("bookmark-remove"));
978 subscribeAction->setIcon(IconUtils::icon("bookmark-new"));
981 MainWindow::instance()->setupAction(subscribeAction);
984 void MediaView::updateSubscriptionActionForChannel(const QString & channelId) {
985 QString channelTitle = tr("channel");
986 YTChannel *channel = YTChannel::forId(channelId);
987 if (nullptr != channel && !channel->getDisplayName().isEmpty()) {
988 channelTitle = channel->getDisplayName();
991 bool subscribed = YTChannel::isSubscribed(channelId);
993 currentSubscriptionChannelId = channelId;
994 currentSubscriptionChannelTitle = channelTitle;
995 updateSubscriptionAction(subscribed);
998 void MediaView::updateSubscriptionActionForVideo(Video *video, bool subscribed) {
1000 currentSubscriptionChannelId = "";
1001 currentSubscriptionChannelTitle = "";
1002 updateSubscriptionAction(false);
1004 currentSubscriptionChannelId = video->getChannelId();
1005 currentSubscriptionChannelTitle = video->getChannelTitle();
1006 updateSubscriptionAction(subscribed);
1010 void MediaView::reloadCurrentVideo() {
1011 Video *video = playlistModel->activeVideo();
1014 int oldFormat = video->getDefinitionCode();
1016 QObject *context = new QObject();
1017 connect(video, &Video::gotStreamUrl, context,
1018 [this, oldFormat, video, context](const QString &videoUrl, const QString &audioUrl) {
1019 context->deleteLater();
1020 if (oldFormat == video->getDefinitionCode()) return;
1021 QObject *context2 = new QObject();
1022 const qint64 position = media->position();
1023 connect(media, &Media::stateChanged, context2,
1024 [position, this, context2](Media::State state) {
1025 if (state == Media::PlayingState) {
1026 media->seek(position);
1027 context2->deleteLater();
1028 Video *video = playlistModel->activeVideo();
1029 QString msg = tr("Switched to %1")
1030 .arg(VideoDefinition::forCode(
1031 video->getDefinitionCode())
1033 MainWindow::instance()->showMessage(msg);
1037 if (audioUrl.isEmpty()) {
1038 media->play(videoUrl);
1040 media->playSeparateAudioAndVideo(videoUrl, audioUrl);
1043 video->loadStreamUrl();
1046 void MediaView::toggleSubscription() {
1047 //Video *video = playlistModel->activeVideo();
1048 if (currentSubscriptionChannelId.isEmpty()) {
1052 bool subscribed = YTChannel::isSubscribed(currentSubscriptionChannelId);
1054 YTChannel::unsubscribe(currentSubscriptionChannelId);
1055 MainWindow::instance()->showMessage(
1056 tr("Unsubscribed from %1").arg(currentSubscriptionChannelTitle));
1058 YTChannel::subscribe(currentSubscriptionChannelId);
1059 MainWindow::instance()->showMessage(tr("Subscribed to %1").arg(currentSubscriptionChannelTitle));
1062 updateSubscriptionAction(!subscribed);
1065 void MediaView::adjustWindowSize() {
1066 qDebug() << "Adjusting window size";
1067 Video *video = playlistModel->activeVideo();
1069 QWidget *window = this->window();
1070 if (!window->isMaximized() && !window->isFullScreen()) {
1071 const double ratio = 16. / 9.;
1072 const double w = (double)videoAreaWidget->width();
1073 const double h = (double)videoAreaWidget->height();
1074 const double currentVideoRatio = w / h;
1075 if (currentVideoRatio != ratio) {
1076 qDebug() << "Adjust size";
1077 int newHeight = std::round((window->height() - h) + (w / ratio));
1078 window->resize(window->width(), newHeight);