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 #include "ytjschannelsource.h"
62 #include "ytjssearch.h"
63 #include "ytjssinglevideosource.h"
65 MediaView *MediaView::instance() {
66 static MediaView *i = new MediaView();
70 MediaView::MediaView(QWidget *parent)
71 : View(parent), splitter(nullptr), stopped(false)
74 snapshotSettings(nullptr)
80 void MediaView::initialize() {
81 MainWindow *mainWindow = MainWindow::instance();
83 QBoxLayout *layout = new QVBoxLayout(this);
86 splitter = new MiniSplitter();
87 layout->addWidget(splitter);
89 playlistView = new PlaylistView();
90 playlistView->setParent(this);
91 connect(playlistView, SIGNAL(activated(const QModelIndex &)),
92 SLOT(onItemActivated(const QModelIndex &)));
94 playlistModel = new PlaylistModel();
95 connect(playlistModel, &PlaylistModel::activeVideoChanged, this,
96 &MediaView::activeVideoChanged);
97 // needed to restore the selection after dragndrop
98 connect(playlistModel, SIGNAL(needSelectionFor(QVector<Video *>)),
99 SLOT(selectVideos(QVector<Video *>)));
100 playlistView->setModel(playlistModel);
102 connect(playlistView->selectionModel(),
103 SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
104 SLOT(selectionChanged(const QItemSelection &, const QItemSelection &)));
106 connect(playlistView, SIGNAL(authorPushed(QModelIndex)), SLOT(onAuthorPushed(QModelIndex)));
108 sidebar = new SidebarWidget(this);
109 sidebar->setPlaylist(playlistView);
110 sidebar->setMaximumWidth(playlistView->minimumWidth() * 3);
111 connect(sidebar->getRefineSearchWidget(), SIGNAL(searchRefined()), SLOT(searchAgain()));
112 connect(playlistModel, SIGNAL(haveSuggestions(const QStringList &)), sidebar,
113 SLOT(showSuggestions(const QStringList &)));
114 connect(sidebar, SIGNAL(suggestionAccepted(QString)), mainWindow, SLOT(search(QString)));
115 splitter->addWidget(sidebar);
117 videoAreaWidget = new VideoArea(this);
118 videoAreaWidget->setListModel(playlistModel);
120 loadingWidget = new LoadingWidget(this);
121 videoAreaWidget->setLoadingWidget(loadingWidget);
123 splitter->addWidget(videoAreaWidget);
125 // restore splitter state
127 if (settings.contains("splitter"))
128 splitter->restoreState(settings.value("splitter").toByteArray());
130 int sidebarDefaultWidth = 180;
131 splitter->setSizes(QList<int>() << sidebarDefaultWidth
132 << splitter->size().width() - sidebarDefaultWidth);
134 splitter->setChildrenCollapsible(false);
135 connect(splitter, SIGNAL(splitterMoved(int, int)), SLOT(adjustWindowSize()));
137 errorTimer = new QTimer(this);
138 errorTimer->setSingleShot(true);
139 errorTimer->setInterval(3000);
140 connect(errorTimer, SIGNAL(timeout()), SLOT(skipVideo()));
142 #ifdef APP_ACTIVATION
143 demoTimer = new QTimer(this);
144 demoTimer->setSingleShot(true);
146 demoTimer, &QTimer::timeout, this,
148 if (media->state() != Media::PlayingState) return;
151 ActivationView::instance(), &ActivationView::done, media,
152 [this] { media->play(); }, Qt::UniqueConnection);
153 MainWindow::instance()->showActivationView();
155 Qt::QueuedConnection);
158 connect(videoAreaWidget, SIGNAL(doubleClicked()), mainWindow->getAction("fullscreen"),
161 QAction *refineSearchAction = mainWindow->getAction("refineSearch");
162 connect(refineSearchAction, SIGNAL(toggled(bool)), sidebar, SLOT(toggleRefineSearch(bool)));
164 const QVector<const char *> videoActionNames = {
168 "webpage", "pagelink", "videolink", "openInBrowser", "findVideoParts",
169 "skip", "previous", "stopafterthis", "relatedVideos", "refineSearch",
170 "twitter", "facebook", "email"};
171 currentVideoActions.reserve(videoActionNames.size());
172 for (auto *name : videoActionNames) {
173 currentVideoActions.append(mainWindow->getAction(name));
176 for (int i = 0; i < 10; ++i) {
177 QAction *action = new QAction(QString());
178 action->setShortcut(Qt::Key_0 + i);
179 action->setAutoRepeat(false);
180 connect(action, &QAction::triggered, this, [this, i] {
181 qint64 duration = media->duration();
182 // dur : pos = 100 : i*10
183 qint64 position = (duration * (i * 10)) / 100;
184 media->seek(position);
187 playingVideoActions << action;
190 QAction *leftAction = new QAction(tr("Rewind %1 seconds").arg(10));
191 leftAction->setShortcut(Qt::Key_Left);
192 leftAction->setAutoRepeat(false);
193 connect(leftAction, &QAction::triggered, this, [this] {
194 qint64 position = media->position();
196 if (position < 0) position = 0;
197 media->seek(position);
199 addAction(leftAction);
200 playingVideoActions << leftAction;
202 QAction *rightAction = new QAction(tr("Fast forward %1 seconds").arg(10));
203 rightAction->setShortcut(Qt::Key_Right);
204 rightAction->setAutoRepeat(false);
205 connect(rightAction, &QAction::triggered, this, [this] {
206 qint64 position = media->position();
208 qint64 duration = media->duration();
209 if (position > duration) position = duration;
210 media->seek(position);
212 addAction(rightAction);
213 playingVideoActions << rightAction;
216 void MediaView::setMedia(Media *media) {
219 videoWidget = media->videoWidget();
220 videoAreaWidget->setVideoWidget(videoWidget);
222 connect(media, &Media::finished, this, &MediaView::onPlaybackFinished);
223 connect(media, &Media::stateChanged, this, &MediaView::mediaStateChanged);
224 connect(media, &Media::aboutToFinish, this, &MediaView::onAboutToFinish);
225 connect(media, &Media::bufferStatus, loadingWidget, &LoadingWidget::bufferStatus);
228 SearchParams *MediaView::getSearchParams() {
229 VideoSource *videoSource = playlistModel->getVideoSource();
230 if (!videoSource) return nullptr;
231 auto clazz = videoSource->metaObject()->className();
232 if (clazz == QLatin1String("YTSearch")) {
233 auto search = qobject_cast<YTSearch *>(videoSource);
234 return search->getSearchParams();
236 if (clazz == QLatin1String("IVSearch")) {
237 auto search = qobject_cast<IVSearch *>(videoSource);
238 return search->getSearchParams();
240 if (clazz == QLatin1String("IVChannelSource")) {
241 auto search = qobject_cast<IVChannelSource *>(videoSource);
242 return search->getSearchParams();
244 if (clazz == QLatin1String("YTJSSearch")) {
245 auto search = qobject_cast<YTJSSearch *>(videoSource);
246 return search->getSearchParams();
248 if (clazz == QLatin1String("YTJSChannelSource")) {
249 auto search = qobject_cast<YTJSChannelSource *>(videoSource);
250 return search->getSearchParams();
255 void MediaView::search(SearchParams *searchParams) {
256 if (!searchParams->keywords().isEmpty()) {
257 if (searchParams->keywords().startsWith("http://") ||
258 searchParams->keywords().startsWith("https://")) {
259 QString videoId = YTSearch::videoIdFromUrl(searchParams->keywords());
260 if (!videoId.isEmpty()) {
261 VideoSource *singleVideoSource = nullptr;
262 if (VideoAPI::impl() == VideoAPI::YT3) {
263 auto source = new YTSingleVideoSource(this);
264 source->setVideoId(videoId);
265 singleVideoSource = source;
266 } else if (VideoAPI::impl() == VideoAPI::IV) {
267 auto source = new IVSingleVideoSource(this);
268 source->setVideoId(videoId);
269 singleVideoSource = source;
270 } else if (VideoAPI::impl() == VideoAPI::JS) {
271 auto source = new YTJSSingleVideoSource(this);
272 source->setVideoId(videoId);
273 singleVideoSource = source;
275 setVideoSource(singleVideoSource);
277 QTime tstamp = YTSearch::videoTimestampFromUrl(searchParams->keywords());
278 pauseTime = QTime(0, 0).msecsTo(tstamp);
284 VideoSource *search = nullptr;
285 if (VideoAPI::impl() == VideoAPI::YT3) {
286 YTSearch *ytSearch = new YTSearch(searchParams);
287 ytSearch->setAsyncDetails(true);
288 connect(ytSearch, SIGNAL(gotDetails()), playlistModel, SLOT(emitDataChanged()));
290 } else if (VideoAPI::impl() == VideoAPI::IV) {
291 if (searchParams->channelId().isEmpty()) {
292 search = new IVSearch(searchParams);
294 search = new IVChannelSource(searchParams);
296 } else if (VideoAPI::impl() == VideoAPI::JS) {
297 if (searchParams->channelId().isEmpty()) {
298 search = new YTJSSearch(searchParams);
300 search = new YTJSChannelSource(searchParams);
303 setVideoSource(search);
306 void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool back) {
311 // qDebug() << "Adding VideoSource" << videoSource->getName() << videoSource;
314 int currentIndex = getHistoryIndex();
315 if (currentIndex >= 0 && currentIndex < history.size() - 1) {
316 while (history.size() > currentIndex + 1) {
317 VideoSource *vs = history.takeLast();
319 qDebug() << "Deleting VideoSource" << vs->getName() << vs;
324 history.append(videoSource);
328 if (history.size() > 1)
329 Extra::slideTransition(playlistView->viewport(), playlistView->viewport(), back);
332 playlistModel->setVideoSource(videoSource);
334 if (media->state() == Media::StoppedState) {
336 if (settings.value("manualplay", false).toBool()) {
337 videoAreaWidget->showPickMessage();
341 SearchParams *searchParams = getSearchParams();
343 sidebar->showPlaylist();
344 sidebar->getRefineSearchWidget()->setSearchParams(searchParams);
345 sidebar->hideSuggestions();
346 sidebar->getHeader()->updateInfo();
348 bool isChannel = searchParams && !searchParams->channelId().isEmpty();
350 updateSubscriptionActionForChannel(searchParams->channelId());
352 playlistView->setClickableAuthors(!isChannel);
355 void MediaView::searchAgain() {
356 VideoSource *currentVideoSource = playlistModel->getVideoSource();
357 setVideoSource(currentVideoSource, false);
360 bool MediaView::canGoBack() {
361 return getHistoryIndex() > 0;
364 void MediaView::goBack() {
365 if (history.size() > 1) {
366 int currentIndex = getHistoryIndex();
367 if (currentIndex > 0) {
368 VideoSource *previousVideoSource = history.at(currentIndex - 1);
369 setVideoSource(previousVideoSource, false, true);
374 bool MediaView::canGoForward() {
375 int currentIndex = getHistoryIndex();
376 return currentIndex >= 0 && currentIndex < history.size() - 1;
379 void MediaView::goForward() {
380 if (canGoForward()) {
381 int currentIndex = getHistoryIndex();
382 VideoSource *nextVideoSource = history.at(currentIndex + 1);
383 setVideoSource(nextVideoSource, false);
387 int MediaView::getHistoryIndex() {
388 return history.lastIndexOf(playlistModel->getVideoSource());
391 void MediaView::appear() {
392 MainWindow::instance()->showToolbar();
394 Video *currentVideo = playlistModel->activeVideo();
396 MainWindow::instance()->setWindowTitle(currentVideo->getTitle() + " - " + Constants::NAME);
399 playlistView->setFocus();
402 void MediaView::disappear() {
403 MainWindow::instance()->hideToolbar();
406 void MediaView::handleError(const QString &message) {
407 qWarning() << __PRETTY_FUNCTION__ << message;
408 #ifndef QT_NO_DEBUG_OUTPUT
409 MainWindow::instance()->showMessage(message);
413 void MediaView::mediaStateChanged(Media::State state) {
414 if (pauseTime > 0 && (state == Media::PlayingState || state == Media::BufferingState)) {
415 qDebug() << "Seeking to" << pauseTime;
416 media->seek(pauseTime);
419 if (state == Media::PlayingState) {
420 videoAreaWidget->showVideo();
421 } else if (state == Media::ErrorState) {
422 handleError(media->errorString());
425 bool enablePlayingVideoActions = state == Media::PlayingState || state == Media::PausedState;
426 for (QAction *action : qAsConst(playingVideoActions))
427 action->setEnabled(enablePlayingVideoActions);
429 if (state == Media::PlayingState) {
430 bool res = Idle::preventDisplaySleep(QString("%1 is playing").arg(Constants::NAME));
431 if (!res) qWarning() << "Error disabling idle display sleep" << Idle::displayErrorMessage();
432 } else if (state == Media::PausedState || state == Media::StoppedState) {
433 bool res = Idle::allowDisplaySleep();
434 if (!res) qWarning() << "Error enabling idle display sleep" << Idle::displayErrorMessage();
438 void MediaView::pause() {
439 switch (media->state()) {
440 case Media::PlayingState:
445 if (pauseTimer.hasExpired(60000)) {
446 pauseTimer.invalidate();
447 connect(playlistModel->activeVideo(), &Video::gotStreamUrl, this,
448 &MediaView::resumeWithNewStreamUrl);
449 playlistModel->activeVideo()->loadStreamUrl();
456 QRegExp MediaView::wordRE(const QString &s) {
457 return QRegExp("\\W" + s + "\\W?", Qt::CaseInsensitive);
460 void MediaView::stop() {
463 while (!history.isEmpty()) {
464 VideoSource *videoSource = history.takeFirst();
465 // Don't delete videoSource in the Browse view
466 if (!videoSource->parent()) {
467 videoSource->deleteLater();
471 playlistModel->abortSearch();
472 videoAreaWidget->clear();
473 videoAreaWidget->update();
475 playlistView->selectionModel()->clearSelection();
477 MainWindow::instance()->getAction("refineSearch")->setChecked(false);
478 updateSubscriptionActionForVideo(nullptr, false);
479 #ifdef APP_ACTIVATION
483 for (QAction *action : currentVideoActions)
484 action->setEnabled(false);
486 QAction *a = MainWindow::instance()->getAction("download");
487 a->setEnabled(false);
488 a->setVisible(false);
492 currentVideoId.clear();
495 if (snapshotSettings) {
496 delete snapshotSettings;
497 snapshotSettings = nullptr;
502 const QString &MediaView::getCurrentVideoId() {
503 return currentVideoId;
506 void MediaView::activeVideoChanged(Video *video, Video *previousVideo) {
512 if (previousVideo && previousVideo != video) {
513 if (previousVideo->isLoadingStreamUrl()) previousVideo->abortLoadStreamUrl();
516 // optimize window for 16:9 video
519 videoAreaWidget->showLoading(video);
521 connect(video, &Video::gotStreamUrl, this, &MediaView::gotStreamUrl, Qt::UniqueConnection);
522 connect(video, SIGNAL(errorStreamUrl(QString)), SLOT(skip()), Qt::UniqueConnection);
523 video->loadStreamUrl();
525 // video title in titlebar
526 MainWindow::instance()->setWindowTitle(video->getTitle() + QLatin1String(" - ") +
527 QLatin1String(Constants::NAME));
529 // ensure active item is visible
530 int row = playlistModel->rowForVideo(video);
532 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
533 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
536 // enable/disable actions
537 MainWindow::instance()
538 ->getAction("download")
539 ->setEnabled(DownloadManager::instance()->itemForVideo(video) == nullptr);
540 MainWindow::instance()->getAction("previous")->setEnabled(row > 0);
541 MainWindow::instance()->getAction("stopafterthis")->setEnabled(true);
542 MainWindow::instance()->getAction("relatedVideos")->setEnabled(true);
544 bool enableDownload = video->getLicense() == Video::LicenseCC;
545 #ifdef APP_ACTIVATION
546 enableDownload = enableDownload || Activation::instance().isLegacy();
549 enableDownload = true;
551 QAction *a = MainWindow::instance()->getAction("download");
552 a->setEnabled(enableDownload);
553 a->setVisible(enableDownload);
555 updateSubscriptionActionForVideo(video, YTChannel::isSubscribed(video->getChannelId()));
557 for (QAction *action : currentVideoActions)
558 action->setEnabled(true);
561 if (snapshotSettings) {
562 delete snapshotSettings;
563 snapshotSettings = nullptr;
564 MainWindow::instance()->adjustStatusBarVisibility();
568 // see you in gotStreamUrl...
571 void MediaView::gotStreamUrl(const QString &streamUrl, const QString &audioUrl) {
573 if (streamUrl.isEmpty()) {
574 qWarning() << "Empty stream url";
579 Video *video = static_cast<Video *>(sender());
581 qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
584 video->disconnect(this);
586 currentVideoId = video->getId();
588 if (audioUrl.isEmpty()) {
589 qDebug() << "Playing" << streamUrl;
590 media->play(streamUrl);
592 qDebug() << "Playing" << streamUrl << audioUrl;
593 media->playSeparateAudioAndVideo(streamUrl, audioUrl);
596 // ensure we always have videos ahead
597 playlistModel->searchNeeded();
599 // ensure active item is visible
600 int row = playlistModel->activeRow();
602 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
603 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
606 #ifdef APP_ACTIVATION
607 if (!demoTimer->isActive() && !Activation::instance().isActivated()) {
608 int ms = (60000 * 2) + (QRandomGenerator::global()->generate() % (60000 * 2));
609 demoTimer->start(ms);
614 Extra::notify(video->getTitle(), video->getChannelTitle(), video->getFormattedDuration());
617 ChannelAggregator::instance()->videoWatched(video);
620 void MediaView::onItemActivated(const QModelIndex &index) {
621 if (playlistModel->rowExists(index.row())) {
622 // if it's the current video, just rewind and play
623 Video *activeVideo = playlistModel->activeVideo();
624 Video *video = playlistModel->videoAt(index.row());
625 if (activeVideo && video && activeVideo == video) {
628 playlistModel->setActiveRow(index.row());
630 // the user doubleclicked on the "Search More" item
632 playlistModel->searchMore();
633 playlistView->selectionModel()->clearSelection();
637 void MediaView::skipVideo() {
638 // skippedVideo is useful for DELAYED skip operations
639 // in order to be sure that we're skipping the video we wanted
640 // and not another one
642 if (playlistModel->activeVideo() != skippedVideo) {
643 qDebug() << "Skip of video canceled";
646 int nextRow = playlistModel->rowForVideo(skippedVideo);
648 if (nextRow == -1) return;
649 playlistModel->setActiveRow(nextRow);
653 void MediaView::skip() {
654 int nextRow = playlistModel->nextRow();
655 if (nextRow == -1) return;
656 playlistModel->setActiveRow(nextRow);
659 void MediaView::skipBackward() {
660 int prevRow = playlistModel->previousRow();
661 if (prevRow == -1) return;
662 playlistModel->setActiveRow(prevRow);
665 void MediaView::onAboutToFinish() {
666 qint64 currentTime = media->position();
667 qint64 totalTime = media->duration();
668 // qDebug() << __PRETTY_FUNCTION__ << currentTime << totalTime;
669 if (totalTime < 1 || currentTime + 10000 < totalTime) {
670 // QTimer::singleShot(500, this, SLOT(playbackResume()));
671 media->seek(currentTime);
676 void MediaView::onPlaybackFinished() {
679 const qint64 totalTime = media->duration();
680 const qint64 currentTime = media->position();
681 // qDebug() << __PRETTY_FUNCTION__ << mediaObject->currentTime() << totalTime;
682 // add 10 secs for imprecise Phonon backends (VLC, Xine)
683 if (currentTime > 0 && currentTime + 10000 < totalTime) {
684 // mediaObject->seek(currentTime);
685 QTimer::singleShot(500, this, SLOT(resumePlayback()));
687 QAction *stopAfterThisAction = MainWindow::instance()->getAction("stopafterthis");
688 if (stopAfterThisAction->isChecked()) {
689 stopAfterThisAction->setChecked(false);
695 void MediaView::resumePlayback() {
697 const qint64 currentTime = media->position();
698 // qDebug() << __PRETTY_FUNCTION__ << currentTime;
699 if (currentTime > 0) media->seek(currentTime);
703 void MediaView::openWebPage() {
704 Video *video = playlistModel->activeVideo();
708 video->getWebpage() + QLatin1String("&t=") + QString::number(media->position() / 1000);
709 QDesktopServices::openUrl(url);
712 void MediaView::copyWebPage() {
713 Video *video = playlistModel->activeVideo();
715 QString address = video->getWebpage();
716 QApplication::clipboard()->setText(address);
717 QString message = tr("You can now paste the YouTube link into another application");
718 MainWindow::instance()->showMessage(message);
721 void MediaView::copyVideoLink() {
722 Video *video = playlistModel->activeVideo();
724 QApplication::clipboard()->setText(video->getStreamUrl());
725 QString message = tr("You can now paste the video stream URL into another application") + ". " +
726 tr("The link will be valid only for a limited time.");
727 MainWindow::instance()->showMessage(message);
730 void MediaView::openInBrowser() {
731 Video *video = playlistModel->activeVideo();
734 QDesktopServices::openUrl(video->getStreamUrl());
737 void MediaView::removeSelected() {
738 if (!playlistView->selectionModel()->hasSelection()) return;
739 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
740 playlistModel->removeIndexes(indexes);
743 void MediaView::selectVideos(const QVector<Video *> &videos) {
744 for (Video *video : videos) {
745 QModelIndex index = playlistModel->indexForVideo(video);
746 playlistView->selectionModel()->select(index, QItemSelectionModel::Select);
747 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
751 void MediaView::selectionChanged(const QItemSelection & /*selected*/,
752 const QItemSelection & /*deselected*/) {
753 const bool gotSelection = playlistView->selectionModel()->hasSelection();
754 MainWindow::instance()->getAction("remove")->setEnabled(gotSelection);
755 MainWindow::instance()->getAction("moveUp")->setEnabled(gotSelection);
756 MainWindow::instance()->getAction("moveDown")->setEnabled(gotSelection);
759 void MediaView::moveUpSelected() {
760 if (!playlistView->selectionModel()->hasSelection()) return;
762 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
763 qStableSort(indexes.begin(), indexes.end());
764 playlistModel->move(indexes, true);
766 // set current index after row moves to something more intuitive
767 int row = indexes.at(0).row();
768 playlistView->selectionModel()->setCurrentIndex(playlistModel->index(row > 1 ? row : 1),
769 QItemSelectionModel::NoUpdate);
772 void MediaView::moveDownSelected() {
773 if (!playlistView->selectionModel()->hasSelection()) return;
775 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
776 qStableSort(indexes.begin(), indexes.end(), qGreater<QModelIndex>());
777 playlistModel->move(indexes, false);
779 // set current index after row moves to something more intuitive
780 // (respect 1 static item on bottom)
781 int row = indexes.at(0).row() + 1, max = playlistModel->rowCount() - 2;
782 playlistView->selectionModel()->setCurrentIndex(playlistModel->index(row > max ? max : row),
783 QItemSelectionModel::NoUpdate);
786 void MediaView::setSidebarVisibility(bool visible) {
787 if (sidebar->isVisible() == visible) return;
788 sidebar->setVisible(visible);
791 sidebar->resize(sidebar->width(), window()->height());
793 playlistView->setFocus();
797 void MediaView::removeSidebar() {
799 sidebar->setParent(window());
802 void MediaView::restoreSidebar() {
804 splitter->insertWidget(0, sidebar);
807 bool MediaView::isSidebarVisible() {
808 return sidebar->isVisible();
811 void MediaView::saveSplitterState() {
813 if (splitter) settings.setValue("splitter", splitter->saveState());
816 void MediaView::downloadVideo() {
817 Video *video = playlistModel->activeVideo();
819 DownloadManager::instance()->addItem(video);
820 MainWindow::instance()->showActionsInStatusBar({MainWindow::instance()->getAction("downloads")},
822 QString message = tr("Downloading %1").arg(video->getTitle());
823 MainWindow::instance()->showMessage(message);
827 void MediaView::snapshot() {
828 qint64 currentTime = media->position() / 1000;
830 QObject *context = new QObject();
831 connect(media, &Media::snapshotReady, context,
832 [this, currentTime, context](const QImage &image) {
833 context->deleteLater();
835 if (image.isNull()) {
836 qWarning() << "Null snapshot";
840 QPixmap pixmap = QPixmap::fromImage(image.scaled(
841 videoWidget->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
842 videoAreaWidget->showSnapshotPreview(pixmap);
844 Video *video = playlistModel->activeVideo();
847 QString location = SnapshotSettings::getCurrentLocation();
849 if (!dir.exists()) dir.mkpath(location);
850 QString basename = video->getTitle();
851 QString format = video->getDuration() > 3600 ? "h_mm_ss" : "m_ss";
852 basename += " (" + QTime(0, 0, 0).addSecs(currentTime).toString(format) + ")";
853 basename = DataUtils::stringToFilename(basename);
854 QString filename = location + "/" + basename + ".png";
855 qDebug() << filename;
856 image.save(filename, "PNG");
858 if (snapshotSettings) delete snapshotSettings;
859 snapshotSettings = new SnapshotSettings(videoWidget);
860 snapshotSettings->setSnapshot(pixmap, filename);
861 QStatusBar *statusBar = MainWindow::instance()->statusBar();
863 Extra::fadeInWidget(statusBar, statusBar);
865 statusBar->insertPermanentWidget(0, snapshotSettings);
866 snapshotSettings->show();
867 MainWindow::instance()->setStatusBarVisibility(true);
875 void MediaView::fullscreen() {
876 videoAreaWidget->setParent(nullptr);
877 videoAreaWidget->showFullScreen();
880 void MediaView::resumeWithNewStreamUrl(const QString &streamUrl, const QString &audioUrl) {
881 pauseTime = media->position();
883 if (audioUrl.isEmpty()) {
884 qDebug() << "Playing" << streamUrl;
885 media->play(streamUrl);
887 qDebug() << "Playing" << streamUrl << audioUrl;
888 media->playSeparateAudioAndVideo(streamUrl, audioUrl);
891 Video *video = static_cast<Video *>(sender());
893 qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
896 video->disconnect(this);
899 void MediaView::findVideoParts() {
900 Video *video = playlistModel->activeVideo();
903 QString query = video->getTitle();
905 const QLatin1String optionalSpace("\\s*");
906 const QLatin1String staticCounterSeparators("[\\/\\-]");
907 const QString counterSeparators =
908 QLatin1String("( of | ") + tr("of", "Used in video parts, as in '2 of 3'") +
909 QLatin1String(" |") + staticCounterSeparators + QLatin1String(")");
911 // numbers from 1 to 15
912 const QLatin1String counterNumber("([1-9]|1[0-5])");
914 // query.remove(QRegExp(counterSeparators + optionalSpace + counterNumber));
915 query.remove(QRegExp(counterNumber + optionalSpace + counterSeparators + optionalSpace +
917 query.remove(wordRE("pr?t\\.?" + optionalSpace + counterNumber));
918 query.remove(wordRE("ep\\.?" + optionalSpace + counterNumber));
919 query.remove(wordRE("part" + optionalSpace + counterNumber));
920 query.remove(wordRE("episode" + optionalSpace + counterNumber));
921 query.remove(wordRE(tr("part", "This is for video parts, as in 'Cool video - part 1'") +
922 optionalSpace + counterNumber));
923 query.remove(wordRE(tr("episode", "This is for video parts, as in 'Cool series - episode 1'") +
924 optionalSpace + counterNumber));
925 query.remove(QRegExp("[\\(\\)\\[\\]]"));
927 #define NUMBERS "one|two|three|four|five|six|seven|eight|nine|ten"
929 QRegExp englishNumberRE = QRegExp(QLatin1String(".*(") + NUMBERS + ").*", Qt::CaseInsensitive);
930 // bool numberAsWords = englishNumberRE.exactMatch(query);
931 query.remove(englishNumberRE);
933 QRegExp localizedNumberRE =
934 QRegExp(QLatin1String(".*(") + tr(NUMBERS) + ").*", Qt::CaseInsensitive);
935 // if (!numberAsWords) numberAsWords = localizedNumberRE.exactMatch(query);
936 query.remove(localizedNumberRE);
938 SearchParams *searchParams = new SearchParams();
939 searchParams->setTransient(true);
940 searchParams->setKeywords(query);
941 searchParams->setChannelId(video->getChannelId());
944 if (!numberAsWords) {
945 qDebug() << "We don't have number as words";
946 // searchParams->setSortBy(SearchParams::SortByNewest);
947 // TODO searchParams->setReverseOrder(true);
948 // TODO searchParams->setMax(50);
952 search(searchParams);
955 void MediaView::relatedVideos() {
956 Video *video = playlistModel->activeVideo();
959 if (VideoAPI::impl() == VideoAPI::YT3) {
960 YTSingleVideoSource *singleVideoSource = new YTSingleVideoSource();
961 singleVideoSource->setVideo(video->clone());
962 singleVideoSource->setAsyncDetails(true);
963 setVideoSource(singleVideoSource);
964 } else if (VideoAPI::impl() == VideoAPI::IV) {
965 auto source = new IVSingleVideoSource(this);
966 source->setVideo(video->clone());
967 setVideoSource(source);
968 } else if (VideoAPI::impl() == VideoAPI::JS) {
969 auto source = new YTJSSingleVideoSource(this);
970 source->setVideo(video->clone());
971 setVideoSource(source);
974 MainWindow::instance()->getAction("relatedVideos")->setEnabled(false);
977 void MediaView::shareViaTwitter() {
978 Video *video = playlistModel->activeVideo();
980 QUrl url("https://twitter.com/intent/tweet");
982 q.addQueryItem("via", "minitubeapp");
983 q.addQueryItem("text", video->getTitle());
984 q.addQueryItem("url", video->getWebpage());
986 QDesktopServices::openUrl(url);
989 void MediaView::shareViaFacebook() {
990 Video *video = playlistModel->activeVideo();
992 QUrl url("https://www.facebook.com/sharer.php");
994 q.addQueryItem("t", video->getTitle());
995 q.addQueryItem("u", video->getWebpage());
997 QDesktopServices::openUrl(url);
1000 void MediaView::shareViaEmail() {
1001 Video *video = playlistModel->activeVideo();
1003 QUrl url("mailto:");
1005 q.addQueryItem("subject", video->getTitle());
1006 const QString body = video->getTitle() + "\n" + video->getWebpage() + "\n\n" +
1007 tr("Sent from %1").arg(Constants::NAME) + "\n" + Constants::WEBSITE;
1008 q.addQueryItem("body", body);
1010 QDesktopServices::openUrl(url);
1013 void MediaView::onAuthorPushed(QModelIndex index) {
1014 Video *video = playlistModel->videoAt(index.row());
1017 QString channelId = video->getChannelId();
1018 // if (channelId.isEmpty()) channelId = video->channelTitle();
1019 if (channelId.isEmpty()) return;
1021 SearchParams *searchParams = new SearchParams();
1022 searchParams->setChannelId(channelId);
1023 searchParams->setSortBy(SearchParams::SortByNewest);
1026 search(searchParams);
1030 void MediaView::updateSubscriptionAction(bool subscribed) {
1031 QAction *subscribeAction = MainWindow::instance()->getAction("subscribeChannel");
1033 QString subscribeTip;
1034 QString subscribeText;
1036 if (currentSubscriptionChannelId.isEmpty()) {
1037 subscribeText = subscribeAction->property("originalText").toString();
1038 subscribeAction->setEnabled(false);
1039 } else if (subscribed) {
1040 subscribeText = tr("Unsubscribe from %1").arg(currentSubscriptionChannelTitle);
1041 subscribeTip = subscribeText;
1042 subscribeAction->setEnabled(true);
1044 subscribeText = tr("Subscribe to %1").arg(currentSubscriptionChannelTitle);
1045 subscribeTip = subscribeText;
1046 subscribeAction->setEnabled(true);
1048 subscribeAction->setText(subscribeText);
1049 subscribeAction->setStatusTip(subscribeTip);
1052 subscribeAction->setIcon(IconUtils::icon("bookmark-remove"));
1054 subscribeAction->setIcon(IconUtils::icon("bookmark-new"));
1057 MainWindow::instance()->setupAction(subscribeAction);
1060 void MediaView::updateSubscriptionActionForChannel(const QString & channelId) {
1061 QString channelTitle = tr("channel");
1062 YTChannel *channel = YTChannel::forId(channelId);
1063 if (nullptr != channel && !channel->getDisplayName().isEmpty()) {
1064 channelTitle = channel->getDisplayName();
1067 bool subscribed = YTChannel::isSubscribed(channelId);
1069 currentSubscriptionChannelId = channelId;
1070 currentSubscriptionChannelTitle = channelTitle;
1071 updateSubscriptionAction(subscribed);
1074 void MediaView::updateSubscriptionActionForVideo(Video *video, bool subscribed) {
1076 currentSubscriptionChannelId = "";
1077 currentSubscriptionChannelTitle = "";
1078 updateSubscriptionAction(false);
1080 currentSubscriptionChannelId = video->getChannelId();
1081 currentSubscriptionChannelTitle = video->getChannelTitle();
1082 updateSubscriptionAction(subscribed);
1086 void MediaView::reloadCurrentVideo() {
1087 Video *video = playlistModel->activeVideo();
1090 int oldFormat = video->getDefinitionCode();
1092 QObject *context = new QObject();
1093 connect(video, &Video::gotStreamUrl, context,
1094 [this, oldFormat, video, context](const QString &videoUrl, const QString &audioUrl) {
1095 context->deleteLater();
1096 if (oldFormat == video->getDefinitionCode()) return;
1097 QObject *context2 = new QObject();
1098 const qint64 position = media->position();
1099 connect(media, &Media::stateChanged, context2,
1100 [position, this, context2](Media::State state) {
1101 if (state == Media::PlayingState) {
1102 media->seek(position);
1103 context2->deleteLater();
1104 Video *video = playlistModel->activeVideo();
1105 QString msg = tr("Switched to %1")
1106 .arg(VideoDefinition::forCode(
1107 video->getDefinitionCode())
1109 MainWindow::instance()->showMessage(msg);
1113 if (audioUrl.isEmpty()) {
1114 media->play(videoUrl);
1116 media->playSeparateAudioAndVideo(videoUrl, audioUrl);
1119 video->loadStreamUrl();
1122 void MediaView::toggleSubscription() {
1123 //Video *video = playlistModel->activeVideo();
1124 if (currentSubscriptionChannelId.isEmpty()) {
1128 bool subscribed = YTChannel::isSubscribed(currentSubscriptionChannelId);
1130 YTChannel::unsubscribe(currentSubscriptionChannelId);
1131 MainWindow::instance()->showMessage(
1132 tr("Unsubscribed from %1").arg(currentSubscriptionChannelTitle));
1134 YTChannel::subscribe(currentSubscriptionChannelId);
1135 MainWindow::instance()->showMessage(tr("Subscribed to %1").arg(currentSubscriptionChannelTitle));
1138 updateSubscriptionAction(!subscribed);
1141 void MediaView::adjustWindowSize() {
1142 qDebug() << "Adjusting window size";
1143 Video *video = playlistModel->activeVideo();
1145 QWidget *window = this->window();
1146 if (!window->isMaximized() && !window->isFullScreen()) {
1147 const double ratio = 16. / 9.;
1148 const double w = (double)videoAreaWidget->width();
1149 const double h = (double)videoAreaWidget->height();
1150 const double currentVideoRatio = w / h;
1151 if (currentVideoRatio != ratio) {
1152 qDebug() << "Adjust size";
1153 int newHeight = std::round((window->height() - h) + (w / ratio));
1154 window->resize(window->width(), newHeight);