Clementine now skips songs that don't exist anymore (updates issue #250)

This commit is contained in:
Paweł Bara 2011-03-10 18:01:35 +00:00
parent d8cd7ae80a
commit 8722e00103
15 changed files with 163 additions and 45 deletions

View File

@ -51,6 +51,11 @@ Player::Player(PlaylistManagerInterface* playlists, LastFMService* lastfm,
SetVolume(settings_.value("volume", 50).toInt());
connect(engine_.get(), SIGNAL(Error(QString)), SIGNAL(Error(QString)));
connect(engine_.get(), SIGNAL(ValidSongRequested(QUrl)), SLOT(ValidSongRequested(QUrl)));
connect(engine_.get(), SIGNAL(InvalidSongRequested(QUrl)), SLOT(InvalidSongRequested(QUrl)));
connect(playlists, SIGNAL(CurrentSongChanged(Song)), SLOT(CurrentMetadataChanged(Song)));
}
Player::~Player() {
@ -272,6 +277,11 @@ void Player::PlayAt(int index, Engine::TrackChangeType change, bool reshuffle) {
}
void Player::CurrentMetadataChanged(const Song& metadata) {
// those things might have changed (especially when a previously invalid
// song was reloaded) so we push the latest version into Engine
engine_->Load(metadata.url(), Engine::Auto,
metadata.beginning_nanosec(), metadata.end_nanosec());
#ifdef HAVE_LIBLASTFM
lastfm_->NowPlaying(metadata);
#endif
@ -441,3 +451,15 @@ void Player::TrackAboutToEnd() {
next_item_->Metadata().end_nanosec());
}
}
void Player::ValidSongRequested(const QUrl& url) {
emit SongChangeRequestProcessed(url, true);
}
void Player::InvalidSongRequested(const QUrl& url) {
// first send the notification to others...
emit SongChangeRequestProcessed(url, false);
// ... and now when our listeners have completed their processing of the
// current item we can change the current item by skipping to the next song
NextItem(Engine::Auto);
}

View File

@ -92,6 +92,10 @@ signals:
// Emitted when there's a manual change to the current's track position.
void Seeked(qlonglong microseconds);
// Emitted when Player has processed a request to play another song. This contains
// the URL of the song and a flag saying whether it was able to play the song.
void SongChangeRequestProcessed(const QUrl& url, bool valid);
void ForceShowOSD(Song);
};
@ -147,6 +151,9 @@ public slots:
void NextInternal(Engine::TrackChangeType);
void ValidSongRequested(const QUrl&);
void InvalidSongRequested(const QUrl&);
private:
PlaylistManagerInterface* playlists_;
LastFMService* lastfm_;

View File

@ -263,19 +263,22 @@ void Song::InitFromFile(const QString& filename, int directory_id) {
if (qApp->thread() == QThread::currentThread())
qWarning() << Q_FUNC_INFO << "on GUI thread!";
#endif
d->init_from_file_ = true;
d->filename_ = filename;
d->directory_id_ = directory_id;
QFileInfo info(filename);
d->basefilename_ = info.fileName();
QMutexLocker l(&taglib_mutex_);
scoped_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
if(fileref->isNull())
if(fileref->isNull()) {
return;
}
d->init_from_file_ = true;
QFileInfo info(filename);
d->basefilename_ = info.fileName();
d->filesize_ = info.size();
d->mtime_ = info.lastModified().toTime_t();
d->ctime_ = info.created().toTime_t();
@ -928,13 +931,13 @@ void Song::InitFromLastFM(const lastfm::Track& track) {
#endif // Q_OS_WIN32
void Song::MergeFromSimpleMetaBundle(const Engine::SimpleMetaBundle &bundle) {
d->valid_ = true;
if (d->init_from_file_) {
// This Song was already loaded using taglib. Our tags are probably better than the engine's.
return;
}
d->valid_ = true;
UniversalEncodingHandler detector(NS_FILTER_NON_CJK);
QTextCodec* codec = detector.Guess(bundle);
@ -1028,6 +1031,12 @@ void Song::ToLastFM(lastfm::Track* track) const {
}
#endif // HAVE_LIBLASTFM
QUrl Song::url() const {
return QFile::exists(filename())
? QUrl::fromLocalFile(filename())
: filename();
}
QString Song::PrettyTitle() const {
QString title(d->title_);

View File

@ -197,6 +197,9 @@ class Song {
int directory_id() const { return d->directory_id_; }
const QString& filename() const { return d->filename_; }
// Returns this Song's URL which may point either to a file or to another type
// of stream.
QUrl url() const;
const QString& basefilename() const { return d->basefilename_; }
uint mtime() const { return d->mtime_; }
uint ctime() const { return d->ctime_; }

View File

@ -102,6 +102,12 @@ class Base : public QObject, boost::noncopyable {
void StatusText(const QString&);
void Error(const QString&);
// Emitted when Engine was unable to play a song with the given QUrl.
void InvalidSongRequested(const QUrl&);
// Emitted when Engine successfully started playing a song with the
// given QUrl.
void ValidSongRequested(const QUrl&);
void MetaData(const Engine::SimpleMetaBundle&);
// Signals that the engine's state has changed (a stream was stopped for example).

View File

@ -64,8 +64,6 @@ const char* GstEngine::kHypnotoadPipeline =
"band0=-24 band1=-3 band2=7.5 band3=12 band4=8 "
"band5=6 band6=5 band7=6 band8=0 band9=-24";
// TODO: weird analyzer problems with .cues
GstEngine::GstEngine()
: Engine::Base(),
delayq_(g_queue_new()),
@ -530,8 +528,9 @@ void GstEngine::PlayDone() {
GstStateChangeReturn ret = watcher->result();
quint64 offset_nanosec = watcher->data();
if (!current_pipeline_)
if (!current_pipeline_) {
return;
}
if (ret == GST_STATE_CHANGE_FAILURE) {
// Failure, but we got a redirection URL - try loading that instead
@ -559,6 +558,8 @@ void GstEngine::PlayDone() {
}
emit StateChanged(Engine::Playing);
// we've successfully started playing a media stream with this url
emit ValidSongRequested(url_);
}
@ -692,14 +693,20 @@ void GstEngine::timerEvent(QTimerEvent* e) {
}
}
void GstEngine::HandlePipelineError(const QString& message) {
void GstEngine::HandlePipelineError(const QString& message, int domain, int error_code) {
qWarning() << "Gstreamer error:" << message;
current_pipeline_.reset();
emit Error(message);
emit StateChanged(Engine::Empty);
}
emit StateChanged(Engine::Empty);
// unable to play media stream with this url
emit InvalidSongRequested(url_);
// no user error message when the error is 'no such URI'
if(!(domain == GST_RESOURCE_ERROR && error_code == GST_RESOURCE_ERROR_NOT_FOUND)) {
emit Error(message);
}
}
void GstEngine::EndOfStreamReached(bool has_next_track) {
GstEnginePipeline* pipeline_sender = qobject_cast<GstEnginePipeline*>(sender());
@ -774,11 +781,12 @@ shared_ptr<GstEnginePipeline> GstEngine::CreatePipeline() {
ret->set_buffer_duration_nanosec(buffer_duration_nanosec_);
ret->AddBufferConsumer(this);
foreach (BufferConsumer* consumer, buffer_consumers_)
foreach (BufferConsumer* consumer, buffer_consumers_) {
ret->AddBufferConsumer(consumer);
}
connect(ret.get(), SIGNAL(EndOfStreamReached(bool)), SLOT(EndOfStreamReached(bool)));
connect(ret.get(), SIGNAL(Error(QString)), SLOT(HandlePipelineError(QString)));
connect(ret.get(), SIGNAL(Error(QString,int,int)), SLOT(HandlePipelineError(QString,int,int)));
connect(ret.get(), SIGNAL(MetadataFound(Engine::SimpleMetaBundle)),
SLOT(NewMetaData(Engine::SimpleMetaBundle)));
connect(ret.get(), SIGNAL(destroyed()), SLOT(ClearScopeBuffers()));

View File

@ -114,7 +114,7 @@ class GstEngine : public Engine::Base, public BufferConsumer {
private slots:
void EndOfStreamReached(bool has_next_track);
void HandlePipelineError(const QString& message);
void HandlePipelineError(const QString& message, int domain, int error_code);
void NewMetaData(const Engine::SimpleMetaBundle& bundle);
void ClearScopeBuffers();
void AddBufferToScope(GstBuffer* buf, GstEnginePipeline* pipeline);

View File

@ -56,6 +56,7 @@ GstEnginePipeline::GstEnginePipeline(GstEngine* engine)
ignore_tags_(false),
pipeline_is_initialised_(false),
pipeline_is_connected_(false),
pipeline_error_(PipelineError()),
pending_seek_nanosec_(-1),
volume_percent_(100),
volume_modifier_(1.0),
@ -131,16 +132,20 @@ bool GstEnginePipeline::ReplaceDecodeBin(const QUrl& url) {
GstElement* GstEnginePipeline::CreateDecodeBinFromString(const char* pipeline) {
GError* error = NULL;
GstElement* bin = gst_parse_bin_from_description(pipeline, TRUE, &error);
if (error) {
QString message = QString::fromLocal8Bit(error->message);
int domain = error->domain;
int code = error->code;
g_error_free(error);
qWarning() << message;
emit Error(message);
return NULL;
}
emit Error(message, domain, code);
return bin;
return NULL;
} else {
return bin;
}
}
bool GstEnginePipeline::Init() {
@ -264,6 +269,11 @@ GstEnginePipeline::~GstEnginePipeline() {
gst_element_set_state(pipeline_, GST_STATE_NULL);
gst_object_unref(GST_OBJECT(pipeline_));
}
if(!pipeline_error_.message.isEmpty()) {
emit Error(pipeline_error_.message, pipeline_error_.domain,
pipeline_error_.error_code);
}
}
@ -293,6 +303,7 @@ gboolean GstEnginePipeline::BusCallback(GstBus*, GstMessage* msg, gpointer self)
GstBusSyncReply GstEnginePipeline::BusCallbackSync(GstBus*, GstMessage* msg, gpointer self) {
GstEnginePipeline* instance = reinterpret_cast<GstEnginePipeline*>(self);
switch (GST_MESSAGE_TYPE(msg)) {
case GST_MESSAGE_EOS:
emit instance->EndOfStreamReached(false);
@ -317,6 +328,7 @@ GstBusSyncReply GstEnginePipeline::BusCallbackSync(GstBus*, GstMessage* msg, gpo
default:
break;
}
return GST_BUS_PASS;
}
@ -340,7 +352,8 @@ void GstEnginePipeline::ErrorMessageReceived(GstMessage* msg) {
gst_message_parse_error(msg, &error, &debugs);
QString message = QString::fromLocal8Bit(error->message);
QString debugstr = QString::fromLocal8Bit(debugs);
int domain = error->domain;
int code = error->code;
g_error_free(error);
free(debugs);
@ -352,8 +365,13 @@ void GstEnginePipeline::ErrorMessageReceived(GstMessage* msg) {
return;
}
// we'll send the error later, when pipeline is done with it's state changes
pipeline_error_ = PipelineError();
pipeline_error_.message = message;
pipeline_error_.domain = domain;
pipeline_error_.error_code = code;
qDebug() << debugstr;
emit Error(message);
}
void GstEnginePipeline::TagMessageReceived(GstMessage* msg) {
@ -410,7 +428,6 @@ void GstEnginePipeline::StateChangedMessageReceived(GstMessage* msg) {
}
}
void GstEnginePipeline::NewPadCallback(GstElement*, GstPad* pad, gpointer self) {
GstEnginePipeline* instance = reinterpret_cast<GstEnginePipeline*>(self);
GstPad* const audiopad = gst_element_get_pad(instance->audiobin_, "sink");
@ -431,7 +448,6 @@ void GstEnginePipeline::NewPadCallback(GstElement*, GstPad* pad, gpointer self)
}
}
bool GstEnginePipeline::HandoffCallback(GstPad*, GstBuffer* buf, gpointer self) {
GstEnginePipeline* instance = reinterpret_cast<GstEnginePipeline*>(self);
@ -538,7 +554,6 @@ qint64 GstEnginePipeline::position() const {
return value;
}
qint64 GstEnginePipeline::length() const {
GstFormat fmt = GST_FORMAT_TIME;
gint64 value = 0;
@ -547,7 +562,6 @@ qint64 GstEnginePipeline::length() const {
return value;
}
GstState GstEnginePipeline::state() const {
GstState s, sp;
if (gst_element_get_state(pipeline_, &s, &sp, kGstStateTimeoutNanosecs) ==
@ -582,7 +596,6 @@ void GstEnginePipeline::SetEqualizerEnabled(bool enabled) {
UpdateEqualizer();
}
void GstEnginePipeline::SetEqualizerParams(int preamp, const QList<int>& band_gains) {
eq_preamp_ = preamp;
eq_band_gains_ = band_gains;

View File

@ -40,6 +40,12 @@ class GstEnginePipeline : public QObject {
Q_OBJECT
public:
struct PipelineError {
QString message;
int domain;
int error_code;
};
GstEnginePipeline(GstEngine* engine);
~GstEnginePipeline();
@ -92,7 +98,9 @@ class GstEnginePipeline : public QObject {
signals:
void EndOfStreamReached(bool has_next_track);
void MetadataFound(const Engine::SimpleMetaBundle& bundle);
void Error(const QString& message);
// This indicates an error, delegated from GStreamer, in the pipeline.
// The message, domain and error_code are related to GStreamer's GError.
void Error(const QString& message, int domain, int error_code);
void FaderFinished();
protected:
@ -107,10 +115,12 @@ class GstEnginePipeline : public QObject {
static bool HandoffCallback(GstPad*, GstBuffer*, gpointer);
static bool EventHandoffCallback(GstPad*, GstEvent*, gpointer);
static void SourceDrainedCallback(GstURIDecodeBin*, gpointer);
void TagMessageReceived(GstMessage*);
void ErrorMessageReceived(GstMessage*);
void ElementMessageReceived(GstMessage*);
void StateChangedMessageReceived(GstMessage*);
QString ParseTag(GstTagList* list, const char* tag) const;
bool Init();
@ -191,6 +201,8 @@ class GstEnginePipeline : public QObject {
// Also we have to wait for the decodebin to be connected.
bool pipeline_is_initialised_;
bool pipeline_is_connected_;
// Cached error thrown from GStreamer during pipeline's initialization.
PipelineError pipeline_error_;
qint64 pending_seek_nanosec_;
int volume_percent_;

View File

@ -237,11 +237,7 @@ QVariant Playlist::data(const QModelIndex& index, int role) const {
// Don't forget to change Playlist::CompareItems when adding new columns
switch (index.column()) {
case Column_Title:
if (!song.title().isEmpty())
return song.title();
if (!song.basefilename().isEmpty())
return song.basefilename();
return song.filename();
return song.PrettyTitle();
case Column_Artist: return song.artist();
case Column_Album: return song.album();
case Column_Length: return song.length_nanosec();
@ -526,8 +522,9 @@ void Playlist::set_current_row(int i) {
}
if (current_item_index_.isValid()) {
emit dataChanged(current_item_index_, current_item_index_.sibling(current_item_index_.row(), ColumnCount-1));
emit CurrentSongChanged(current_item_metadata());
InformOfCurrentSongChange(current_item_index_,
current_item_index_.sibling(current_item_index_.row(), ColumnCount-1),
current_item_metadata());
}
// Update the virtual index
@ -1251,8 +1248,9 @@ void Playlist::SetStreamMetadata(const QUrl& url, const Song& song) {
current_item_->SetTemporaryMetadata(song);
UpdateScrobblePoint();
emit dataChanged(index(current_item_index_.row(), 0), index(current_item_index_.row(), ColumnCount-1));
emit CurrentSongChanged(song);
InformOfCurrentSongChange(index(current_item_index_.row(), 0),
index(current_item_index_.row(), ColumnCount-1),
song);
}
void Playlist::ClearStreamMetadata() {
@ -1353,7 +1351,8 @@ void Playlist::RemoveItemsNotInQueue() {
void Playlist::ReloadItems(const QList<int>& rows) {
foreach (int row, rows) {
item_at(row)->Reload();
emit dataChanged(index(row, 0), index(row, ColumnCount-1));
InformOfCurrentSongChange(index(row, 0), index(row, ColumnCount-1),
item_at(row)->Metadata());
}
}
@ -1518,3 +1517,13 @@ void Playlist::set_column_align_center(int column) {
void Playlist::set_column_align_right(int column) {
column_alignments_[column] = (Qt::AlignRight | Qt::AlignVCenter);
}
void Playlist::InformOfCurrentSongChange(const QModelIndex& top_left, const QModelIndex& bottom_right,
const Song& metadata) {
emit dataChanged(top_left, bottom_right);
// if the song is invalid, we won't play it - there's no point in
// informing anybody about the change
if(metadata.is_valid()) {
emit CurrentSongChanged(metadata);
}
}

View File

@ -271,6 +271,9 @@ class Playlist : public QAbstractListModel {
void RemoveItemsNotInQueue();
void InformOfCurrentSongChange(const QModelIndex& top_left, const QModelIndex& bottom_right,
const Song& metadata);
private slots:
void TracksAboutToBeDequeued(const QModelIndex&, int begin, int end);
void TracksDequeued();

View File

@ -329,3 +329,23 @@ void PlaylistManager::PlaySmartPlaylist(GeneratorPtr generator, bool as_new, boo
current()->InsertSmartPlaylist(generator);
}
// When Player has processed the new song chosen by the user...
void PlaylistManager::SongChangeRequestProcessed(const QUrl& url, bool valid) {
foreach(Playlist* playlist, GetAllPlaylists()) {
PlaylistItemPtr current = playlist->current_item();
if(current) {
Song current_song = current->Metadata();
// if validity has changed, reload the item
if(current_song.url() == url && current_song.filetype() != Song::Type_Stream &&
current_song.is_valid() != QFile::exists(current_song.filename())) {
playlist->ReloadItems(QList<int>() << playlist->current_row());
}
// we have at most one current item
break;
}
}
}

View File

@ -71,6 +71,8 @@ public slots:
virtual void Remove(int id) = 0;
virtual void ChangePlaylistOrder(const QList<int>& ids) = 0;
virtual void SongChangeRequestProcessed(const QUrl& url, bool valid) = 0;
virtual void SetCurrentPlaylist(int id) = 0;
virtual void SetActivePlaylist(int id) = 0;
virtual void SetActiveToCurrent() = 0;
@ -173,6 +175,8 @@ public slots:
void PlaySmartPlaylist(smart_playlists::GeneratorPtr generator, bool as_new, bool clear);
void SongChangeRequestProcessed(const QUrl& url, bool valid);
private slots:
void OneOfPlaylistsChanged();
void UpdateSummaryText();

View File

@ -85,15 +85,15 @@ QVariant SongPlaylistItem::DatabaseValue(DatabaseColumn column) const {
}
QUrl SongPlaylistItem::Url() const {
if (QFile::exists(song_.filename())) {
return QUrl::fromLocalFile(song_.filename());
} else {
return song_.filename();
}
return song_.url();
}
void SongPlaylistItem::Reload() {
song_.InitFromFile(song_.filename(), song_.directory_id());
QString old_filename = song_.filename();
int old_directory_id = song_.directory_id();
song_ = Song();
song_.InitFromFile(old_filename, old_directory_id);
}
Song SongPlaylistItem::Metadata() const {

View File

@ -380,6 +380,8 @@ MainWindow::MainWindow(
connect(ui_->volume, SIGNAL(valueChanged(int)), player_, SLOT(SetVolume(int)));
connect(player_, SIGNAL(Error(QString)), SLOT(ShowErrorDialog(QString)));
connect(player_, SIGNAL(SongChangeRequestProcessed(QUrl,bool)), playlists_, SLOT(SongChangeRequestProcessed(QUrl,bool)));
connect(player_, SIGNAL(Paused()), SLOT(MediaPaused()));
connect(player_, SIGNAL(Playing()), SLOT(MediaPlaying()));
connect(player_, SIGNAL(Stopped()), SLOT(MediaStopped()));