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;
262 history.append(videoSource);
266 if (history.size() > 1)
267 Extra::slideTransition(playlistView->viewport(), playlistView->viewport(), back);
270 playlistModel->setVideoSource(videoSource);
272 if (media->state() == Media::StoppedState) {
274 if (settings.value("manualplay", false).toBool()) {
275 videoAreaWidget->showPickMessage();
279 SearchParams *searchParams = getSearchParams();
281 sidebar->showPlaylist();
282 sidebar->getRefineSearchWidget()->setSearchParams(searchParams);
283 sidebar->hideSuggestions();
284 sidebar->getHeader()->updateInfo();
286 bool isChannel = searchParams && !searchParams->channelId().isEmpty();
288 updateSubscriptionActionForChannel(searchParams->channelId());
290 playlistView->setClickableAuthors(!isChannel);
293 void MediaView::searchAgain() {
294 VideoSource *currentVideoSource = playlistModel->getVideoSource();
295 setVideoSource(currentVideoSource, false);
298 bool MediaView::canGoBack() {
299 return getHistoryIndex() > 0;
302 void MediaView::goBack() {
303 if (history.size() > 1) {
304 int currentIndex = getHistoryIndex();
305 if (currentIndex > 0) {
306 VideoSource *previousVideoSource = history.at(currentIndex - 1);
307 setVideoSource(previousVideoSource, false, true);
312 bool MediaView::canGoForward() {
313 int currentIndex = getHistoryIndex();
314 return currentIndex >= 0 && currentIndex < history.size() - 1;
317 void MediaView::goForward() {
318 if (canGoForward()) {
319 int currentIndex = getHistoryIndex();
320 VideoSource *nextVideoSource = history.at(currentIndex + 1);
321 setVideoSource(nextVideoSource, false);
325 int MediaView::getHistoryIndex() {
326 return history.lastIndexOf(playlistModel->getVideoSource());
329 void MediaView::appear() {
330 MainWindow::instance()->showToolbar();
332 Video *currentVideo = playlistModel->activeVideo();
334 MainWindow::instance()->setWindowTitle(currentVideo->getTitle() + " - " + Constants::NAME);
337 playlistView->setFocus();
340 void MediaView::disappear() {
341 MainWindow::instance()->hideToolbar();
344 void MediaView::handleError(const QString &message) {
345 qWarning() << __PRETTY_FUNCTION__ << message;
346 #ifndef QT_NO_DEBUG_OUTPUT
347 MainWindow::instance()->showMessage(message);
351 void MediaView::mediaStateChanged(Media::State state) {
352 if (pauseTime > 0 && (state == Media::PlayingState || state == Media::BufferingState)) {
353 qDebug() << "Seeking to" << pauseTime;
354 media->seek(pauseTime);
358 if (state == Media::PlayingState) {
359 videoAreaWidget->showVideo();
360 } else if (state == Media::ErrorState) {
361 handleError(media->errorString());
364 bool enablePlayingVideoActions = state != Media::StoppedState;
365 for (QAction *action : qAsConst(playingVideoActions))
366 action->setEnabled(enablePlayingVideoActions);
368 if (state == Media::PlayingState) {
369 bool res = Idle::preventDisplaySleep(QString("%1 is playing").arg(Constants::NAME));
370 if (!res) qWarning() << "Error disabling idle display sleep" << Idle::displayErrorMessage();
371 } else if (state == Media::PausedState || state == Media::StoppedState) {
372 bool res = Idle::allowDisplaySleep();
373 if (!res) qWarning() << "Error enabling idle display sleep" << Idle::displayErrorMessage();
377 void MediaView::pause() {
378 switch (media->state()) {
379 case Media::PlayingState:
380 qDebug() << "Pausing";
385 if (pauseTimer.isValid() && pauseTimer.hasExpired(60000)) {
386 qDebug() << "Pause timer expired";
387 pauseTimer.invalidate();
388 auto activeVideo = playlistModel->activeVideo();
390 connect(activeVideo, &Video::gotStreamUrl, this,
391 &MediaView::resumeWithNewStreamUrl);
392 activeVideo->loadStreamUrl();
394 qDebug() << "No active video";
396 qDebug() << "Playing" << media->file();
403 QRegExp MediaView::wordRE(const QString &s) {
404 return QRegExp("\\W" + s + "\\W?", Qt::CaseInsensitive);
407 void MediaView::stop() {
410 while (!history.isEmpty()) {
411 VideoSource *videoSource = history.takeFirst();
412 // Don't delete videoSource in the Browse view
413 if (!videoSource->parent()) {
414 videoSource->abort();
415 videoSource->deleteLater();
419 playlistModel->abortSearch();
420 videoAreaWidget->clear();
421 videoAreaWidget->update();
423 playlistView->selectionModel()->clearSelection();
425 MainWindow::instance()->getAction("refineSearch")->setChecked(false);
426 updateSubscriptionActionForVideo(nullptr, false);
427 #ifdef APP_ACTIVATION
431 for (QAction *action : qAsConst(currentVideoActions))
432 action->setEnabled(false);
434 QAction *a = MainWindow::instance()->getAction("download");
435 a->setEnabled(false);
436 a->setVisible(false);
440 currentVideoId.clear();
443 if (snapshotSettings) {
444 delete snapshotSettings;
445 snapshotSettings = nullptr;
450 const QString &MediaView::getCurrentVideoId() {
451 return currentVideoId;
454 void MediaView::activeVideoChanged(Video *video, Video *previousVideo) {
460 if (previousVideo && previousVideo != video) {
461 if (previousVideo->isLoadingStreamUrl()) previousVideo->abortLoadStreamUrl();
464 // optimize window for 16:9 video
467 videoAreaWidget->showLoading(video);
469 connect(video, &Video::gotStreamUrl, this, &MediaView::gotStreamUrl, Qt::UniqueConnection);
470 connect(video, SIGNAL(errorStreamUrl(QString)), SLOT(skip()), Qt::UniqueConnection);
471 video->loadStreamUrl();
473 // video title in titlebar
474 MainWindow::instance()->setWindowTitle(video->getTitle() + QLatin1String(" - ") +
475 QLatin1String(Constants::NAME));
477 // ensure active item is visible
478 int row = playlistModel->rowForVideo(video);
480 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
481 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
484 // enable/disable actions
485 MainWindow::instance()
486 ->getAction("download")
487 ->setEnabled(DownloadManager::instance()->itemForVideo(video) == nullptr);
488 MainWindow::instance()->getAction("previous")->setEnabled(row > 0);
489 MainWindow::instance()->getAction("stopafterthis")->setEnabled(true);
490 MainWindow::instance()->getAction("relatedVideos")->setEnabled(true);
492 bool enableDownload = video->getLicense() == Video::LicenseCC;
493 #ifdef APP_ACTIVATION
494 enableDownload = enableDownload || Activation::instance().isLegacy();
497 enableDownload = true;
499 QAction *a = MainWindow::instance()->getAction("download");
500 a->setEnabled(enableDownload);
501 a->setVisible(enableDownload);
503 updateSubscriptionActionForVideo(video, YTChannel::isSubscribed(video->getChannelId()));
505 for (QAction *action : currentVideoActions)
506 action->setEnabled(true);
509 if (snapshotSettings) {
510 delete snapshotSettings;
511 snapshotSettings = nullptr;
512 MainWindow::instance()->adjustStatusBarVisibility();
516 // see you in gotStreamUrl...
519 void MediaView::gotStreamUrl(const QString &streamUrl, const QString &audioUrl) {
521 if (streamUrl.isEmpty()) {
522 qWarning() << "Empty stream url";
527 Video *video = static_cast<Video *>(sender());
529 qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
532 video->disconnect(this);
534 currentVideoId = video->getId();
536 if (audioUrl.isEmpty()) {
537 qDebug() << "Playing" << streamUrl;
538 media->play(streamUrl);
540 qDebug() << "Playing" << streamUrl << audioUrl;
541 media->playSeparateAudioAndVideo(streamUrl, audioUrl);
544 // ensure we always have videos ahead
545 playlistModel->searchNeeded();
547 // ensure active item is visible
548 int row = playlistModel->activeRow();
550 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
551 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
554 #ifdef APP_ACTIVATION
555 if (!demoTimer->isActive() && !Activation::instance().isActivated()) {
556 int ms = (60000 * 2) + (QRandomGenerator::global()->generate() % (60000 * 2));
557 demoTimer->start(ms);
562 Extra::notify(video->getTitle(), video->getChannelTitle(), video->getFormattedDuration());
565 ChannelAggregator::instance()->videoWatched(video);
568 void MediaView::onItemActivated(const QModelIndex &index) {
569 if (playlistModel->rowExists(index.row())) {
570 // if it's the current video, just rewind and play
571 Video *activeVideo = playlistModel->activeVideo();
572 Video *video = playlistModel->videoAt(index.row());
573 if (activeVideo && video && activeVideo == video) {
576 playlistModel->setActiveRow(index.row());
578 // the user doubleclicked on the "Search More" item
580 playlistModel->searchMore();
581 playlistView->selectionModel()->clearSelection();
585 void MediaView::skipVideo() {
586 // skippedVideo is useful for DELAYED skip operations
587 // in order to be sure that we're skipping the video we wanted
588 // and not another one
590 if (playlistModel->activeVideo() != skippedVideo) {
591 qDebug() << "Skip of video canceled";
594 int nextRow = playlistModel->rowForVideo(skippedVideo);
596 if (nextRow == -1) return;
597 playlistModel->setActiveRow(nextRow);
601 void MediaView::skip() {
602 int nextRow = playlistModel->nextRow();
603 if (nextRow == -1) return;
604 playlistModel->setActiveRow(nextRow);
607 void MediaView::skipBackward() {
608 int prevRow = playlistModel->previousRow();
609 if (prevRow == -1) return;
610 playlistModel->setActiveRow(prevRow);
613 void MediaView::onAboutToFinish() {
617 void MediaView::onPlaybackFinished() {
620 const qint64 totalTime = media->duration();
621 const qint64 currentTime = media->position();
622 // qDebug() << __PRETTY_FUNCTION__ << mediaObject->currentTime() << totalTime;
623 // add 10 secs for imprecise Phonon backends (VLC, Xine)
624 if (currentTime > 0 && currentTime + 10000 < totalTime) {
625 // mediaObject->seek(currentTime);
626 QTimer::singleShot(500, this, SLOT(resumePlayback()));
628 QAction *stopAfterThisAction = MainWindow::instance()->getAction("stopafterthis");
629 if (stopAfterThisAction->isChecked()) {
630 stopAfterThisAction->setChecked(false);
636 void MediaView::resumePlayback() {
638 const qint64 currentTime = media->position();
639 // qDebug() << __PRETTY_FUNCTION__ << currentTime;
640 if (currentTime > 0) media->seek(currentTime);
644 void MediaView::openWebPage() {
645 Video *video = playlistModel->activeVideo();
649 video->getWebpage() + QLatin1String("&t=") + QString::number(media->position() / 1000);
650 QDesktopServices::openUrl(url);
653 void MediaView::copyWebPage() {
654 Video *video = playlistModel->activeVideo();
656 QString address = video->getWebpage();
657 QApplication::clipboard()->setText(address);
658 QString message = tr("You can now paste the YouTube link into another application");
659 MainWindow::instance()->showMessage(message);
662 void MediaView::copyVideoLink() {
663 Video *video = playlistModel->activeVideo();
665 QApplication::clipboard()->setText(video->getStreamUrl());
666 QString message = tr("You can now paste the video stream URL into another application") + ". " +
667 tr("The link will be valid only for a limited time.");
668 MainWindow::instance()->showMessage(message);
671 void MediaView::openInBrowser() {
672 Video *video = playlistModel->activeVideo();
675 QDesktopServices::openUrl(video->getStreamUrl());
678 void MediaView::removeSelected() {
679 if (!playlistView->selectionModel()->hasSelection()) return;
680 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
681 playlistModel->removeIndexes(indexes);
684 void MediaView::selectVideos(const QVector<Video *> &videos) {
685 for (Video *video : videos) {
686 QModelIndex index = playlistModel->indexForVideo(video);
687 playlistView->selectionModel()->select(index, QItemSelectionModel::Select);
688 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
692 void MediaView::selectionChanged(const QItemSelection & /*selected*/,
693 const QItemSelection & /*deselected*/) {
694 const bool gotSelection = playlistView->selectionModel()->hasSelection();
695 MainWindow::instance()->getAction("remove")->setEnabled(gotSelection);
696 MainWindow::instance()->getAction("moveUp")->setEnabled(gotSelection);
697 MainWindow::instance()->getAction("moveDown")->setEnabled(gotSelection);
700 void MediaView::moveUpSelected() {
701 if (!playlistView->selectionModel()->hasSelection()) return;
703 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
704 std::stable_sort(indexes.begin(), indexes.end());
705 playlistModel->move(indexes, true);
707 // set current index after row moves to something more intuitive
708 int row = indexes.at(0).row();
709 playlistView->selectionModel()->setCurrentIndex(playlistModel->index(row > 1 ? row : 1),
710 QItemSelectionModel::NoUpdate);
713 void MediaView::moveDownSelected() {
714 if (!playlistView->selectionModel()->hasSelection()) return;
716 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
717 std::stable_sort(indexes.begin(), indexes.end(),
718 [](const QModelIndex &a, const QModelIndex &b) { return b < a; });
719 playlistModel->move(indexes, false);
721 // set current index after row moves to something more intuitive
722 // (respect 1 static item on bottom)
723 int row = indexes.at(0).row() + 1, max = playlistModel->rowCount() - 2;
724 playlistView->selectionModel()->setCurrentIndex(playlistModel->index(row > max ? max : row),
725 QItemSelectionModel::NoUpdate);
728 void MediaView::setSidebarVisibility(bool visible) {
729 if (sidebar->isVisible() == visible) return;
730 sidebar->setVisible(visible);
733 sidebar->resize(sidebar->width(), window()->height());
735 playlistView->setFocus();
739 void MediaView::removeSidebar() {
741 sidebar->setParent(window());
744 void MediaView::restoreSidebar() {
746 splitter->insertWidget(0, sidebar);
749 bool MediaView::isSidebarVisible() {
750 return sidebar->isVisible();
753 void MediaView::saveSplitterState() {
755 if (splitter) settings.setValue("splitter", splitter->saveState());
758 void MediaView::downloadVideo() {
759 Video *video = playlistModel->activeVideo();
761 DownloadManager::instance()->addItem(video);
762 MainWindow::instance()->showActionsInStatusBar({MainWindow::instance()->getAction("downloads")},
764 QString message = tr("Downloading %1").arg(video->getTitle());
765 MainWindow::instance()->showMessage(message);
769 void MediaView::snapshot() {
770 qint64 currentTime = media->position() / 1000;
772 QObject *context = new QObject();
773 connect(media, &Media::snapshotReady, context,
774 [this, currentTime, context](const QImage &image) {
775 context->deleteLater();
777 if (image.isNull()) {
778 qWarning() << "Null snapshot";
782 QPixmap pixmap = QPixmap::fromImage(image.scaled(
783 videoWidget->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
784 videoAreaWidget->showSnapshotPreview(pixmap);
786 Video *video = playlistModel->activeVideo();
789 QString location = SnapshotSettings::getCurrentLocation();
791 if (!dir.exists()) dir.mkpath(location);
792 QString basename = video->getTitle();
793 QString format = video->getDuration() > 3600 ? "h_mm_ss" : "m_ss";
794 basename += " (" + QTime(0, 0, 0).addSecs(currentTime).toString(format) + ")";
795 basename = DataUtils::stringToFilename(basename);
796 QString filename = location + "/" + basename + ".png";
797 qDebug() << filename;
798 image.save(filename, "PNG");
800 if (snapshotSettings) delete snapshotSettings;
801 snapshotSettings = new SnapshotSettings(videoWidget);
802 snapshotSettings->setSnapshot(pixmap, filename);
803 QStatusBar *statusBar = MainWindow::instance()->statusBar();
805 Extra::fadeInWidget(statusBar, statusBar);
807 statusBar->insertPermanentWidget(0, snapshotSettings);
808 snapshotSettings->show();
809 MainWindow::instance()->setStatusBarVisibility(true);
817 void MediaView::fullscreen() {
818 videoAreaWidget->setParent(nullptr);
819 videoAreaWidget->showFullScreen();
822 void MediaView::resumeWithNewStreamUrl(const QString &streamUrl, const QString &audioUrl) {
823 pauseTime = media->position();
825 if (audioUrl.isEmpty()) {
826 qDebug() << "Playing" << streamUrl;
827 media->play(streamUrl);
829 qDebug() << "Playing" << streamUrl << audioUrl;
830 media->playSeparateAudioAndVideo(streamUrl, audioUrl);
833 Video *video = static_cast<Video *>(sender());
835 qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
838 video->disconnect(this);
841 void MediaView::findVideoParts() {
842 Video *video = playlistModel->activeVideo();
845 QString query = video->getTitle();
847 const QLatin1String optionalSpace("\\s*");
848 const QLatin1String staticCounterSeparators("[\\/\\-]");
849 const QString counterSeparators =
850 QLatin1String("( of | ") + tr("of", "Used in video parts, as in '2 of 3'") +
851 QLatin1String(" |") + staticCounterSeparators + QLatin1String(")");
853 // numbers from 1 to 15
854 const QLatin1String counterNumber("([1-9]|1[0-5])");
856 // query.remove(QRegExp(counterSeparators + optionalSpace + counterNumber));
857 query.remove(QRegExp(counterNumber + optionalSpace + counterSeparators + optionalSpace +
859 query.remove(wordRE("pr?t\\.?" + optionalSpace + counterNumber));
860 query.remove(wordRE("ep\\.?" + optionalSpace + counterNumber));
861 query.remove(wordRE("part" + optionalSpace + counterNumber));
862 query.remove(wordRE("episode" + optionalSpace + counterNumber));
863 query.remove(wordRE(tr("part", "This is for video parts, as in 'Cool video - part 1'") +
864 optionalSpace + counterNumber));
865 query.remove(wordRE(tr("episode", "This is for video parts, as in 'Cool series - episode 1'") +
866 optionalSpace + counterNumber));
867 query.remove(QRegExp("[\\(\\)\\[\\]]"));
869 #define NUMBERS "one|two|three|four|five|six|seven|eight|nine|ten"
871 QRegExp englishNumberRE = QRegExp(QLatin1String(".*(") + NUMBERS + ").*", Qt::CaseInsensitive);
872 // bool numberAsWords = englishNumberRE.exactMatch(query);
873 query.remove(englishNumberRE);
875 QRegExp localizedNumberRE =
876 QRegExp(QLatin1String(".*(") + tr(NUMBERS) + ").*", Qt::CaseInsensitive);
877 // if (!numberAsWords) numberAsWords = localizedNumberRE.exactMatch(query);
878 query.remove(localizedNumberRE);
880 SearchParams *searchParams = new SearchParams();
881 searchParams->setTransient(true);
882 searchParams->setKeywords(query);
883 searchParams->setChannelId(video->getChannelId());
886 if (!numberAsWords) {
887 qDebug() << "We don't have number as words";
888 // searchParams->setSortBy(SearchParams::SortByNewest);
889 // TODO searchParams->setReverseOrder(true);
890 // TODO searchParams->setMax(50);
894 search(searchParams);
897 void MediaView::relatedVideos() {
898 Video *video = playlistModel->activeVideo();
901 auto source = new SingleVideoSource(this);
902 source->setVideo(video->clone());
903 setVideoSource(source);
905 MainWindow::instance()->getAction("relatedVideos")->setEnabled(false);
908 void MediaView::shareViaTwitter() {
909 Video *video = playlistModel->activeVideo();
911 QUrl url("https://twitter.com/intent/tweet");
913 q.addQueryItem("via", "minitubeapp");
914 q.addQueryItem("text", video->getTitle());
915 q.addQueryItem("url", video->getWebpage());
917 QDesktopServices::openUrl(url);
920 void MediaView::shareViaFacebook() {
921 Video *video = playlistModel->activeVideo();
923 QUrl url("https://www.facebook.com/sharer.php");
925 q.addQueryItem("t", video->getTitle());
926 q.addQueryItem("u", video->getWebpage());
928 QDesktopServices::openUrl(url);
931 void MediaView::shareViaEmail() {
932 Video *video = playlistModel->activeVideo();
936 q.addQueryItem("subject", video->getTitle());
937 const QString body = video->getTitle() + "\n" + video->getWebpage() + "\n\n" +
938 tr("Sent from %1").arg(Constants::NAME) + "\n" + Constants::WEBSITE;
939 q.addQueryItem("body", body);
941 QDesktopServices::openUrl(url);
944 void MediaView::onAuthorPushed(QModelIndex index) {
945 Video *video = playlistModel->videoAt(index.row());
948 QString channelId = video->getChannelId();
949 // if (channelId.isEmpty()) channelId = video->channelTitle();
950 if (channelId.isEmpty()) return;
952 SearchParams *searchParams = new SearchParams();
953 searchParams->setChannelId(channelId);
954 searchParams->setSortBy(SearchParams::SortByNewest);
957 search(searchParams);
961 void MediaView::updateSubscriptionAction(bool subscribed) {
962 QAction *subscribeAction = MainWindow::instance()->getAction("subscribeChannel");
964 QString subscribeTip;
965 QString subscribeText;
967 if (currentSubscriptionChannelId.isEmpty()) {
968 subscribeText = subscribeAction->property("originalText").toString();
969 subscribeAction->setEnabled(false);
970 } else if (subscribed) {
971 subscribeText = tr("Unsubscribe from %1").arg(currentSubscriptionChannelTitle);
972 subscribeTip = subscribeText;
973 subscribeAction->setEnabled(true);
975 subscribeText = tr("Subscribe to %1").arg(currentSubscriptionChannelTitle);
976 subscribeTip = subscribeText;
977 subscribeAction->setEnabled(true);
979 subscribeAction->setText(subscribeText);
980 subscribeAction->setStatusTip(subscribeTip);
983 subscribeAction->setIcon(IconUtils::icon("bookmark-remove"));
985 subscribeAction->setIcon(IconUtils::icon("bookmark-new"));
988 MainWindow::instance()->setupAction(subscribeAction);
991 void MediaView::updateSubscriptionActionForChannel(const QString & channelId) {
992 QString channelTitle = tr("channel");
993 YTChannel *channel = YTChannel::forId(channelId);
994 if (nullptr != channel && !channel->getDisplayName().isEmpty()) {
995 channelTitle = channel->getDisplayName();
998 bool subscribed = YTChannel::isSubscribed(channelId);
1000 currentSubscriptionChannelId = channelId;
1001 currentSubscriptionChannelTitle = channelTitle;
1002 updateSubscriptionAction(subscribed);
1005 void MediaView::updateSubscriptionActionForVideo(Video *video, bool subscribed) {
1007 currentSubscriptionChannelId = "";
1008 currentSubscriptionChannelTitle = "";
1009 updateSubscriptionAction(false);
1011 currentSubscriptionChannelId = video->getChannelId();
1012 currentSubscriptionChannelTitle = video->getChannelTitle();
1013 updateSubscriptionAction(subscribed);
1017 void MediaView::reloadCurrentVideo() {
1018 Video *video = playlistModel->activeVideo();
1021 int oldFormat = video->getDefinitionCode();
1023 QObject *context = new QObject();
1024 connect(video, &Video::gotStreamUrl, context,
1025 [this, oldFormat, video, context](const QString &videoUrl, const QString &audioUrl) {
1026 context->deleteLater();
1027 if (oldFormat == video->getDefinitionCode()) return;
1028 QObject *context2 = new QObject();
1029 const qint64 position = media->position();
1030 connect(media, &Media::stateChanged, context2,
1031 [position, this, context2](Media::State state) {
1032 if (state == Media::PlayingState) {
1033 media->seek(position);
1034 context2->deleteLater();
1035 Video *video = playlistModel->activeVideo();
1036 QString msg = tr("Switched to %1")
1037 .arg(VideoDefinition::forCode(
1038 video->getDefinitionCode())
1040 MainWindow::instance()->showMessage(msg);
1044 if (audioUrl.isEmpty()) {
1045 media->play(videoUrl);
1047 media->playSeparateAudioAndVideo(videoUrl, audioUrl);
1050 video->loadStreamUrl();
1053 void MediaView::toggleSubscription() {
1054 //Video *video = playlistModel->activeVideo();
1055 if (currentSubscriptionChannelId.isEmpty()) {
1059 bool subscribed = YTChannel::isSubscribed(currentSubscriptionChannelId);
1061 YTChannel::unsubscribe(currentSubscriptionChannelId);
1062 MainWindow::instance()->showMessage(
1063 tr("Unsubscribed from %1").arg(currentSubscriptionChannelTitle));
1065 YTChannel::subscribe(currentSubscriptionChannelId);
1066 MainWindow::instance()->showMessage(tr("Subscribed to %1").arg(currentSubscriptionChannelTitle));
1069 updateSubscriptionAction(!subscribed);
1072 void MediaView::adjustWindowSize() {
1073 qDebug() << "Adjusting window size";
1074 Video *video = playlistModel->activeVideo();
1076 QWidget *window = this->window();
1077 if (!window->isMaximized() && !window->isFullScreen()) {
1078 const double ratio = 16. / 9.;
1079 const double w = (double)videoAreaWidget->width();
1080 const double h = (double)videoAreaWidget->height();
1081 const double currentVideoRatio = w / h;
1082 if (currentVideoRatio != ratio) {
1083 qDebug() << "Adjust size";
1084 int newHeight = std::round((window->height() - h) + (w / ratio));
1085 window->resize(window->width(), newHeight);