From 84c0ea81f3ac0f76ade963dc615f2fab60d58c7e Mon Sep 17 00:00:00 2001 From: teamcons Date: Mon, 9 Jun 2025 18:40:58 +0200 Subject: [PATCH 01/41] Add support for loading/saving m3u playlists --- .gitignore | 2 + po/POTFILES | 1 + src/Application.vala | 21 +++++++++- src/Services/M3U.vala | 90 +++++++++++++++++++++++++++++++++++++++++++ src/meson.build | 1 + 5 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 src/Services/M3U.vala diff --git a/.gitignore b/.gitignore index 89c0dbc9c..c81bc410d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ .flatpak-builder .flatpak build +.vscode/settings.json +.gitignore \ No newline at end of file diff --git a/po/POTFILES b/po/POTFILES index 5f3c92996..1bc38e490 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -4,3 +4,4 @@ src/PlaybackManager.vala src/Views/NowPlayingView.vala src/Widgets/AlbumImage.vala src/Widgets/SeekBar.vala +src/Services/M3U.vala \ No newline at end of file diff --git a/src/Application.vala b/src/Application.vala index bb924aeec..be5fa61d0 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -12,6 +12,7 @@ public class Music.Application : Gtk.Application { public const string ACTION_FIND = "action-find"; public const string ACTION_CLEAR_QUEUE = "action-clear-queue"; public const string ACTION_QUIT = "action-quit"; + public const string ACTION_SAVE_M3U_PLAYLIST = "action-save-m3u-playlist"; private const ActionEntry[] ACTION_ENTRIES = { { ACTION_PLAY_PAUSE, action_play_pause, null, "false" }, @@ -20,9 +21,11 @@ public class Music.Application : Gtk.Application { { ACTION_SHUFFLE, action_shuffle }, { ACTION_FIND, action_find }, { ACTION_CLEAR_QUEUE, action_clear_queue }, - { ACTION_QUIT, quit } + { ACTION_QUIT, quit }, + { ACTION_SAVE_M3U_PLAYLIST, action_save_m3u_playlist} }; + private MainWindow main_window; private PlaybackManager? playback_manager = null; public Application () { @@ -48,6 +51,7 @@ public class Music.Application : Gtk.Application { set_accels_for_action (ACTION_PREFIX + ACTION_FIND, {"F"}); set_accels_for_action (ACTION_PREFIX + ACTION_QUIT, {"Q"}); + set_accels_for_action (ACTION_PREFIX + ACTION_SAVE_M3U_PLAYLIST, {"S"}); ((SimpleAction) lookup_action (ACTION_PLAY_PAUSE)).set_enabled (false); ((SimpleAction) lookup_action (ACTION_PLAY_PAUSE)).set_state (false); @@ -93,7 +97,7 @@ public class Music.Application : Gtk.Application { warning ("Could not initialize MPRIS session.\n"); } - var main_window = new MainWindow () { + main_window = new MainWindow () { title = _("Music") }; main_window.present (); @@ -160,6 +164,15 @@ public class Music.Application : Gtk.Application { continue; } + if (file_path.ascii_down ().has_suffix (".m3u")) { + foreach (var track in M3U.parse_playlist (file)) { + elements += track; + } + + // Avoid adding the m3u - Else its content gets re-added every startup + continue; + } + elements += file; } @@ -200,6 +213,10 @@ public class Music.Application : Gtk.Application { playback_manager.clear_queue (); } + private void action_save_m3u_playlist () { + M3U.save_playlist ((MainWindow)active_window, playback_manager.queue_liststore); + } + private void on_bus_acquired (DBusConnection connection, string name) { try { connection.register_object ("/org/mpris/MediaPlayer2", new MprisRoot ()); diff --git a/src/Services/M3U.vala b/src/Services/M3U.vala new file mode 100644 index 000000000..2bf497b20 --- /dev/null +++ b/src/Services/M3U.vala @@ -0,0 +1,90 @@ +/* + * SPDX-License-Identifier: LGPL-3.0-or-later + * SPDX-FileCopyrightText: 2021 elementary, Inc. (https://elementary.io) + */ + +namespace Music.M3U { + + // Standard specification here: https://en.wikipedia.org/wiki/M3U + public File[] parse_playlist (File playlist) { + debug ("Parsing playlist: " + playlist.get_basename () + "\n"); + File[] list = {}; + + try { + FileInputStream @is = playlist.read (); + DataInputStream dis = new DataInputStream (@is); + string line; + + while ((line = dis.read_line ()) != null) { + print ("%s\n", line); + + // Skip extended + if (line.has_prefix ("#EXT")) { + print ("Skipping EXTM3U: " + line + "\n"); + + } else { + File target; + + if (line.ascii_down ().has_prefix ("file:///")) { + target = File.new_for_uri (line); + + } else { + target = File.new_for_path (line); + + }; + + // We do not need to test yet whether files exist + list += target; + + } + } + + + } catch (Error e) { + print ("Error: %s\n", e.message); + } + + return list; + + } + + + + + + public void save_playlist (MainWindow parent, ListStore queue_liststore) { + debug ("Saving queue as playlist" + "\n"); + string content = ""; + + for (var i = 0; i < queue_liststore.n_items; i++) { + var item = (Music.AudioObject)queue_liststore.get_item (i); + content = content + item.uri + "\n"; + } + + var save_dialog = new Gtk.FileDialog () { + initial_name = _("New playlist.m3u") + }; + + save_dialog.save.begin (parent, null, (obj, res) => { + try { + var file = save_dialog.save.end (res); + var dostream = new DataOutputStream ( + file.replace ( + null, + false, + GLib.FileCreateFlags.REPLACE_DESTINATION + ) + ); + + dostream.put_string (content); + + } catch (Error err) { + warning ("Failed to save file: %s", err.message); + } + }); + + + } + + +} diff --git a/src/meson.build b/src/meson.build index e4b85f439..6cd3e587a 100644 --- a/src/meson.build +++ b/src/meson.build @@ -9,6 +9,7 @@ sources = [ 'Widgets/AlbumImage.vala', 'Widgets/SeekBar.vala', 'Widgets/TrackRow.vala', + 'Services/M3U.vala', ] executable( From b8c1fc938b9ff9eefc991d65f0ec3f50ebf7fd83 Mon Sep 17 00:00:00 2001 From: teamcons Date: Mon, 9 Jun 2025 18:54:13 +0200 Subject: [PATCH 02/41] clean up a little --- src/Services/M3U.vala | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Services/M3U.vala b/src/Services/M3U.vala index 2bf497b20..95a1f1b9d 100644 --- a/src/Services/M3U.vala +++ b/src/Services/M3U.vala @@ -28,6 +28,10 @@ namespace Music.M3U { if (line.ascii_down ().has_prefix ("file:///")) { target = File.new_for_uri (line); + //FIXME: URL get skipped. + //} else if (line.ascii_down ().has_prefix ("http")) { + // print ("URL are currently unsupported:" + line + "\n"); + } else { target = File.new_for_path (line); @@ -35,11 +39,9 @@ namespace Music.M3U { // We do not need to test yet whether files exist list += target; - } } - } catch (Error e) { print ("Error: %s\n", e.message); } @@ -48,10 +50,6 @@ namespace Music.M3U { } - - - - public void save_playlist (MainWindow parent, ListStore queue_liststore) { debug ("Saving queue as playlist" + "\n"); string content = ""; @@ -85,6 +83,4 @@ namespace Music.M3U { } - - } From 2bf6d5daea2236d300f3f73b17cfeba61f4ba31a Mon Sep 17 00:00:00 2001 From: Stella and Charlie <147658063+teamcons@users.noreply.github.com> Date: Tue, 10 Jun 2025 00:57:34 +0200 Subject: [PATCH 03/41] forgot m3u8 --- src/Application.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application.vala b/src/Application.vala index be5fa61d0..7363452ac 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -164,7 +164,7 @@ public class Music.Application : Gtk.Application { continue; } - if (file_path.ascii_down ().has_suffix (".m3u")) { + if ((file_path.ascii_down ().has_suffix (".m3u") || (file_path.ascii_down ().has_suffix (".m3u8"))) { foreach (var track in M3U.parse_playlist (file)) { elements += track; } From 9a870e55cdba12d2c44926a3111072f6bc6bf6ce Mon Sep 17 00:00:00 2001 From: Stella and Charlie <147658063+teamcons@users.noreply.github.com> Date: Tue, 10 Jun 2025 01:01:39 +0200 Subject: [PATCH 04/41] Update Application.vala --- src/Application.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application.vala b/src/Application.vala index 7363452ac..61cf6d4e2 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -164,7 +164,7 @@ public class Music.Application : Gtk.Application { continue; } - if ((file_path.ascii_down ().has_suffix (".m3u") || (file_path.ascii_down ().has_suffix (".m3u8"))) { + if ((file_path.ascii_down ().has_suffix (".m3u")) || (file_path.ascii_down ().has_suffix (".m3u8"))) { foreach (var track in M3U.parse_playlist (file)) { elements += track; } From 9e63326be53fa84d9824eace665601ca10b0e437 Mon Sep 17 00:00:00 2001 From: teamcons Date: Sat, 14 Jun 2025 12:39:13 +0200 Subject: [PATCH 05/41] Remove unrelated change --- src/Application.vala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Application.vala b/src/Application.vala index 61cf6d4e2..da1ff5775 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -25,7 +25,6 @@ public class Music.Application : Gtk.Application { { ACTION_SAVE_M3U_PLAYLIST, action_save_m3u_playlist} }; - private MainWindow main_window; private PlaybackManager? playback_manager = null; public Application () { @@ -97,7 +96,7 @@ public class Music.Application : Gtk.Application { warning ("Could not initialize MPRIS session.\n"); } - main_window = new MainWindow () { + var main_window = new MainWindow () { title = _("Music") }; main_window.present (); From 3a9d0b21bf2e199b705961910e6bb2f78459bc7d Mon Sep 17 00:00:00 2001 From: Stella and Charlie <147658063+teamcons@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:49:12 +0200 Subject: [PATCH 06/41] Update SPDX header Co-authored-by: Ryan Kornheisl --- src/Services/M3U.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/M3U.vala b/src/Services/M3U.vala index 95a1f1b9d..f9a898619 100644 --- a/src/Services/M3U.vala +++ b/src/Services/M3U.vala @@ -1,6 +1,6 @@ /* * SPDX-License-Identifier: LGPL-3.0-or-later - * SPDX-FileCopyrightText: 2021 elementary, Inc. (https://elementary.io) + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) */ namespace Music.M3U { From 8c068d4f27ba175a5fe3a091b945e4f508a264d4 Mon Sep 17 00:00:00 2001 From: Stella and Charlie <147658063+teamcons@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:49:38 +0200 Subject: [PATCH 07/41] Add action Co-authored-by: Ryo Nakano --- src/Application.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application.vala b/src/Application.vala index da1ff5775..58af56b57 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -22,7 +22,7 @@ public class Music.Application : Gtk.Application { { ACTION_FIND, action_find }, { ACTION_CLEAR_QUEUE, action_clear_queue }, { ACTION_QUIT, quit }, - { ACTION_SAVE_M3U_PLAYLIST, action_save_m3u_playlist} + { ACTION_SAVE_M3U_PLAYLIST, action_save_m3u_playlist } }; private PlaybackManager? playback_manager = null; From 6fc1350a6aa430bbae8a6e0118dfbb2f3e36c61f Mon Sep 17 00:00:00 2001 From: Stella and Charlie <147658063+teamcons@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:50:06 +0200 Subject: [PATCH 08/41] Remove mess --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index c81bc410d..89c0dbc9c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,3 @@ .flatpak-builder .flatpak build -.vscode/settings.json -.gitignore \ No newline at end of file From 94f46734f4f5956d75c9e210f2a8ea4ebb7d966a Mon Sep 17 00:00:00 2001 From: Stella and Charlie <147658063+teamcons@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:54:45 +0200 Subject: [PATCH 09/41] Proper debug line Co-authored-by: Ryo Nakano --- src/Services/M3U.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/M3U.vala b/src/Services/M3U.vala index f9a898619..55b883d4a 100644 --- a/src/Services/M3U.vala +++ b/src/Services/M3U.vala @@ -7,7 +7,7 @@ namespace Music.M3U { // Standard specification here: https://en.wikipedia.org/wiki/M3U public File[] parse_playlist (File playlist) { - debug ("Parsing playlist: " + playlist.get_basename () + "\n"); + debug ("Parsing playlist: %s", playlist.get_path ()); File[] list = {}; try { From 5031481973f360df9a458ebe4d7cc322e7de9358 Mon Sep 17 00:00:00 2001 From: Ryo Nakano Date: Tue, 5 Aug 2025 22:00:28 +0900 Subject: [PATCH 10/41] Separate frontend and backend --- src/Application.vala | 16 +++++++++++++++- src/Services/M3U.vala | 33 ++++++++++++--------------------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/Application.vala b/src/Application.vala index 58af56b57..2614bb163 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -213,7 +213,21 @@ public class Music.Application : Gtk.Application { } private void action_save_m3u_playlist () { - M3U.save_playlist ((MainWindow)active_window, playback_manager.queue_liststore); + var save_dialog = new Gtk.FileDialog () { + initial_name = _("New playlist.m3u") + }; + + save_dialog.save.begin ((MainWindow)active_window, null, (obj, res) => { + File? file; + try { + file = save_dialog.save.end (res); + } catch (Error err) { + warning ("Failed to save file: %s", err.message); + return; + } + + M3U.save_playlist (playback_manager.queue_liststore, file); + }); } private void on_bus_acquired (DBusConnection connection, string name) { diff --git a/src/Services/M3U.vala b/src/Services/M3U.vala index 55b883d4a..ed06a4aa2 100644 --- a/src/Services/M3U.vala +++ b/src/Services/M3U.vala @@ -50,7 +50,7 @@ namespace Music.M3U { } - public void save_playlist (MainWindow parent, ListStore queue_liststore) { + public void save_playlist (ListStore queue_liststore, File playlist) { debug ("Saving queue as playlist" + "\n"); string content = ""; @@ -59,28 +59,19 @@ namespace Music.M3U { content = content + item.uri + "\n"; } - var save_dialog = new Gtk.FileDialog () { - initial_name = _("New playlist.m3u") - }; - - save_dialog.save.begin (parent, null, (obj, res) => { - try { - var file = save_dialog.save.end (res); - var dostream = new DataOutputStream ( - file.replace ( - null, - false, - GLib.FileCreateFlags.REPLACE_DESTINATION - ) - ); + try { + var dostream = new DataOutputStream ( + playlist.replace ( + null, + false, + GLib.FileCreateFlags.REPLACE_DESTINATION + ) + ); dostream.put_string (content); - } catch (Error err) { - warning ("Failed to save file: %s", err.message); - } - }); - - + } catch (Error err) { + warning ("Failed to save file: %s", err.message); + } } } From 52266f4cc436c4184279c959e01bcd58a03978c5 Mon Sep 17 00:00:00 2001 From: Ryo Nakano Date: Tue, 5 Aug 2025 22:01:24 +0900 Subject: [PATCH 11/41] Fix indentation --- src/Services/M3U.vala | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Services/M3U.vala b/src/Services/M3U.vala index ed06a4aa2..eea1bfd73 100644 --- a/src/Services/M3U.vala +++ b/src/Services/M3U.vala @@ -60,13 +60,8 @@ namespace Music.M3U { } try { - var dostream = new DataOutputStream ( - playlist.replace ( - null, - false, - GLib.FileCreateFlags.REPLACE_DESTINATION - ) - ); + var ostream = playlist.replace (null, false, GLib.FileCreateFlags.REPLACE_DESTINATION); + var dostream = new DataOutputStream (ostream); dostream.put_string (content); From eeaf75495fea379125bf2837ea2069b89ac35381 Mon Sep 17 00:00:00 2001 From: Ryo Nakano Date: Tue, 5 Aug 2025 22:34:40 +0900 Subject: [PATCH 12/41] Use regex to check file suffix --- src/Application.vala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Application.vala b/src/Application.vala index 2614bb163..d29bc33c0 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -163,7 +163,9 @@ public class Music.Application : Gtk.Application { continue; } - if ((file_path.ascii_down ().has_suffix (".m3u")) || (file_path.ascii_down ().has_suffix (".m3u8"))) { + // Check if the file has M3U suffix: "foo.m3u", "bar.M3U8", etc. + var m3u_suffix = /^.+.m3u8?$/i; + if (m3u_suffix.match (file_path)) { foreach (var track in M3U.parse_playlist (file)) { elements += track; } From bdd5ce7226a7f2e653fdc380195d4b83d77e6445 Mon Sep 17 00:00:00 2001 From: Ryo Nakano Date: Tue, 5 Aug 2025 22:44:15 +0900 Subject: [PATCH 13/41] Do not return incomplete File array if error --- src/Application.vala | 7 ++++++- src/Services/M3U.vala | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Application.vala b/src/Application.vala index d29bc33c0..eefd831dc 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -166,7 +166,12 @@ public class Music.Application : Gtk.Application { // Check if the file has M3U suffix: "foo.m3u", "bar.M3U8", etc. var m3u_suffix = /^.+.m3u8?$/i; if (m3u_suffix.match (file_path)) { - foreach (var track in M3U.parse_playlist (file)) { + File[] tracks = M3U.parse_playlist (file); + if (tracks == null) { + continue; + } + + foreach (var track in tracks) { elements += track; } diff --git a/src/Services/M3U.vala b/src/Services/M3U.vala index eea1bfd73..7661d214b 100644 --- a/src/Services/M3U.vala +++ b/src/Services/M3U.vala @@ -6,7 +6,7 @@ namespace Music.M3U { // Standard specification here: https://en.wikipedia.org/wiki/M3U - public File[] parse_playlist (File playlist) { + public File[]? parse_playlist (File playlist) { debug ("Parsing playlist: %s", playlist.get_path ()); File[] list = {}; @@ -44,6 +44,7 @@ namespace Music.M3U { } catch (Error e) { print ("Error: %s\n", e.message); + return null; } return list; From b3283612b3c6b87704603363ef7c616c2b26b310 Mon Sep 17 00:00:00 2001 From: Ryo Nakano Date: Tue, 5 Aug 2025 22:48:15 +0900 Subject: [PATCH 14/41] Do not use print for debug/warning logs --- src/Services/M3U.vala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Services/M3U.vala b/src/Services/M3U.vala index 7661d214b..3a63829e5 100644 --- a/src/Services/M3U.vala +++ b/src/Services/M3U.vala @@ -16,11 +16,11 @@ namespace Music.M3U { string line; while ((line = dis.read_line ()) != null) { - print ("%s\n", line); + debug ("%s", line); - // Skip extended + // Skip extended if (line.has_prefix ("#EXT")) { - print ("Skipping EXTM3U: " + line + "\n"); + debug ("Skipping EXTM3U: " + line); } else { File target; @@ -30,7 +30,7 @@ namespace Music.M3U { //FIXME: URL get skipped. //} else if (line.ascii_down ().has_prefix ("http")) { - // print ("URL are currently unsupported:" + line + "\n"); + // debug ("URL are currently unsupported:" + line); } else { target = File.new_for_path (line); @@ -43,7 +43,7 @@ namespace Music.M3U { } } catch (Error e) { - print ("Error: %s\n", e.message); + warning ("Error: %s", e.message); return null; } @@ -52,7 +52,7 @@ namespace Music.M3U { } public void save_playlist (ListStore queue_liststore, File playlist) { - debug ("Saving queue as playlist" + "\n"); + debug ("Saving queue as playlist"); string content = ""; for (var i = 0; i < queue_liststore.n_items; i++) { From 63316af934352d043081b66157e0c6c0382a629c Mon Sep 17 00:00:00 2001 From: Ryo Nakano Date: Tue, 5 Aug 2025 22:54:47 +0900 Subject: [PATCH 15/41] Remove unnecessary else and newlines --- src/Services/M3U.vala | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/src/Services/M3U.vala b/src/Services/M3U.vala index 3a63829e5..ff7203e68 100644 --- a/src/Services/M3U.vala +++ b/src/Services/M3U.vala @@ -21,34 +21,29 @@ namespace Music.M3U { // Skip extended if (line.has_prefix ("#EXT")) { debug ("Skipping EXTM3U: " + line); + continue; + } - } else { - File target; - - if (line.ascii_down ().has_prefix ("file:///")) { - target = File.new_for_uri (line); - - //FIXME: URL get skipped. - //} else if (line.ascii_down ().has_prefix ("http")) { - // debug ("URL are currently unsupported:" + line); - - } else { - target = File.new_for_path (line); + File target; - }; + if (line.ascii_down ().has_prefix ("file:///")) { + target = File.new_for_uri (line); + //FIXME: URL get skipped. + //} else if (line.ascii_down ().has_prefix ("http")) { + // debug ("URL are currently unsupported:" + line); + } else { + target = File.new_for_path (line); + }; - // We do not need to test yet whether files exist - list += target; - } + // We do not need to test yet whether files exist + list += target; } - } catch (Error e) { warning ("Error: %s", e.message); return null; } return list; - } public void save_playlist (ListStore queue_liststore, File playlist) { @@ -65,7 +60,6 @@ namespace Music.M3U { var dostream = new DataOutputStream (ostream); dostream.put_string (content); - } catch (Error err) { warning ("Failed to save file: %s", err.message); } From 1ac76dc17de42b1843404dd47774668e0a880571 Mon Sep 17 00:00:00 2001 From: Ryo Nakano Date: Tue, 5 Aug 2025 23:10:16 +0900 Subject: [PATCH 16/41] Handle error when saving playlist --- src/Application.vala | 21 ++++++++++++++++++--- src/Services/M3U.vala | 3 ++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/Application.vala b/src/Application.vala index eefd831dc..556a157c1 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -228,12 +228,27 @@ public class Music.Application : Gtk.Application { File? file; try { file = save_dialog.save.end (res); + + M3U.save_playlist (playback_manager.queue_liststore, file); } catch (Error err) { + if (err.matches (Gtk.DialogError.quark (), Gtk.DialogError.DISMISSED)) { + return; + } + warning ("Failed to save file: %s", err.message); - return; - } - M3U.save_playlist (playback_manager.queue_liststore, file); + var dialog = new Granite.MessageDialog ( + _("Couldn't save playlist"), + err.message, + new ThemedIcon ("playlist-queue") + ) { + badge_icon = new ThemedIcon ("dialog-error"), + modal = true, + transient_for = active_window + }; + dialog.present (); + dialog.response.connect (dialog.destroy); + } }); } diff --git a/src/Services/M3U.vala b/src/Services/M3U.vala index ff7203e68..d12e00381 100644 --- a/src/Services/M3U.vala +++ b/src/Services/M3U.vala @@ -46,7 +46,7 @@ namespace Music.M3U { return list; } - public void save_playlist (ListStore queue_liststore, File playlist) { + public void save_playlist (ListStore queue_liststore, File playlist) throws Error { debug ("Saving queue as playlist"); string content = ""; @@ -62,6 +62,7 @@ namespace Music.M3U { dostream.put_string (content); } catch (Error err) { warning ("Failed to save file: %s", err.message); + throw err; } } } From 5e4f4948ff4fa8d543c4b444a737e00c0ffd39bd Mon Sep 17 00:00:00 2001 From: Ryo Nakano Date: Tue, 5 Aug 2025 23:11:11 +0900 Subject: [PATCH 17/41] Distinguish log message --- src/Application.vala | 2 +- src/Services/M3U.vala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Application.vala b/src/Application.vala index 556a157c1..155c849f1 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -235,7 +235,7 @@ public class Music.Application : Gtk.Application { return; } - warning ("Failed to save file: %s", err.message); + warning ("Failed to save playlist: %s", err.message); var dialog = new Granite.MessageDialog ( _("Couldn't save playlist"), diff --git a/src/Services/M3U.vala b/src/Services/M3U.vala index d12e00381..ea6b0cd5a 100644 --- a/src/Services/M3U.vala +++ b/src/Services/M3U.vala @@ -61,7 +61,7 @@ namespace Music.M3U { dostream.put_string (content); } catch (Error err) { - warning ("Failed to save file: %s", err.message); + warning ("Failed to writing to playlist: %s", err.message); throw err; } } From d0b99dc95abca4e453a5b834a3369e6b46150d9e Mon Sep 17 00:00:00 2001 From: Ryo Nakano Date: Tue, 5 Aug 2025 23:12:02 +0900 Subject: [PATCH 18/41] Remove unnecessary cast --- src/Application.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application.vala b/src/Application.vala index 155c849f1..96c242c96 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -224,7 +224,7 @@ public class Music.Application : Gtk.Application { initial_name = _("New playlist.m3u") }; - save_dialog.save.begin ((MainWindow)active_window, null, (obj, res) => { + save_dialog.save.begin (active_window, null, (obj, res) => { File? file; try { file = save_dialog.save.end (res); From 0b8637d9508d7ea0ee27df21c8fbb7f964ab6083 Mon Sep 17 00:00:00 2001 From: Ryo Nakano Date: Tue, 5 Aug 2025 23:46:01 +0900 Subject: [PATCH 19/41] Update POTFILES --- po/POTFILES | 1 - 1 file changed, 1 deletion(-) diff --git a/po/POTFILES b/po/POTFILES index 1bc38e490..5f3c92996 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -4,4 +4,3 @@ src/PlaybackManager.vala src/Views/NowPlayingView.vala src/Widgets/AlbumImage.vala src/Widgets/SeekBar.vala -src/Services/M3U.vala \ No newline at end of file From 3bdf7d9bb86e5150ddece8d58358d0ea73e21112 Mon Sep 17 00:00:00 2001 From: Ryo Nakano Date: Wed, 6 Aug 2025 06:49:41 +0900 Subject: [PATCH 20/41] Remove unnecessary semicolon and newlines --- src/Application.vala | 1 - src/Services/M3U.vala | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Application.vala b/src/Application.vala index 96c242c96..43a2654b7 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -228,7 +228,6 @@ public class Music.Application : Gtk.Application { File? file; try { file = save_dialog.save.end (res); - M3U.save_playlist (playback_manager.queue_liststore, file); } catch (Error err) { if (err.matches (Gtk.DialogError.quark (), Gtk.DialogError.DISMISSED)) { diff --git a/src/Services/M3U.vala b/src/Services/M3U.vala index ea6b0cd5a..982de5da5 100644 --- a/src/Services/M3U.vala +++ b/src/Services/M3U.vala @@ -33,7 +33,7 @@ namespace Music.M3U { // debug ("URL are currently unsupported:" + line); } else { target = File.new_for_path (line); - }; + } // We do not need to test yet whether files exist list += target; @@ -58,7 +58,6 @@ namespace Music.M3U { try { var ostream = playlist.replace (null, false, GLib.FileCreateFlags.REPLACE_DESTINATION); var dostream = new DataOutputStream (ostream); - dostream.put_string (content); } catch (Error err) { warning ("Failed to writing to playlist: %s", err.message); From b0494446847bb8105eef99d5b7abe7e99d5f511b Mon Sep 17 00:00:00 2001 From: teamcons Date: Sun, 10 Aug 2025 10:59:49 +0200 Subject: [PATCH 21/41] Add accel for open --- src/Application.vala | 9 ++++++++- src/MainWindow.vala | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Application.vala b/src/Application.vala index 43a2654b7..59b4ad8c5 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -13,6 +13,7 @@ public class Music.Application : Gtk.Application { public const string ACTION_CLEAR_QUEUE = "action-clear-queue"; public const string ACTION_QUIT = "action-quit"; public const string ACTION_SAVE_M3U_PLAYLIST = "action-save-m3u-playlist"; + public const string ACTION_OPEN = "action-open"; private const ActionEntry[] ACTION_ENTRIES = { { ACTION_PLAY_PAUSE, action_play_pause, null, "false" }, @@ -22,7 +23,8 @@ public class Music.Application : Gtk.Application { { ACTION_FIND, action_find }, { ACTION_CLEAR_QUEUE, action_clear_queue }, { ACTION_QUIT, quit }, - { ACTION_SAVE_M3U_PLAYLIST, action_save_m3u_playlist } + { ACTION_SAVE_M3U_PLAYLIST, action_save_m3u_playlist }, + { ACTION_OPEN, action_open } }; private PlaybackManager? playback_manager = null; @@ -51,6 +53,7 @@ public class Music.Application : Gtk.Application { set_accels_for_action (ACTION_PREFIX + ACTION_FIND, {"F"}); set_accels_for_action (ACTION_PREFIX + ACTION_QUIT, {"Q"}); set_accels_for_action (ACTION_PREFIX + ACTION_SAVE_M3U_PLAYLIST, {"S"}); + set_accels_for_action (ACTION_PREFIX + ACTION_OPEN, {"O"}); ((SimpleAction) lookup_action (ACTION_PLAY_PAUSE)).set_enabled (false); ((SimpleAction) lookup_action (ACTION_PLAY_PAUSE)).set_state (false); @@ -251,6 +254,10 @@ public class Music.Application : Gtk.Application { }); } + private void action_open () { + ((MainWindow)active_window).action_open (); + } + private void on_bus_acquired (DBusConnection connection, string name) { try { connection.register_object ("/org/mpris/MediaPlayer2", new MprisRoot ()); diff --git a/src/MainWindow.vala b/src/MainWindow.vala index a3fa034ae..a983cefb6 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -251,7 +251,7 @@ public class Music.MainWindow : Gtk.ApplicationWindow { } } - private void action_open () { + public void action_open () { var all_files_filter = new Gtk.FileFilter () { name = _("All files"), }; From d5a9b66eeec5064e44731a607fdd39c9afa92772 Mon Sep 17 00:00:00 2001 From: teamcons Date: Sun, 10 Aug 2025 11:10:31 +0200 Subject: [PATCH 22/41] cleaner fixme --- src/Services/M3U.vala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Services/M3U.vala b/src/Services/M3U.vala index 982de5da5..f49f6282a 100644 --- a/src/Services/M3U.vala +++ b/src/Services/M3U.vala @@ -28,9 +28,11 @@ namespace Music.M3U { if (line.ascii_down ().has_prefix ("file:///")) { target = File.new_for_uri (line); - //FIXME: URL get skipped. + + //FIXME: Music does not handle URL //} else if (line.ascii_down ().has_prefix ("http")) { - // debug ("URL are currently unsupported:" + line); + // target = File.new_for_url (line); + } else { target = File.new_for_path (line); } From f9b2f16b6bb5de3c0456b261a245bf7ecb42c8cf Mon Sep 17 00:00:00 2001 From: teamcons Date: Mon, 11 Aug 2025 00:56:16 +0200 Subject: [PATCH 23/41] Add filters for saving --- src/Application.vala | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Application.vala b/src/Application.vala index 59b4ad8c5..331281879 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -223,7 +223,26 @@ public class Music.Application : Gtk.Application { } private void action_save_m3u_playlist () { + var all_files_filter = new Gtk.FileFilter () { + name = _("All files"), + }; + all_files_filter.add_pattern ("*"); + + var playlist_filter = new Gtk.FileFilter () { + name = _("M3U Playlists"), + }; + playlist_filter.add_mime_type ("audio/x-mpegurl"); + + var filter_model = new ListStore (typeof (Gtk.FileFilter)); + filter_model.append (all_files_filter); + filter_model.append (playlist_filter); + var save_dialog = new Gtk.FileDialog () { + accept_label = _("Save"), + default_filter = playlist_filter, + filters = filter_model, + modal = true, + title = _("Save playlist"), initial_name = _("New playlist.m3u") }; From b575832f387f13e5ccf11c5ad54850a3ba0855de Mon Sep 17 00:00:00 2001 From: teamcons Date: Mon, 11 Aug 2025 01:04:01 +0200 Subject: [PATCH 24/41] Move saving dialog with opening dialog so we dont jump code to keep an eye on both --- src/Application.vala | 49 +--------------------------------------- src/MainWindow.vala | 54 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 49 deletions(-) diff --git a/src/Application.vala b/src/Application.vala index 331281879..5b8ea9a57 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -223,54 +223,7 @@ public class Music.Application : Gtk.Application { } private void action_save_m3u_playlist () { - var all_files_filter = new Gtk.FileFilter () { - name = _("All files"), - }; - all_files_filter.add_pattern ("*"); - - var playlist_filter = new Gtk.FileFilter () { - name = _("M3U Playlists"), - }; - playlist_filter.add_mime_type ("audio/x-mpegurl"); - - var filter_model = new ListStore (typeof (Gtk.FileFilter)); - filter_model.append (all_files_filter); - filter_model.append (playlist_filter); - - var save_dialog = new Gtk.FileDialog () { - accept_label = _("Save"), - default_filter = playlist_filter, - filters = filter_model, - modal = true, - title = _("Save playlist"), - initial_name = _("New playlist.m3u") - }; - - save_dialog.save.begin (active_window, null, (obj, res) => { - File? file; - try { - file = save_dialog.save.end (res); - M3U.save_playlist (playback_manager.queue_liststore, file); - } catch (Error err) { - if (err.matches (Gtk.DialogError.quark (), Gtk.DialogError.DISMISSED)) { - return; - } - - warning ("Failed to save playlist: %s", err.message); - - var dialog = new Granite.MessageDialog ( - _("Couldn't save playlist"), - err.message, - new ThemedIcon ("playlist-queue") - ) { - badge_icon = new ThemedIcon ("dialog-error"), - modal = true, - transient_for = active_window - }; - dialog.present (); - dialog.response.connect (dialog.destroy); - } - }); + ((MainWindow)active_window).action_save_m3u_playlist (); } private void action_open () { diff --git a/src/MainWindow.vala b/src/MainWindow.vala index a983cefb6..daa141a53 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -14,9 +14,10 @@ public class Music.MainWindow : Gtk.ApplicationWindow { private Gtk.SingleSelection selection_model; private Gtk.Stack queue_stack; private Settings settings; + public PlaybackManager playback_manager; construct { - var playback_manager = PlaybackManager.get_default (); + playback_manager = PlaybackManager.get_default (); var start_window_controls = new Gtk.WindowControls (Gtk.PackType.START); @@ -305,6 +306,57 @@ public class Music.MainWindow : Gtk.ApplicationWindow { }); } + public void action_save_m3u_playlist () { + var all_files_filter = new Gtk.FileFilter () { + name = _("All files"), + }; + all_files_filter.add_pattern ("*"); + + var playlist_filter = new Gtk.FileFilter () { + name = _("M3U Playlists"), + }; + playlist_filter.add_mime_type ("audio/x-mpegurl"); + + var filter_model = new ListStore (typeof (Gtk.FileFilter)); + filter_model.append (all_files_filter); + filter_model.append (playlist_filter); + + var save_dialog = new Gtk.FileDialog () { + accept_label = _("Save"), + default_filter = playlist_filter, + filters = filter_model, + modal = true, + title = _("Save playlist"), + initial_name = _("New playlist.m3u") + }; + + save_dialog.save.begin (this, null, (obj, res) => { + File? file; + try { + file = save_dialog.save.end (res); + M3U.save_playlist (playback_manager.queue_liststore, file); + } catch (Error err) { + if (err.matches (Gtk.DialogError.quark (), Gtk.DialogError.DISMISSED)) { + return; + } + + warning ("Failed to save playlist: %s", err.message); + + var dialog = new Granite.MessageDialog ( + _("Couldn't save playlist"), + err.message, + new ThemedIcon ("playlist-queue") + ) { + badge_icon = new ThemedIcon ("dialog-error"), + modal = true, + transient_for = this + }; + dialog.present (); + dialog.response.connect (dialog.destroy); + } + }); + } + private void update_repeat_button () { switch (settings.get_string ("repeat-mode")) { case "disabled": From 9db1d467e19f227b7c7a97ff2f05c542aab158b3 Mon Sep 17 00:00:00 2001 From: Stella and Charlie <147658063+teamcons@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:53:14 +0200 Subject: [PATCH 25/41] Make suffix untranslatable Co-authored-by: Ryo Nakano --- src/MainWindow.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index daa141a53..1a6dc6a3d 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -327,7 +327,7 @@ public class Music.MainWindow : Gtk.ApplicationWindow { filters = filter_model, modal = true, title = _("Save playlist"), - initial_name = _("New playlist.m3u") + initial_name = "%s.m3u".printf (_("New playlist")) }; save_dialog.save.begin (this, null, (obj, res) => { From 42ad97d7d4096d621a51fbf6245620ac52c838d9 Mon Sep 17 00:00:00 2001 From: Stella and Charlie <147658063+teamcons@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:54:29 +0200 Subject: [PATCH 26/41] Reformulate comment Co-authored-by: Ryo Nakano --- src/Services/M3U.vala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Services/M3U.vala b/src/Services/M3U.vala index f49f6282a..00f3cd8ed 100644 --- a/src/Services/M3U.vala +++ b/src/Services/M3U.vala @@ -37,7 +37,8 @@ namespace Music.M3U { target = File.new_for_path (line); } - // We do not need to test yet whether files exist + // The caller is responsible for testing whether files exist and + // are valid using PlaybackManager.queue_files() instead of here list += target; } } catch (Error e) { From f684cc9ebaf214f7c6426d7dff75f738d1e0a46b Mon Sep 17 00:00:00 2001 From: teamcons Date: Mon, 11 Aug 2025 10:02:19 +0200 Subject: [PATCH 27/41] rework skip URL --- src/Services/M3U.vala | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Services/M3U.vala b/src/Services/M3U.vala index 00f3cd8ed..85c317313 100644 --- a/src/Services/M3U.vala +++ b/src/Services/M3U.vala @@ -24,15 +24,16 @@ namespace Music.M3U { continue; } + // Skip URL + if (line.ascii_down ().has_prefix ("http")) { + debug ("Skipping URL: " + line); + continue; + } + File target; if (line.ascii_down ().has_prefix ("file:///")) { target = File.new_for_uri (line); - - //FIXME: Music does not handle URL - //} else if (line.ascii_down ().has_prefix ("http")) { - // target = File.new_for_url (line); - } else { target = File.new_for_path (line); } From 463142ed317a7565ee4ab6ba7c6ded604a3247c9 Mon Sep 17 00:00:00 2001 From: Stella and Charlie <147658063+teamcons@users.noreply.github.com> Date: Mon, 11 Aug 2025 10:04:43 +0200 Subject: [PATCH 28/41] Do not debug() m3u lines Co-authored-by: Ryo Nakano --- src/Services/M3U.vala | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Services/M3U.vala b/src/Services/M3U.vala index 85c317313..3cb4441b1 100644 --- a/src/Services/M3U.vala +++ b/src/Services/M3U.vala @@ -16,8 +16,6 @@ namespace Music.M3U { string line; while ((line = dis.read_line ()) != null) { - debug ("%s", line); - // Skip extended if (line.has_prefix ("#EXT")) { debug ("Skipping EXTM3U: " + line); From ba807b3a32172ff5450d8bcd2368eef654178ad8 Mon Sep 17 00:00:00 2001 From: teamcons Date: Mon, 11 Aug 2025 10:09:26 +0200 Subject: [PATCH 29/41] Stay with a local variable for playback_manager --- src/MainWindow.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 1a6dc6a3d..9cb20fe49 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -14,10 +14,9 @@ public class Music.MainWindow : Gtk.ApplicationWindow { private Gtk.SingleSelection selection_model; private Gtk.Stack queue_stack; private Settings settings; - public PlaybackManager playback_manager; construct { - playback_manager = PlaybackManager.get_default (); + var playback_manager = PlaybackManager.get_default (); var start_window_controls = new Gtk.WindowControls (Gtk.PackType.START); @@ -334,6 +333,7 @@ public class Music.MainWindow : Gtk.ApplicationWindow { File? file; try { file = save_dialog.save.end (res); + var playback_manager = PlaybackManager.get_default (); M3U.save_playlist (playback_manager.queue_liststore, file); } catch (Error err) { if (err.matches (Gtk.DialogError.quark (), Gtk.DialogError.DISMISSED)) { From 8146856154a5fd1844874ed448e4c6c76f0cf7c6 Mon Sep 17 00:00:00 2001 From: teamcons Date: Mon, 11 Aug 2025 10:13:16 +0200 Subject: [PATCH 30/41] Leave open accel for a separate PR --- src/Application.vala | 9 +-------- src/MainWindow.vala | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/Application.vala b/src/Application.vala index 5b8ea9a57..a7d0e8d0a 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -13,7 +13,6 @@ public class Music.Application : Gtk.Application { public const string ACTION_CLEAR_QUEUE = "action-clear-queue"; public const string ACTION_QUIT = "action-quit"; public const string ACTION_SAVE_M3U_PLAYLIST = "action-save-m3u-playlist"; - public const string ACTION_OPEN = "action-open"; private const ActionEntry[] ACTION_ENTRIES = { { ACTION_PLAY_PAUSE, action_play_pause, null, "false" }, @@ -23,8 +22,7 @@ public class Music.Application : Gtk.Application { { ACTION_FIND, action_find }, { ACTION_CLEAR_QUEUE, action_clear_queue }, { ACTION_QUIT, quit }, - { ACTION_SAVE_M3U_PLAYLIST, action_save_m3u_playlist }, - { ACTION_OPEN, action_open } + { ACTION_SAVE_M3U_PLAYLIST, action_save_m3u_playlist } }; private PlaybackManager? playback_manager = null; @@ -53,7 +51,6 @@ public class Music.Application : Gtk.Application { set_accels_for_action (ACTION_PREFIX + ACTION_FIND, {"F"}); set_accels_for_action (ACTION_PREFIX + ACTION_QUIT, {"Q"}); set_accels_for_action (ACTION_PREFIX + ACTION_SAVE_M3U_PLAYLIST, {"S"}); - set_accels_for_action (ACTION_PREFIX + ACTION_OPEN, {"O"}); ((SimpleAction) lookup_action (ACTION_PLAY_PAUSE)).set_enabled (false); ((SimpleAction) lookup_action (ACTION_PLAY_PAUSE)).set_state (false); @@ -226,10 +223,6 @@ public class Music.Application : Gtk.Application { ((MainWindow)active_window).action_save_m3u_playlist (); } - private void action_open () { - ((MainWindow)active_window).action_open (); - } - private void on_bus_acquired (DBusConnection connection, string name) { try { connection.register_object ("/org/mpris/MediaPlayer2", new MprisRoot ()); diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 9cb20fe49..05905bbd0 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -251,7 +251,7 @@ public class Music.MainWindow : Gtk.ApplicationWindow { } } - public void action_open () { + private void action_open () { var all_files_filter = new Gtk.FileFilter () { name = _("All files"), }; From 0202edbb6e920d7940b053872053cc942895286f Mon Sep 17 00:00:00 2001 From: Stella and Charlie <147658063+teamcons@users.noreply.github.com> Date: Wed, 13 Aug 2025 17:03:26 +0200 Subject: [PATCH 31/41] make a private unowned playback manager --- src/MainWindow.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 05905bbd0..15f202de8 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -14,9 +14,10 @@ public class Music.MainWindow : Gtk.ApplicationWindow { private Gtk.SingleSelection selection_model; private Gtk.Stack queue_stack; private Settings settings; + private unowned PlaybackManager playback_manager; construct { - var playback_manager = PlaybackManager.get_default (); + playback_manager = PlaybackManager.get_default (); var start_window_controls = new Gtk.WindowControls (Gtk.PackType.START); @@ -333,7 +334,6 @@ public class Music.MainWindow : Gtk.ApplicationWindow { File? file; try { file = save_dialog.save.end (res); - var playback_manager = PlaybackManager.get_default (); M3U.save_playlist (playback_manager.queue_liststore, file); } catch (Error err) { if (err.matches (Gtk.DialogError.quark (), Gtk.DialogError.DISMISSED)) { From 3a702c8e6a77547671859bec7300f290f072f582 Mon Sep 17 00:00:00 2001 From: teamcons Date: Thu, 14 Aug 2025 21:11:24 +0200 Subject: [PATCH 32/41] Move savem3u action elsewhere --- src/Application.vala | 9 +-------- src/MainWindow.vala | 10 ++++++++++ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Application.vala b/src/Application.vala index a7d0e8d0a..030cb9d70 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -12,7 +12,6 @@ public class Music.Application : Gtk.Application { public const string ACTION_FIND = "action-find"; public const string ACTION_CLEAR_QUEUE = "action-clear-queue"; public const string ACTION_QUIT = "action-quit"; - public const string ACTION_SAVE_M3U_PLAYLIST = "action-save-m3u-playlist"; private const ActionEntry[] ACTION_ENTRIES = { { ACTION_PLAY_PAUSE, action_play_pause, null, "false" }, @@ -21,8 +20,7 @@ public class Music.Application : Gtk.Application { { ACTION_SHUFFLE, action_shuffle }, { ACTION_FIND, action_find }, { ACTION_CLEAR_QUEUE, action_clear_queue }, - { ACTION_QUIT, quit }, - { ACTION_SAVE_M3U_PLAYLIST, action_save_m3u_playlist } + { ACTION_QUIT, quit } }; private PlaybackManager? playback_manager = null; @@ -50,7 +48,6 @@ public class Music.Application : Gtk.Application { set_accels_for_action (ACTION_PREFIX + ACTION_FIND, {"F"}); set_accels_for_action (ACTION_PREFIX + ACTION_QUIT, {"Q"}); - set_accels_for_action (ACTION_PREFIX + ACTION_SAVE_M3U_PLAYLIST, {"S"}); ((SimpleAction) lookup_action (ACTION_PLAY_PAUSE)).set_enabled (false); ((SimpleAction) lookup_action (ACTION_PLAY_PAUSE)).set_state (false); @@ -219,10 +216,6 @@ public class Music.Application : Gtk.Application { playback_manager.clear_queue (); } - private void action_save_m3u_playlist () { - ((MainWindow)active_window).action_save_m3u_playlist (); - } - private void on_bus_acquired (DBusConnection connection, string name) { try { connection.register_object ("/org/mpris/MediaPlayer2", new MprisRoot ()); diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 15f202de8..9a44c514b 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -4,6 +4,9 @@ */ public class Music.MainWindow : Gtk.ApplicationWindow { + private const string ACTION_PREFIX = "win."; + private const string ACTION_SAVE_M3U_PLAYLIST = "action-save-m3u-playlist"; + private Granite.Placeholder queue_placeholder; private Gtk.Button repeat_button; private Gtk.Button shuffle_button; @@ -244,6 +247,13 @@ public class Music.MainWindow : Gtk.ApplicationWindow { playback_manager.current_audio = selected_audio; } }); + + var save_action = new SimpleAction (ACTION_SAVE_M3U_PLAYLIST, null); + save_action.activate.connect (action_save_m3u_playlist); + + unowned var app = ((Gtk.Application) GLib.Application.get_default ()); + app.set_accels_for_action (ACTION_PREFIX + ACTION_SAVE_M3U_PLAYLIST, {"S"}); + add_action (save_action); } public void start_search () { From 9b11698eabd2480dff2b3c41bd88ac46bc21fbe1 Mon Sep 17 00:00:00 2001 From: teamcons Date: Fri, 15 Aug 2025 21:14:42 +0200 Subject: [PATCH 33/41] Move actions --- src/MainWindow.vala | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 3d7f27863..9eb551400 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -86,6 +86,9 @@ public class Music.MainWindow : Gtk.ApplicationWindow { var open_action = new SimpleAction (ACTION_OPEN, null); open_action.activate.connect (open_files); + var save_action = new SimpleAction (ACTION_SAVE_M3U_PLAYLIST, null); + save_action.activate.connect (action_save_m3u_playlist); + var add_button = new Gtk.Button () { child = add_button_box, action_name = ACTION_PREFIX + ACTION_OPEN @@ -182,9 +185,11 @@ public class Music.MainWindow : Gtk.ApplicationWindow { unowned var app = ((Gtk.Application) GLib.Application.get_default ()); app.set_accels_for_action (ACTION_PREFIX + ACTION_OPEN, {"O"}); - add_action (open_action); + app.set_accels_for_action (ACTION_PREFIX + ACTION_SAVE_M3U_PLAYLIST, {"S"}); + add_action (save_action); + drop_target.drop.connect ((target, value, x, y) => { if (value.type () == typeof (Gdk.FileList)) { var list = (Gdk.FileList)value; @@ -250,13 +255,6 @@ public class Music.MainWindow : Gtk.ApplicationWindow { playback_manager.current_audio = selected_audio; } }); - - var save_action = new SimpleAction (ACTION_SAVE_M3U_PLAYLIST, null); - save_action.activate.connect (action_save_m3u_playlist); - - unowned var app = ((Gtk.Application) GLib.Application.get_default ()); - app.set_accels_for_action (ACTION_PREFIX + ACTION_SAVE_M3U_PLAYLIST, {"S"}); - add_action (save_action); } public void start_search () { From b5dfbda4ab7ba73dbbafb1622646fd97fe4704c1 Mon Sep 17 00:00:00 2001 From: Stella and Charlie <147658063+teamcons@users.noreply.github.com> Date: Fri, 15 Aug 2025 21:52:17 +0200 Subject: [PATCH 34/41] themedIcon name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Danielle Foré --- src/MainWindow.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 9eb551400..4afa01295 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -356,7 +356,7 @@ public class Music.MainWindow : Gtk.ApplicationWindow { var dialog = new Granite.MessageDialog ( _("Couldn't save playlist"), err.message, - new ThemedIcon ("playlist-queue") + new ThemedIcon ("audio-x-playlist") ) { badge_icon = new ThemedIcon ("dialog-error"), modal = true, From d415f57388ee6e6f13c925b8f7fac7ae8561793a Mon Sep 17 00:00:00 2001 From: Stella and Charlie <147658063+teamcons@users.noreply.github.com> Date: Fri, 15 Aug 2025 21:52:55 +0200 Subject: [PATCH 35/41] no trycatch for dostream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Danielle Foré --- src/Services/M3U.vala | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Services/M3U.vala b/src/Services/M3U.vala index 3cb4441b1..e6f7f4015 100644 --- a/src/Services/M3U.vala +++ b/src/Services/M3U.vala @@ -57,13 +57,8 @@ namespace Music.M3U { content = content + item.uri + "\n"; } - try { - var ostream = playlist.replace (null, false, GLib.FileCreateFlags.REPLACE_DESTINATION); - var dostream = new DataOutputStream (ostream); - dostream.put_string (content); - } catch (Error err) { - warning ("Failed to writing to playlist: %s", err.message); - throw err; - } + var ostream = playlist.replace (null, false, GLib.FileCreateFlags.REPLACE_DESTINATION); + var dostream = new DataOutputStream (ostream); + dostream.put_string (content); } } From 9b9b766a172aab2c917a12ab75ad437cdc2404da Mon Sep 17 00:00:00 2001 From: teamcons Date: Sat, 20 Sep 2025 20:42:08 +0200 Subject: [PATCH 36/41] Have the Application handle errors instead of leaving it to the parser --- src/Application.vala | 10 +++++++- src/Services/M3U.vala | 53 ++++++++++++++++++++----------------------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/Application.vala b/src/Application.vala index 291767aaf..85f348fde 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -152,7 +152,15 @@ public class Music.Application : Gtk.Application { // Check if the file has M3U suffix: "foo.m3u", "bar.M3U8", etc. var m3u_suffix = /^.+.m3u8?$/i; if (m3u_suffix.match (file_path)) { - File[] tracks = M3U.parse_playlist (file); + + File[]? tracks = null; + try { + tracks = M3U.parse_playlist (file); + + } catch (Error e) { + warning (e.message); + } + if (tracks == null) { continue; } diff --git a/src/Services/M3U.vala b/src/Services/M3U.vala index e6f7f4015..cd6c76e4a 100644 --- a/src/Services/M3U.vala +++ b/src/Services/M3U.vala @@ -6,43 +6,38 @@ namespace Music.M3U { // Standard specification here: https://en.wikipedia.org/wiki/M3U - public File[]? parse_playlist (File playlist) { + public File[]? parse_playlist (File playlist) throws Error { debug ("Parsing playlist: %s", playlist.get_path ()); File[] list = {}; - try { - FileInputStream @is = playlist.read (); - DataInputStream dis = new DataInputStream (@is); - string line; + FileInputStream @is = playlist.read (); + DataInputStream dis = new DataInputStream (@is); + string line; - while ((line = dis.read_line ()) != null) { - // Skip extended - if (line.has_prefix ("#EXT")) { - debug ("Skipping EXTM3U: " + line); - continue; - } - - // Skip URL - if (line.ascii_down ().has_prefix ("http")) { - debug ("Skipping URL: " + line); - continue; - } + while ((line = dis.read_line ()) != null) { + // Skip extended + if (line.has_prefix ("#EXT")) { + debug ("Skipping EXTM3U: " + line); + continue; + } - File target; + // Skip URL + if (line.ascii_down ().has_prefix ("http")) { + debug ("Skipping URL: " + line); + continue; + } - if (line.ascii_down ().has_prefix ("file:///")) { - target = File.new_for_uri (line); - } else { - target = File.new_for_path (line); - } + File target; - // The caller is responsible for testing whether files exist and - // are valid using PlaybackManager.queue_files() instead of here - list += target; + if (line.ascii_down ().has_prefix ("file:///")) { + target = File.new_for_uri (line); + } else { + target = File.new_for_path (line); } - } catch (Error e) { - warning ("Error: %s", e.message); - return null; + + // The caller is responsible for testing whether files exist and + // are valid using PlaybackManager.queue_files() instead of here + list += target; } return list; From d2f73ae3ca880c8c280793774a965bfb8b94b8cc Mon Sep 17 00:00:00 2001 From: teamcons Date: Sat, 20 Sep 2025 21:15:51 +0200 Subject: [PATCH 37/41] Introduce M3U.is_playlist() method --- src/Application.vala | 4 +--- src/Services/M3U.vala | 13 +++++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Application.vala b/src/Application.vala index 85f348fde..a38aedceb 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -149,9 +149,7 @@ public class Music.Application : Gtk.Application { continue; } - // Check if the file has M3U suffix: "foo.m3u", "bar.M3U8", etc. - var m3u_suffix = /^.+.m3u8?$/i; - if (m3u_suffix.match (file_path)) { + if (M3U.is_playlist (file)) { File[]? tracks = null; try { diff --git a/src/Services/M3U.vala b/src/Services/M3U.vala index cd6c76e4a..b0c96d4e8 100644 --- a/src/Services/M3U.vala +++ b/src/Services/M3U.vala @@ -4,6 +4,19 @@ */ namespace Music.M3U { + public bool is_playlist (File suspicious_file) { + bool if_playlist = false; + try { + var info = suspicious_file.query_info (GLib.FileAttribute.STANDARD_CONTENT_TYPE, FileQueryInfoFlags.NONE); + var mimetype = info.get_content_type () ?? ""; + if_playlist = mimetype == "audio/x-mpegurl"; + + } catch (Error e) { + warning (e.message); + } + + return if_playlist; + } // Standard specification here: https://en.wikipedia.org/wiki/M3U public File[]? parse_playlist (File playlist) throws Error { From a1a2599d00e6c33737f0bbdd6e983cc3d925fece Mon Sep 17 00:00:00 2001 From: teamcons Date: Sat, 20 Sep 2025 21:20:48 +0200 Subject: [PATCH 38/41] Allow filtering specifically for playlists --- src/MainWindow.vala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 94f8f0f7f..377382cb5 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -285,9 +285,15 @@ public class Music.MainWindow : Gtk.ApplicationWindow { }; music_files_filter.add_mime_type ("audio/*"); + var playlist_filter = new Gtk.FileFilter () { + name = _("M3U Playlists"), + }; + playlist_filter.add_mime_type ("audio/x-mpegurl"); + var filter_model = new ListStore (typeof (Gtk.FileFilter)); filter_model.append (all_files_filter); filter_model.append (music_files_filter); + filter_model.append (playlist_filter); var file_dialog = new Gtk.FileDialog () { accept_label = _("Open"), From 7d4b719cc566b2a381f702a34fd4431dc8b05da5 Mon Sep 17 00:00:00 2001 From: Stella and Charlie <147658063+teamcons@users.noreply.github.com> Date: Sun, 26 Oct 2025 21:43:52 +0100 Subject: [PATCH 39/41] Update src/Application.vala Co-authored-by: Ryo Nakano --- src/Application.vala | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Application.vala b/src/Application.vala index a38aedceb..9928e41a9 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -154,7 +154,6 @@ public class Music.Application : Gtk.Application { File[]? tracks = null; try { tracks = M3U.parse_playlist (file); - } catch (Error e) { warning (e.message); } From fdb11372b6e75fb55281ca9a995ac53137fa479f Mon Sep 17 00:00:00 2001 From: Stella and Charlie <147658063+teamcons@users.noreply.github.com> Date: Sun, 26 Oct 2025 22:32:28 +0100 Subject: [PATCH 40/41] move code out of trycatch block Co-authored-by: Ryo Nakano --- src/Services/M3U.vala | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Services/M3U.vala b/src/Services/M3U.vala index b0c96d4e8..e1ba69122 100644 --- a/src/Services/M3U.vala +++ b/src/Services/M3U.vala @@ -4,18 +4,21 @@ */ namespace Music.M3U { - public bool is_playlist (File suspicious_file) { - bool if_playlist = false; + public bool is_playlist (File file) { try { - var info = suspicious_file.query_info (GLib.FileAttribute.STANDARD_CONTENT_TYPE, FileQueryInfoFlags.NONE); - var mimetype = info.get_content_type () ?? ""; - if_playlist = mimetype == "audio/x-mpegurl"; - + var info = file.query_info (GLib.FileAttribute.STANDARD_CONTENT_TYPE, FileQueryInfoFlags.NONE); } catch (Error e) { warning (e.message); + return false; + } + + var mimetype = info.get_content_type (); + if (mimetype == null) { + warning ("Failed to get content type"); + return false; } - return if_playlist; + return mimetype == "audio/x-mpegurl"; } // Standard specification here: https://en.wikipedia.org/wiki/M3U From 8e8bec972b04c27f91630a7967b7b108436d8e70 Mon Sep 17 00:00:00 2001 From: Ryo Nakano Date: Mon, 27 Oct 2025 21:39:44 +0900 Subject: [PATCH 41/41] Fix invisible local variable --- src/Services/M3U.vala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Services/M3U.vala b/src/Services/M3U.vala index e1ba69122..10ce34cab 100644 --- a/src/Services/M3U.vala +++ b/src/Services/M3U.vala @@ -5,8 +5,10 @@ namespace Music.M3U { public bool is_playlist (File file) { + FileInfo info; + try { - var info = file.query_info (GLib.FileAttribute.STANDARD_CONTENT_TYPE, FileQueryInfoFlags.NONE); + info = file.query_info (GLib.FileAttribute.STANDARD_CONTENT_TYPE, FileQueryInfoFlags.NONE); } catch (Error e) { warning (e.message); return false;