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 "downloaditem.h"
24 #include "downloadmanager.h"
26 #include "loadingwidget.h"
27 #include "mainwindow.h"
28 #include "minisplitter.h"
29 #include "playlistmodel.h"
30 #include "playlistview.h"
31 #include "refinesearchwidget.h"
32 #include "sidebarheader.h"
33 #include "sidebarwidget.h"
34 #include "temporary.h"
35 #include "videoareawidget.h"
37 #include "activation.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"
55 MediaView *MediaView::instance() {
56 static MediaView *i = new MediaView();
60 MediaView::MediaView(QWidget *parent)
61 : View(parent), stopped(false), downloadItem(0)
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(itemActivated(const QModelIndex &)));
84 playlistModel = new PlaylistModel();
85 connect(playlistModel, SIGNAL(activeRowChanged(int)), SLOT(activeRowChanged(int)));
86 // needed to restore the selection after dragndrop
87 connect(playlistModel, SIGNAL(needSelectionFor(QVector<Video *>)),
88 SLOT(selectVideos(QVector<Video *>)));
89 playlistView->setModel(playlistModel);
91 connect(playlistView->selectionModel(),
92 SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
93 SLOT(selectionChanged(const QItemSelection &, const QItemSelection &)));
95 connect(playlistView, SIGNAL(authorPushed(QModelIndex)), SLOT(authorPushed(QModelIndex)));
97 sidebar = new SidebarWidget(this);
98 sidebar->setPlaylist(playlistView);
99 connect(sidebar->getRefineSearchWidget(), SIGNAL(searchRefined()), SLOT(searchAgain()));
100 connect(playlistModel, SIGNAL(haveSuggestions(const QStringList &)), sidebar,
101 SLOT(showSuggestions(const QStringList &)));
102 connect(sidebar, SIGNAL(suggestionAccepted(QString)), mainWindow, SLOT(search(QString)));
103 splitter->addWidget(sidebar);
105 videoAreaWidget = new VideoAreaWidget(this);
108 videoWidget = new Phonon::VideoWidget(this);
109 videoAreaWidget->setVideoWidget(videoWidget);
111 videoAreaWidget->setListModel(playlistModel);
113 loadingWidget = new LoadingWidget(this);
114 videoAreaWidget->setLoadingWidget(loadingWidget);
116 splitter->addWidget(videoAreaWidget);
118 // restore splitter state
120 if (settings.contains("splitter"))
121 splitter->restoreState(settings.value("splitter").toByteArray());
123 int sidebarDefaultWidth = 180;
124 splitter->setSizes(QList<int>() << sidebarDefaultWidth
125 << splitter->size().width() - sidebarDefaultWidth);
127 splitter->setChildrenCollapsible(false);
128 connect(splitter, SIGNAL(splitterMoved(int, int)), SLOT(adjustWindowSize()));
130 errorTimer = new QTimer(this);
131 errorTimer->setSingleShot(true);
132 errorTimer->setInterval(3000);
133 connect(errorTimer, SIGNAL(timeout()), SLOT(skipVideo()));
135 #ifdef APP_ACTIVATION
136 demoTimer = new QTimer(this);
137 demoTimer->setSingleShot(true);
138 connect(demoTimer, &QTimer::timeout, mainWindow, &MainWindow::showActivationView,
139 Qt::QueuedConnection);
142 connect(videoAreaWidget, SIGNAL(doubleClicked()), mainWindow->getAction("fullscreen"),
145 QAction *refineSearchAction = mainWindow->getAction("refineSearch");
146 connect(refineSearchAction, SIGNAL(toggled(bool)), sidebar, SLOT(toggleRefineSearch(bool)));
148 const QVector<const char *> videoActionNames = {
152 "webpage", "pagelink", "videolink", "openInBrowser", "findVideoParts",
153 "skip", "previous", "stopafterthis", "relatedVideos", "refineSearch",
154 "twitter", "facebook", "email"};
155 currentVideoActions.reserve(videoActionNames.size());
156 for (auto *name : videoActionNames) {
157 currentVideoActions.append(mainWindow->getAction(name));
160 #ifndef APP_PHONON_SEEK
161 QSlider *slider = mainWindow->getSlider();
162 connect(slider, SIGNAL(valueChanged(int)), SLOT(sliderMoved(int)));
167 void MediaView::setMediaObject(Phonon::MediaObject *mediaObject) {
168 this->mediaObject = mediaObject;
169 Phonon::createPath(mediaObject, videoWidget);
170 connect(mediaObject, SIGNAL(finished()), SLOT(playbackFinished()));
171 connect(mediaObject, SIGNAL(stateChanged(Phonon::State, Phonon::State)),
172 SLOT(stateChanged(Phonon::State, Phonon::State)));
173 connect(mediaObject, SIGNAL(aboutToFinish()), SLOT(aboutToFinish()));
174 connect(mediaObject, SIGNAL(bufferStatus(int)), loadingWidget, SLOT(bufferStatus(int)));
178 SearchParams *MediaView::getSearchParams() {
179 VideoSource *videoSource = playlistModel->getVideoSource();
180 if (videoSource && videoSource->metaObject()->className() == QLatin1String("YTSearch")) {
181 YTSearch *search = qobject_cast<YTSearch *>(videoSource);
182 return search->getSearchParams();
187 void MediaView::search(SearchParams *searchParams) {
188 if (!searchParams->keywords().isEmpty()) {
189 if (searchParams->keywords().startsWith("http://") ||
190 searchParams->keywords().startsWith("https://")) {
191 QString videoId = YTSearch::videoIdFromUrl(searchParams->keywords());
192 if (!videoId.isEmpty()) {
193 YTSingleVideoSource *singleVideoSource = new YTSingleVideoSource(this);
194 singleVideoSource->setVideoId(videoId);
195 setVideoSource(singleVideoSource);
196 QTime tstamp = YTSearch::videoTimestampFromUrl(searchParams->keywords());
197 pauseTime = QTime(0, 0).msecsTo(tstamp);
202 YTSearch *ytSearch = new YTSearch(searchParams);
203 ytSearch->setAsyncDetails(true);
204 connect(ytSearch, SIGNAL(gotDetails()), playlistModel, SLOT(emitDataChanged()));
205 setVideoSource(ytSearch);
208 void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool back) {
213 // qDebug() << "Adding VideoSource" << videoSource->getName() << videoSource;
216 int currentIndex = getHistoryIndex();
217 if (currentIndex >= 0 && currentIndex < history.size() - 1) {
218 while (history.size() > currentIndex + 1) {
219 VideoSource *vs = history.takeLast();
221 qDebug() << "Deleting VideoSource" << vs->getName() << vs;
226 history.append(videoSource);
230 if (history.size() > 1)
231 Extra::slideTransition(playlistView->viewport(), playlistView->viewport(), back);
234 playlistModel->setVideoSource(videoSource);
237 if (settings.value("manualplay", false).toBool()) {
238 videoAreaWidget->showPickMessage();
241 sidebar->showPlaylist();
242 sidebar->getRefineSearchWidget()->setSearchParams(getSearchParams());
243 sidebar->hideSuggestions();
244 sidebar->getHeader()->updateInfo();
246 SearchParams *searchParams = getSearchParams();
247 bool isChannel = searchParams && !searchParams->channelId().isEmpty();
248 playlistView->setClickableAuthors(!isChannel);
251 void MediaView::searchAgain() {
252 VideoSource *currentVideoSource = playlistModel->getVideoSource();
253 setVideoSource(currentVideoSource, false);
256 bool MediaView::canGoBack() {
257 return getHistoryIndex() > 0;
260 void MediaView::goBack() {
261 if (history.size() > 1) {
262 int currentIndex = getHistoryIndex();
263 if (currentIndex > 0) {
264 VideoSource *previousVideoSource = history.at(currentIndex - 1);
265 setVideoSource(previousVideoSource, false, true);
270 bool MediaView::canGoForward() {
271 int currentIndex = getHistoryIndex();
272 return currentIndex >= 0 && currentIndex < history.size() - 1;
275 void MediaView::goForward() {
276 if (canGoForward()) {
277 int currentIndex = getHistoryIndex();
278 VideoSource *nextVideoSource = history.at(currentIndex + 1);
279 setVideoSource(nextVideoSource, false);
283 int MediaView::getHistoryIndex() {
284 return history.lastIndexOf(playlistModel->getVideoSource());
287 void MediaView::appear() {
288 MainWindow::instance()->showToolbar();
290 Video *currentVideo = playlistModel->activeVideo();
292 MainWindow::instance()->setWindowTitle(currentVideo->getTitle() + " - " + Constants::NAME);
295 playlistView->setFocus();
298 void MediaView::disappear() {
299 MainWindow::instance()->hideToolbar();
302 void MediaView::handleError(const QString &message) {
303 qWarning() << __PRETTY_FUNCTION__ << message;
304 #ifdef APP_PHONON_SEEK
307 QTimer::singleShot(500, this, SLOT(startPlaying()));
312 void MediaView::stateChanged(Phonon::State newState, Phonon::State oldState) {
313 if (pauseTime > 0 && (newState == Phonon::PlayingState || newState == Phonon::BufferingState)) {
314 mediaObject->seek(pauseTime);
317 if (newState == Phonon::PlayingState) {
318 videoAreaWidget->showVideo();
319 } else if (newState == Phonon::ErrorState) {
320 qWarning() << "Phonon error:" << mediaObject->errorString() << mediaObject->errorType();
321 if (mediaObject->errorType() == Phonon::FatalError) handleError(mediaObject->errorString());
324 if (newState == Phonon::PlayingState) {
325 bool res = Idle::preventDisplaySleep(QString("%1 is playing").arg(Constants::NAME));
326 if (!res) qWarning() << "Error disabling idle display sleep" << Idle::displayErrorMessage();
327 } else if (oldState == Phonon::PlayingState) {
328 bool res = Idle::allowDisplaySleep();
329 if (!res) qWarning() << "Error enabling idle display sleep" << Idle::displayErrorMessage();
334 void MediaView::pause() {
336 switch (mediaObject->state()) {
337 case Phonon::PlayingState:
338 mediaObject->pause();
342 if (pauseTimer.hasExpired(60000)) {
343 pauseTimer.invalidate();
344 connect(playlistModel->activeVideo(), SIGNAL(gotStreamUrl(QUrl)),
345 SLOT(resumeWithNewStreamUrl(QUrl)));
346 playlistModel->activeVideo()->loadStreamUrl();
354 QRegExp MediaView::wordRE(const QString &s) {
355 return QRegExp("\\W" + s + "\\W?", Qt::CaseInsensitive);
358 void MediaView::stop() {
361 while (!history.isEmpty()) {
362 VideoSource *videoSource = history.takeFirst();
363 // Don't delete videoSource in the Browse view
364 if (!videoSource->parent()) {
365 videoSource->deleteLater();
369 playlistModel->abortSearch();
370 videoAreaWidget->clear();
371 videoAreaWidget->update();
373 playlistView->selectionModel()->clearSelection();
375 downloadItem->stop();
378 currentVideoSize = 0;
380 MainWindow::instance()->getAction("refineSearch")->setChecked(false);
381 updateSubscriptionAction(0, false);
382 #ifdef APP_ACTIVATION
386 for (QAction *action : currentVideoActions)
387 action->setEnabled(false);
389 QAction *a = MainWindow::instance()->getAction("download");
390 a->setEnabled(false);
391 a->setVisible(false);
395 mediaObject->clear();
397 currentVideoId.clear();
399 #ifndef APP_PHONON_SEEK
400 QSlider *slider = MainWindow::instance()->getSlider();
401 slider->setEnabled(false);
404 // Phonon::SeekSlider *slider = MainWindow::instance()->getSeekSlider();
408 if (snapshotSettings) {
409 delete snapshotSettings;
410 snapshotSettings = 0;
415 const QString &MediaView::getCurrentVideoId() {
416 return currentVideoId;
419 void MediaView::activeRowChanged(int row) {
428 downloadItem->stop();
431 currentVideoSize = 0;
434 Video *video = playlistModel->videoAt(row);
437 // optimize window for 16:9 video
440 videoAreaWidget->showLoading(video);
442 connect(video, SIGNAL(gotStreamUrl(QUrl)), SLOT(gotStreamUrl(QUrl)), Qt::UniqueConnection);
443 connect(video, SIGNAL(errorStreamUrl(QString)), SLOT(skip()), Qt::UniqueConnection);
444 video->loadStreamUrl();
446 // video title in titlebar
447 MainWindow::instance()->setWindowTitle(video->getTitle() + QLatin1String(" - ") +
448 QLatin1String(Constants::NAME));
450 // ensure active item is visible
452 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
453 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
456 // enable/disable actions
457 MainWindow::instance()
458 ->getAction("download")
459 ->setEnabled(DownloadManager::instance()->itemForVideo(video) == 0);
460 MainWindow::instance()->getAction("previous")->setEnabled(row > 0);
461 MainWindow::instance()->getAction("stopafterthis")->setEnabled(true);
462 MainWindow::instance()->getAction("relatedVideos")->setEnabled(true);
464 bool enableDownload = video->getLicense() == Video::LicenseCC;
465 #ifdef APP_ACTIVATION
466 enableDownload = enableDownload || Activation::instance().isLegacy();
469 enableDownload = true;
471 QAction *a = MainWindow::instance()->getAction("download");
472 a->setEnabled(enableDownload);
473 a->setVisible(enableDownload);
475 updateSubscriptionAction(video, YTChannel::isSubscribed(video->getChannelId()));
477 for (QAction *action : currentVideoActions)
478 action->setEnabled(true);
480 #ifndef APP_PHONON_SEEK
481 QSlider *slider = MainWindow::instance()->getSlider();
482 slider->setEnabled(false);
487 if (snapshotSettings) {
488 delete snapshotSettings;
489 snapshotSettings = 0;
490 MainWindow::instance()->adjustStatusBarVisibility();
494 // see you in gotStreamUrl...
497 void MediaView::gotStreamUrl(QUrl streamUrl) {
499 if (!streamUrl.isValid()) {
504 Video *video = static_cast<Video *>(sender());
506 qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
509 video->disconnect(this);
511 currentVideoId = video->getId();
513 #ifdef APP_PHONON_SEEK
514 mediaObject->setCurrentSource(streamUrl);
520 // ensure we always have videos ahead
521 playlistModel->searchNeeded();
523 // ensure active item is visible
524 int row = playlistModel->activeRow();
526 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
527 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
530 #ifdef APP_ACTIVATION
531 if (!Activation::instance().isActivated() && !demoTimer->isActive()) {
532 int ms = (60000 * 5) + (qrand() % (60000 * 5));
533 demoTimer->start(ms);
538 Extra::notify(video->getTitle(), video->getChannelTitle(), video->getFormattedDuration());
541 ChannelAggregator::instance()->videoWatched(video);
544 void MediaView::downloadStatusChanged() {
545 // qDebug() << __PRETTY_FUNCTION__;
546 switch (downloadItem->status()) {
548 // qDebug() << "Downloading";
549 if (downloadItem->offset() == 0)
553 // qDebug() << "Seeking to" << downloadItem->offset();
554 mediaObject->seek(offsetToTime(downloadItem->offset()));
560 // qDebug() << "Starting";
563 // qDebug() << "Finished" << mediaObject->state();
564 #ifdef APP_PHONON_SEEK
565 MainWindow::instance()->getSeekSlider()->setEnabled(mediaObject->isSeekable());
569 // qDebug() << "Failed";
573 // qDebug() << "Idle";
578 void MediaView::startPlaying() {
579 // qDebug() << __PRETTY_FUNCTION__;
586 if (downloadItem->offset() == 0) {
587 currentVideoSize = downloadItem->bytesTotal();
588 // qDebug() << "currentVideoSize" << currentVideoSize;
592 QString source = downloadItem->currentFilename();
593 qDebug() << "Playing" << source << QFile::exists(source);
595 mediaObject->setCurrentSource(QUrl::fromLocalFile(source));
598 #ifdef APP_PHONON_SEEK
599 MainWindow::instance()->getSeekSlider()->setEnabled(false);
601 QSlider *slider = MainWindow::instance()->getSlider();
602 slider->setEnabled(true);
606 void MediaView::itemActivated(const QModelIndex &index) {
607 if (playlistModel->rowExists(index.row())) {
608 // if it's the current video, just rewind and play
609 Video *activeVideo = playlistModel->activeVideo();
610 Video *video = playlistModel->videoAt(index.row());
611 if (activeVideo && video && activeVideo == video) {
612 // mediaObject->seek(0);
618 playlistModel->setActiveRow(index.row());
620 // the user doubleclicked on the "Search More" item
622 playlistModel->searchMore();
623 playlistView->selectionModel()->clearSelection();
627 void MediaView::skipVideo() {
628 // skippedVideo is useful for DELAYED skip operations
629 // in order to be sure that we're skipping the video we wanted
630 // and not another one
632 if (playlistModel->activeVideo() != skippedVideo) {
633 qDebug() << "Skip of video canceled";
636 int nextRow = playlistModel->rowForVideo(skippedVideo);
638 if (nextRow == -1) return;
639 playlistModel->setActiveRow(nextRow);
643 void MediaView::skip() {
644 int nextRow = playlistModel->nextRow();
645 if (nextRow == -1) return;
646 playlistModel->setActiveRow(nextRow);
649 void MediaView::skipBackward() {
650 int prevRow = playlistModel->previousRow();
651 if (prevRow == -1) return;
652 playlistModel->setActiveRow(prevRow);
655 void MediaView::aboutToFinish() {
657 qint64 currentTime = mediaObject->currentTime();
658 qint64 totalTime = mediaObject->totalTime();
659 // qDebug() << __PRETTY_FUNCTION__ << currentTime << totalTime;
660 if (totalTime < 1 || currentTime + 10000 < totalTime) {
661 // QTimer::singleShot(500, this, SLOT(playbackResume()));
662 mediaObject->seek(currentTime);
668 void MediaView::playbackFinished() {
672 const qint64 totalTime = mediaObject->totalTime();
673 const qint64 currentTime = mediaObject->currentTime();
674 // qDebug() << __PRETTY_FUNCTION__ << mediaObject->currentTime() << totalTime;
675 // add 10 secs for imprecise Phonon backends (VLC, Xine)
676 if (currentTime > 0 && currentTime + 10000 < totalTime) {
677 // mediaObject->seek(currentTime);
678 QTimer::singleShot(500, this, SLOT(playbackResume()));
680 QAction *stopAfterThisAction = MainWindow::instance()->getAction("stopafterthis");
681 if (stopAfterThisAction->isChecked()) {
682 stopAfterThisAction->setChecked(false);
689 void MediaView::playbackResume() {
692 const qint64 currentTime = mediaObject->currentTime();
693 // qDebug() << __PRETTY_FUNCTION__ << currentTime;
694 if (currentTime > 0) mediaObject->seek(currentTime);
699 void MediaView::openWebPage() {
700 Video *video = playlistModel->activeVideo();
703 mediaObject->pause();
705 QString url = video->getWebpage() + QLatin1String("&t=") +
706 QString::number(mediaObject->currentTime() / 1000);
707 QDesktopServices::openUrl(url);
710 void MediaView::copyWebPage() {
711 Video *video = playlistModel->activeVideo();
713 QString address = video->getWebpage();
714 QApplication::clipboard()->setText(address);
715 QString message = tr("You can now paste the YouTube link into another application");
716 MainWindow::instance()->showMessage(message);
719 void MediaView::copyVideoLink() {
720 Video *video = playlistModel->activeVideo();
722 QApplication::clipboard()->setText(video->getStreamUrl().toEncoded());
723 QString message = tr("You can now paste the video stream URL into another application") + ". " +
724 tr("The link will be valid only for a limited time.");
725 MainWindow::instance()->showMessage(message);
728 void MediaView::openInBrowser() {
729 Video *video = playlistModel->activeVideo();
732 mediaObject->pause();
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);
790 playlistView->setFocus();
793 void MediaView::removeSidebar() {
796 sidebar->setParent(window());
802 void MediaView::restoreSidebar() {
805 splitter->insertWidget(0, sidebar);
809 bool MediaView::isSidebarVisible() {
810 return sidebar->isVisible();
813 void MediaView::saveSplitterState() {
815 settings.setValue("splitter", splitter->saveState());
818 void MediaView::downloadVideo() {
819 Video *video = playlistModel->activeVideo();
821 DownloadManager::instance()->addItem(video);
822 MainWindow::instance()->showActionInStatusBar(MainWindow::instance()->getAction("downloads"),
824 QString message = tr("Downloading %1").arg(video->getTitle());
825 MainWindow::instance()->showMessage(message);
829 void MediaView::snapshot() {
830 qint64 currentTime = mediaObject->currentTime() / 1000;
832 QImage image = videoWidget->snapshot();
833 if (image.isNull()) {
834 qWarning() << "Null snapshot";
838 // QPixmap pixmap = QPixmap::grabWindow(videoWidget->winId());
839 QPixmap pixmap = QPixmap::fromImage(
840 image.scaled(videoWidget->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
841 videoAreaWidget->showSnapshotPreview(pixmap);
843 Video *video = playlistModel->activeVideo();
846 QString location = SnapshotSettings::getCurrentLocation();
848 if (!dir.exists()) dir.mkpath(location);
849 QString basename = video->getTitle();
850 QString format = video->getDuration() > 3600 ? "h_mm_ss" : "m_ss";
851 basename += " (" + QTime(0, 0, 0).addSecs(currentTime).toString(format) + ")";
852 basename = DataUtils::stringToFilename(basename);
853 QString filename = location + "/" + basename + ".png";
854 qDebug() << filename;
855 image.save(filename, "PNG");
857 if (snapshotSettings) delete snapshotSettings;
858 snapshotSettings = new SnapshotSettings(videoWidget);
859 snapshotSettings->setSnapshot(pixmap, filename);
860 QStatusBar *statusBar = MainWindow::instance()->statusBar();
862 Extra::fadeInWidget(statusBar, statusBar);
864 statusBar->insertPermanentWidget(0, snapshotSettings);
865 snapshotSettings->show();
866 MainWindow::instance()->setStatusBarVisibility(true);
870 void MediaView::fullscreen() {
871 videoAreaWidget->setParent(0);
872 videoAreaWidget->showFullScreen();
875 void MediaView::startDownloading() {
876 Video *video = playlistModel->activeVideo();
878 Video *videoCopy = video->clone();
880 downloadItem->stop();
883 QString tempFile = Temporary::filename();
884 downloadItem = new DownloadItem(videoCopy, video->getStreamUrl(), tempFile, this);
885 connect(downloadItem, SIGNAL(statusChanged()), SLOT(downloadStatusChanged()),
886 Qt::UniqueConnection);
887 connect(downloadItem, SIGNAL(bufferProgress(int)), loadingWidget, SLOT(bufferStatus(int)),
888 Qt::UniqueConnection);
889 // connect(downloadItem, SIGNAL(finished()), SLOT(itemFinished()));
890 connect(video, SIGNAL(errorStreamUrl(QString)), SLOT(handleError(QString)),
891 Qt::UniqueConnection);
892 connect(downloadItem, SIGNAL(error(QString)), SLOT(handleError(QString)), Qt::UniqueConnection);
893 downloadItem->start();
896 void MediaView::resumeWithNewStreamUrl(const QUrl &streamUrl) {
897 pauseTime = mediaObject->currentTime();
898 mediaObject->setCurrentSource(streamUrl);
901 Video *video = static_cast<Video *>(sender());
903 qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
906 video->disconnect(this);
909 void MediaView::sliderMoved(int value) {
912 #ifndef APP_PHONON_SEEK
914 if (currentVideoSize <= 0 || !downloadItem || !mediaObject->isSeekable()) return;
916 QSlider *slider = MainWindow::instance()->getSlider();
917 if (slider->isSliderDown()) return;
919 qint64 offset = (currentVideoSize * value) / slider->maximum();
921 bool needsDownload = downloadItem->needsDownload(offset);
923 if (downloadItem->isBuffered(offset)) {
924 qint64 realOffset = downloadItem->blankAtOffset(offset);
925 if (offset < currentVideoSize) downloadItem->seekTo(realOffset, false);
926 mediaObject->seek(offsetToTime(offset));
928 mediaObject->pause();
929 downloadItem->seekTo(offset);
932 // qDebug() << "simple seek";
933 mediaObject->seek(offsetToTime(offset));
939 qint64 MediaView::offsetToTime(qint64 offset) {
941 const qint64 totalTime = mediaObject->totalTime();
942 return ((offset * totalTime) / currentVideoSize);
946 void MediaView::findVideoParts() {
947 Video *video = playlistModel->activeVideo();
950 QString query = video->getTitle();
952 const QLatin1String optionalSpace("\\s*");
953 const QLatin1String staticCounterSeparators("[\\/\\-]");
954 const QString counterSeparators =
955 QLatin1String("( of | ") + tr("of", "Used in video parts, as in '2 of 3'") +
956 QLatin1String(" |") + staticCounterSeparators + QLatin1String(")");
958 // numbers from 1 to 15
959 const QLatin1String counterNumber("([1-9]|1[0-5])");
961 // query.remove(QRegExp(counterSeparators + optionalSpace + counterNumber));
962 query.remove(QRegExp(counterNumber + optionalSpace + counterSeparators + optionalSpace +
964 query.remove(wordRE("pr?t\\.?" + optionalSpace + counterNumber));
965 query.remove(wordRE("ep\\.?" + optionalSpace + counterNumber));
966 query.remove(wordRE("part" + optionalSpace + counterNumber));
967 query.remove(wordRE("episode" + optionalSpace + counterNumber));
968 query.remove(wordRE(tr("part", "This is for video parts, as in 'Cool video - part 1'") +
969 optionalSpace + counterNumber));
970 query.remove(wordRE(tr("episode", "This is for video parts, as in 'Cool series - episode 1'") +
971 optionalSpace + counterNumber));
972 query.remove(QRegExp("[\\(\\)\\[\\]]"));
974 #define NUMBERS "one|two|three|four|five|six|seven|eight|nine|ten"
976 QRegExp englishNumberRE = QRegExp(QLatin1String(".*(") + NUMBERS + ").*", Qt::CaseInsensitive);
977 // bool numberAsWords = englishNumberRE.exactMatch(query);
978 query.remove(englishNumberRE);
980 QRegExp localizedNumberRE =
981 QRegExp(QLatin1String(".*(") + tr(NUMBERS) + ").*", Qt::CaseInsensitive);
982 // if (!numberAsWords) numberAsWords = localizedNumberRE.exactMatch(query);
983 query.remove(localizedNumberRE);
985 SearchParams *searchParams = new SearchParams();
986 searchParams->setTransient(true);
987 searchParams->setKeywords(query);
988 searchParams->setChannelId(video->getChannelId());
991 if (!numberAsWords) {
992 qDebug() << "We don't have number as words";
993 // searchParams->setSortBy(SearchParams::SortByNewest);
994 // TODO searchParams->setReverseOrder(true);
995 // TODO searchParams->setMax(50);
999 search(searchParams);
1002 void MediaView::relatedVideos() {
1003 Video *video = playlistModel->activeVideo();
1005 YTSingleVideoSource *singleVideoSource = new YTSingleVideoSource();
1006 singleVideoSource->setVideo(video->clone());
1007 singleVideoSource->setAsyncDetails(true);
1008 setVideoSource(singleVideoSource);
1009 MainWindow::instance()->getAction("relatedVideos")->setEnabled(false);
1012 void MediaView::shareViaTwitter() {
1013 Video *video = playlistModel->activeVideo();
1015 QUrl url("https://twitter.com/intent/tweet");
1017 q.addQueryItem("via", "minitubeapp");
1018 q.addQueryItem("text", video->getTitle());
1019 q.addQueryItem("url", video->getWebpage());
1021 QDesktopServices::openUrl(url);
1024 void MediaView::shareViaFacebook() {
1025 Video *video = playlistModel->activeVideo();
1027 QUrl url("https://www.facebook.com/sharer.php");
1029 q.addQueryItem("t", video->getTitle());
1030 q.addQueryItem("u", video->getWebpage());
1032 QDesktopServices::openUrl(url);
1035 void MediaView::shareViaEmail() {
1036 Video *video = playlistModel->activeVideo();
1038 QUrl url("mailto:");
1040 q.addQueryItem("subject", video->getTitle());
1041 const QString body = video->getTitle() + "\n" + video->getWebpage() + "\n\n" +
1042 tr("Sent from %1").arg(Constants::NAME) + "\n" + Constants::WEBSITE;
1043 q.addQueryItem("body", body);
1045 QDesktopServices::openUrl(url);
1048 void MediaView::authorPushed(QModelIndex index) {
1049 Video *video = playlistModel->videoAt(index.row());
1052 QString channelId = video->getChannelId();
1053 // if (channelId.isEmpty()) channelId = video->channelTitle();
1054 if (channelId.isEmpty()) return;
1056 SearchParams *searchParams = new SearchParams();
1057 searchParams->setChannelId(channelId);
1058 searchParams->setSortBy(SearchParams::SortByNewest);
1061 search(searchParams);
1064 void MediaView::updateSubscriptionAction(Video *video, bool subscribed) {
1065 QAction *subscribeAction = MainWindow::instance()->getAction("subscribeChannel");
1067 QString subscribeTip;
1068 QString subscribeText;
1070 subscribeText = subscribeAction->property("originalText").toString();
1071 subscribeAction->setEnabled(false);
1072 } else if (subscribed) {
1073 subscribeText = tr("Unsubscribe from %1").arg(video->getChannelTitle());
1074 subscribeTip = subscribeText;
1075 subscribeAction->setEnabled(true);
1077 subscribeText = tr("Subscribe to %1").arg(video->getChannelTitle());
1078 subscribeTip = subscribeText;
1079 subscribeAction->setEnabled(true);
1081 subscribeAction->setText(subscribeText);
1082 subscribeAction->setStatusTip(subscribeTip);
1086 static QIcon tintedIcon;
1087 if (tintedIcon.isNull()) {
1088 QVector<QSize> sizes;
1089 sizes << QSize(16, 16);
1090 tintedIcon = IconUtils::tintedIcon("bookmark-new", QColor(254, 240, 0), sizes);
1092 subscribeAction->setIcon(tintedIcon);
1094 subscribeAction->setIcon(IconUtils::icon("bookmark-remove"));
1097 subscribeAction->setIcon(IconUtils::icon("bookmark-new"));
1100 IconUtils::setupAction(subscribeAction);
1103 void MediaView::toggleSubscription() {
1104 Video *video = playlistModel->activeVideo();
1106 QString userId = video->getChannelId();
1107 if (userId.isEmpty()) return;
1108 bool subscribed = YTChannel::isSubscribed(userId);
1110 YTChannel::unsubscribe(userId);
1111 MainWindow::instance()->showMessage(
1112 tr("Unsubscribed from %1").arg(video->getChannelTitle()));
1114 YTChannel::subscribe(userId);
1115 MainWindow::instance()->showMessage(tr("Subscribed to %1").arg(video->getChannelTitle()));
1117 updateSubscriptionAction(video, !subscribed);
1120 void MediaView::adjustWindowSize() {
1121 Video *video = playlistModel->activeVideo();
1123 QWidget *window = this->window();
1124 if (!window->isMaximized() && !window->isFullScreen()) {
1125 const double ratio = 16. / 9.;
1126 const double w = (double)videoAreaWidget->width();
1127 const double h = (double)videoAreaWidget->height();
1128 const double currentVideoRatio = w / h;
1129 if (currentVideoRatio != ratio) {
1130 int newHeight = std::round((window->height() - h) + (w / ratio));
1131 window->resize(window->width(), newHeight);