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 #include "ivchannelsource.h"
58 #include "ivsinglevideosource.h"
61 MediaView *MediaView::instance() {
62 static MediaView *i = new MediaView();
66 MediaView::MediaView(QWidget *parent)
67 : View(parent), splitter(nullptr), stopped(false)
70 snapshotSettings(nullptr)
76 void MediaView::initialize() {
77 MainWindow *mainWindow = MainWindow::instance();
79 QBoxLayout *layout = new QVBoxLayout(this);
82 splitter = new MiniSplitter();
83 layout->addWidget(splitter);
85 playlistView = new PlaylistView();
86 playlistView->setParent(this);
87 connect(playlistView, SIGNAL(activated(const QModelIndex &)),
88 SLOT(onItemActivated(const QModelIndex &)));
90 playlistModel = new PlaylistModel();
91 connect(playlistModel, &PlaylistModel::activeVideoChanged, this,
92 &MediaView::activeVideoChanged);
93 // needed to restore the selection after dragndrop
94 connect(playlistModel, SIGNAL(needSelectionFor(QVector<Video *>)),
95 SLOT(selectVideos(QVector<Video *>)));
96 playlistView->setModel(playlistModel);
98 connect(playlistView->selectionModel(),
99 SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
100 SLOT(selectionChanged(const QItemSelection &, const QItemSelection &)));
102 connect(playlistView, SIGNAL(authorPushed(QModelIndex)), SLOT(onAuthorPushed(QModelIndex)));
104 sidebar = new SidebarWidget(this);
105 sidebar->setPlaylist(playlistView);
106 sidebar->setMaximumWidth(playlistView->minimumWidth() * 3);
107 connect(sidebar->getRefineSearchWidget(), SIGNAL(searchRefined()), SLOT(searchAgain()));
108 connect(playlistModel, SIGNAL(haveSuggestions(const QStringList &)), sidebar,
109 SLOT(showSuggestions(const QStringList &)));
110 connect(sidebar, SIGNAL(suggestionAccepted(QString)), mainWindow, SLOT(search(QString)));
111 splitter->addWidget(sidebar);
113 videoAreaWidget = new VideoArea(this);
114 videoAreaWidget->setListModel(playlistModel);
116 loadingWidget = new LoadingWidget(this);
117 videoAreaWidget->setLoadingWidget(loadingWidget);
119 splitter->addWidget(videoAreaWidget);
121 // restore splitter state
123 if (settings.contains("splitter"))
124 splitter->restoreState(settings.value("splitter").toByteArray());
126 int sidebarDefaultWidth = 180;
127 splitter->setSizes(QList<int>() << sidebarDefaultWidth
128 << splitter->size().width() - sidebarDefaultWidth);
130 splitter->setChildrenCollapsible(false);
131 connect(splitter, SIGNAL(splitterMoved(int, int)), SLOT(adjustWindowSize()));
133 errorTimer = new QTimer(this);
134 errorTimer->setSingleShot(true);
135 errorTimer->setInterval(3000);
136 connect(errorTimer, SIGNAL(timeout()), SLOT(skipVideo()));
138 #ifdef APP_ACTIVATION
139 demoTimer = new QTimer(this);
140 demoTimer->setSingleShot(true);
142 demoTimer, &QTimer::timeout, this,
144 if (media->state() != Media::PlayingState) return;
147 ActivationView::instance(), &ActivationView::done, media,
148 [this] { media->play(); }, Qt::UniqueConnection);
149 MainWindow::instance()->showActivationView();
151 Qt::QueuedConnection);
154 connect(videoAreaWidget, SIGNAL(doubleClicked()), mainWindow->getAction("fullscreen"),
157 QAction *refineSearchAction = mainWindow->getAction("refineSearch");
158 connect(refineSearchAction, SIGNAL(toggled(bool)), sidebar, SLOT(toggleRefineSearch(bool)));
160 const QVector<const char *> videoActionNames = {
164 "webpage", "pagelink", "videolink", "openInBrowser", "findVideoParts",
165 "skip", "previous", "stopafterthis", "relatedVideos", "refineSearch",
166 "twitter", "facebook", "email"};
167 currentVideoActions.reserve(videoActionNames.size());
168 for (auto *name : videoActionNames) {
169 currentVideoActions.append(mainWindow->getAction(name));
172 for (int i = 0; i < 10; ++i) {
173 QAction *action = new QAction(QString());
174 action->setShortcut(Qt::Key_0 + i);
175 action->setAutoRepeat(false);
176 connect(action, &QAction::triggered, this, [this, i] {
177 qint64 duration = media->duration();
178 // dur : pos = 100 : i*10
179 qint64 position = (duration * (i * 10)) / 100;
180 media->seek(position);
183 playingVideoActions << action;
186 QAction *leftAction = new QAction(tr("Rewind %1 seconds").arg(10));
187 leftAction->setShortcut(Qt::Key_Left);
188 leftAction->setAutoRepeat(false);
189 connect(leftAction, &QAction::triggered, this, [this] {
190 qint64 position = media->position();
192 if (position < 0) position = 0;
193 media->seek(position);
195 addAction(leftAction);
196 playingVideoActions << leftAction;
198 QAction *rightAction = new QAction(tr("Fast forward %1 seconds").arg(10));
199 rightAction->setShortcut(Qt::Key_Right);
200 rightAction->setAutoRepeat(false);
201 connect(rightAction, &QAction::triggered, this, [this] {
202 qint64 position = media->position();
204 qint64 duration = media->duration();
205 if (position > duration) position = duration;
206 media->seek(position);
208 addAction(rightAction);
209 playingVideoActions << rightAction;
212 void MediaView::setMedia(Media *media) {
215 videoWidget = media->videoWidget();
216 videoAreaWidget->setVideoWidget(videoWidget);
218 connect(media, &Media::finished, this, &MediaView::onPlaybackFinished);
219 connect(media, &Media::stateChanged, this, &MediaView::mediaStateChanged);
220 connect(media, &Media::aboutToFinish, this, &MediaView::onAboutToFinish);
221 connect(media, &Media::bufferStatus, loadingWidget, &LoadingWidget::bufferStatus);
224 SearchParams *MediaView::getSearchParams() {
225 VideoSource *videoSource = playlistModel->getVideoSource();
226 if (!videoSource) return nullptr;
227 auto clazz = videoSource->metaObject()->className();
228 if (clazz == QLatin1String("YTSearch")) {
229 auto search = qobject_cast<YTSearch *>(videoSource);
230 return search->getSearchParams();
232 if (clazz == QLatin1String("IVSearch")) {
233 auto search = qobject_cast<IVSearch *>(videoSource);
234 return search->getSearchParams();
236 if (clazz == QLatin1String("IVChannelSource")) {
237 auto search = qobject_cast<IVChannelSource *>(videoSource);
238 return search->getSearchParams();
243 void MediaView::search(SearchParams *searchParams) {
244 if (!searchParams->keywords().isEmpty()) {
245 if (searchParams->keywords().startsWith("http://") ||
246 searchParams->keywords().startsWith("https://")) {
247 QString videoId = YTSearch::videoIdFromUrl(searchParams->keywords());
248 if (!videoId.isEmpty()) {
249 VideoSource *singleVideoSource = nullptr;
250 if (VideoAPI::impl() == VideoAPI::YT3) {
251 auto source = new YTSingleVideoSource(this);
252 source->setVideoId(videoId);
253 singleVideoSource = source;
254 } else if (VideoAPI::impl() == VideoAPI::IV) {
255 auto source = new IVSingleVideoSource(this);
256 source->setVideoId(videoId);
257 singleVideoSource = source;
259 setVideoSource(singleVideoSource);
261 QTime tstamp = YTSearch::videoTimestampFromUrl(searchParams->keywords());
262 pauseTime = QTime(0, 0).msecsTo(tstamp);
268 VideoSource *search = nullptr;
269 if (VideoAPI::impl() == VideoAPI::YT3) {
270 YTSearch *ytSearch = new YTSearch(searchParams);
271 ytSearch->setAsyncDetails(true);
272 connect(ytSearch, SIGNAL(gotDetails()), playlistModel, SLOT(emitDataChanged()));
274 } else if (VideoAPI::impl() == VideoAPI::IV) {
275 if (searchParams->channelId().isEmpty()) {
276 search = new IVSearch(searchParams);
278 search = new IVChannelSource(searchParams);
281 setVideoSource(search);
284 void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool back) {
289 // qDebug() << "Adding VideoSource" << videoSource->getName() << videoSource;
292 int currentIndex = getHistoryIndex();
293 if (currentIndex >= 0 && currentIndex < history.size() - 1) {
294 while (history.size() > currentIndex + 1) {
295 VideoSource *vs = history.takeLast();
297 qDebug() << "Deleting VideoSource" << vs->getName() << vs;
302 history.append(videoSource);
306 if (history.size() > 1)
307 Extra::slideTransition(playlistView->viewport(), playlistView->viewport(), back);
310 playlistModel->setVideoSource(videoSource);
312 if (media->state() == Media::StoppedState) {
314 if (settings.value("manualplay", false).toBool()) {
315 videoAreaWidget->showPickMessage();
319 SearchParams *searchParams = getSearchParams();
321 sidebar->showPlaylist();
322 sidebar->getRefineSearchWidget()->setSearchParams(searchParams);
323 sidebar->hideSuggestions();
324 sidebar->getHeader()->updateInfo();
326 bool isChannel = searchParams && !searchParams->channelId().isEmpty();
328 updateSubscriptionActionForChannel(searchParams->channelId());
330 playlistView->setClickableAuthors(!isChannel);
333 void MediaView::searchAgain() {
334 VideoSource *currentVideoSource = playlistModel->getVideoSource();
335 setVideoSource(currentVideoSource, false);
338 bool MediaView::canGoBack() {
339 return getHistoryIndex() > 0;
342 void MediaView::goBack() {
343 if (history.size() > 1) {
344 int currentIndex = getHistoryIndex();
345 if (currentIndex > 0) {
346 VideoSource *previousVideoSource = history.at(currentIndex - 1);
347 setVideoSource(previousVideoSource, false, true);
352 bool MediaView::canGoForward() {
353 int currentIndex = getHistoryIndex();
354 return currentIndex >= 0 && currentIndex < history.size() - 1;
357 void MediaView::goForward() {
358 if (canGoForward()) {
359 int currentIndex = getHistoryIndex();
360 VideoSource *nextVideoSource = history.at(currentIndex + 1);
361 setVideoSource(nextVideoSource, false);
365 int MediaView::getHistoryIndex() {
366 return history.lastIndexOf(playlistModel->getVideoSource());
369 void MediaView::appear() {
370 MainWindow::instance()->showToolbar();
372 Video *currentVideo = playlistModel->activeVideo();
374 MainWindow::instance()->setWindowTitle(currentVideo->getTitle() + " - " + Constants::NAME);
377 playlistView->setFocus();
380 void MediaView::disappear() {
381 MainWindow::instance()->hideToolbar();
384 void MediaView::handleError(const QString &message) {
385 qWarning() << __PRETTY_FUNCTION__ << message;
386 #ifndef QT_NO_DEBUG_OUTPUT
387 MainWindow::instance()->showMessage(message);
391 void MediaView::mediaStateChanged(Media::State state) {
392 if (pauseTime > 0 && (state == Media::PlayingState || state == Media::BufferingState)) {
393 qDebug() << "Seeking to" << pauseTime;
394 media->seek(pauseTime);
397 if (state == Media::PlayingState) {
398 videoAreaWidget->showVideo();
399 } else if (state == Media::ErrorState) {
400 handleError(media->errorString());
403 bool enablePlayingVideoActions = state == Media::PlayingState || state == Media::PausedState;
404 for (QAction *action : qAsConst(playingVideoActions))
405 action->setEnabled(enablePlayingVideoActions);
407 if (state == Media::PlayingState) {
408 bool res = Idle::preventDisplaySleep(QString("%1 is playing").arg(Constants::NAME));
409 if (!res) qWarning() << "Error disabling idle display sleep" << Idle::displayErrorMessage();
410 } else if (state == Media::PausedState || state == Media::StoppedState) {
411 bool res = Idle::allowDisplaySleep();
412 if (!res) qWarning() << "Error enabling idle display sleep" << Idle::displayErrorMessage();
416 void MediaView::pause() {
417 switch (media->state()) {
418 case Media::PlayingState:
423 if (pauseTimer.hasExpired(60000)) {
424 pauseTimer.invalidate();
425 connect(playlistModel->activeVideo(), &Video::gotStreamUrl, this,
426 &MediaView::resumeWithNewStreamUrl);
427 playlistModel->activeVideo()->loadStreamUrl();
434 QRegExp MediaView::wordRE(const QString &s) {
435 return QRegExp("\\W" + s + "\\W?", Qt::CaseInsensitive);
438 void MediaView::stop() {
441 while (!history.isEmpty()) {
442 VideoSource *videoSource = history.takeFirst();
443 // Don't delete videoSource in the Browse view
444 if (!videoSource->parent()) {
445 videoSource->deleteLater();
449 playlistModel->abortSearch();
450 videoAreaWidget->clear();
451 videoAreaWidget->update();
453 playlistView->selectionModel()->clearSelection();
455 MainWindow::instance()->getAction("refineSearch")->setChecked(false);
456 updateSubscriptionActionForVideo(nullptr, false);
457 #ifdef APP_ACTIVATION
461 for (QAction *action : currentVideoActions)
462 action->setEnabled(false);
464 QAction *a = MainWindow::instance()->getAction("download");
465 a->setEnabled(false);
466 a->setVisible(false);
470 currentVideoId.clear();
473 if (snapshotSettings) {
474 delete snapshotSettings;
475 snapshotSettings = nullptr;
480 const QString &MediaView::getCurrentVideoId() {
481 return currentVideoId;
484 void MediaView::activeVideoChanged(Video *video, Video *previousVideo) {
490 if (previousVideo && previousVideo != video) {
491 if (previousVideo->isLoadingStreamUrl()) previousVideo->abortLoadStreamUrl();
494 // optimize window for 16:9 video
497 videoAreaWidget->showLoading(video);
499 connect(video, &Video::gotStreamUrl, this, &MediaView::gotStreamUrl, Qt::UniqueConnection);
500 connect(video, SIGNAL(errorStreamUrl(QString)), SLOT(skip()), Qt::UniqueConnection);
501 video->loadStreamUrl();
503 // video title in titlebar
504 MainWindow::instance()->setWindowTitle(video->getTitle() + QLatin1String(" - ") +
505 QLatin1String(Constants::NAME));
507 // ensure active item is visible
508 int row = playlistModel->rowForVideo(video);
510 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
511 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
514 // enable/disable actions
515 MainWindow::instance()
516 ->getAction("download")
517 ->setEnabled(DownloadManager::instance()->itemForVideo(video) == nullptr);
518 MainWindow::instance()->getAction("previous")->setEnabled(row > 0);
519 MainWindow::instance()->getAction("stopafterthis")->setEnabled(true);
520 MainWindow::instance()->getAction("relatedVideos")->setEnabled(true);
522 bool enableDownload = video->getLicense() == Video::LicenseCC;
523 #ifdef APP_ACTIVATION
524 enableDownload = enableDownload || Activation::instance().isLegacy();
527 enableDownload = true;
529 QAction *a = MainWindow::instance()->getAction("download");
530 a->setEnabled(enableDownload);
531 a->setVisible(enableDownload);
533 updateSubscriptionActionForVideo(video, YTChannel::isSubscribed(video->getChannelId()));
535 for (QAction *action : currentVideoActions)
536 action->setEnabled(true);
539 if (snapshotSettings) {
540 delete snapshotSettings;
541 snapshotSettings = nullptr;
542 MainWindow::instance()->adjustStatusBarVisibility();
546 // see you in gotStreamUrl...
549 void MediaView::gotStreamUrl(const QString &streamUrl, const QString &audioUrl) {
551 if (streamUrl.isEmpty()) {
552 qWarning() << "Empty stream url";
557 Video *video = static_cast<Video *>(sender());
559 qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
562 video->disconnect(this);
564 currentVideoId = video->getId();
566 if (audioUrl.isEmpty()) {
567 qDebug() << "Playing" << streamUrl;
568 media->play(streamUrl);
570 qDebug() << "Playing" << streamUrl << audioUrl;
571 media->playSeparateAudioAndVideo(streamUrl, audioUrl);
574 // ensure we always have videos ahead
575 playlistModel->searchNeeded();
577 // ensure active item is visible
578 int row = playlistModel->activeRow();
580 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
581 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
584 #ifdef APP_ACTIVATION
585 if (!demoTimer->isActive() && !Activation::instance().isActivated()) {
586 int ms = (60000 * 2) + (QRandomGenerator::global()->generate() % (60000 * 2));
587 demoTimer->start(ms);
592 Extra::notify(video->getTitle(), video->getChannelTitle(), video->getFormattedDuration());
595 ChannelAggregator::instance()->videoWatched(video);
598 void MediaView::onItemActivated(const QModelIndex &index) {
599 if (playlistModel->rowExists(index.row())) {
600 // if it's the current video, just rewind and play
601 Video *activeVideo = playlistModel->activeVideo();
602 Video *video = playlistModel->videoAt(index.row());
603 if (activeVideo && video && activeVideo == video) {
606 playlistModel->setActiveRow(index.row());
608 // the user doubleclicked on the "Search More" item
610 playlistModel->searchMore();
611 playlistView->selectionModel()->clearSelection();
615 void MediaView::skipVideo() {
616 // skippedVideo is useful for DELAYED skip operations
617 // in order to be sure that we're skipping the video we wanted
618 // and not another one
620 if (playlistModel->activeVideo() != skippedVideo) {
621 qDebug() << "Skip of video canceled";
624 int nextRow = playlistModel->rowForVideo(skippedVideo);
626 if (nextRow == -1) return;
627 playlistModel->setActiveRow(nextRow);
631 void MediaView::skip() {
632 int nextRow = playlistModel->nextRow();
633 if (nextRow == -1) return;
634 playlistModel->setActiveRow(nextRow);
637 void MediaView::skipBackward() {
638 int prevRow = playlistModel->previousRow();
639 if (prevRow == -1) return;
640 playlistModel->setActiveRow(prevRow);
643 void MediaView::onAboutToFinish() {
644 qint64 currentTime = media->position();
645 qint64 totalTime = media->duration();
646 // qDebug() << __PRETTY_FUNCTION__ << currentTime << totalTime;
647 if (totalTime < 1 || currentTime + 10000 < totalTime) {
648 // QTimer::singleShot(500, this, SLOT(playbackResume()));
649 media->seek(currentTime);
654 void MediaView::onPlaybackFinished() {
657 const qint64 totalTime = media->duration();
658 const qint64 currentTime = media->position();
659 // qDebug() << __PRETTY_FUNCTION__ << mediaObject->currentTime() << totalTime;
660 // add 10 secs for imprecise Phonon backends (VLC, Xine)
661 if (currentTime > 0 && currentTime + 10000 < totalTime) {
662 // mediaObject->seek(currentTime);
663 QTimer::singleShot(500, this, SLOT(resumePlayback()));
665 QAction *stopAfterThisAction = MainWindow::instance()->getAction("stopafterthis");
666 if (stopAfterThisAction->isChecked()) {
667 stopAfterThisAction->setChecked(false);
673 void MediaView::resumePlayback() {
675 const qint64 currentTime = media->position();
676 // qDebug() << __PRETTY_FUNCTION__ << currentTime;
677 if (currentTime > 0) media->seek(currentTime);
681 void MediaView::openWebPage() {
682 Video *video = playlistModel->activeVideo();
686 video->getWebpage() + QLatin1String("&t=") + QString::number(media->position() / 1000);
687 QDesktopServices::openUrl(url);
690 void MediaView::copyWebPage() {
691 Video *video = playlistModel->activeVideo();
693 QString address = video->getWebpage();
694 QApplication::clipboard()->setText(address);
695 QString message = tr("You can now paste the YouTube link into another application");
696 MainWindow::instance()->showMessage(message);
699 void MediaView::copyVideoLink() {
700 Video *video = playlistModel->activeVideo();
702 QApplication::clipboard()->setText(video->getStreamUrl());
703 QString message = tr("You can now paste the video stream URL into another application") + ". " +
704 tr("The link will be valid only for a limited time.");
705 MainWindow::instance()->showMessage(message);
708 void MediaView::openInBrowser() {
709 Video *video = playlistModel->activeVideo();
712 QDesktopServices::openUrl(video->getStreamUrl());
715 void MediaView::removeSelected() {
716 if (!playlistView->selectionModel()->hasSelection()) return;
717 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
718 playlistModel->removeIndexes(indexes);
721 void MediaView::selectVideos(const QVector<Video *> &videos) {
722 for (Video *video : videos) {
723 QModelIndex index = playlistModel->indexForVideo(video);
724 playlistView->selectionModel()->select(index, QItemSelectionModel::Select);
725 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
729 void MediaView::selectionChanged(const QItemSelection & /*selected*/,
730 const QItemSelection & /*deselected*/) {
731 const bool gotSelection = playlistView->selectionModel()->hasSelection();
732 MainWindow::instance()->getAction("remove")->setEnabled(gotSelection);
733 MainWindow::instance()->getAction("moveUp")->setEnabled(gotSelection);
734 MainWindow::instance()->getAction("moveDown")->setEnabled(gotSelection);
737 void MediaView::moveUpSelected() {
738 if (!playlistView->selectionModel()->hasSelection()) return;
740 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
741 qStableSort(indexes.begin(), indexes.end());
742 playlistModel->move(indexes, true);
744 // set current index after row moves to something more intuitive
745 int row = indexes.at(0).row();
746 playlistView->selectionModel()->setCurrentIndex(playlistModel->index(row > 1 ? row : 1),
747 QItemSelectionModel::NoUpdate);
750 void MediaView::moveDownSelected() {
751 if (!playlistView->selectionModel()->hasSelection()) return;
753 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
754 qStableSort(indexes.begin(), indexes.end(), qGreater<QModelIndex>());
755 playlistModel->move(indexes, false);
757 // set current index after row moves to something more intuitive
758 // (respect 1 static item on bottom)
759 int row = indexes.at(0).row() + 1, max = playlistModel->rowCount() - 2;
760 playlistView->selectionModel()->setCurrentIndex(playlistModel->index(row > max ? max : row),
761 QItemSelectionModel::NoUpdate);
764 void MediaView::setSidebarVisibility(bool visible) {
765 if (sidebar->isVisible() == visible) return;
766 sidebar->setVisible(visible);
769 sidebar->resize(sidebar->width(), window()->height());
771 playlistView->setFocus();
775 void MediaView::removeSidebar() {
777 sidebar->setParent(window());
780 void MediaView::restoreSidebar() {
782 splitter->insertWidget(0, sidebar);
785 bool MediaView::isSidebarVisible() {
786 return sidebar->isVisible();
789 void MediaView::saveSplitterState() {
791 if (splitter) settings.setValue("splitter", splitter->saveState());
794 void MediaView::downloadVideo() {
795 Video *video = playlistModel->activeVideo();
797 DownloadManager::instance()->addItem(video);
798 MainWindow::instance()->showActionsInStatusBar({MainWindow::instance()->getAction("downloads")},
800 QString message = tr("Downloading %1").arg(video->getTitle());
801 MainWindow::instance()->showMessage(message);
805 void MediaView::snapshot() {
806 qint64 currentTime = media->position() / 1000;
808 QObject *context = new QObject();
809 connect(media, &Media::snapshotReady, context,
810 [this, currentTime, context](const QImage &image) {
811 context->deleteLater();
813 if (image.isNull()) {
814 qWarning() << "Null snapshot";
818 QPixmap pixmap = QPixmap::fromImage(image.scaled(
819 videoWidget->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
820 videoAreaWidget->showSnapshotPreview(pixmap);
822 Video *video = playlistModel->activeVideo();
825 QString location = SnapshotSettings::getCurrentLocation();
827 if (!dir.exists()) dir.mkpath(location);
828 QString basename = video->getTitle();
829 QString format = video->getDuration() > 3600 ? "h_mm_ss" : "m_ss";
830 basename += " (" + QTime(0, 0, 0).addSecs(currentTime).toString(format) + ")";
831 basename = DataUtils::stringToFilename(basename);
832 QString filename = location + "/" + basename + ".png";
833 qDebug() << filename;
834 image.save(filename, "PNG");
836 if (snapshotSettings) delete snapshotSettings;
837 snapshotSettings = new SnapshotSettings(videoWidget);
838 snapshotSettings->setSnapshot(pixmap, filename);
839 QStatusBar *statusBar = MainWindow::instance()->statusBar();
841 Extra::fadeInWidget(statusBar, statusBar);
843 statusBar->insertPermanentWidget(0, snapshotSettings);
844 snapshotSettings->show();
845 MainWindow::instance()->setStatusBarVisibility(true);
853 void MediaView::fullscreen() {
854 videoAreaWidget->setParent(nullptr);
855 videoAreaWidget->showFullScreen();
858 void MediaView::resumeWithNewStreamUrl(const QString &streamUrl, const QString &audioUrl) {
859 pauseTime = media->position();
861 if (audioUrl.isEmpty()) {
862 qDebug() << "Playing" << streamUrl;
863 media->play(streamUrl);
865 qDebug() << "Playing" << streamUrl << audioUrl;
866 media->playSeparateAudioAndVideo(streamUrl, audioUrl);
869 Video *video = static_cast<Video *>(sender());
871 qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
874 video->disconnect(this);
877 void MediaView::findVideoParts() {
878 Video *video = playlistModel->activeVideo();
881 QString query = video->getTitle();
883 const QLatin1String optionalSpace("\\s*");
884 const QLatin1String staticCounterSeparators("[\\/\\-]");
885 const QString counterSeparators =
886 QLatin1String("( of | ") + tr("of", "Used in video parts, as in '2 of 3'") +
887 QLatin1String(" |") + staticCounterSeparators + QLatin1String(")");
889 // numbers from 1 to 15
890 const QLatin1String counterNumber("([1-9]|1[0-5])");
892 // query.remove(QRegExp(counterSeparators + optionalSpace + counterNumber));
893 query.remove(QRegExp(counterNumber + optionalSpace + counterSeparators + optionalSpace +
895 query.remove(wordRE("pr?t\\.?" + optionalSpace + counterNumber));
896 query.remove(wordRE("ep\\.?" + optionalSpace + counterNumber));
897 query.remove(wordRE("part" + optionalSpace + counterNumber));
898 query.remove(wordRE("episode" + optionalSpace + counterNumber));
899 query.remove(wordRE(tr("part", "This is for video parts, as in 'Cool video - part 1'") +
900 optionalSpace + counterNumber));
901 query.remove(wordRE(tr("episode", "This is for video parts, as in 'Cool series - episode 1'") +
902 optionalSpace + counterNumber));
903 query.remove(QRegExp("[\\(\\)\\[\\]]"));
905 #define NUMBERS "one|two|three|four|five|six|seven|eight|nine|ten"
907 QRegExp englishNumberRE = QRegExp(QLatin1String(".*(") + NUMBERS + ").*", Qt::CaseInsensitive);
908 // bool numberAsWords = englishNumberRE.exactMatch(query);
909 query.remove(englishNumberRE);
911 QRegExp localizedNumberRE =
912 QRegExp(QLatin1String(".*(") + tr(NUMBERS) + ").*", Qt::CaseInsensitive);
913 // if (!numberAsWords) numberAsWords = localizedNumberRE.exactMatch(query);
914 query.remove(localizedNumberRE);
916 SearchParams *searchParams = new SearchParams();
917 searchParams->setTransient(true);
918 searchParams->setKeywords(query);
919 searchParams->setChannelId(video->getChannelId());
922 if (!numberAsWords) {
923 qDebug() << "We don't have number as words";
924 // searchParams->setSortBy(SearchParams::SortByNewest);
925 // TODO searchParams->setReverseOrder(true);
926 // TODO searchParams->setMax(50);
930 search(searchParams);
933 void MediaView::relatedVideos() {
934 Video *video = playlistModel->activeVideo();
937 if (VideoAPI::impl() == VideoAPI::YT3) {
938 YTSingleVideoSource *singleVideoSource = new YTSingleVideoSource();
939 singleVideoSource->setVideo(video->clone());
940 singleVideoSource->setAsyncDetails(true);
941 setVideoSource(singleVideoSource);
942 } else if (VideoAPI::impl() == VideoAPI::IV) {
943 auto source = new IVSingleVideoSource(this);
944 source->setVideo(video->clone());
945 setVideoSource(source);
948 MainWindow::instance()->getAction("relatedVideos")->setEnabled(false);
951 void MediaView::shareViaTwitter() {
952 Video *video = playlistModel->activeVideo();
954 QUrl url("https://twitter.com/intent/tweet");
956 q.addQueryItem("via", "minitubeapp");
957 q.addQueryItem("text", video->getTitle());
958 q.addQueryItem("url", video->getWebpage());
960 QDesktopServices::openUrl(url);
963 void MediaView::shareViaFacebook() {
964 Video *video = playlistModel->activeVideo();
966 QUrl url("https://www.facebook.com/sharer.php");
968 q.addQueryItem("t", video->getTitle());
969 q.addQueryItem("u", video->getWebpage());
971 QDesktopServices::openUrl(url);
974 void MediaView::shareViaEmail() {
975 Video *video = playlistModel->activeVideo();
979 q.addQueryItem("subject", video->getTitle());
980 const QString body = video->getTitle() + "\n" + video->getWebpage() + "\n\n" +
981 tr("Sent from %1").arg(Constants::NAME) + "\n" + Constants::WEBSITE;
982 q.addQueryItem("body", body);
984 QDesktopServices::openUrl(url);
987 void MediaView::onAuthorPushed(QModelIndex index) {
988 Video *video = playlistModel->videoAt(index.row());
991 QString channelId = video->getChannelId();
992 // if (channelId.isEmpty()) channelId = video->channelTitle();
993 if (channelId.isEmpty()) return;
995 SearchParams *searchParams = new SearchParams();
996 searchParams->setChannelId(channelId);
997 searchParams->setSortBy(SearchParams::SortByNewest);
1000 search(searchParams);
1004 void MediaView::updateSubscriptionAction(bool subscribed) {
1005 QAction *subscribeAction = MainWindow::instance()->getAction("subscribeChannel");
1007 QString subscribeTip;
1008 QString subscribeText;
1010 if (currentSubscriptionChannelId.isEmpty()) {
1011 subscribeText = subscribeAction->property("originalText").toString();
1012 subscribeAction->setEnabled(false);
1013 } else if (subscribed) {
1014 subscribeText = tr("Unsubscribe from %1").arg(currentSubscriptionChannelTitle);
1015 subscribeTip = subscribeText;
1016 subscribeAction->setEnabled(true);
1018 subscribeText = tr("Subscribe to %1").arg(currentSubscriptionChannelTitle);
1019 subscribeTip = subscribeText;
1020 subscribeAction->setEnabled(true);
1022 subscribeAction->setText(subscribeText);
1023 subscribeAction->setStatusTip(subscribeTip);
1026 subscribeAction->setIcon(IconUtils::icon("bookmark-remove"));
1028 subscribeAction->setIcon(IconUtils::icon("bookmark-new"));
1031 MainWindow::instance()->setupAction(subscribeAction);
1034 void MediaView::updateSubscriptionActionForChannel(const QString & channelId) {
1035 QString channelTitle = tr("channel");
1036 YTChannel *channel = YTChannel::forId(channelId);
1037 if (nullptr != channel && !channel->getDisplayName().isEmpty()) {
1038 channelTitle = channel->getDisplayName();
1041 bool subscribed = YTChannel::isSubscribed(channelId);
1043 currentSubscriptionChannelId = channelId;
1044 currentSubscriptionChannelTitle = channelTitle;
1045 updateSubscriptionAction(subscribed);
1048 void MediaView::updateSubscriptionActionForVideo(Video *video, bool subscribed) {
1050 currentSubscriptionChannelId = "";
1051 currentSubscriptionChannelTitle = "";
1052 updateSubscriptionAction(false);
1054 currentSubscriptionChannelId = video->getChannelId();
1055 currentSubscriptionChannelTitle = video->getChannelTitle();
1056 updateSubscriptionAction(subscribed);
1060 void MediaView::reloadCurrentVideo() {
1061 Video *video = playlistModel->activeVideo();
1064 int oldFormat = video->getDefinitionCode();
1066 QObject *context = new QObject();
1067 connect(video, &Video::gotStreamUrl, context,
1068 [this, oldFormat, video, context](const QString &videoUrl, const QString &audioUrl) {
1069 context->deleteLater();
1070 if (oldFormat == video->getDefinitionCode()) return;
1071 QObject *context2 = new QObject();
1072 const qint64 position = media->position();
1073 connect(media, &Media::stateChanged, context2,
1074 [position, this, context2](Media::State state) {
1075 if (state == Media::PlayingState) {
1076 media->seek(position);
1077 context2->deleteLater();
1078 Video *video = playlistModel->activeVideo();
1079 QString msg = tr("Switched to %1")
1080 .arg(VideoDefinition::forCode(
1081 video->getDefinitionCode())
1083 MainWindow::instance()->showMessage(msg);
1087 if (audioUrl.isEmpty()) {
1088 media->play(videoUrl);
1090 media->playSeparateAudioAndVideo(videoUrl, audioUrl);
1093 video->loadStreamUrl();
1096 void MediaView::toggleSubscription() {
1097 //Video *video = playlistModel->activeVideo();
1098 if (currentSubscriptionChannelId.isEmpty()) {
1102 bool subscribed = YTChannel::isSubscribed(currentSubscriptionChannelId);
1104 YTChannel::unsubscribe(currentSubscriptionChannelId);
1105 MainWindow::instance()->showMessage(
1106 tr("Unsubscribed from %1").arg(currentSubscriptionChannelTitle));
1108 YTChannel::subscribe(currentSubscriptionChannelId);
1109 MainWindow::instance()->showMessage(tr("Subscribed to %1").arg(currentSubscriptionChannelTitle));
1112 updateSubscriptionAction(!subscribed);
1115 void MediaView::adjustWindowSize() {
1116 qDebug() << "Adjusting window size";
1117 Video *video = playlistModel->activeVideo();
1119 QWidget *window = this->window();
1120 if (!window->isMaximized() && !window->isFullScreen()) {
1121 const double ratio = 16. / 9.;
1122 const double w = (double)videoAreaWidget->width();
1123 const double h = (double)videoAreaWidget->height();
1124 const double currentVideoRatio = w / h;
1125 if (currentVideoRatio != ratio) {
1126 qDebug() << "Adjust size";
1127 int newHeight = std::round((window->height() - h) + (w / ratio));
1128 window->resize(window->width(), newHeight);