]> git.sur5r.net Git - minitube/blob - src/mediaview.cpp
286d56f226c9b3f3b622f06b0cedef0956c31b84
[minitube] / src / mediaview.cpp
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 "downloaditem.h"
24 #include "downloadmanager.h"
25 #include "http.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"
36 #ifdef APP_ACTIVATION
37 #include "activation.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 #include "ytsinglevideosource.h"
49 #ifdef APP_SNAPSHOT
50 #include "snapshotsettings.h"
51 #endif
52 #include "datautils.h"
53 #include "idle.h"
54
55 MediaView *MediaView::instance() {
56     static MediaView *i = new MediaView();
57     return i;
58 }
59
60 MediaView::MediaView(QWidget *parent)
61     : View(parent), stopped(false), downloadItem(0)
62 #ifdef APP_SNAPSHOT
63       ,
64       snapshotSettings(0)
65 #endif
66       ,
67       pauseTime(0) {
68 }
69
70 void MediaView::initialize() {
71     MainWindow *mainWindow = MainWindow::instance();
72
73     QBoxLayout *layout = new QVBoxLayout(this);
74     layout->setMargin(0);
75
76     splitter = new MiniSplitter();
77     layout->addWidget(splitter);
78
79     playlistView = new PlaylistView();
80     playlistView->setParent(this);
81     connect(playlistView, SIGNAL(activated(const QModelIndex &)),
82             SLOT(itemActivated(const QModelIndex &)));
83
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);
90
91     connect(playlistView->selectionModel(),
92             SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
93             SLOT(selectionChanged(const QItemSelection &, const QItemSelection &)));
94
95     connect(playlistView, SIGNAL(authorPushed(QModelIndex)), SLOT(authorPushed(QModelIndex)));
96
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);
104
105     videoAreaWidget = new VideoAreaWidget(this);
106
107 #ifdef APP_PHONON
108     videoWidget = new Phonon::VideoWidget(this);
109     videoAreaWidget->setVideoWidget(videoWidget);
110 #endif
111     videoAreaWidget->setListModel(playlistModel);
112
113     loadingWidget = new LoadingWidget(this);
114     videoAreaWidget->setLoadingWidget(loadingWidget);
115
116     splitter->addWidget(videoAreaWidget);
117
118     // restore splitter state
119     QSettings settings;
120     if (settings.contains("splitter"))
121         splitter->restoreState(settings.value("splitter").toByteArray());
122     else {
123         int sidebarDefaultWidth = 180;
124         splitter->setSizes(QList<int>() << sidebarDefaultWidth
125                                         << splitter->size().width() - sidebarDefaultWidth);
126     }
127     splitter->setChildrenCollapsible(false);
128     connect(splitter, SIGNAL(splitterMoved(int, int)), SLOT(adjustWindowSize()));
129
130     errorTimer = new QTimer(this);
131     errorTimer->setSingleShot(true);
132     errorTimer->setInterval(3000);
133     connect(errorTimer, SIGNAL(timeout()), SLOT(skipVideo()));
134
135 #ifdef APP_ACTIVATION
136     demoTimer = new QTimer(this);
137     demoTimer->setSingleShot(true);
138     connect(demoTimer, &QTimer::timeout, mainWindow, &MainWindow::showActivationView,
139             Qt::QueuedConnection);
140 #endif
141
142     connect(videoAreaWidget, SIGNAL(doubleClicked()), mainWindow->getAction("fullscreen"),
143             SLOT(trigger()));
144
145     QAction *refineSearchAction = mainWindow->getAction("refineSearch");
146     connect(refineSearchAction, SIGNAL(toggled(bool)), sidebar, SLOT(toggleRefineSearch(bool)));
147
148     const QVector<const char *> videoActionNames = {
149 #ifdef APP_SNAPSHOT
150             "snapshot",
151 #endif
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));
158     }
159
160 #ifndef APP_PHONON_SEEK
161     QSlider *slider = mainWindow->getSlider();
162     connect(slider, SIGNAL(valueChanged(int)), SLOT(sliderMoved(int)));
163 #endif
164 }
165
166 #ifdef APP_PHONON
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)));
175 }
176 #endif
177
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();
183     }
184     return 0;
185 }
186
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);
198                 return;
199             }
200         }
201     }
202     YTSearch *ytSearch = new YTSearch(searchParams);
203     ytSearch->setAsyncDetails(true);
204     connect(ytSearch, SIGNAL(gotDetails()), playlistModel, SLOT(emitDataChanged()));
205     setVideoSource(ytSearch);
206 }
207
208 void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool back) {
209     Q_UNUSED(back);
210     stopped = false;
211     errorTimer->stop();
212
213     // qDebug() << "Adding VideoSource" << videoSource->getName() << videoSource;
214
215     if (addToHistory) {
216         int currentIndex = getHistoryIndex();
217         if (currentIndex >= 0 && currentIndex < history.size() - 1) {
218             while (history.size() > currentIndex + 1) {
219                 VideoSource *vs = history.takeLast();
220                 if (!vs->parent()) {
221                     qDebug() << "Deleting VideoSource" << vs->getName() << vs;
222                     delete vs;
223                 }
224             }
225         }
226         history.append(videoSource);
227     }
228
229 #ifdef APP_EXTRA
230     if (history.size() > 1)
231         Extra::slideTransition(playlistView->viewport(), playlistView->viewport(), back);
232 #endif
233
234     playlistModel->setVideoSource(videoSource);
235
236     QSettings settings;
237     if (settings.value("manualplay", false).toBool()) {
238         videoAreaWidget->showPickMessage();
239     }
240
241     sidebar->showPlaylist();
242     sidebar->getRefineSearchWidget()->setSearchParams(getSearchParams());
243     sidebar->hideSuggestions();
244     sidebar->getHeader()->updateInfo();
245
246     SearchParams *searchParams = getSearchParams();
247     bool isChannel = searchParams && !searchParams->channelId().isEmpty();
248     playlistView->setClickableAuthors(!isChannel);
249 }
250
251 void MediaView::searchAgain() {
252     VideoSource *currentVideoSource = playlistModel->getVideoSource();
253     setVideoSource(currentVideoSource, false);
254 }
255
256 bool MediaView::canGoBack() {
257     return getHistoryIndex() > 0;
258 }
259
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);
266         }
267     }
268 }
269
270 bool MediaView::canGoForward() {
271     int currentIndex = getHistoryIndex();
272     return currentIndex >= 0 && currentIndex < history.size() - 1;
273 }
274
275 void MediaView::goForward() {
276     if (canGoForward()) {
277         int currentIndex = getHistoryIndex();
278         VideoSource *nextVideoSource = history.at(currentIndex + 1);
279         setVideoSource(nextVideoSource, false);
280     }
281 }
282
283 int MediaView::getHistoryIndex() {
284     return history.lastIndexOf(playlistModel->getVideoSource());
285 }
286
287 void MediaView::appear() {
288     MainWindow::instance()->showToolbar();
289
290     Video *currentVideo = playlistModel->activeVideo();
291     if (currentVideo) {
292         MainWindow::instance()->setWindowTitle(currentVideo->getTitle() + " - " + Constants::NAME);
293     }
294
295     playlistView->setFocus();
296 }
297
298 void MediaView::disappear() {
299     MainWindow::instance()->hideToolbar();
300 }
301
302 void MediaView::handleError(const QString &message) {
303     qWarning() << __PRETTY_FUNCTION__ << message;
304 #ifdef APP_PHONON_SEEK
305     mediaObject->play();
306 #else
307     QTimer::singleShot(500, this, SLOT(startPlaying()));
308 #endif
309 }
310
311 #ifdef APP_PHONON
312 void MediaView::stateChanged(Phonon::State newState, Phonon::State oldState) {
313     if (pauseTime > 0 && (newState == Phonon::PlayingState || newState == Phonon::BufferingState)) {
314         mediaObject->seek(pauseTime);
315         pauseTime = 0;
316     }
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());
322     }
323
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();
330     }
331 }
332 #endif
333
334 void MediaView::pause() {
335 #ifdef APP_PHONON
336     switch (mediaObject->state()) {
337     case Phonon::PlayingState:
338         mediaObject->pause();
339         pauseTimer.start();
340         break;
341     default:
342         if (pauseTimer.hasExpired(60000)) {
343             pauseTimer.invalidate();
344             connect(playlistModel->activeVideo(), SIGNAL(gotStreamUrl(QUrl)),
345                     SLOT(resumeWithNewStreamUrl(QUrl)));
346             playlistModel->activeVideo()->loadStreamUrl();
347         } else
348             mediaObject->play();
349         break;
350     }
351 #endif
352 }
353
354 QRegExp MediaView::wordRE(const QString &s) {
355     return QRegExp("\\W" + s + "\\W?", Qt::CaseInsensitive);
356 }
357
358 void MediaView::stop() {
359     stopped = true;
360
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();
366         }
367     }
368
369     playlistModel->abortSearch();
370     videoAreaWidget->clear();
371     videoAreaWidget->update();
372     errorTimer->stop();
373     playlistView->selectionModel()->clearSelection();
374     if (downloadItem) {
375         downloadItem->stop();
376         delete downloadItem;
377         downloadItem = 0;
378         currentVideoSize = 0;
379     }
380     MainWindow::instance()->getAction("refineSearch")->setChecked(false);
381     updateSubscriptionAction(0, false);
382 #ifdef APP_ACTIVATION
383     demoTimer->stop();
384 #endif
385
386     for (QAction *action : currentVideoActions)
387         action->setEnabled(false);
388
389     QAction *a = MainWindow::instance()->getAction("download");
390     a->setEnabled(false);
391     a->setVisible(false);
392
393 #ifdef APP_PHONON
394     mediaObject->stop();
395     mediaObject->clear();
396 #endif
397     currentVideoId.clear();
398
399 #ifndef APP_PHONON_SEEK
400     QSlider *slider = MainWindow::instance()->getSlider();
401     slider->setEnabled(false);
402     slider->setValue(0);
403 #else
404 // Phonon::SeekSlider *slider = MainWindow::instance()->getSeekSlider();
405 #endif
406
407 #ifdef APP_SNAPSHOT
408     if (snapshotSettings) {
409         delete snapshotSettings;
410         snapshotSettings = 0;
411     }
412 #endif
413 }
414
415 const QString &MediaView::getCurrentVideoId() {
416     return currentVideoId;
417 }
418
419 void MediaView::activeRowChanged(int row) {
420     if (stopped) return;
421
422     errorTimer->stop();
423
424 #ifdef APP_PHONON
425     mediaObject->stop();
426 #endif
427     if (downloadItem) {
428         downloadItem->stop();
429         delete downloadItem;
430         downloadItem = 0;
431         currentVideoSize = 0;
432     }
433
434     Video *video = playlistModel->videoAt(row);
435     if (!video) return;
436
437     // optimize window for 16:9 video
438     adjustWindowSize();
439
440     videoAreaWidget->showLoading(video);
441
442     connect(video, SIGNAL(gotStreamUrl(QUrl)), SLOT(gotStreamUrl(QUrl)), Qt::UniqueConnection);
443     connect(video, SIGNAL(errorStreamUrl(QString)), SLOT(skip()), Qt::UniqueConnection);
444     video->loadStreamUrl();
445
446     // video title in titlebar
447     MainWindow::instance()->setWindowTitle(video->getTitle() + QLatin1String(" - ") +
448                                            QLatin1String(Constants::NAME));
449
450     // ensure active item is visible
451     if (row != -1) {
452         QModelIndex index = playlistModel->index(row, 0, QModelIndex());
453         playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
454     }
455
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);
463
464     bool enableDownload = video->getLicense() == Video::LicenseCC;
465 #ifdef APP_ACTIVATION
466     enableDownload = enableDownload || Activation::instance().isLegacy();
467 #endif
468 #ifdef APP_DOWNLOADS
469     enableDownload = true;
470 #endif
471     QAction *a = MainWindow::instance()->getAction("download");
472     a->setEnabled(enableDownload);
473     a->setVisible(enableDownload);
474
475     updateSubscriptionAction(video, YTChannel::isSubscribed(video->getChannelId()));
476
477     for (QAction *action : currentVideoActions)
478         action->setEnabled(true);
479
480 #ifndef APP_PHONON_SEEK
481     QSlider *slider = MainWindow::instance()->getSlider();
482     slider->setEnabled(false);
483     slider->setValue(0);
484 #endif
485
486 #ifdef APP_SNAPSHOT
487     if (snapshotSettings) {
488         delete snapshotSettings;
489         snapshotSettings = 0;
490         MainWindow::instance()->adjustStatusBarVisibility();
491     }
492 #endif
493
494     // see you in gotStreamUrl...
495 }
496
497 void MediaView::gotStreamUrl(QUrl streamUrl) {
498     if (stopped) return;
499     if (!streamUrl.isValid()) {
500         skip();
501         return;
502     }
503
504     Video *video = static_cast<Video *>(sender());
505     if (!video) {
506         qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
507         return;
508     }
509     video->disconnect(this);
510
511     currentVideoId = video->getId();
512
513 #ifdef APP_PHONON_SEEK
514     mediaObject->setCurrentSource(streamUrl);
515     mediaObject->play();
516 #else
517     startDownloading();
518 #endif
519
520     // ensure we always have videos ahead
521     playlistModel->searchNeeded();
522
523     // ensure active item is visible
524     int row = playlistModel->activeRow();
525     if (row != -1) {
526         QModelIndex index = playlistModel->index(row, 0, QModelIndex());
527         playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
528     }
529
530 #ifdef APP_ACTIVATION
531     if (!Activation::instance().isActivated() && !demoTimer->isActive()) {
532         int ms = (60000 * 5) + (qrand() % (60000 * 5));
533         demoTimer->start(ms);
534     }
535 #endif
536
537 #ifdef APP_EXTRA
538     Extra::notify(video->getTitle(), video->getChannelTitle(), video->getFormattedDuration());
539 #endif
540
541     ChannelAggregator::instance()->videoWatched(video);
542 }
543
544 void MediaView::downloadStatusChanged() {
545     // qDebug() << __PRETTY_FUNCTION__;
546     switch (downloadItem->status()) {
547     case Downloading:
548         // qDebug() << "Downloading";
549         if (downloadItem->offset() == 0)
550             startPlaying();
551         else {
552 #ifdef APP_PHONON
553             // qDebug() << "Seeking to" << downloadItem->offset();
554             mediaObject->seek(offsetToTime(downloadItem->offset()));
555             mediaObject->play();
556 #endif
557         }
558         break;
559     case Starting:
560         // qDebug() << "Starting";
561         break;
562     case Finished:
563 // qDebug() << "Finished" << mediaObject->state();
564 #ifdef APP_PHONON_SEEK
565         MainWindow::instance()->getSeekSlider()->setEnabled(mediaObject->isSeekable());
566 #endif
567         break;
568     case Failed:
569         // qDebug() << "Failed";
570         skip();
571         break;
572     case Idle:
573         // qDebug() << "Idle";
574         break;
575     }
576 }
577
578 void MediaView::startPlaying() {
579     // qDebug() << __PRETTY_FUNCTION__;
580     if (stopped) return;
581     if (!downloadItem) {
582         skip();
583         return;
584     }
585
586     if (downloadItem->offset() == 0) {
587         currentVideoSize = downloadItem->bytesTotal();
588         // qDebug() << "currentVideoSize" << currentVideoSize;
589     }
590
591     // go!
592     QString source = downloadItem->currentFilename();
593     qDebug() << "Playing" << source << QFile::exists(source);
594 #ifdef APP_PHONON
595     mediaObject->setCurrentSource(QUrl::fromLocalFile(source));
596     mediaObject->play();
597 #endif
598 #ifdef APP_PHONON_SEEK
599     MainWindow::instance()->getSeekSlider()->setEnabled(false);
600 #else
601     QSlider *slider = MainWindow::instance()->getSlider();
602     slider->setEnabled(true);
603 #endif
604 }
605
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);
613             sliderMoved(0);
614 #ifdef APP_PHONON
615             mediaObject->play();
616 #endif
617         } else
618             playlistModel->setActiveRow(index.row());
619
620         // the user doubleclicked on the "Search More" item
621     } else {
622         playlistModel->searchMore();
623         playlistView->selectionModel()->clearSelection();
624     }
625 }
626
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
631     if (skippedVideo) {
632         if (playlistModel->activeVideo() != skippedVideo) {
633             qDebug() << "Skip of video canceled";
634             return;
635         }
636         int nextRow = playlistModel->rowForVideo(skippedVideo);
637         nextRow++;
638         if (nextRow == -1) return;
639         playlistModel->setActiveRow(nextRow);
640     }
641 }
642
643 void MediaView::skip() {
644     int nextRow = playlistModel->nextRow();
645     if (nextRow == -1) return;
646     playlistModel->setActiveRow(nextRow);
647 }
648
649 void MediaView::skipBackward() {
650     int prevRow = playlistModel->previousRow();
651     if (prevRow == -1) return;
652     playlistModel->setActiveRow(prevRow);
653 }
654
655 void MediaView::aboutToFinish() {
656 #ifdef APP_PHONON
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);
663         mediaObject->play();
664     }
665 #endif
666 }
667
668 void MediaView::playbackFinished() {
669     if (stopped) return;
670
671 #ifdef APP_PHONON
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()));
679     } else {
680         QAction *stopAfterThisAction = MainWindow::instance()->getAction("stopafterthis");
681         if (stopAfterThisAction->isChecked()) {
682             stopAfterThisAction->setChecked(false);
683         } else
684             skip();
685     }
686 #endif
687 }
688
689 void MediaView::playbackResume() {
690     if (stopped) return;
691 #ifdef APP_PHONON
692     const qint64 currentTime = mediaObject->currentTime();
693     // qDebug() << __PRETTY_FUNCTION__ << currentTime;
694     if (currentTime > 0) mediaObject->seek(currentTime);
695     mediaObject->play();
696 #endif
697 }
698
699 void MediaView::openWebPage() {
700     Video *video = playlistModel->activeVideo();
701     if (!video) return;
702 #ifdef APP_PHONON
703     mediaObject->pause();
704 #endif
705     QString url = video->getWebpage() + QLatin1String("&t=") +
706                   QString::number(mediaObject->currentTime() / 1000);
707     QDesktopServices::openUrl(url);
708 }
709
710 void MediaView::copyWebPage() {
711     Video *video = playlistModel->activeVideo();
712     if (!video) return;
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);
717 }
718
719 void MediaView::copyVideoLink() {
720     Video *video = playlistModel->activeVideo();
721     if (!video) return;
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);
726 }
727
728 void MediaView::openInBrowser() {
729     Video *video = playlistModel->activeVideo();
730     if (!video) return;
731 #ifdef APP_PHONON
732     mediaObject->pause();
733 #endif
734     QDesktopServices::openUrl(video->getStreamUrl());
735 }
736
737 void MediaView::removeSelected() {
738     if (!playlistView->selectionModel()->hasSelection()) return;
739     QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
740     playlistModel->removeIndexes(indexes);
741 }
742
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);
748     }
749 }
750
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);
757 }
758
759 void MediaView::moveUpSelected() {
760     if (!playlistView->selectionModel()->hasSelection()) return;
761
762     QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
763     qStableSort(indexes.begin(), indexes.end());
764     playlistModel->move(indexes, true);
765
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);
770 }
771
772 void MediaView::moveDownSelected() {
773     if (!playlistView->selectionModel()->hasSelection()) return;
774
775     QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
776     qStableSort(indexes.begin(), indexes.end(), qGreater<QModelIndex>());
777     playlistModel->move(indexes, false);
778
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);
784 }
785
786 void MediaView::setSidebarVisibility(bool visible) {
787     if (sidebar->isVisible() == visible) return;
788     sidebar->setVisible(visible);
789     sidebar->raise();
790     playlistView->setFocus();
791 }
792
793 void MediaView::removeSidebar() {
794     sidebar->hide();
795 #ifndef APP_MAC
796     sidebar->setParent(window());
797     sidebar->move(0, 0);
798     sidebar->raise();
799 #endif
800 }
801
802 void MediaView::restoreSidebar() {
803     sidebar->show();
804 #ifndef APP_MAC
805     splitter->insertWidget(0, sidebar);
806 #endif
807 }
808
809 bool MediaView::isSidebarVisible() {
810     return sidebar->isVisible();
811 }
812
813 void MediaView::saveSplitterState() {
814     QSettings settings;
815     settings.setValue("splitter", splitter->saveState());
816 }
817
818 void MediaView::downloadVideo() {
819     Video *video = playlistModel->activeVideo();
820     if (!video) return;
821     DownloadManager::instance()->addItem(video);
822     MainWindow::instance()->showActionInStatusBar(MainWindow::instance()->getAction("downloads"),
823                                                   true);
824     QString message = tr("Downloading %1").arg(video->getTitle());
825     MainWindow::instance()->showMessage(message);
826 }
827
828 #ifdef APP_SNAPSHOT
829 void MediaView::snapshot() {
830     qint64 currentTime = mediaObject->currentTime() / 1000;
831
832     QImage image = videoWidget->snapshot();
833     if (image.isNull()) {
834         qWarning() << "Null snapshot";
835         return;
836     }
837
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);
842
843     Video *video = playlistModel->activeVideo();
844     if (!video) return;
845
846     QString location = SnapshotSettings::getCurrentLocation();
847     QDir dir(location);
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");
856
857     if (snapshotSettings) delete snapshotSettings;
858     snapshotSettings = new SnapshotSettings(videoWidget);
859     snapshotSettings->setSnapshot(pixmap, filename);
860     QStatusBar *statusBar = MainWindow::instance()->statusBar();
861 #ifdef APP_EXTRA
862     Extra::fadeInWidget(statusBar, statusBar);
863 #endif
864     statusBar->insertPermanentWidget(0, snapshotSettings);
865     snapshotSettings->show();
866     MainWindow::instance()->setStatusBarVisibility(true);
867 }
868 #endif
869
870 void MediaView::fullscreen() {
871     videoAreaWidget->setParent(0);
872     videoAreaWidget->showFullScreen();
873 }
874
875 void MediaView::startDownloading() {
876     Video *video = playlistModel->activeVideo();
877     if (!video) return;
878     Video *videoCopy = video->clone();
879     if (downloadItem) {
880         downloadItem->stop();
881         delete downloadItem;
882     }
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();
894 }
895
896 void MediaView::resumeWithNewStreamUrl(const QUrl &streamUrl) {
897     pauseTime = mediaObject->currentTime();
898     mediaObject->setCurrentSource(streamUrl);
899     mediaObject->play();
900
901     Video *video = static_cast<Video *>(sender());
902     if (!video) {
903         qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
904         return;
905     }
906     video->disconnect(this);
907 }
908
909 void MediaView::sliderMoved(int value) {
910     Q_UNUSED(value);
911 #ifdef APP_PHONON
912 #ifndef APP_PHONON_SEEK
913
914     if (currentVideoSize <= 0 || !downloadItem || !mediaObject->isSeekable()) return;
915
916     QSlider *slider = MainWindow::instance()->getSlider();
917     if (slider->isSliderDown()) return;
918
919     qint64 offset = (currentVideoSize * value) / slider->maximum();
920
921     bool needsDownload = downloadItem->needsDownload(offset);
922     if (needsDownload) {
923         if (downloadItem->isBuffered(offset)) {
924             qint64 realOffset = downloadItem->blankAtOffset(offset);
925             if (offset < currentVideoSize) downloadItem->seekTo(realOffset, false);
926             mediaObject->seek(offsetToTime(offset));
927         } else {
928             mediaObject->pause();
929             downloadItem->seekTo(offset);
930         }
931     } else {
932         // qDebug() << "simple seek";
933         mediaObject->seek(offsetToTime(offset));
934     }
935 #endif
936 #endif
937 }
938
939 qint64 MediaView::offsetToTime(qint64 offset) {
940 #ifdef APP_PHONON
941     const qint64 totalTime = mediaObject->totalTime();
942     return ((offset * totalTime) / currentVideoSize);
943 #endif
944 }
945
946 void MediaView::findVideoParts() {
947     Video *video = playlistModel->activeVideo();
948     if (!video) return;
949
950     QString query = video->getTitle();
951
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(")");
957
958     // numbers from 1 to 15
959     const QLatin1String counterNumber("([1-9]|1[0-5])");
960
961     // query.remove(QRegExp(counterSeparators + optionalSpace + counterNumber));
962     query.remove(QRegExp(counterNumber + optionalSpace + counterSeparators + optionalSpace +
963                          counterNumber));
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("[\\(\\)\\[\\]]"));
973
974 #define NUMBERS "one|two|three|four|five|six|seven|eight|nine|ten"
975
976     QRegExp englishNumberRE = QRegExp(QLatin1String(".*(") + NUMBERS + ").*", Qt::CaseInsensitive);
977     // bool numberAsWords = englishNumberRE.exactMatch(query);
978     query.remove(englishNumberRE);
979
980     QRegExp localizedNumberRE =
981             QRegExp(QLatin1String(".*(") + tr(NUMBERS) + ").*", Qt::CaseInsensitive);
982     // if (!numberAsWords) numberAsWords = localizedNumberRE.exactMatch(query);
983     query.remove(localizedNumberRE);
984
985     SearchParams *searchParams = new SearchParams();
986     searchParams->setTransient(true);
987     searchParams->setKeywords(query);
988     searchParams->setChannelId(video->getChannelId());
989
990     /*
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);
996     }
997     */
998
999     search(searchParams);
1000 }
1001
1002 void MediaView::relatedVideos() {
1003     Video *video = playlistModel->activeVideo();
1004     if (!video) return;
1005     YTSingleVideoSource *singleVideoSource = new YTSingleVideoSource();
1006     singleVideoSource->setVideo(video->clone());
1007     singleVideoSource->setAsyncDetails(true);
1008     setVideoSource(singleVideoSource);
1009     MainWindow::instance()->getAction("relatedVideos")->setEnabled(false);
1010 }
1011
1012 void MediaView::shareViaTwitter() {
1013     Video *video = playlistModel->activeVideo();
1014     if (!video) return;
1015     QUrl url("https://twitter.com/intent/tweet");
1016     QUrlQuery q;
1017     q.addQueryItem("via", "minitubeapp");
1018     q.addQueryItem("text", video->getTitle());
1019     q.addQueryItem("url", video->getWebpage());
1020     url.setQuery(q);
1021     QDesktopServices::openUrl(url);
1022 }
1023
1024 void MediaView::shareViaFacebook() {
1025     Video *video = playlistModel->activeVideo();
1026     if (!video) return;
1027     QUrl url("https://www.facebook.com/sharer.php");
1028     QUrlQuery q;
1029     q.addQueryItem("t", video->getTitle());
1030     q.addQueryItem("u", video->getWebpage());
1031     url.setQuery(q);
1032     QDesktopServices::openUrl(url);
1033 }
1034
1035 void MediaView::shareViaEmail() {
1036     Video *video = playlistModel->activeVideo();
1037     if (!video) return;
1038     QUrl url("mailto:");
1039     QUrlQuery q;
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);
1044     url.setQuery(q);
1045     QDesktopServices::openUrl(url);
1046 }
1047
1048 void MediaView::authorPushed(QModelIndex index) {
1049     Video *video = playlistModel->videoAt(index.row());
1050     if (!video) return;
1051
1052     QString channelId = video->getChannelId();
1053     // if (channelId.isEmpty()) channelId = video->channelTitle();
1054     if (channelId.isEmpty()) return;
1055
1056     SearchParams *searchParams = new SearchParams();
1057     searchParams->setChannelId(channelId);
1058     searchParams->setSortBy(SearchParams::SortByNewest);
1059
1060     // go!
1061     search(searchParams);
1062 }
1063
1064 void MediaView::updateSubscriptionAction(Video *video, bool subscribed) {
1065     QAction *subscribeAction = MainWindow::instance()->getAction("subscribeChannel");
1066
1067     QString subscribeTip;
1068     QString subscribeText;
1069     if (!video) {
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);
1076     } else {
1077         subscribeText = tr("Subscribe to %1").arg(video->getChannelTitle());
1078         subscribeTip = subscribeText;
1079         subscribeAction->setEnabled(true);
1080     }
1081     subscribeAction->setText(subscribeText);
1082     subscribeAction->setStatusTip(subscribeTip);
1083
1084     if (subscribed) {
1085 #ifdef APP_LINUX_NO
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);
1091         }
1092         subscribeAction->setIcon(tintedIcon);
1093 #else
1094         subscribeAction->setIcon(IconUtils::icon("bookmark-remove"));
1095 #endif
1096     } else {
1097         subscribeAction->setIcon(IconUtils::icon("bookmark-new"));
1098     }
1099
1100     IconUtils::setupAction(subscribeAction);
1101 }
1102
1103 void MediaView::toggleSubscription() {
1104     Video *video = playlistModel->activeVideo();
1105     if (!video) return;
1106     QString userId = video->getChannelId();
1107     if (userId.isEmpty()) return;
1108     bool subscribed = YTChannel::isSubscribed(userId);
1109     if (subscribed) {
1110         YTChannel::unsubscribe(userId);
1111         MainWindow::instance()->showMessage(
1112                 tr("Unsubscribed from %1").arg(video->getChannelTitle()));
1113     } else {
1114         YTChannel::subscribe(userId);
1115         MainWindow::instance()->showMessage(tr("Subscribed to %1").arg(video->getChannelTitle()));
1116     }
1117     updateSubscriptionAction(video, !subscribed);
1118 }
1119
1120 void MediaView::adjustWindowSize() {
1121     Video *video = playlistModel->activeVideo();
1122     if (!video) return;
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);
1132         }
1133     }
1134 }