Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
84c0ea8
Add support for loading/saving m3u playlists
teamcons Jun 9, 2025
b8c1fc9
clean up a little
teamcons Jun 9, 2025
2bf6d5d
forgot m3u8
teamcons Jun 9, 2025
9a870e5
Update Application.vala
teamcons Jun 9, 2025
9e63326
Remove unrelated change
teamcons Jun 14, 2025
1043e81
Merge branch 'main' into m3u-support
zeebok Aug 2, 2025
3a9d0b2
Update SPDX header
teamcons Aug 4, 2025
8c068d4
Add action
teamcons Aug 4, 2025
6fc1350
Remove mess
teamcons Aug 4, 2025
94f4673
Proper debug line
teamcons Aug 4, 2025
5031481
Separate frontend and backend
ryonakano Aug 5, 2025
52266f4
Fix indentation
ryonakano Aug 5, 2025
eeaf754
Use regex to check file suffix
ryonakano Aug 5, 2025
bdd5ce7
Do not return incomplete File array if error
ryonakano Aug 5, 2025
b328361
Do not use print for debug/warning logs
ryonakano Aug 5, 2025
63316af
Remove unnecessary else and newlines
ryonakano Aug 5, 2025
1ac76dc
Handle error when saving playlist
ryonakano Aug 5, 2025
5e4f494
Distinguish log message
ryonakano Aug 5, 2025
d0b99dc
Remove unnecessary cast
ryonakano Aug 5, 2025
0b8637d
Update POTFILES
ryonakano Aug 5, 2025
3bdf7d9
Remove unnecessary semicolon and newlines
ryonakano Aug 5, 2025
8cd61dc
Merge pull request #1 from ryonakano/m3u-support-suggestion
teamcons Aug 10, 2025
837e010
Merge branch 'main' into m3u-support
ryonakano Aug 10, 2025
b049444
Add accel for open
teamcons Aug 10, 2025
d5a9b66
cleaner fixme
teamcons Aug 10, 2025
f9b2f16
Add filters for saving
teamcons Aug 10, 2025
b575832
Move saving dialog with opening dialog so we dont jump code to keep a…
teamcons Aug 10, 2025
9db1d46
Make suffix untranslatable
teamcons Aug 11, 2025
42ad97d
Reformulate comment
teamcons Aug 11, 2025
f684cc9
rework skip URL
teamcons Aug 11, 2025
463142e
Do not debug() m3u lines
teamcons Aug 11, 2025
ba807b3
Stay with a local variable for playback_manager
teamcons Aug 11, 2025
8146856
Leave open accel for a separate PR
teamcons Aug 11, 2025
0202edb
make a private unowned playback manager
teamcons Aug 13, 2025
c64b855
Merge branch 'main' into m3u-support
teamcons Aug 13, 2025
7fb3cdb
Merge branch 'main' into m3u-support
teamcons Aug 14, 2025
bca0b45
Merge branch 'main' into m3u-support
teamcons Aug 14, 2025
3a702c8
Move savem3u action elsewhere
teamcons Aug 14, 2025
96f3798
Merge branch 'main' into m3u-support
teamcons Aug 15, 2025
9b11698
Move actions
teamcons Aug 15, 2025
b5dfbda
themedIcon name
teamcons Aug 15, 2025
d415f57
no trycatch for dostream
teamcons Aug 15, 2025
fb304ea
Merge branch 'main' into m3u-support
zeebok Aug 18, 2025
07fdbc7
Merge branch 'main' into m3u-support
teamcons Aug 31, 2025
31c6d12
Merge branch 'main' into m3u-support
teamcons Sep 2, 2025
f2836a1
Merge branch 'main' into m3u-support
teamcons Sep 19, 2025
9b9b766
Have the Application handle errors instead of leaving it to the parser
teamcons Sep 20, 2025
d2f73ae
Introduce M3U.is_playlist() method
teamcons Sep 20, 2025
a1a2599
Allow filtering specifically for playlists
teamcons Sep 20, 2025
b2c2fa4
Merge branch 'main' into m3u-support
teamcons Sep 20, 2025
676015c
Merge branch 'main' into m3u-support
teamcons Oct 10, 2025
4f05230
Merge branch 'main' into m3u-support
zeebok Oct 25, 2025
7d4b719
Update src/Application.vala
teamcons Oct 26, 2025
fdb1137
move code out of trycatch block
teamcons Oct 26, 2025
8e8bec9
Fix invisible local variable
ryonakano Oct 27, 2025
c0936d8
Merge branch 'main' into m3u-support
teamcons Nov 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/Application.vala
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,27 @@ public class Music.Application : Gtk.Application {
continue;
}

if (M3U.is_playlist (file)) {

File[]? tracks = null;
try {
tracks = M3U.parse_playlist (file);
} catch (Error e) {
warning (e.message);
}

if (tracks == null) {
continue;
}

foreach (var track in tracks) {
elements += track;
}

// Avoid adding the m3u - Else its content gets re-added every startup
continue;
}

elements += file;
}

Expand Down
68 changes: 66 additions & 2 deletions src/MainWindow.vala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
public class Music.MainWindow : Gtk.ApplicationWindow {
private const string ACTION_PREFIX = "win.";
private const string ACTION_OPEN = "action-open";
private const string ACTION_SAVE_M3U_PLAYLIST = "action-save-m3u-playlist";

private Granite.Placeholder queue_placeholder;
private Granite.Placeholder search_placeholder;
Expand All @@ -18,9 +19,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);

Expand Down Expand Up @@ -89,6 +91,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
Expand Down Expand Up @@ -185,9 +190,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, {"<Ctrl>O"});

add_action (open_action);

app.set_accels_for_action (ACTION_PREFIX + ACTION_SAVE_M3U_PLAYLIST, {"<Ctrl>S"});
add_action (save_action);

drop_target.drop.connect ((target, value, x, y) => {
if (value.type () == typeof (Gdk.FileList)) {
var list = (Gdk.FileList)value;
Expand Down Expand Up @@ -278,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"),
Expand Down Expand Up @@ -321,6 +334,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 = "%s.m3u".printf (_("New playlist"))
};

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 ("audio-x-playlist")
) {
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":
Expand Down
77 changes: 77 additions & 0 deletions src/Services/M3U.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* SPDX-License-Identifier: LGPL-3.0-or-later
* SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io)
*/

namespace Music.M3U {
public bool is_playlist (File file) {
FileInfo info;

try {
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 mimetype == "audio/x-mpegurl";
}

// Standard specification here: https://en.wikipedia.org/wiki/M3U
public File[]? parse_playlist (File playlist) throws Error {
debug ("Parsing playlist: %s", playlist.get_path ());
File[] list = {};

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;
}

File target;

if (line.ascii_down ().has_prefix ("file:///")) {
target = File.new_for_uri (line);
} else {
target = File.new_for_path (line);
}

// The caller is responsible for testing whether files exist and
// are valid using PlaybackManager.queue_files() instead of here
list += target;
}

return list;
}

public void save_playlist (ListStore queue_liststore, File playlist) throws Error {
debug ("Saving queue as playlist");
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 ostream = playlist.replace (null, false, GLib.FileCreateFlags.REPLACE_DESTINATION);
var dostream = new DataOutputStream (ostream);
dostream.put_string (content);
}
}
1 change: 1 addition & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ sources = [
'Widgets/SearchBar.vala',
'Widgets/SeekBar.vala',
'Widgets/TrackRow.vala',
'Services/M3U.vala',
]

executable(
Expand Down