playermanager.cpp 13.4 KB
Newer Older
1 2 3
/**
 * Copyright (C) 2004 Scott Wheeler <wheeler@kde.org>
 * Copyright (C) 2007 Matthias Kretz <kretz@kde.org>
4
 * Copyright (C) 2008, 2009, 2018 Michael Pyne <mpyne@kde.org>
5 6 7 8 9 10 11 12 13 14 15 16 17
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program.  If not, see <http://www.gnu.org/licenses/>.
 */
18

19 20
#include "playermanager.h"

Matthias Kretz's avatar
Matthias Kretz committed
21
#include <kmessagebox.h>
22 23 24
#include <kactioncollection.h>
#include <kselectaction.h>
#include <ktoggleaction.h>
Michael Pyne's avatar
Michael Pyne committed
25
#include <KLocalizedString>
26

27 28
#include <Phonon/AudioOutput>
#include <Phonon/MediaObject>
29
#include <Phonon/MediaSource>
30

Laurent Montel's avatar
Laurent Montel committed
31
#include <QPixmap>
32
#include <QTimer>
Michael Pyne's avatar
Michael Pyne committed
33
#include <QUrl>
34

35
#include <algorithm>
36

37
#include "playlistinterface.h"
38
#include "playeradaptor.h"
39
#include "slideraction.h"
40
#include "statuslabel.h"
41
#include "actioncollection.h"
42
#include "collectionlist.h"
Michael Pyne's avatar
Michael Pyne committed
43
#include "coverinfo.h"
44
#include "tag.h"
45
#include "scrobbler.h"
46
#include "juk.h"
Michael Pyne's avatar
Michael Pyne committed
47
#include "juk_debug.h"
48 49

using namespace ActionCollection;
50

Stephan Kulow's avatar
Stephan Kulow committed
51
enum PlayerManagerStatus { StatusStopped = -1, StatusPaused = 1, StatusPlaying = 2 };
52

53 54 55 56 57 58 59 60 61 62 63 64
////////////////////////////////////////////////////////////////////////////////
// static functions
////////////////////////////////////////////////////////////////////////////////
static void updateWindowTitle(const FileHandle &file)
{
    JuK::JuKInstance()->setWindowTitle(i18nc(
        "%1 is the artist and %2 is the title of the currently playing track.", 
        "%1 - %2 :: JuK",
        file.tag()->artist(),
        file.tag()->title()));
}

65 66 67 68 69
////////////////////////////////////////////////////////////////////////////////
// protected members
////////////////////////////////////////////////////////////////////////////////

PlayerManager::PlayerManager() :
70
    QObject(),
71
    m_playlistInterface(nullptr),
72 73 74
    m_output(new Phonon::AudioOutput(Phonon::MusicCategory, this)),
    m_media( new Phonon::MediaObject(this)),
    m_audioPath(Phonon::createPath(m_media, m_output))
75
{
76
    setupAudio();
77
    new PlayerAdaptor(this);
78
    QDBusConnection::sessionBus().registerObject("/Player", this);
79 80 81 82 83 84
}

////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////

85 86
bool PlayerManager::playing() const
{
87
    Phonon::State state = m_media->state();
88
    return (state == Phonon::PlayingState || state == Phonon::BufferingState);
89 90 91 92
}

bool PlayerManager::paused() const
{
93
    return m_media->state() == Phonon::PausedState;
94 95
}

96 97
bool PlayerManager::muted() const
{
98
    return m_output->isMuted();
99 100
}

101 102
float PlayerManager::volume() const
{
103
    return m_output->volume();
104 105
}

106 107
int PlayerManager::status() const
{
108
    if(paused())
109
        return StatusPaused;
110

111
    if(playing())
112
        return StatusPlaying;
113

114
    return StatusStopped;
115 116
}

Scott Wheeler's avatar
Scott Wheeler committed
117
int PlayerManager::totalTime() const
118 119 120 121 122 123 124 125 126 127
{
    return totalTimeMSecs() / 1000;
}

int PlayerManager::currentTime() const
{
    return currentTimeMSecs() / 1000;
}

int PlayerManager::totalTimeMSecs() const
128
{
129
    return m_media->totalTime();
130 131
}

132
int PlayerManager::currentTimeMSecs() const
133
{
134
    return m_media->currentTime();
135 136
}

137 138
bool PlayerManager::seekable() const
{
139
    return m_media->isSeekable();
140 141
}

142 143 144 145 146 147 148
QStringList PlayerManager::trackProperties()
{
    return FileHandle::properties();
}

QString PlayerManager::trackProperty(const QString &property) const
{
Michael Pyne's avatar
Michael Pyne committed
149
    if(!playing() && !paused())
150
        return QString();
151 152 153 154

    return m_file.property(property);
}

Michael Pyne's avatar
Michael Pyne committed
155 156 157 158 159
QPixmap PlayerManager::trackCover(const QString &size) const
{
    if(!playing() && !paused())
        return QPixmap();

160
    if(size.toLower() == "small")
Michael Pyne's avatar
Michael Pyne committed
161
        return m_file.coverInfo()->pixmap(CoverInfo::Thumbnail);
162
    if(size.toLower() == "large")
Michael Pyne's avatar
Michael Pyne committed
163 164 165 166 167
        return m_file.coverInfo()->pixmap(CoverInfo::FullSize);

    return QPixmap();
}

168 169 170 171 172
FileHandle PlayerManager::playingFile() const
{
    return m_file;
}

173 174
QString PlayerManager::playingString() const
{
175
    if(!playing() || m_file.isNull())
176
        return QString();
177

178
    return m_file.tag()->playingString();
179 180
}

181
void PlayerManager::setPlaylistInterface(PlaylistInterface *interface)
182 183 184 185 186 187 188 189
{
    m_playlistInterface = interface;
}

////////////////////////////////////////////////////////////////////////////////
// public slots
////////////////////////////////////////////////////////////////////////////////

190
void PlayerManager::play(const FileHandle &file)
191
{
192
    if(!m_media || !m_playlistInterface || file.isNull())
193 194
        return;

195 196
    m_media->setCurrentSource(QUrl::fromLocalFile(file.absFilePath()));
    m_media->play();
197

198 199
    if(m_file != file)
        emit signalItemChanged(file);
200

201
    m_file = file;
202

203 204
    // Our state changed handler will perform the follow up actions necessary
    // once we actually start playing.
205 206
}

207 208 209 210 211 212 213 214 215
void PlayerManager::play(const QString &file)
{
    CollectionListItem *item = CollectionList::instance()->lookup(file);
    if(item) {
        Playlist::setPlaying(item);
        play(item->file());
    }
}

Scott Wheeler's avatar
Scott Wheeler committed
216 217
void PlayerManager::play()
{
218 219 220 221 222 223 224 225 226 227 228 229
    if(paused())
        m_media->play();
    else if(playing()) {
        m_media->seek(0);
        emit seeked(0);
    }
    else {
        m_playlistInterface->playNext();
        const auto file = m_playlistInterface->currentFile();

        play(file);
    }
Scott Wheeler's avatar
Scott Wheeler committed
230 231
}

232 233
void PlayerManager::pause()
{
234
    if(paused())
235 236
        return;

237
    action("pause")->setEnabled(false);
238

239
    m_media->pause();
240 241 242 243
}

void PlayerManager::stop()
{
244
    if(!m_playlistInterface)
245 246
        return;

247 248 249 250
    action("pause")->setEnabled(false);
    action("stop")->setEnabled(false);
    action("back")->setEnabled(false);
    action("forward")->setEnabled(false);
251
    action("forwardAlbum")->setEnabled(false);
252

253
    if(!m_file.isNull()) {
254
        m_file = FileHandle();
255 256
        emit signalItemChanged(m_file);
    }
257 258

    m_media->stop();
259 260
}

Laurent Montel's avatar
Laurent Montel committed
261
void PlayerManager::setVolume(float volume)
262
{
263
    m_output->setVolume(volume);
264 265
}

266
void PlayerManager::seek(int seekTime)
267
{
268
    if(m_media->currentTime() == seekTime)
269
        return;
270

271
    m_media->seek(seekTime);
272
    emit seeked(seekTime);
273 274 275 276
}

void PlayerManager::seekForward()
{
277 278
    const qint64 total = m_media->totalTime();
    const qint64 newtime = m_media->currentTime() + total / 100;
279
    const qint64 seekTo = qMin(total, newtime);
280

281
    m_media->seek(seekTo);
282
    emit seeked(seekTo);
283 284 285 286
}

void PlayerManager::seekBack()
{
287 288
    const qint64 total = m_media->totalTime();
    const qint64 newtime = m_media->currentTime() - total / 100;
289
    const qint64 seekTo = qMax(qint64(0), newtime);
290

291
    m_media->seek(seekTo);
292
    emit seeked(seekTo);
293 294
}

295 296
void PlayerManager::playPause()
{
David Faure's avatar
David Faure committed
297
    playing() ? action("pause")->trigger() : action("play")->trigger();
298 299
}

300 301
void PlayerManager::forward()
{
302 303
    m_playlistInterface->playNext();
    FileHandle file = m_playlistInterface->currentFile();
304

305 306 307 308 309 310 311 312
    if(!file.isNull())
        play(file);
    else
        stop();
}

void PlayerManager::back()
{
313 314
    m_playlistInterface->playPrevious();
    FileHandle file = m_playlistInterface->currentFile();
315

316 317 318 319 320 321
    if(!file.isNull())
        play(file);
    else
        stop();
}

322 323
void PlayerManager::volumeUp()
{
324 325
    const auto newVolume = std::min(m_output->volume() + 0.04, 1.0);
    m_output->setVolume(newVolume); // 4% up
326 327 328 329
}

void PlayerManager::volumeDown()
{
330 331
    const auto newVolume = std::max(m_output->volume() - 0.04, 0.0);
    m_output->setVolume(newVolume); // 4% down
332 333
}

334
void PlayerManager::setMuted(bool m)
335
{
336
    m_output->setMuted(m);
337 338 339 340 341 342 343
}

bool PlayerManager::mute()
{
    bool newState = !muted();
    setMuted(newState);
    return newState;
344 345
}

346 347 348
////////////////////////////////////////////////////////////////////////////////
// private slots
////////////////////////////////////////////////////////////////////////////////
349

350 351
void PlayerManager::slotFinished()
{
352 353
    // It is possible to end up in this function if a file simply fails to play or if the
    // user moves the slider all the way to the end, therefore see if we can keep playing
354
    // and if we can, do so.  Otherwise, stop.
355

356
    m_playlistInterface->playNext();
357
    play(m_playlistInterface->currentFile());
358 359
}

360 361
void PlayerManager::slotLength(qint64 msec)
{
362
    emit totalTimeChanged(msec);
363 364
}

365
void PlayerManager::slotTick(qint64 msec)
366
{
367
    emit tick(msec);
368 369
}

370
void PlayerManager::slotStateChanged(Phonon::State newstate, Phonon::State)
Matthias Kretz's avatar
Matthias Kretz committed
371
{
372
    if(newstate == Phonon::ErrorState) {
373 374 375
        QString errorMessage =
            i18nc(
              "%1 will be the /path/to/file, %2 will be some string from Phonon describing the error",
376 377
              "JuK is unable to play the audio file<nl/><filename>%1</filename><nl/>"
                "for the following reason:<nl/><message>%2</message>",
378
              m_file.absFilePath(),
379
              m_media->errorString()
380 381
            );

382 383 384 385 386
        qCWarning(JUK_LOG)
                << "Phonon is in error state" << m_media->errorString()
                << "while playing" << m_file.absFilePath();

        switch(m_media->errorType()) {
387
            case Phonon::NoError:
Michael Pyne's avatar
Michael Pyne committed
388
                qCDebug(JUK_LOG) << "received a state change to ErrorState but errorType is NoError!?";
389
                break;
390

Matthias Kretz's avatar
Matthias Kretz committed
391
            case Phonon::NormalError:
392
                KMessageBox::information(0, errorMessage);
Matthias Kretz's avatar
Matthias Kretz committed
393
                break;
394

Matthias Kretz's avatar
Matthias Kretz committed
395
            case Phonon::FatalError:
396
                KMessageBox::sorry(0, errorMessage);
Matthias Kretz's avatar
Matthias Kretz committed
397 398
                break;
        }
399

400
        stop();
401
        return;
402
    }
403 404

    // "normal" path
405
    if(newstate == Phonon::StoppedState && m_file.isNull()) {
406 407 408
        JuK::JuKInstance()->setWindowTitle(i18n("JuK"));
        emit signalStop();
    }
409 410 411 412
    else if(newstate == Phonon::PausedState) {
        emit signalPause();
    }
    else { // PlayingState or BufferingState
413 414 415 416 417 418 419
        action("pause")->setEnabled(true);
        action("stop")->setEnabled(true);
        action("forward")->setEnabled(true);
        if(action<KToggleAction>("albumRandomPlay")->isChecked())
            action("forwardAlbum")->setEnabled(true);
        action("back")->setEnabled(true);

420
        updateWindowTitle(m_file);
421 422
        emit signalPlay();
    }
423 424
}

425 426 427 428 429 430 431 432 433 434 435 436
void PlayerManager::slotSeekableChanged(bool isSeekable)
{
    emit seekableChanged(isSeekable);
}

void PlayerManager::slotMutedChanged(bool muted)
{
    emit mutedChanged(muted);
}

void PlayerManager::slotVolumeChanged(qreal volume)
{
437
    if(qFuzzyCompare(m_output->volume(), volume))
438
    {
439
        return;
440
    }
441 442 443 444

    emit volumeChanged(volume);
}

445 446 447 448
////////////////////////////////////////////////////////////////////////////////
// private members
////////////////////////////////////////////////////////////////////////////////

449
void PlayerManager::setupAudio()
450
{
451 452 453
    using namespace Phonon;
    connect(m_output, &AudioOutput::mutedChanged,  this, &PlayerManager::slotMutedChanged);
    connect(m_output, &AudioOutput::volumeChanged, this, &PlayerManager::slotVolumeChanged);
454

455
    connect(m_media, &MediaObject::stateChanged, this, &PlayerManager::slotStateChanged);
456
    connect(m_media, &MediaObject::currentSourceChanged, this, &PlayerManager::trackHasChanged);
457 458
    connect(m_media, &MediaObject::totalTimeChanged, this, &PlayerManager::slotLength);
    connect(m_media, &MediaObject::tick, this, &PlayerManager::slotTick);
459
    connect(m_media, &MediaObject::aboutToFinish, this, &PlayerManager::trackAboutToFinish);
460 461
    connect(m_media, &MediaObject::finished, this, &PlayerManager::slotFinished);
    connect(m_media, &MediaObject::seekableChanged, this, &PlayerManager::slotSeekableChanged);
462

463
    m_media->setTickInterval(100);
464 465
}

Michael Pyne's avatar
Michael Pyne committed
466 467 468 469 470 471 472 473 474 475 476
QString PlayerManager::randomPlayMode() const
{
    if(action<KToggleAction>("randomPlay")->isChecked())
        return "Random";
    if(action<KToggleAction>("albumRandomPlay")->isChecked())
        return "AlbumRandom";
    return "NoRandom";
}

void PlayerManager::setRandomPlayMode(const QString &randomMode)
{
477
    if(randomMode.toLower() == "random")
Michael Pyne's avatar
Michael Pyne committed
478
        action<KToggleAction>("randomPlay")->setChecked(true);
479
    if(randomMode.toLower() == "albumrandom")
Michael Pyne's avatar
Michael Pyne committed
480
        action<KToggleAction>("albumRandomPlay")->setChecked(true);
481
    if(randomMode.toLower() == "norandom")
Michael Pyne's avatar
Michael Pyne committed
482 483 484
        action<KToggleAction>("disableRandomPlay")->setChecked(true);
}

485 486 487 488 489 490 491 492 493 494
void PlayerManager::trackHasChanged(const Phonon::MediaSource &newSource)
{
    if(newSource.type() == Phonon::MediaSource::Url) {
        const auto item = CollectionList::instance()->lookup(newSource.url().path());
        if(item) {
            const auto newFile = item->file();
            if(m_file != newFile)
                emit signalItemChanged(newFile);
            m_file = newFile;
            updateWindowTitle(m_file);
495
            emit seeked(0);
496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516
        }
    } else {
        qCWarning(JUK_LOG) << "Track has changed so something we didn't set???";
        return;
    }
}

void PlayerManager::trackAboutToFinish()
{
    // Called when playback is in progress and a track is about to finish, gives us a
    // chance to keep audio playback going without Phonon entering StoppedState
    if(!m_playlistInterface)
        return;

    m_playlistInterface->playNext();
    const auto file = m_playlistInterface->currentFile();

    if(!file.isNull())
        m_media->enqueue(QUrl::fromLocalFile(file.absFilePath()));
}

517
// vim: set et sw=4 tw=0 sta: