diff --git a/NEWS b/NEWS
index 0702c9436..7cc085e57 100644
--- a/NEWS
+++ b/NEWS
@@ -19,6 +19,7 @@ ver 0.16 (20??/??/??)
   - added tags "ArtistSort", "AlbumArtistSort"
   - id3: revised "performer" tag support
 * decoders:
+  - don't try a plugin twice (MIME type & suffix)
   - ffmpeg: support multiple tags
   - ffmpeg: convert metadata to generic format
   - sndfile: new decoder plugin based on libsndfile
diff --git a/src/decoder_thread.c b/src/decoder_thread.c
index 1539117f4..631a7a0e3 100644
--- a/src/decoder_thread.c
+++ b/src/decoder_thread.c
@@ -156,45 +156,86 @@ decoder_file_decode(const struct decoder_plugin *plugin,
 	return decoder->dc->state != DECODE_STATE_START;
 }
 
+/**
+ * Hack to allow tracking const decoder plugins in a GSList.
+ */
+static inline gpointer
+deconst_plugin(const struct decoder_plugin *plugin)
+{
+	union {
+		const struct decoder_plugin *in;
+		gpointer out;
+	} u = { .in = plugin };
+
+	return u.out;
+}
+
 /**
  * Try decoding a stream, using plugins matching the stream's MIME type.
+ *
+ * @param tried_r a list of plugins which were tried
  */
 static bool
-decoder_run_stream_mime_type(struct decoder *decoder, struct input_stream *is)
+decoder_run_stream_mime_type(struct decoder *decoder, struct input_stream *is,
+			     GSList **tried_r)
 {
+	assert(tried_r != NULL);
+
 	const struct decoder_plugin *plugin;
 	unsigned int next = 0;
 
 	if (is->mime == NULL)
 		return false;
 
-	while ((plugin = decoder_plugin_from_mime_type(is->mime, next++)))
-		if (plugin->stream_decode != NULL &&
-		    decoder_stream_decode(plugin, decoder, is))
+	while ((plugin = decoder_plugin_from_mime_type(is->mime, next++))) {
+		if (plugin->stream_decode == NULL)
+			continue;
+
+		if (g_slist_find(*tried_r, plugin) != NULL)
+			/* don't try a plugin twice */
+			continue;
+
+		if (decoder_stream_decode(plugin, decoder, is))
 			return true;
 
+		*tried_r = g_slist_prepend(*tried_r, deconst_plugin(plugin));
+	}
+
 	return false;
 }
 
 /**
  * Try decoding a stream, using plugins matching the stream's URI
  * suffix.
+ *
+ * @param tried_r a list of plugins which were tried
  */
 static bool
 decoder_run_stream_suffix(struct decoder *decoder, struct input_stream *is,
-			  const char *uri)
+			  const char *uri, GSList **tried_r)
 {
+	assert(tried_r != NULL);
+
 	const char *suffix = uri_get_suffix(uri);
 	const struct decoder_plugin *plugin = NULL;
 
 	if (suffix == NULL)
 		return false;
 
-	while ((plugin = decoder_plugin_from_suffix(suffix, plugin)) != NULL)
-		if (plugin->stream_decode != NULL &&
-		    decoder_stream_decode(plugin, decoder, is))
+	while ((plugin = decoder_plugin_from_suffix(suffix, plugin)) != NULL) {
+		if (plugin->stream_decode == NULL)
+			continue;
+
+		if (g_slist_find(*tried_r, plugin) != NULL)
+			/* don't try a plugin twice */
+			continue;
+
+		if (decoder_stream_decode(plugin, decoder, is))
 			return true;
 
+		*tried_r = g_slist_prepend(*tried_r, deconst_plugin(plugin));
+	}
+
 	return false;
 }
 
@@ -231,15 +272,20 @@ decoder_run_stream(struct decoder *decoder, const char *uri)
 
 	decoder_lock(dc);
 
+	GSList *tried = NULL;
+
 	success = dc->command == DECODE_COMMAND_STOP ||
 		/* first we try mime types: */
-		decoder_run_stream_mime_type(decoder, input_stream) ||
+		decoder_run_stream_mime_type(decoder, input_stream, &tried) ||
 		/* if that fails, try suffix matching the URL: */
-		decoder_run_stream_suffix(decoder, input_stream, uri) ||
+		decoder_run_stream_suffix(decoder, input_stream, uri,
+					  &tried) ||
 		/* fallback to mp3: this is needed for bastard streams
 		   that don't have a suffix or set the mimeType */
 		decoder_run_stream_fallback(decoder, input_stream);
 
+	g_slist_free(tried);
+
 	decoder_unlock(dc);
 	input_stream_close(input_stream);
 	decoder_lock(dc);