1 /* $BEGIN_LICENSE
2
3 This file is part of Minitube.
4 Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
5
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.
10
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.
15
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/>.
18
19 $END_LICENSE */
20
21 #include "mediaview.h"
22 #include "constants.h"
23 #include "downloadmanager.h"
24 #include "http.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"
35 #ifdef APP_ACTIVATION
36 #include "activation.h"
37 #include "activationview.h"
38 #endif
39 #ifdef APP_EXTRA
40 #include "extra.h"
41 #endif
42 #include "channelaggregator.h"
43 #include "iconutils.h"
44 #include "searchparams.h"
45 #include "videosource.h"
46 #include "ytchannel.h"
47 #include "ytsearch.h"
48 #ifdef APP_SNAPSHOT
49 #include "snapshotsettings.h"
50 #endif
51 #include "datautils.h"
52 #include "idle.h"
53 #include "videodefinition.h"
54
55 #include "searchvideosource.h"
56 #include "singlevideosource.h"
57 #include "videoapi.h"
58
instance()59 MediaView *MediaView::instance() {
60 static MediaView *i = new MediaView();
61 return i;
62 }
63
MediaView(QWidget * parent)64 MediaView::MediaView(QWidget *parent)
65 : View(parent), splitter(nullptr), stopped(false)
66 #ifdef APP_SNAPSHOT
67 ,
68 snapshotSettings(nullptr)
69 #endif
70 ,
71 pauseTime(0) {
72 }
73
initialize()74 void MediaView::initialize() {
75 MainWindow *mainWindow = MainWindow::instance();
76
77 QBoxLayout *layout = new QVBoxLayout(this);
78 layout->setMargin(0);
79
80 splitter = new MiniSplitter();
81 layout->addWidget(splitter);
82
83 playlistView = new PlaylistView();
84 playlistView->setParent(this);
85 connect(playlistView, SIGNAL(activated(const QModelIndex &)),
86 SLOT(onItemActivated(const QModelIndex &)));
87
88 playlistModel = new PlaylistModel();
89 connect(playlistModel, &PlaylistModel::activeVideoChanged, this,
90 &MediaView::activeVideoChanged);
91 // needed to restore the selection after dragndrop
92 connect(playlistModel, SIGNAL(needSelectionFor(QVector<Video *>)),
93 SLOT(selectVideos(QVector<Video *>)));
94 playlistView->setModel(playlistModel);
95
96 connect(playlistView->selectionModel(),
97 SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
98 SLOT(selectionChanged(const QItemSelection &, const QItemSelection &)));
99
100 connect(playlistView, SIGNAL(authorPushed(QModelIndex)), SLOT(onAuthorPushed(QModelIndex)));
101
102 sidebar = new SidebarWidget(this);
103 sidebar->setPlaylist(playlistView);
104 sidebar->setMaximumWidth(playlistView->minimumWidth() * 3);
105 connect(sidebar->getRefineSearchWidget(), SIGNAL(searchRefined()), SLOT(searchAgain()));
106 connect(playlistModel, SIGNAL(haveSuggestions(const QStringList &)), sidebar,
107 SLOT(showSuggestions(const QStringList &)));
108 connect(sidebar, SIGNAL(suggestionAccepted(QString)), mainWindow, SLOT(search(QString)));
109 splitter->addWidget(sidebar);
110
111 videoAreaWidget = new VideoArea(this);
112 videoAreaWidget->setListModel(playlistModel);
113
114 loadingWidget = new LoadingWidget(this);
115 videoAreaWidget->setLoadingWidget(loadingWidget);
116
117 splitter->addWidget(videoAreaWidget);
118
119 // restore splitter state
120 QSettings settings;
121 if (settings.contains("splitter"))
122 splitter->restoreState(settings.value("splitter").toByteArray());
123 else {
124 int sidebarDefaultWidth = 180;
125 splitter->setSizes(QList<int>() << sidebarDefaultWidth
126 << splitter->size().width() - sidebarDefaultWidth);
127 }
128 splitter->setChildrenCollapsible(false);
129 connect(splitter, SIGNAL(splitterMoved(int, int)), SLOT(adjustWindowSize()));
130
131 errorTimer = new QTimer(this);
132 errorTimer->setSingleShot(true);
133 errorTimer->setInterval(3000);
134 connect(errorTimer, SIGNAL(timeout()), SLOT(skipVideo()));
135
136 #ifdef APP_ACTIVATION
137 demoTimer = new QTimer(this);
138 demoTimer->setSingleShot(true);
139 connect(
140 demoTimer, &QTimer::timeout, this,
141 [this] {
142 if (media->state() != Media::PlayingState) return;
143 media->pause();
144 connect(
145 ActivationView::instance(), &ActivationView::done, media,
146 [this] { media->play(); }, Qt::UniqueConnection);
147 MainWindow::instance()->showActivationView();
148 },
149 Qt::QueuedConnection);
150 #endif
151
152 connect(videoAreaWidget, SIGNAL(doubleClicked()), mainWindow->getAction("fullscreen"),
153 SLOT(trigger()));
154
155 QAction *refineSearchAction = mainWindow->getAction("refineSearch");
156 connect(refineSearchAction, SIGNAL(toggled(bool)), sidebar, SLOT(toggleRefineSearch(bool)));
157
158 const QVector<const char *> videoActionNames = {
159 #ifdef APP_SNAPSHOT
160 "snapshot",
161 #endif
162 "webpage", "pagelink", "videolink", "openInBrowser", "findVideoParts",
163 "skip", "previous", "stopafterthis", "relatedVideos", "refineSearch",
164 "twitter", "facebook", "email"};
165 currentVideoActions.reserve(videoActionNames.size());
166 for (auto *name : videoActionNames) {
167 currentVideoActions.append(mainWindow->getAction(name));
168 }
169
170 for (int i = 0; i < 10; ++i) {
171 QAction *action = new QAction(QString());
172 action->setShortcut(Qt::Key_0 + i);
173 action->setAutoRepeat(false);
174 connect(action, &QAction::triggered, this, [this, i] {
175 qint64 duration = media->duration();
176 // dur : pos = 100 : i*10
177 qint64 position = (duration * (i * 10)) / 100;
178 media->seek(position);
179 });
180 addAction(action);
181 playingVideoActions << action;
182 }
183
184 QAction *leftAction = new QAction(tr("Rewind %1 seconds").arg(10));
185 leftAction->setShortcut(Qt::Key_Left);
186 leftAction->setAutoRepeat(false);
187 connect(leftAction, &QAction::triggered, this, [this] { media->relativeSeek(-10000); });
188 addAction(leftAction);
189 playingVideoActions << leftAction;
190
191 QAction *rightAction = new QAction(tr("Fast forward %1 seconds").arg(10));
192 rightAction->setShortcut(Qt::Key_Right);
193 rightAction->setAutoRepeat(false);
194 connect(rightAction, &QAction::triggered, this, [this] { media->relativeSeek(10000); });
195 addAction(rightAction);
196 playingVideoActions << rightAction;
197 }
198
setMedia(Media * media)199 void MediaView::setMedia(Media *media) {
200 this->media = media;
201
202 videoWidget = media->videoWidget();
203 videoAreaWidget->setVideoWidget(videoWidget);
204
205 connect(media, &Media::finished, this, &MediaView::onPlaybackFinished);
206 connect(media, &Media::stateChanged, this, &MediaView::mediaStateChanged);
207 connect(media, &Media::aboutToFinish, this, &MediaView::onAboutToFinish);
208 connect(media, &Media::bufferStatus, loadingWidget, &LoadingWidget::bufferStatus);
209 }
210
getSearchParams()211 SearchParams *MediaView::getSearchParams() {
212 VideoSource *videoSource = playlistModel->getVideoSource();
213 if (!videoSource) return nullptr;
214 auto clazz = videoSource->metaObject()->className();
215 if (clazz == QLatin1String("SearchVideoSource")) {
216 auto search = qobject_cast<SearchVideoSource *>(videoSource);
217 if (search) return search->getSearchParams();
218 }
219 return nullptr;
220 }
221
search(SearchParams * searchParams)222 void MediaView::search(SearchParams *searchParams) {
223 if (!searchParams->keywords().isEmpty()) {
224 if (searchParams->keywords().startsWith("http://") ||
225 searchParams->keywords().startsWith("https://")) {
226 QString videoId = YTSearch::videoIdFromUrl(searchParams->keywords());
227 if (!videoId.isEmpty()) {
228 auto source = new SingleVideoSource(this);
229 source->setVideoId(videoId);
230 setVideoSource(source);
231
232 QTime tstamp = YTSearch::videoTimestampFromUrl(searchParams->keywords());
233 pauseTime = QTime(0, 0).msecsTo(tstamp);
234 return;
235 }
236 }
237 }
238
239 VideoSource *search = new SearchVideoSource(searchParams);
240 setVideoSource(search);
241 }
242
setVideoSource(VideoSource * videoSource,bool addToHistory,bool back)243 void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool back) {
244 Q_UNUSED(back);
245 stopped = false;
246 errorTimer->stop();
247
248 // qDebug() << "Adding VideoSource" << videoSource->getName() << videoSource;
249
250 if (addToHistory) {
251 int currentIndex = getHistoryIndex();
252 if (currentIndex >= 0 && currentIndex < history.size() - 1) {
253 while (history.size() > currentIndex + 1) {
254 VideoSource *vs = history.takeLast();
255 if (!vs->parent()) {
256 qDebug() << "Deleting VideoSource" << vs->getName() << vs;
257 vs->abort();
258 vs->deleteLater();
259 }
260 }
261 }
262 history.append(videoSource);
263 }
264
265 #ifdef APP_EXTRA
266 if (history.size() > 1)
267 Extra::slideTransition(playlistView->viewport(), playlistView->viewport(), back);
268 #endif
269
270 playlistModel->setVideoSource(videoSource);
271
272 if (media->state() == Media::StoppedState) {
273 QSettings settings;
274 if (settings.value("manualplay", false).toBool()) {
275 videoAreaWidget->showPickMessage();
276 }
277 }
278
279 SearchParams *searchParams = getSearchParams();
280
281 sidebar->showPlaylist();
282 sidebar->getRefineSearchWidget()->setSearchParams(searchParams);
283 sidebar->hideSuggestions();
284 sidebar->getHeader()->updateInfo();
285
286 bool isChannel = searchParams && !searchParams->channelId().isEmpty();
287 if (isChannel) {
288 updateSubscriptionActionForChannel(searchParams->channelId());
289 }
290 playlistView->setClickableAuthors(!isChannel);
291 }
292
searchAgain()293 void MediaView::searchAgain() {
294 VideoSource *currentVideoSource = playlistModel->getVideoSource();
295 setVideoSource(currentVideoSource, false);
296 }
297
canGoBack()298 bool MediaView::canGoBack() {
299 return getHistoryIndex() > 0;
300 }
301
goBack()302 void MediaView::goBack() {
303 if (history.size() > 1) {
304 int currentIndex = getHistoryIndex();
305 if (currentIndex > 0) {
306 VideoSource *previousVideoSource = history.at(currentIndex - 1);
307 setVideoSource(previousVideoSource, false, true);
308 }
309 }
310 }
311
canGoForward()312 bool MediaView::canGoForward() {
313 int currentIndex = getHistoryIndex();
314 return currentIndex >= 0 && currentIndex < history.size() - 1;
315 }
316
goForward()317 void MediaView::goForward() {
318 if (canGoForward()) {
319 int currentIndex = getHistoryIndex();
320 VideoSource *nextVideoSource = history.at(currentIndex + 1);
321 setVideoSource(nextVideoSource, false);
322 }
323 }
324
getHistoryIndex()325 int MediaView::getHistoryIndex() {
326 return history.lastIndexOf(playlistModel->getVideoSource());
327 }
328
appear()329 void MediaView::appear() {
330 MainWindow::instance()->showToolbar();
331
332 Video *currentVideo = playlistModel->activeVideo();
333 if (currentVideo) {
334 MainWindow::instance()->setWindowTitle(currentVideo->getTitle() + " - " + Constants::NAME);
335 }
336
337 playlistView->setFocus();
338 }
339
disappear()340 void MediaView::disappear() {
341 MainWindow::instance()->hideToolbar();
342 }
343
handleError(const QString & message)344 void MediaView::handleError(const QString &message) {
345 qWarning() << __PRETTY_FUNCTION__ << message;
346 #ifndef QT_NO_DEBUG_OUTPUT
347 MainWindow::instance()->showMessage(message);
348 #endif
349 }
350
mediaStateChanged(Media::State state)351 void MediaView::mediaStateChanged(Media::State state) {
352 if (pauseTime > 0 && (state == Media::PlayingState || state == Media::BufferingState)) {
353 qDebug() << "Seeking to" << pauseTime;
354 media->seek(pauseTime);
355 media->play();
356 pauseTime = 0;
357 }
358 if (state == Media::PlayingState) {
359 videoAreaWidget->showVideo();
360 } else if (state == Media::ErrorState) {
361 handleError(media->errorString());
362 }
363
364 bool enablePlayingVideoActions = state != Media::StoppedState;
365 for (QAction *action : qAsConst(playingVideoActions))
366 action->setEnabled(enablePlayingVideoActions);
367
368 if (state == Media::PlayingState) {
369 bool res = Idle::preventDisplaySleep(QString("%1 is playing").arg(Constants::NAME));
370 if (!res) qWarning() << "Error disabling idle display sleep" << Idle::displayErrorMessage();
371 } else if (state == Media::PausedState || state == Media::StoppedState) {
372 bool res = Idle::allowDisplaySleep();
373 if (!res) qWarning() << "Error enabling idle display sleep" << Idle::displayErrorMessage();
374 }
375 }
376
pause()377 void MediaView::pause() {
378 switch (media->state()) {
379 case Media::PlayingState:
380 qDebug() << "Pausing";
381 media->pause();
382 pauseTimer.start();
383 break;
384 default:
385 if (pauseTimer.isValid() && pauseTimer.hasExpired(60000)) {
386 qDebug() << "Pause timer expired";
387 pauseTimer.invalidate();
388 auto activeVideo = playlistModel->activeVideo();
389 if (activeVideo) {
390 connect(activeVideo, &Video::gotStreamUrl, this,
391 &MediaView::resumeWithNewStreamUrl);
392 activeVideo->loadStreamUrl();
393 } else
394 qDebug() << "No active video";
395 } else {
396 qDebug() << "Playing" << media->file();
397 media->play();
398 }
399 break;
400 }
401 }
402
wordRE(const QString & s)403 QRegExp MediaView::wordRE(const QString &s) {
404 return QRegExp("\\W" + s + "\\W?", Qt::CaseInsensitive);
405 }
406
stop()407 void MediaView::stop() {
408 stopped = true;
409
410 while (!history.isEmpty()) {
411 VideoSource *videoSource = history.takeFirst();
412 // Don't delete videoSource in the Browse view
413 if (!videoSource->parent()) {
414 videoSource->abort();
415 videoSource->deleteLater();
416 }
417 }
418
419 playlistModel->abortSearch();
420 videoAreaWidget->clear();
421 videoAreaWidget->update();
422 errorTimer->stop();
423 playlistView->selectionModel()->clearSelection();
424
425 MainWindow::instance()->getAction("refineSearch")->setChecked(false);
426 updateSubscriptionActionForVideo(nullptr, false);
427 #ifdef APP_ACTIVATION
428 demoTimer->stop();
429 #endif
430
431 for (QAction *action : qAsConst(currentVideoActions))
432 action->setEnabled(false);
433
434 QAction *a = MainWindow::instance()->getAction("download");
435 a->setEnabled(false);
436 a->setVisible(false);
437
438 media->stop();
439 media->clearQueue();
440 currentVideoId.clear();
441
442 #ifdef APP_SNAPSHOT
443 if (snapshotSettings) {
444 delete snapshotSettings;
445 snapshotSettings = nullptr;
446 }
447 #endif
448 }
449
getCurrentVideoId()450 const QString &MediaView::getCurrentVideoId() {
451 return currentVideoId;
452 }
453
activeVideoChanged(Video * video,Video * previousVideo)454 void MediaView::activeVideoChanged(Video *video, Video *previousVideo) {
455 if (stopped) return;
456
457 media->stop();
458 errorTimer->stop();
459
460 if (previousVideo && previousVideo != video) {
461 if (previousVideo->isLoadingStreamUrl()) previousVideo->abortLoadStreamUrl();
462 }
463
464 // optimize window for 16:9 video
465 adjustWindowSize();
466
467 videoAreaWidget->showLoading(video);
468
469 connect(video, &Video::gotStreamUrl, this, &MediaView::gotStreamUrl, Qt::UniqueConnection);
470 connect(video, SIGNAL(errorStreamUrl(QString)), SLOT(skip()), Qt::UniqueConnection);
471 video->loadStreamUrl();
472
473 // video title in titlebar
474 MainWindow::instance()->setWindowTitle(video->getTitle() + QLatin1String(" - ") +
475 QLatin1String(Constants::NAME));
476
477 // ensure active item is visible
478 int row = playlistModel->rowForVideo(video);
479 if (row != -1) {
480 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
481 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
482 }
483
484 // enable/disable actions
485 MainWindow::instance()
486 ->getAction("download")
487 ->setEnabled(DownloadManager::instance()->itemForVideo(video) == nullptr);
488 MainWindow::instance()->getAction("previous")->setEnabled(row > 0);
489 MainWindow::instance()->getAction("stopafterthis")->setEnabled(true);
490 MainWindow::instance()->getAction("relatedVideos")->setEnabled(true);
491
492 bool enableDownload = video->getLicense() == Video::LicenseCC;
493 #ifdef APP_ACTIVATION
494 enableDownload = enableDownload || Activation::instance().isLegacy();
495 #endif
496 #ifdef APP_DOWNLOADS
497 enableDownload = true;
498 #endif
499 QAction *a = MainWindow::instance()->getAction("download");
500 a->setEnabled(enableDownload);
501 a->setVisible(enableDownload);
502
503 updateSubscriptionActionForVideo(video, YTChannel::isSubscribed(video->getChannelId()));
504
505 for (QAction *action : currentVideoActions)
506 action->setEnabled(true);
507
508 #ifdef APP_SNAPSHOT
509 if (snapshotSettings) {
510 delete snapshotSettings;
511 snapshotSettings = nullptr;
512 MainWindow::instance()->adjustStatusBarVisibility();
513 }
514 #endif
515
516 // see you in gotStreamUrl...
517 }
518
gotStreamUrl(const QString & streamUrl,const QString & audioUrl)519 void MediaView::gotStreamUrl(const QString &streamUrl, const QString &audioUrl) {
520 if (stopped) return;
521 if (streamUrl.isEmpty()) {
522 qWarning() << "Empty stream url";
523 skip();
524 return;
525 }
526
527 Video *video = static_cast<Video *>(sender());
528 if (!video) {
529 qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
530 return;
531 }
532 video->disconnect(this);
533
534 currentVideoId = video->getId();
535
536 if (audioUrl.isEmpty()) {
537 qDebug() << "Playing" << streamUrl;
538 media->play(streamUrl);
539 } else {
540 qDebug() << "Playing" << streamUrl << audioUrl;
541 media->playSeparateAudioAndVideo(streamUrl, audioUrl);
542 }
543
544 // ensure we always have videos ahead
545 playlistModel->searchNeeded();
546
547 // ensure active item is visible
548 int row = playlistModel->activeRow();
549 if (row != -1) {
550 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
551 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
552 }
553
554 #ifdef APP_ACTIVATION
555 if (!demoTimer->isActive() && !Activation::instance().isActivated()) {
556 int ms = (60000 * 2) + (QRandomGenerator::global()->generate() % (60000 * 2));
557 demoTimer->start(ms);
558 }
559 #endif
560
561 #ifdef APP_EXTRA
562 Extra::notify(video->getTitle(), video->getChannelTitle(), video->getFormattedDuration());
563 #endif
564
565 ChannelAggregator::instance()->videoWatched(video);
566 }
567
onItemActivated(const QModelIndex & index)568 void MediaView::onItemActivated(const QModelIndex &index) {
569 if (playlistModel->rowExists(index.row())) {
570 // if it's the current video, just rewind and play
571 Video *activeVideo = playlistModel->activeVideo();
572 Video *video = playlistModel->videoAt(index.row());
573 if (activeVideo && video && activeVideo == video) {
574 media->play();
575 } else
576 playlistModel->setActiveRow(index.row());
577
578 // the user doubleclicked on the "Search More" item
579 } else {
580 playlistModel->searchMore();
581 playlistView->selectionModel()->clearSelection();
582 }
583 }
584
skipVideo()585 void MediaView::skipVideo() {
586 // skippedVideo is useful for DELAYED skip operations
587 // in order to be sure that we're skipping the video we wanted
588 // and not another one
589 if (skippedVideo) {
590 if (playlistModel->activeVideo() != skippedVideo) {
591 qDebug() << "Skip of video canceled";
592 return;
593 }
594 int nextRow = playlistModel->rowForVideo(skippedVideo);
595 nextRow++;
596 if (nextRow == -1) return;
597 playlistModel->setActiveRow(nextRow);
598 }
599 }
600
skip()601 void MediaView::skip() {
602 int nextRow = playlistModel->nextRow();
603 if (nextRow == -1) return;
604 playlistModel->setActiveRow(nextRow);
605 }
606
skipBackward()607 void MediaView::skipBackward() {
608 int prevRow = playlistModel->previousRow();
609 if (prevRow == -1) return;
610 playlistModel->setActiveRow(prevRow);
611 }
612
onAboutToFinish()613 void MediaView::onAboutToFinish() {
614
615 }
616
onPlaybackFinished()617 void MediaView::onPlaybackFinished() {
618 if (stopped) return;
619
620 const qint64 totalTime = media->duration();
621 const qint64 currentTime = media->position();
622 // qDebug() << __PRETTY_FUNCTION__ << mediaObject->currentTime() << totalTime;
623 // add 10 secs for imprecise Phonon backends (VLC, Xine)
624 if (currentTime > 0 && currentTime + 10000 < totalTime) {
625 // mediaObject->seek(currentTime);
626 QTimer::singleShot(500, this, SLOT(resumePlayback()));
627 } else {
628 QAction *stopAfterThisAction = MainWindow::instance()->getAction("stopafterthis");
629 if (stopAfterThisAction->isChecked()) {
630 stopAfterThisAction->setChecked(false);
631 } else
632 skip();
633 }
634 }
635
resumePlayback()636 void MediaView::resumePlayback() {
637 if (stopped) return;
638 const qint64 currentTime = media->position();
639 // qDebug() << __PRETTY_FUNCTION__ << currentTime;
640 if (currentTime > 0) media->seek(currentTime);
641 media->play();
642 }
643
openWebPage()644 void MediaView::openWebPage() {
645 Video *video = playlistModel->activeVideo();
646 if (!video) return;
647 media->pause();
648 QString url =
649 video->getWebpage() + QLatin1String("&t=") + QString::number(media->position() / 1000);
650 QDesktopServices::openUrl(url);
651 }
652
copyWebPage()653 void MediaView::copyWebPage() {
654 Video *video = playlistModel->activeVideo();
655 if (!video) return;
656 QString address = video->getWebpage();
657 QApplication::clipboard()->setText(address);
658 QString message = tr("You can now paste the YouTube link into another application");
659 MainWindow::instance()->showMessage(message);
660 }
661
copyVideoLink()662 void MediaView::copyVideoLink() {
663 Video *video = playlistModel->activeVideo();
664 if (!video) return;
665 QApplication::clipboard()->setText(video->getStreamUrl());
666 QString message = tr("You can now paste the video stream URL into another application") + ". " +
667 tr("The link will be valid only for a limited time.");
668 MainWindow::instance()->showMessage(message);
669 }
670
openInBrowser()671 void MediaView::openInBrowser() {
672 Video *video = playlistModel->activeVideo();
673 if (!video) return;
674 media->pause();
675 QDesktopServices::openUrl(video->getStreamUrl());
676 }
677
removeSelected()678 void MediaView::removeSelected() {
679 if (!playlistView->selectionModel()->hasSelection()) return;
680 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
681 playlistModel->removeIndexes(indexes);
682 }
683
selectVideos(const QVector<Video * > & videos)684 void MediaView::selectVideos(const QVector<Video *> &videos) {
685 for (Video *video : videos) {
686 QModelIndex index = playlistModel->indexForVideo(video);
687 playlistView->selectionModel()->select(index, QItemSelectionModel::Select);
688 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
689 }
690 }
691
selectionChanged(const QItemSelection &,const QItemSelection &)692 void MediaView::selectionChanged(const QItemSelection & /*selected*/,
693 const QItemSelection & /*deselected*/) {
694 const bool gotSelection = playlistView->selectionModel()->hasSelection();
695 MainWindow::instance()->getAction("remove")->setEnabled(gotSelection);
696 MainWindow::instance()->getAction("moveUp")->setEnabled(gotSelection);
697 MainWindow::instance()->getAction("moveDown")->setEnabled(gotSelection);
698 }
699
moveUpSelected()700 void MediaView::moveUpSelected() {
701 if (!playlistView->selectionModel()->hasSelection()) return;
702
703 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
704 std::stable_sort(indexes.begin(), indexes.end());
705 playlistModel->move(indexes, true);
706
707 // set current index after row moves to something more intuitive
708 int row = indexes.at(0).row();
709 playlistView->selectionModel()->setCurrentIndex(playlistModel->index(row > 1 ? row : 1),
710 QItemSelectionModel::NoUpdate);
711 }
712
moveDownSelected()713 void MediaView::moveDownSelected() {
714 if (!playlistView->selectionModel()->hasSelection()) return;
715
716 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
717 std::stable_sort(indexes.begin(), indexes.end(),
718 [](const QModelIndex &a, const QModelIndex &b) { return b < a; });
719 playlistModel->move(indexes, false);
720
721 // set current index after row moves to something more intuitive
722 // (respect 1 static item on bottom)
723 int row = indexes.at(0).row() + 1, max = playlistModel->rowCount() - 2;
724 playlistView->selectionModel()->setCurrentIndex(playlistModel->index(row > max ? max : row),
725 QItemSelectionModel::NoUpdate);
726 }
727
setSidebarVisibility(bool visible)728 void MediaView::setSidebarVisibility(bool visible) {
729 if (sidebar->isVisible() == visible) return;
730 sidebar->setVisible(visible);
731 if (visible) {
732 sidebar->move(0, 0);
733 sidebar->resize(sidebar->width(), window()->height());
734 sidebar->raise();
735 playlistView->setFocus();
736 }
737 }
738
removeSidebar()739 void MediaView::removeSidebar() {
740 sidebar->hide();
741 sidebar->setParent(window());
742 }
743
restoreSidebar()744 void MediaView::restoreSidebar() {
745 sidebar->show();
746 splitter->insertWidget(0, sidebar);
747 }
748
isSidebarVisible()749 bool MediaView::isSidebarVisible() {
750 return sidebar->isVisible();
751 }
752
saveSplitterState()753 void MediaView::saveSplitterState() {
754 QSettings settings;
755 if (splitter) settings.setValue("splitter", splitter->saveState());
756 }
757
downloadVideo()758 void MediaView::downloadVideo() {
759 Video *video = playlistModel->activeVideo();
760 if (!video) return;
761 DownloadManager::instance()->addItem(video);
762 MainWindow::instance()->showActionsInStatusBar({MainWindow::instance()->getAction("downloads")},
763 true);
764 QString message = tr("Downloading %1").arg(video->getTitle());
765 MainWindow::instance()->showMessage(message);
766 }
767
768 #ifdef APP_SNAPSHOT
snapshot()769 void MediaView::snapshot() {
770 qint64 currentTime = media->position() / 1000;
771
772 QObject *context = new QObject();
773 connect(media, &Media::snapshotReady, context,
774 [this, currentTime, context](const QImage &image) {
775 context->deleteLater();
776
777 if (image.isNull()) {
778 qWarning() << "Null snapshot";
779 return;
780 }
781
782 QPixmap pixmap = QPixmap::fromImage(image.scaled(
783 videoWidget->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
784 videoAreaWidget->showSnapshotPreview(pixmap);
785
786 Video *video = playlistModel->activeVideo();
787 if (!video) return;
788
789 QString location = SnapshotSettings::getCurrentLocation();
790 QDir dir(location);
791 if (!dir.exists()) dir.mkpath(location);
792 QString basename = video->getTitle();
793 QString format = video->getDuration() > 3600 ? "h_mm_ss" : "m_ss";
794 basename += " (" + QTime(0, 0, 0).addSecs(currentTime).toString(format) + ")";
795 basename = DataUtils::stringToFilename(basename);
796 QString filename = location + "/" + basename + ".png";
797 qDebug() << filename;
798 image.save(filename, "PNG");
799
800 if (snapshotSettings) delete snapshotSettings;
801 snapshotSettings = new SnapshotSettings(videoWidget);
802 snapshotSettings->setSnapshot(pixmap, filename);
803 QStatusBar *statusBar = MainWindow::instance()->statusBar();
804 #ifdef APP_EXTRA
805 Extra::fadeInWidget(statusBar, statusBar);
806 #endif
807 statusBar->insertPermanentWidget(0, snapshotSettings);
808 snapshotSettings->show();
809 MainWindow::instance()->setStatusBarVisibility(true);
810 }
811 #endif
812 );
813
814 media->snapshot();
815 }
816
fullscreen()817 void MediaView::fullscreen() {
818 videoAreaWidget->setParent(nullptr);
819 videoAreaWidget->showFullScreen();
820 }
821
resumeWithNewStreamUrl(const QString & streamUrl,const QString & audioUrl)822 void MediaView::resumeWithNewStreamUrl(const QString &streamUrl, const QString &audioUrl) {
823 pauseTime = media->position();
824
825 if (audioUrl.isEmpty()) {
826 qDebug() << "Playing" << streamUrl;
827 media->play(streamUrl);
828 } else {
829 qDebug() << "Playing" << streamUrl << audioUrl;
830 media->playSeparateAudioAndVideo(streamUrl, audioUrl);
831 }
832
833 Video *video = static_cast<Video *>(sender());
834 if (!video) {
835 qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
836 return;
837 }
838 video->disconnect(this);
839 }
840
findVideoParts()841 void MediaView::findVideoParts() {
842 Video *video = playlistModel->activeVideo();
843 if (!video) return;
844
845 QString query = video->getTitle();
846
847 const QLatin1String optionalSpace("\\s*");
848 const QLatin1String staticCounterSeparators("[\\/\\-]");
849 const QString counterSeparators =
850 QLatin1String("( of | ") + tr("of", "Used in video parts, as in '2 of 3'") +
851 QLatin1String(" |") + staticCounterSeparators + QLatin1String(")");
852
853 // numbers from 1 to 15
854 const QLatin1String counterNumber("([1-9]|1[0-5])");
855
856 // query.remove(QRegExp(counterSeparators + optionalSpace + counterNumber));
857 query.remove(QRegExp(counterNumber + optionalSpace + counterSeparators + optionalSpace +
858 counterNumber));
859 query.remove(wordRE("pr?t\\.?" + optionalSpace + counterNumber));
860 query.remove(wordRE("ep\\.?" + optionalSpace + counterNumber));
861 query.remove(wordRE("part" + optionalSpace + counterNumber));
862 query.remove(wordRE("episode" + optionalSpace + counterNumber));
863 query.remove(wordRE(tr("part", "This is for video parts, as in 'Cool video - part 1'") +
864 optionalSpace + counterNumber));
865 query.remove(wordRE(tr("episode", "This is for video parts, as in 'Cool series - episode 1'") +
866 optionalSpace + counterNumber));
867 query.remove(QRegExp("[\\(\\)\\[\\]]"));
868
869 #define NUMBERS "one|two|three|four|five|six|seven|eight|nine|ten"
870
871 QRegExp englishNumberRE = QRegExp(QLatin1String(".*(") + NUMBERS + ").*", Qt::CaseInsensitive);
872 // bool numberAsWords = englishNumberRE.exactMatch(query);
873 query.remove(englishNumberRE);
874
875 QRegExp localizedNumberRE =
876 QRegExp(QLatin1String(".*(") + tr(NUMBERS) + ").*", Qt::CaseInsensitive);
877 // if (!numberAsWords) numberAsWords = localizedNumberRE.exactMatch(query);
878 query.remove(localizedNumberRE);
879
880 SearchParams *searchParams = new SearchParams();
881 searchParams->setTransient(true);
882 searchParams->setKeywords(query);
883 searchParams->setChannelId(video->getChannelId());
884
885 /*
886 if (!numberAsWords) {
887 qDebug() << "We don't have number as words";
888 // searchParams->setSortBy(SearchParams::SortByNewest);
889 // TODO searchParams->setReverseOrder(true);
890 // TODO searchParams->setMax(50);
891 }
892 */
893
894 search(searchParams);
895 }
896
relatedVideos()897 void MediaView::relatedVideos() {
898 Video *video = playlistModel->activeVideo();
899 if (!video) return;
900
901 auto source = new SingleVideoSource(this);
902 source->setVideo(video->clone());
903 setVideoSource(source);
904
905 MainWindow::instance()->getAction("relatedVideos")->setEnabled(false);
906 }
907
shareViaTwitter()908 void MediaView::shareViaTwitter() {
909 Video *video = playlistModel->activeVideo();
910 if (!video) return;
911 QUrl url("https://twitter.com/intent/tweet");
912 QUrlQuery q;
913 q.addQueryItem("via", "minitubeapp");
914 q.addQueryItem("text", video->getTitle());
915 q.addQueryItem("url", video->getWebpage());
916 url.setQuery(q);
917 QDesktopServices::openUrl(url);
918 }
919
shareViaFacebook()920 void MediaView::shareViaFacebook() {
921 Video *video = playlistModel->activeVideo();
922 if (!video) return;
923 QUrl url("https://www.facebook.com/sharer.php");
924 QUrlQuery q;
925 q.addQueryItem("t", video->getTitle());
926 q.addQueryItem("u", video->getWebpage());
927 url.setQuery(q);
928 QDesktopServices::openUrl(url);
929 }
930
shareViaEmail()931 void MediaView::shareViaEmail() {
932 Video *video = playlistModel->activeVideo();
933 if (!video) return;
934 QUrl url("mailto:");
935 QUrlQuery q;
936 q.addQueryItem("subject", video->getTitle());
937 const QString body = video->getTitle() + "\n" + video->getWebpage() + "\n\n" +
938 tr("Sent from %1").arg(Constants::NAME) + "\n" + Constants::WEBSITE;
939 q.addQueryItem("body", body);
940 url.setQuery(q);
941 QDesktopServices::openUrl(url);
942 }
943
onAuthorPushed(QModelIndex index)944 void MediaView::onAuthorPushed(QModelIndex index) {
945 Video *video = playlistModel->videoAt(index.row());
946 if (!video) return;
947
948 QString channelId = video->getChannelId();
949 // if (channelId.isEmpty()) channelId = video->channelTitle();
950 if (channelId.isEmpty()) return;
951
952 SearchParams *searchParams = new SearchParams();
953 searchParams->setChannelId(channelId);
954 searchParams->setSortBy(SearchParams::SortByNewest);
955
956 // go!
957 search(searchParams);
958 }
959
960
updateSubscriptionAction(bool subscribed)961 void MediaView::updateSubscriptionAction(bool subscribed) {
962 QAction *subscribeAction = MainWindow::instance()->getAction("subscribeChannel");
963
964 QString subscribeTip;
965 QString subscribeText;
966
967 if (currentSubscriptionChannelId.isEmpty()) {
968 subscribeText = subscribeAction->property("originalText").toString();
969 subscribeAction->setEnabled(false);
970 } else if (subscribed) {
971 subscribeText = tr("Unsubscribe from %1").arg(currentSubscriptionChannelTitle);
972 subscribeTip = subscribeText;
973 subscribeAction->setEnabled(true);
974 } else {
975 subscribeText = tr("Subscribe to %1").arg(currentSubscriptionChannelTitle);
976 subscribeTip = subscribeText;
977 subscribeAction->setEnabled(true);
978 }
979 subscribeAction->setText(subscribeText);
980 subscribeAction->setStatusTip(subscribeTip);
981
982 if (subscribed) {
983 subscribeAction->setIcon(IconUtils::icon("bookmark-remove"));
984 } else {
985 subscribeAction->setIcon(IconUtils::icon("bookmark-new"));
986 }
987
988 MainWindow::instance()->setupAction(subscribeAction);
989 }
990
updateSubscriptionActionForChannel(const QString & channelId)991 void MediaView::updateSubscriptionActionForChannel(const QString & channelId) {
992 QString channelTitle = tr("channel");
993 YTChannel *channel = YTChannel::forId(channelId);
994 if (nullptr != channel && !channel->getDisplayName().isEmpty()) {
995 channelTitle = channel->getDisplayName();
996 }
997
998 bool subscribed = YTChannel::isSubscribed(channelId);
999
1000 currentSubscriptionChannelId = channelId;
1001 currentSubscriptionChannelTitle = channelTitle;
1002 updateSubscriptionAction(subscribed);
1003 }
1004
updateSubscriptionActionForVideo(Video * video,bool subscribed)1005 void MediaView::updateSubscriptionActionForVideo(Video *video, bool subscribed) {
1006 if (!video) {
1007 currentSubscriptionChannelId = "";
1008 currentSubscriptionChannelTitle = "";
1009 updateSubscriptionAction(false);
1010 } else {
1011 currentSubscriptionChannelId = video->getChannelId();
1012 currentSubscriptionChannelTitle = video->getChannelTitle();
1013 updateSubscriptionAction(subscribed);
1014 }
1015 }
1016
reloadCurrentVideo()1017 void MediaView::reloadCurrentVideo() {
1018 Video *video = playlistModel->activeVideo();
1019 if (!video) return;
1020
1021 int oldFormat = video->getDefinitionCode();
1022
1023 QObject *context = new QObject();
1024 connect(video, &Video::gotStreamUrl, context,
1025 [this, oldFormat, video, context](const QString &videoUrl, const QString &audioUrl) {
1026 context->deleteLater();
1027 if (oldFormat == video->getDefinitionCode()) return;
1028 QObject *context2 = new QObject();
1029 const qint64 position = media->position();
1030 connect(media, &Media::stateChanged, context2,
1031 [position, this, context2](Media::State state) {
1032 if (state == Media::PlayingState) {
1033 media->seek(position);
1034 context2->deleteLater();
1035 Video *video = playlistModel->activeVideo();
1036 QString msg = tr("Switched to %1")
1037 .arg(VideoDefinition::forCode(
1038 video->getDefinitionCode())
1039 .getName());
1040 MainWindow::instance()->showMessage(msg);
1041 }
1042 });
1043
1044 if (audioUrl.isEmpty()) {
1045 media->play(videoUrl);
1046 } else {
1047 media->playSeparateAudioAndVideo(videoUrl, audioUrl);
1048 }
1049 });
1050 video->loadStreamUrl();
1051 }
1052
toggleSubscription()1053 void MediaView::toggleSubscription() {
1054 //Video *video = playlistModel->activeVideo();
1055 if (currentSubscriptionChannelId.isEmpty()) {
1056 return;
1057 }
1058
1059 bool subscribed = YTChannel::isSubscribed(currentSubscriptionChannelId);
1060 if (subscribed) {
1061 YTChannel::unsubscribe(currentSubscriptionChannelId);
1062 MainWindow::instance()->showMessage(
1063 tr("Unsubscribed from %1").arg(currentSubscriptionChannelTitle));
1064 } else {
1065 YTChannel::subscribe(currentSubscriptionChannelId);
1066 MainWindow::instance()->showMessage(tr("Subscribed to %1").arg(currentSubscriptionChannelTitle));
1067 }
1068
1069 updateSubscriptionAction(!subscribed);
1070 }
1071
adjustWindowSize()1072 void MediaView::adjustWindowSize() {
1073 qDebug() << "Adjusting window size";
1074 Video *video = playlistModel->activeVideo();
1075 if (!video) return;
1076 QWidget *window = this->window();
1077 if (!window->isMaximized() && !window->isFullScreen()) {
1078 const double ratio = 16. / 9.;
1079 const double w = (double)videoAreaWidget->width();
1080 const double h = (double)videoAreaWidget->height();
1081 const double currentVideoRatio = w / h;
1082 if (currentVideoRatio != ratio) {
1083 qDebug() << "Adjust size";
1084 int newHeight = std::round((window->height() - h) + (w / ratio));
1085 window->resize(window->width(), newHeight);
1086 }
1087 }
1088 }
1089