diff --git a/src/m3u/m3u.cc b/src/m3u/m3u.cc
index 795e25b92..e88fa1bac 100644
--- a/src/m3u/m3u.cc
+++ b/src/m3u/m3u.cc
@@ -18,18 +18,28 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  */
 
+#include <stdlib.h>
 #include <string.h>
 
+#include <libaudcore/preferences.h>
 #include <libaudcore/audstrings.h>
 #include <libaudcore/i18n.h>
 #include <libaudcore/plugin.h>
+#include <libaudcore/runtime.h>
 
 static const char * const m3u_exts[] = {"m3u", "m3u8"};
 
 class M3ULoader : public PlaylistPlugin
 {
 public:
-    static constexpr PluginInfo info = {N_("M3U Playlists"), PACKAGE};
+    static const PreferencesWidget widgets[];
+    static const PluginPreferences prefs;
+    static constexpr PluginInfo info = {
+        N_("M3U Playlists"),
+        PACKAGE,
+        nullptr,
+        & prefs
+    };
 
     constexpr M3ULoader () : PlaylistPlugin (info, m3u_exts, true) {}
 
@@ -58,6 +68,12 @@ static char * split_line (char * line)
 bool M3ULoader::load (const char * filename, VFSFile & file, String & title,
  Index<PlaylistAddItem> & items)
 {
+    enum extDataType {NA, ALB, ART, GENRE, INF};
+    bool Extended_m3u = false;
+    bool firstline = true;
+    bool refreshTuple = true;
+    Tuple tuple = Tuple ();
+
     Index<char> text = file.read_all ();
     if (! text.len ())
         return false;
@@ -75,13 +91,106 @@ bool M3ULoader::load (const char * filename, VFSFile & file, String & title,
         while (* parse == ' ' || * parse == '\t')
             parse ++;
 
-        if (* parse && * parse != '#')
+        if (* parse)
         {
-            StringBuf s = uri_construct (parse, filename);
-            if (s)
-                items.append (String (s));
+            if (* parse != '#')
+            {
+                String s = String (uri_construct (parse, filename));
+
+                if (s && s[0])
+                {
+                    if (Extended_m3u)
+                    {
+                        tuple.set_filename (s);
+                        tuple.set_state (Tuple::Valid);
+                        items.append (s, std::move (tuple));
+                        refreshTuple = true;
+                    }
+                    else
+                        items.append (s);
+                }
+            }
+            else if (Extended_m3u)
+            {
+                if (! strncmp (parse, "#EXT-X-", 7))  // WE'RE AN "HLS" STREAM, STAND DOWN & LET ffaudio PLUGIN HANDLE!:
+                {
+                    AUDINFO ("i:HLS STREAM(%s) - STOP PARSING & JUST ADD PLAYLIST AS SINGLE ENTRY!\n", filename);
+                    items.append (String (filename));
+                    break;
+                }
+                else if (! strncmp (parse, "#EXT", 4))  // WE'RE A DATA LINE (EXTENDED M3U):
+                {
+                    extDataType extData = NA;
+                    if (refreshTuple)
+                    {
+                        tuple = Tuple ();
+                        refreshTuple = false;
+                    }
+
+                    if (! strncmp (parse, "#EXTINF", 7))        // SET [LENGTH,] TITLE
+                        extData = INF;
+                    else if (! strncmp (parse, "#EXTGENRE", 9)) // SET GENRE
+                    {
+                        parse += 2;
+                        extData = GENRE;
+                    }
+                    else if (! strncmp (parse, "#EXTALB", 7))   // SET ALBUM
+                        extData = ALB;
+                    else if (! strncmp (parse, "#EXTART", 7))   // SET ARTIST
+                        extData = ART;
+
+                    parse += 7;
+                    if (parse < next && * parse == ':')
+                    {
+                        ++parse;
+                        while (parse < next && * parse == ' ')
+                            ++parse;
+
+                        if (* parse && parse < next)
+                        {
+                            Index<String> headerparts = str_list_to_index (parse, ",");
+                            if (extData == INF && headerparts.len () > 1)
+                            {
+                                int tlen = atoi (headerparts[0]) * 1000;
+                                if (tlen <= 0)
+                                    tuple.unset (Tuple::Length);
+                                else
+                                    tuple.set_int (Tuple::Length, tlen);
+
+                                // FIND THE TITLE AND MOVE PAST ANY LEADING SPACES IN IT:
+                                char * c = parse;
+                                while (c < next && * c != ',')
+                                    ++c;
+                                if (c < next && * c)
+                                    ++c;
+                                while (c < next && * c == ' ')
+                                    ++c;
+                                if (*c && c < next)
+                                    tuple.set_str (Tuple::Title, c);
+                            }
+                            else if (headerparts.len () > 0)
+                            {
+                                if (extData == INF)
+                                {
+                                    tuple.unset (Tuple::Length);
+                                    tuple.set_str (Tuple::Title, headerparts[0]);
+                                }
+                                else if (extData == ART)
+                                    tuple.set_str (Tuple::Artist, headerparts[0]);
+                                else if (extData == ALB)
+                                    tuple.set_str (Tuple::Album, headerparts[0]);
+                                else if (extData == GENRE)
+                                    tuple.set_str (Tuple::Genre, headerparts[0]);
+                            }
+                        }
+                    }
+                }
+            }
+            else if (firstline && ! strncmp (parse, "#EXTM3U", 7))  // WE'RE AN EXTENDED M3U:
+                Extended_m3u = true;
         }
 
+        firstline = false;
         parse = next;
     }
 
@@ -91,9 +200,56 @@ bool M3ULoader::load (const char * filename, VFSFile & file, String & title,
 bool M3ULoader::save (const char * filename, VFSFile & file, const char * title,
  const Index<PlaylistAddItem> & items)
 {
+    bool Extended_m3u = aud_get_bool ("m3u", "saveas_extended_m3u");
+
+    if (Extended_m3u && file.fwrite (str_copy("#EXTM3U\n"), 1, 8) != 8)
+        return false;
+
     for (auto & item : items)
     {
         StringBuf path = uri_deconstruct (item.filename, filename);
+        if (Extended_m3u && item.tuple.state () == Tuple::Valid)
+        {
+            int tuplen = item.tuple.get_int (Tuple::Length);
+            if (tuplen >= 0)
+                tuplen /= 1000;
+
+            {
+                String tupstr = item.tuple.get_str (Tuple::Title);
+                if (! tupstr)
+                    tupstr = String (filename_get_base (item.filename));
+                StringBuf line = str_printf ("#EXTINF:%d, %s\n", tuplen, (const char *) tupstr);
+                if (file.fwrite (line, 1, line.len ()) != line.len ())
+                    return false;
+            }
+            {
+                String tupstr = item.tuple.get_str (Tuple::Artist);
+                if (tupstr && tupstr[0])
+                {
+                    StringBuf line = str_printf ("#EXTART:%s\n", (const char *) tupstr);
+                    if (file.fwrite (line, 1, line.len ()) != line.len ())
+                        AUDERR ("m3u: could not write artist to extended m3u file?!\n");
+                }
+            }
+            {
+                String tupstr = item.tuple.get_str (Tuple::Album);
+                if (tupstr && tupstr[0])
+                {
+                    StringBuf line = str_printf ("#EXTALB:%s\n", (const char *) tupstr);
+                    if (file.fwrite (line, 1, line.len ()) != line.len ())
+                        AUDERR ("m3u: could not write album to extended m3u file?!\n");
+                }
+            }
+            {
+                String tupstr = item.tuple.get_str (Tuple::Genre);
+                if (tupstr && tupstr[0])
+                {
+                    StringBuf line = str_printf ("#EXTGENRE:%s\n", (const char *) tupstr);
+                    if (file.fwrite (line, 1, line.len ()) != line.len ())
+                        AUDERR ("m3u: could not write genre to extended m3u file?!\n");
+                }
+            }
+        }
         StringBuf line = str_concat ({path, "\n"});
         if (file.fwrite (line, 1, line.len ()) != line.len ())
             return false;
@@ -101,3 +257,10 @@ bool M3ULoader::save (const char * filename, VFSFile & file, const char * title,
 
     return true;
 }
+
+const PreferencesWidget M3ULoader::widgets[] = {
+    WidgetLabel(N_("<b>M3U Configuration</b>")),
+    WidgetCheck(N_("Save in Extended M3U format?"), WidgetBool("m3u", "saveas_extended_m3u")),
+};
+
+const PluginPreferences M3ULoader::prefs = {{widgets}};
