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"
41 #include "channelaggregator.h"
42 #include "iconutils.h"
43 #include "searchparams.h"
44 #include "videosource.h"
45 #include "ytchannel.h"
47 #include "ytsinglevideosource.h"
49 #include "snapshotsettings.h"
51 #include "datautils.h"
53 #include "videodefinition.h"
55 MediaView *MediaView::instance() {
56 static MediaView *i = new MediaView();
60 MediaView::MediaView(QWidget *parent)
61 : View(parent), splitter(nullptr), stopped(false)
64 snapshotSettings(nullptr)
70 void MediaView::initialize() {
71 MainWindow *mainWindow = MainWindow::instance();
73 QBoxLayout *layout = new QVBoxLayout(this);
76 splitter = new MiniSplitter();
77 layout->addWidget(splitter);
79 playlistView = new PlaylistView();
80 playlistView->setParent(this);
81 connect(playlistView, SIGNAL(activated(const QModelIndex &)),
82 SLOT(onItemActivated(const QModelIndex &)));
84 playlistModel = new PlaylistModel();
85 connect(playlistModel, &PlaylistModel::activeVideoChanged, this,
86 &MediaView::activeVideoChanged);
87 // needed to restore the selection after dragndrop
88 connect(playlistModel, SIGNAL(needSelectionFor(QVector<Video *>)),
89 SLOT(selectVideos(QVector<Video *>)));
90 playlistView->setModel(playlistModel);
92 connect(playlistView->selectionModel(),
93 SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
94 SLOT(selectionChanged(const QItemSelection &, const QItemSelection &)));
96 connect(playlistView, SIGNAL(authorPushed(QModelIndex)), SLOT(onAuthorPushed(QModelIndex)));
98 sidebar = new SidebarWidget(this);
99 sidebar->setPlaylist(playlistView);
100 sidebar->setMaximumWidth(playlistView->minimumWidth() * 3);
101 connect(sidebar->getRefineSearchWidget(), SIGNAL(searchRefined()), SLOT(searchAgain()));
102 connect(playlistModel, SIGNAL(haveSuggestions(const QStringList &)), sidebar,
103 SLOT(showSuggestions(const QStringList &)));
104 connect(sidebar, SIGNAL(suggestionAccepted(QString)), mainWindow, SLOT(search(QString)));
105 splitter->addWidget(sidebar);
107 videoAreaWidget = new VideoArea(this);
108 videoAreaWidget->setListModel(playlistModel);
110 loadingWidget = new LoadingWidget(this);
111 videoAreaWidget->setLoadingWidget(loadingWidget);
113 splitter->addWidget(videoAreaWidget);
115 // restore splitter state
117 if (settings.contains("splitter"))
118 splitter->restoreState(settings.value("splitter").toByteArray());
120 int sidebarDefaultWidth = 180;
121 splitter->setSizes(QList<int>() << sidebarDefaultWidth
122 << splitter->size().width() - sidebarDefaultWidth);
124 splitter->setChildrenCollapsible(false);
125 connect(splitter, SIGNAL(splitterMoved(int, int)), SLOT(adjustWindowSize()));
127 errorTimer = new QTimer(this);
128 errorTimer->setSingleShot(true);
129 errorTimer->setInterval(3000);
130 connect(errorTimer, SIGNAL(timeout()), SLOT(skipVideo()));
132 #ifdef APP_ACTIVATION
133 demoTimer = new QTimer(this);
134 demoTimer->setSingleShot(true);
135 connect(demoTimer, &QTimer::timeout, mainWindow, &MainWindow::showActivationView,
136 Qt::QueuedConnection);
139 connect(videoAreaWidget, SIGNAL(doubleClicked()), mainWindow->getAction("fullscreen"),
142 QAction *refineSearchAction = mainWindow->getAction("refineSearch");
143 connect(refineSearchAction, SIGNAL(toggled(bool)), sidebar, SLOT(toggleRefineSearch(bool)));
145 const QVector<const char *> videoActionNames = {
149 "webpage", "pagelink", "videolink", "openInBrowser", "findVideoParts",
150 "skip", "previous", "stopafterthis", "relatedVideos", "refineSearch",
151 "twitter", "facebook", "email"};
152 currentVideoActions.reserve(videoActionNames.size());
153 for (auto *name : videoActionNames) {
154 currentVideoActions.append(mainWindow->getAction(name));
158 void MediaView::setMedia(Media *media) {
161 videoWidget = media->videoWidget();
162 videoAreaWidget->setVideoWidget(videoWidget);
164 connect(media, &Media::finished, this, &MediaView::onPlaybackFinished);
165 connect(media, &Media::stateChanged, this, &MediaView::mediaStateChanged);
166 connect(media, &Media::aboutToFinish, this, &MediaView::onAboutToFinish);
167 connect(media, &Media::bufferStatus, loadingWidget, &LoadingWidget::bufferStatus);
170 SearchParams *MediaView::getSearchParams() {
171 VideoSource *videoSource = playlistModel->getVideoSource();
172 if (videoSource && videoSource->metaObject()->className() == QLatin1String("YTSearch")) {
173 YTSearch *search = qobject_cast<YTSearch *>(videoSource);
174 return search->getSearchParams();
179 void MediaView::search(SearchParams *searchParams) {
180 if (!searchParams->keywords().isEmpty()) {
181 if (searchParams->keywords().startsWith("http://") ||
182 searchParams->keywords().startsWith("https://")) {
183 QString videoId = YTSearch::videoIdFromUrl(searchParams->keywords());
184 if (!videoId.isEmpty()) {
185 YTSingleVideoSource *singleVideoSource = new YTSingleVideoSource(this);
186 singleVideoSource->setVideoId(videoId);
187 setVideoSource(singleVideoSource);
188 QTime tstamp = YTSearch::videoTimestampFromUrl(searchParams->keywords());
189 pauseTime = QTime(0, 0).msecsTo(tstamp);
194 YTSearch *ytSearch = new YTSearch(searchParams);
195 ytSearch->setAsyncDetails(true);
196 connect(ytSearch, SIGNAL(gotDetails()), playlistModel, SLOT(emitDataChanged()));
197 setVideoSource(ytSearch);
200 void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool back) {
205 // qDebug() << "Adding VideoSource" << videoSource->getName() << videoSource;
207 YTSearch * ytSearch = qobject_cast<YTSearch *>(videoSource);
208 if (nullptr != ytSearch) {
209 if (!ytSearch->getSearchParams()->channelId().isEmpty()) {
210 updateSubscriptionActionForChannel(ytSearch->getSearchParams()->channelId());
215 int currentIndex = getHistoryIndex();
216 if (currentIndex >= 0 && currentIndex < history.size() - 1) {
217 while (history.size() > currentIndex + 1) {
218 VideoSource *vs = history.takeLast();
220 qDebug() << "Deleting VideoSource" << vs->getName() << vs;
225 history.append(videoSource);
229 if (history.size() > 1)
230 Extra::slideTransition(playlistView->viewport(), playlistView->viewport(), back);
233 playlistModel->setVideoSource(videoSource);
235 if (media->state() == Media::StoppedState) {
237 if (settings.value("manualplay", false).toBool()) {
238 videoAreaWidget->showPickMessage();
242 sidebar->showPlaylist();
243 sidebar->getRefineSearchWidget()->setSearchParams(getSearchParams());
244 sidebar->hideSuggestions();
245 sidebar->getHeader()->updateInfo();
247 SearchParams *searchParams = getSearchParams();
248 bool isChannel = searchParams && !searchParams->channelId().isEmpty();
249 playlistView->setClickableAuthors(!isChannel);
252 void MediaView::searchAgain() {
253 VideoSource *currentVideoSource = playlistModel->getVideoSource();
254 setVideoSource(currentVideoSource, false);
257 bool MediaView::canGoBack() {
258 return getHistoryIndex() > 0;
261 void MediaView::goBack() {
262 if (history.size() > 1) {
263 int currentIndex = getHistoryIndex();
264 if (currentIndex > 0) {
265 VideoSource *previousVideoSource = history.at(currentIndex - 1);
266 setVideoSource(previousVideoSource, false, true);
271 bool MediaView::canGoForward() {
272 int currentIndex = getHistoryIndex();
273 return currentIndex >= 0 && currentIndex < history.size() - 1;
276 void MediaView::goForward() {
277 if (canGoForward()) {
278 int currentIndex = getHistoryIndex();
279 VideoSource *nextVideoSource = history.at(currentIndex + 1);
280 setVideoSource(nextVideoSource, false);
284 int MediaView::getHistoryIndex() {
285 return history.lastIndexOf(playlistModel->getVideoSource());
288 void MediaView::appear() {
289 MainWindow::instance()->showToolbar();
291 Video *currentVideo = playlistModel->activeVideo();
293 MainWindow::instance()->setWindowTitle(currentVideo->getTitle() + " - " + Constants::NAME);
296 playlistView->setFocus();
299 void MediaView::disappear() {
300 MainWindow::instance()->hideToolbar();
303 void MediaView::handleError(const QString &message) {
304 qWarning() << __PRETTY_FUNCTION__ << message;
305 #ifndef QT_NO_DEBUG_OUTPUT
306 MainWindow::instance()->showMessage(message);
310 void MediaView::mediaStateChanged(Media::State state) {
311 if (pauseTime > 0 && (state == Media::PlayingState || state == Media::BufferingState)) {
312 qDebug() << "Seeking to" << pauseTime;
313 media->seek(pauseTime);
316 if (state == Media::PlayingState) {
317 videoAreaWidget->showVideo();
318 } else if (state == Media::ErrorState) {
319 handleError(media->errorString());
322 if (state == Media::PlayingState) {
323 bool res = Idle::preventDisplaySleep(QString("%1 is playing").arg(Constants::NAME));
324 if (!res) qWarning() << "Error disabling idle display sleep" << Idle::displayErrorMessage();
325 } else if (state == Media::PausedState || state == Media::StoppedState) {
326 bool res = Idle::allowDisplaySleep();
327 if (!res) qWarning() << "Error enabling idle display sleep" << Idle::displayErrorMessage();
331 void MediaView::pause() {
332 switch (media->state()) {
333 case Media::PlayingState:
338 if (pauseTimer.hasExpired(60000)) {
339 pauseTimer.invalidate();
340 connect(playlistModel->activeVideo(), &Video::gotStreamUrl, this,
341 &MediaView::resumeWithNewStreamUrl);
342 playlistModel->activeVideo()->loadStreamUrl();
349 QRegExp MediaView::wordRE(const QString &s) {
350 return QRegExp("\\W" + s + "\\W?", Qt::CaseInsensitive);
353 void MediaView::stop() {
356 while (!history.isEmpty()) {
357 VideoSource *videoSource = history.takeFirst();
358 // Don't delete videoSource in the Browse view
359 if (!videoSource->parent()) {
360 videoSource->deleteLater();
364 playlistModel->abortSearch();
365 videoAreaWidget->clear();
366 videoAreaWidget->update();
368 playlistView->selectionModel()->clearSelection();
370 MainWindow::instance()->getAction("refineSearch")->setChecked(false);
371 updateSubscriptionActionForVideo(nullptr, false);
372 #ifdef APP_ACTIVATION
376 for (QAction *action : currentVideoActions)
377 action->setEnabled(false);
379 QAction *a = MainWindow::instance()->getAction("download");
380 a->setEnabled(false);
381 a->setVisible(false);
385 currentVideoId.clear();
388 if (snapshotSettings) {
389 delete snapshotSettings;
390 snapshotSettings = nullptr;
395 const QString &MediaView::getCurrentVideoId() {
396 return currentVideoId;
399 void MediaView::activeVideoChanged(Video *video, Video *previousVideo) {
405 if (previousVideo && previousVideo != video) {
406 if (previousVideo->isLoadingStreamUrl()) previousVideo->abortLoadStreamUrl();
409 // optimize window for 16:9 video
412 videoAreaWidget->showLoading(video);
414 connect(video, &Video::gotStreamUrl, this, &MediaView::gotStreamUrl, Qt::UniqueConnection);
415 connect(video, SIGNAL(errorStreamUrl(QString)), SLOT(skip()), Qt::UniqueConnection);
416 video->loadStreamUrl();
418 // video title in titlebar
419 MainWindow::instance()->setWindowTitle(video->getTitle() + QLatin1String(" - ") +
420 QLatin1String(Constants::NAME));
422 // ensure active item is visible
423 int row = playlistModel->rowForVideo(video);
425 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
426 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
429 // enable/disable actions
430 MainWindow::instance()
431 ->getAction("download")
432 ->setEnabled(DownloadManager::instance()->itemForVideo(video) == nullptr);
433 MainWindow::instance()->getAction("previous")->setEnabled(row > 0);
434 MainWindow::instance()->getAction("stopafterthis")->setEnabled(true);
435 MainWindow::instance()->getAction("relatedVideos")->setEnabled(true);
437 bool enableDownload = video->getLicense() == Video::LicenseCC;
438 #ifdef APP_ACTIVATION
439 enableDownload = enableDownload || Activation::instance().isLegacy();
442 enableDownload = true;
444 QAction *a = MainWindow::instance()->getAction("download");
445 a->setEnabled(enableDownload);
446 a->setVisible(enableDownload);
448 updateSubscriptionActionForVideo(video, YTChannel::isSubscribed(video->getChannelId()));
450 for (QAction *action : currentVideoActions)
451 action->setEnabled(true);
454 if (snapshotSettings) {
455 delete snapshotSettings;
456 snapshotSettings = nullptr;
457 MainWindow::instance()->adjustStatusBarVisibility();
461 // see you in gotStreamUrl...
464 void MediaView::gotStreamUrl(const QString &streamUrl, const QString &audioUrl) {
466 if (streamUrl.isEmpty()) {
467 qWarning() << "Empty stream url";
472 Video *video = static_cast<Video *>(sender());
474 qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
477 video->disconnect(this);
479 currentVideoId = video->getId();
481 if (audioUrl.isEmpty()) {
482 qDebug() << "Playing" << streamUrl;
483 media->play(streamUrl);
485 qDebug() << "Playing" << streamUrl << audioUrl;
486 media->playSeparateAudioAndVideo(streamUrl, audioUrl);
489 // ensure we always have videos ahead
490 playlistModel->searchNeeded();
492 // ensure active item is visible
493 int row = playlistModel->activeRow();
495 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
496 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
499 #ifdef APP_ACTIVATION
500 if (!Activation::instance().isActivated() && !demoTimer->isActive()) {
501 int ms = (60000 * 5) + (qrand() % (60000 * 5));
502 demoTimer->start(ms);
507 Extra::notify(video->getTitle(), video->getChannelTitle(), video->getFormattedDuration());
510 ChannelAggregator::instance()->videoWatched(video);
513 void MediaView::onItemActivated(const QModelIndex &index) {
514 if (playlistModel->rowExists(index.row())) {
515 // if it's the current video, just rewind and play
516 Video *activeVideo = playlistModel->activeVideo();
517 Video *video = playlistModel->videoAt(index.row());
518 if (activeVideo && video && activeVideo == video) {
521 playlistModel->setActiveRow(index.row());
523 // the user doubleclicked on the "Search More" item
525 playlistModel->searchMore();
526 playlistView->selectionModel()->clearSelection();
530 void MediaView::skipVideo() {
531 // skippedVideo is useful for DELAYED skip operations
532 // in order to be sure that we're skipping the video we wanted
533 // and not another one
535 if (playlistModel->activeVideo() != skippedVideo) {
536 qDebug() << "Skip of video canceled";
539 int nextRow = playlistModel->rowForVideo(skippedVideo);
541 if (nextRow == -1) return;
542 playlistModel->setActiveRow(nextRow);
546 void MediaView::skip() {
547 int nextRow = playlistModel->nextRow();
548 if (nextRow == -1) return;
549 playlistModel->setActiveRow(nextRow);
552 void MediaView::skipBackward() {
553 int prevRow = playlistModel->previousRow();
554 if (prevRow == -1) return;
555 playlistModel->setActiveRow(prevRow);
558 void MediaView::onAboutToFinish() {
559 qint64 currentTime = media->position();
560 qint64 totalTime = media->duration();
561 // qDebug() << __PRETTY_FUNCTION__ << currentTime << totalTime;
562 if (totalTime < 1 || currentTime + 10000 < totalTime) {
563 // QTimer::singleShot(500, this, SLOT(playbackResume()));
564 media->seek(currentTime);
569 void MediaView::onPlaybackFinished() {
572 const qint64 totalTime = media->duration();
573 const qint64 currentTime = media->position();
574 // qDebug() << __PRETTY_FUNCTION__ << mediaObject->currentTime() << totalTime;
575 // add 10 secs for imprecise Phonon backends (VLC, Xine)
576 if (currentTime > 0 && currentTime + 10000 < totalTime) {
577 // mediaObject->seek(currentTime);
578 QTimer::singleShot(500, this, SLOT(resumePlayback()));
580 QAction *stopAfterThisAction = MainWindow::instance()->getAction("stopafterthis");
581 if (stopAfterThisAction->isChecked()) {
582 stopAfterThisAction->setChecked(false);
588 void MediaView::resumePlayback() {
590 const qint64 currentTime = media->position();
591 // qDebug() << __PRETTY_FUNCTION__ << currentTime;
592 if (currentTime > 0) media->seek(currentTime);
596 void MediaView::openWebPage() {
597 Video *video = playlistModel->activeVideo();
601 video->getWebpage() + QLatin1String("&t=") + QString::number(media->position() / 1000);
602 QDesktopServices::openUrl(url);
605 void MediaView::copyWebPage() {
606 Video *video = playlistModel->activeVideo();
608 QString address = video->getWebpage();
609 QApplication::clipboard()->setText(address);
610 QString message = tr("You can now paste the YouTube link into another application");
611 MainWindow::instance()->showMessage(message);
614 void MediaView::copyVideoLink() {
615 Video *video = playlistModel->activeVideo();
617 QApplication::clipboard()->setText(video->getStreamUrl());
618 QString message = tr("You can now paste the video stream URL into another application") + ". " +
619 tr("The link will be valid only for a limited time.");
620 MainWindow::instance()->showMessage(message);
623 void MediaView::openInBrowser() {
624 Video *video = playlistModel->activeVideo();
627 QDesktopServices::openUrl(video->getStreamUrl());
630 void MediaView::removeSelected() {
631 if (!playlistView->selectionModel()->hasSelection()) return;
632 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
633 playlistModel->removeIndexes(indexes);
636 void MediaView::selectVideos(const QVector<Video *> &videos) {
637 for (Video *video : videos) {
638 QModelIndex index = playlistModel->indexForVideo(video);
639 playlistView->selectionModel()->select(index, QItemSelectionModel::Select);
640 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
644 void MediaView::selectionChanged(const QItemSelection & /*selected*/,
645 const QItemSelection & /*deselected*/) {
646 const bool gotSelection = playlistView->selectionModel()->hasSelection();
647 MainWindow::instance()->getAction("remove")->setEnabled(gotSelection);
648 MainWindow::instance()->getAction("moveUp")->setEnabled(gotSelection);
649 MainWindow::instance()->getAction("moveDown")->setEnabled(gotSelection);
652 void MediaView::moveUpSelected() {
653 if (!playlistView->selectionModel()->hasSelection()) return;
655 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
656 qStableSort(indexes.begin(), indexes.end());
657 playlistModel->move(indexes, true);
659 // set current index after row moves to something more intuitive
660 int row = indexes.at(0).row();
661 playlistView->selectionModel()->setCurrentIndex(playlistModel->index(row > 1 ? row : 1),
662 QItemSelectionModel::NoUpdate);
665 void MediaView::moveDownSelected() {
666 if (!playlistView->selectionModel()->hasSelection()) return;
668 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
669 qStableSort(indexes.begin(), indexes.end(), qGreater<QModelIndex>());
670 playlistModel->move(indexes, false);
672 // set current index after row moves to something more intuitive
673 // (respect 1 static item on bottom)
674 int row = indexes.at(0).row() + 1, max = playlistModel->rowCount() - 2;
675 playlistView->selectionModel()->setCurrentIndex(playlistModel->index(row > max ? max : row),
676 QItemSelectionModel::NoUpdate);
679 void MediaView::setSidebarVisibility(bool visible) {
680 if (sidebar->isVisible() == visible) return;
681 sidebar->setVisible(visible);
684 sidebar->resize(sidebar->width(), window()->height());
686 playlistView->setFocus();
690 void MediaView::removeSidebar() {
692 sidebar->setParent(window());
695 void MediaView::restoreSidebar() {
697 splitter->insertWidget(0, sidebar);
700 bool MediaView::isSidebarVisible() {
701 return sidebar->isVisible();
704 void MediaView::saveSplitterState() {
706 if (splitter) settings.setValue("splitter", splitter->saveState());
709 void MediaView::downloadVideo() {
710 Video *video = playlistModel->activeVideo();
712 DownloadManager::instance()->addItem(video);
713 MainWindow::instance()->showActionsInStatusBar({MainWindow::instance()->getAction("downloads")},
715 QString message = tr("Downloading %1").arg(video->getTitle());
716 MainWindow::instance()->showMessage(message);
720 void MediaView::snapshot() {
721 qint64 currentTime = media->position() / 1000;
723 QObject *context = new QObject();
724 connect(media, &Media::snapshotReady, context,
725 [this, currentTime, context](const QImage &image) {
726 context->deleteLater();
728 if (image.isNull()) {
729 qWarning() << "Null snapshot";
733 QPixmap pixmap = QPixmap::fromImage(image.scaled(
734 videoWidget->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
735 videoAreaWidget->showSnapshotPreview(pixmap);
737 Video *video = playlistModel->activeVideo();
740 QString location = SnapshotSettings::getCurrentLocation();
742 if (!dir.exists()) dir.mkpath(location);
743 QString basename = video->getTitle();
744 QString format = video->getDuration() > 3600 ? "h_mm_ss" : "m_ss";
745 basename += " (" + QTime(0, 0, 0).addSecs(currentTime).toString(format) + ")";
746 basename = DataUtils::stringToFilename(basename);
747 QString filename = location + "/" + basename + ".png";
748 qDebug() << filename;
749 image.save(filename, "PNG");
751 if (snapshotSettings) delete snapshotSettings;
752 snapshotSettings = new SnapshotSettings(videoWidget);
753 snapshotSettings->setSnapshot(pixmap, filename);
754 QStatusBar *statusBar = MainWindow::instance()->statusBar();
756 Extra::fadeInWidget(statusBar, statusBar);
758 statusBar->insertPermanentWidget(0, snapshotSettings);
759 snapshotSettings->show();
760 MainWindow::instance()->setStatusBarVisibility(true);
768 void MediaView::fullscreen() {
769 videoAreaWidget->setParent(nullptr);
770 videoAreaWidget->showFullScreen();
773 void MediaView::resumeWithNewStreamUrl(const QString &streamUrl, const QString &audioUrl) {
774 pauseTime = media->position();
776 if (audioUrl.isEmpty()) {
777 qDebug() << "Playing" << streamUrl;
778 media->play(streamUrl);
780 qDebug() << "Playing" << streamUrl << audioUrl;
781 media->playSeparateAudioAndVideo(streamUrl, audioUrl);
784 Video *video = static_cast<Video *>(sender());
786 qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
789 video->disconnect(this);
792 void MediaView::findVideoParts() {
793 Video *video = playlistModel->activeVideo();
796 QString query = video->getTitle();
798 const QLatin1String optionalSpace("\\s*");
799 const QLatin1String staticCounterSeparators("[\\/\\-]");
800 const QString counterSeparators =
801 QLatin1String("( of | ") + tr("of", "Used in video parts, as in '2 of 3'") +
802 QLatin1String(" |") + staticCounterSeparators + QLatin1String(")");
804 // numbers from 1 to 15
805 const QLatin1String counterNumber("([1-9]|1[0-5])");
807 // query.remove(QRegExp(counterSeparators + optionalSpace + counterNumber));
808 query.remove(QRegExp(counterNumber + optionalSpace + counterSeparators + optionalSpace +
810 query.remove(wordRE("pr?t\\.?" + optionalSpace + counterNumber));
811 query.remove(wordRE("ep\\.?" + optionalSpace + counterNumber));
812 query.remove(wordRE("part" + optionalSpace + counterNumber));
813 query.remove(wordRE("episode" + optionalSpace + counterNumber));
814 query.remove(wordRE(tr("part", "This is for video parts, as in 'Cool video - part 1'") +
815 optionalSpace + counterNumber));
816 query.remove(wordRE(tr("episode", "This is for video parts, as in 'Cool series - episode 1'") +
817 optionalSpace + counterNumber));
818 query.remove(QRegExp("[\\(\\)\\[\\]]"));
820 #define NUMBERS "one|two|three|four|five|six|seven|eight|nine|ten"
822 QRegExp englishNumberRE = QRegExp(QLatin1String(".*(") + NUMBERS + ").*", Qt::CaseInsensitive);
823 // bool numberAsWords = englishNumberRE.exactMatch(query);
824 query.remove(englishNumberRE);
826 QRegExp localizedNumberRE =
827 QRegExp(QLatin1String(".*(") + tr(NUMBERS) + ").*", Qt::CaseInsensitive);
828 // if (!numberAsWords) numberAsWords = localizedNumberRE.exactMatch(query);
829 query.remove(localizedNumberRE);
831 SearchParams *searchParams = new SearchParams();
832 searchParams->setTransient(true);
833 searchParams->setKeywords(query);
834 searchParams->setChannelId(video->getChannelId());
837 if (!numberAsWords) {
838 qDebug() << "We don't have number as words";
839 // searchParams->setSortBy(SearchParams::SortByNewest);
840 // TODO searchParams->setReverseOrder(true);
841 // TODO searchParams->setMax(50);
845 search(searchParams);
848 void MediaView::relatedVideos() {
849 Video *video = playlistModel->activeVideo();
851 YTSingleVideoSource *singleVideoSource = new YTSingleVideoSource();
852 singleVideoSource->setVideo(video->clone());
853 singleVideoSource->setAsyncDetails(true);
854 setVideoSource(singleVideoSource);
855 MainWindow::instance()->getAction("relatedVideos")->setEnabled(false);
858 void MediaView::shareViaTwitter() {
859 Video *video = playlistModel->activeVideo();
861 QUrl url("https://twitter.com/intent/tweet");
863 q.addQueryItem("via", "minitubeapp");
864 q.addQueryItem("text", video->getTitle());
865 q.addQueryItem("url", video->getWebpage());
867 QDesktopServices::openUrl(url);
870 void MediaView::shareViaFacebook() {
871 Video *video = playlistModel->activeVideo();
873 QUrl url("https://www.facebook.com/sharer.php");
875 q.addQueryItem("t", video->getTitle());
876 q.addQueryItem("u", video->getWebpage());
878 QDesktopServices::openUrl(url);
881 void MediaView::shareViaEmail() {
882 Video *video = playlistModel->activeVideo();
886 q.addQueryItem("subject", video->getTitle());
887 const QString body = video->getTitle() + "\n" + video->getWebpage() + "\n\n" +
888 tr("Sent from %1").arg(Constants::NAME) + "\n" + Constants::WEBSITE;
889 q.addQueryItem("body", body);
891 QDesktopServices::openUrl(url);
894 void MediaView::onAuthorPushed(QModelIndex index) {
895 Video *video = playlistModel->videoAt(index.row());
898 QString channelId = video->getChannelId();
899 // if (channelId.isEmpty()) channelId = video->channelTitle();
900 if (channelId.isEmpty()) return;
902 SearchParams *searchParams = new SearchParams();
903 searchParams->setChannelId(channelId);
904 searchParams->setSortBy(SearchParams::SortByNewest);
907 search(searchParams);
911 void MediaView::updateSubscriptionAction(bool subscribed) {
912 QAction *subscribeAction = MainWindow::instance()->getAction("subscribeChannel");
914 QString subscribeTip;
915 QString subscribeText;
917 if (currentSubscriptionChannelId.isEmpty()) {
918 subscribeText = subscribeAction->property("originalText").toString();
919 subscribeAction->setEnabled(false);
920 } else if (subscribed) {
921 subscribeText = tr("Unsubscribe from %1").arg(currentSubscriptionChannelTitle);
922 subscribeTip = subscribeText;
923 subscribeAction->setEnabled(true);
925 subscribeText = tr("Subscribe to %1").arg(currentSubscriptionChannelTitle);
926 subscribeTip = subscribeText;
927 subscribeAction->setEnabled(true);
929 subscribeAction->setText(subscribeText);
930 subscribeAction->setStatusTip(subscribeTip);
933 subscribeAction->setIcon(IconUtils::icon("bookmark-remove"));
935 subscribeAction->setIcon(IconUtils::icon("bookmark-new"));
938 MainWindow::instance()->setupAction(subscribeAction);
941 void MediaView::updateSubscriptionActionForChannel(const QString & channelId) {
942 QString channelTitle = tr("channel");
943 YTChannel *channel = YTChannel::forId(channelId);
944 if (nullptr != channel && !channel->getDisplayName().isEmpty()) {
945 channelTitle = channel->getDisplayName();
948 bool subscribed = YTChannel::isSubscribed(channelId);
950 currentSubscriptionChannelId = channelId;
951 currentSubscriptionChannelTitle = channelTitle;
952 updateSubscriptionAction(subscribed);
955 void MediaView::updateSubscriptionActionForVideo(Video *video, bool subscribed) {
957 currentSubscriptionChannelId = "";
958 currentSubscriptionChannelTitle = "";
959 updateSubscriptionAction(false);
961 currentSubscriptionChannelId = video->getChannelId();
962 currentSubscriptionChannelTitle = video->getChannelTitle();
963 updateSubscriptionAction(subscribed);
967 void MediaView::reloadCurrentVideo() {
968 Video *video = playlistModel->activeVideo();
971 int oldFormat = video->getDefinitionCode();
973 QObject *context = new QObject();
974 connect(video, &Video::gotStreamUrl, context,
975 [this, oldFormat, video, context](const QString &videoUrl, const QString &audioUrl) {
976 context->deleteLater();
977 if (oldFormat == video->getDefinitionCode()) return;
978 QObject *context2 = new QObject();
979 const qint64 position = media->position();
980 connect(media, &Media::stateChanged, context2,
981 [position, this, context2](Media::State state) {
982 if (state == Media::PlayingState) {
983 media->seek(position);
984 context2->deleteLater();
985 Video *video = playlistModel->activeVideo();
986 QString msg = tr("Switched to %1")
987 .arg(VideoDefinition::forCode(
988 video->getDefinitionCode())
990 MainWindow::instance()->showMessage(msg);
994 if (audioUrl.isEmpty()) {
995 media->play(videoUrl);
997 media->playSeparateAudioAndVideo(videoUrl, audioUrl);
1000 video->loadStreamUrl();
1003 void MediaView::toggleSubscription() {
1004 //Video *video = playlistModel->activeVideo();
1005 if (currentSubscriptionChannelId.isEmpty()) {
1009 bool subscribed = YTChannel::isSubscribed(currentSubscriptionChannelId);
1011 YTChannel::unsubscribe(currentSubscriptionChannelId);
1012 MainWindow::instance()->showMessage(
1013 tr("Unsubscribed from %1").arg(currentSubscriptionChannelTitle));
1015 YTChannel::subscribe(currentSubscriptionChannelId);
1016 MainWindow::instance()->showMessage(tr("Subscribed to %1").arg(currentSubscriptionChannelTitle));
1019 updateSubscriptionAction(!subscribed);
1022 void MediaView::adjustWindowSize() {
1023 qDebug() << "Adjusting window size";
1024 Video *video = playlistModel->activeVideo();
1026 QWidget *window = this->window();
1027 if (!window->isMaximized() && !window->isFullScreen()) {
1028 const double ratio = 16. / 9.;
1029 const double w = (double)videoAreaWidget->width();
1030 const double h = (double)videoAreaWidget->height();
1031 const double currentVideoRatio = w / h;
1032 if (currentVideoRatio != ratio) {
1033 qDebug() << "Adjust size";
1034 int newHeight = std::round((window->height() - h) + (w / ratio));
1035 window->resize(window->width(), newHeight);