Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions src/Application.vala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public class Music.Application : Gtk.Application {
public const string ACTION_SHUFFLE = "action-shuffle";
public const string ACTION_FIND = "action-find";
public const string ACTION_CLEAR_QUEUE = "action-clear-queue";
public const string ACTION_SAVE_TO_PLAYLIST = "action-save-to-playlist";
public const string ACTION_QUIT = "action-quit";

private const ActionEntry[] ACTION_ENTRIES = {
Expand Down Expand Up @@ -45,6 +46,7 @@ public class Music.Application : Gtk.Application {

set_accels_for_action (ACTION_PREFIX + ACTION_FIND, {"<Ctrl>F"});
set_accels_for_action (ACTION_PREFIX + ACTION_QUIT, {"<Ctrl>Q"});
set_accels_for_action (ACTION_PREFIX + ACTION_SAVE_TO_PLAYLIST, {"<Ctrl>S"});

var granite_settings = Granite.Settings.get_default ();
var gtk_settings = Gtk.Settings.get_default ();
Expand Down Expand Up @@ -148,6 +150,17 @@ public class Music.Application : Gtk.Application {

continue;
}
else if (PlaylistObject.is_playlist (file)) {
PlaylistObject playlist = new PlaylistObject (file);
playlist.load_playlist ();

foreach (File playlist_file in playlist.get_uri_list ()) {
elements += playlist_file;
}

// Don't add the playlist file itself
continue;
}

@zeebok zeebok Mar 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about the code more, I am curious if this should maybe live in PlaybackManager.queue_file and check if the playlist file is a supported type, that way anything that falls under "audio/x-mpegurl" but is not an m3u causes the invalid file toast instead of being added to the queue as a broken AudioObject?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

audio/x-mpegurl should be m3u or m3u8

dont confuse with audio/mpeg

unless mime-type.com speaks lies, but mimetype.io seems to say the same thing

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well for some reason playlist extensions of other programs (like a fake .pls file) will be visible with that filter? Maybe I shouldn't worry about it until it is actually a problem that happens with other people lol


elements += file;
}
Expand Down
6 changes: 6 additions & 0 deletions src/MainWindow.vala
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,15 @@ public class Music.MainWindow : Gtk.ApplicationWindow {
};
music_files_filter.add_mime_type ("audio/*");

var playlist_files_filter = new Gtk.FileFilter () {
name = _("Playlist files"),
};
playlist_files_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_files_filter);

var file_dialog = new Gtk.FileDialog () {
accept_label = _("Open"),
Expand Down
59 changes: 59 additions & 0 deletions src/PlaybackManager.vala
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public class Music.PlaybackManager : Object {
private SimpleAction play_pause_action;
private SimpleAction previous_action;
private SimpleAction shuffle_action;
private SimpleAction save_playlist_action;

private PlaybackManager () {}

Expand Down Expand Up @@ -76,19 +77,25 @@ public class Music.PlaybackManager : Object {
shuffle_action = new SimpleAction (Application.ACTION_SHUFFLE, null);
shuffle_action.activate.connect (shuffle);

save_playlist_action = new SimpleAction (Application.ACTION_SAVE_TO_PLAYLIST, null);
save_playlist_action.activate.connect (save_queue_to_playlist);

next_action.set_enabled (false);
play_pause_action.set_enabled (false);
previous_action.set_enabled (false);
shuffle_action.set_enabled (false);
save_playlist_action.set_enabled (false);

unowned var app = GLib.Application.get_default ();
app.add_action (clear_action);
app.add_action (next_action);
app.add_action (play_pause_action);
app.add_action (previous_action);
app.add_action (shuffle_action);
app.add_action (save_playlist_action);

bind_property ("has-items", clear_action, "enabled", SYNC_CREATE);
bind_property ("has-items", save_playlist_action, "enabled", SYNC_CREATE);
}

public void seek_to_progress (double percent) {
Expand Down Expand Up @@ -404,4 +411,56 @@ public class Music.PlaybackManager : Object {
current_audio = (AudioObject) queue_liststore.get_item (position);
}
}

public void save_queue_to_playlist () {
var all_files_filter = new Gtk.FileFilter () {
name = _("All files"),
};
all_files_filter.add_pattern ("*");

var playlist_filter = new Gtk.FileFilter () {
name = _("Playlist files"),
};
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 queue to playlist"),
initial_name = "%s.m3u".printf (_("New Playlist")),
};

save_dialog.save.begin (null, null, (obj, res) => {
try {
File? file;
file = save_dialog.save.end (res);

PlaylistObject playlist = new PlaylistObject (file);
playlist.save_playlist (queue_liststore);
} 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 (
_("Could not save playlist"),
err.message,
new ThemedIcon ("audio-x-playlist")
) {
badge_icon = new ThemedIcon ("dialog-error"),
modal = true
};
dialog.present ();
dialog.response.connect (dialog.destroy);
}
});
}
}
67 changes: 67 additions & 0 deletions src/PlaylistObject.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* SPDX-License-Identifier: LGPL-3.0-or-later
* SPDX-FileCopyrightText: 2026 elementary, Inc. (https://elementary.io)
*/

public class Music.PlaylistObject : Object {
public File playlist_file { get; construct; }
private File[] uri_list = {};

public PlaylistObject (File playlist) {
Object (playlist_file: playlist);
}

public static bool is_playlist (File playlist) {
FileInfo info;

try {
info = playlist.query_info (GLib.FileAttribute.STANDARD_CONTENT_TYPE, GLib.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";
}

public File[]? get_uri_list () {
return uri_list;
}

public void load_playlist () requires (playlist_file != null) {
FileInputStream fis = playlist_file.read ();
DataInputStream dis = new DataInputStream (fis);
string current_line;

while ((current_line = dis.read_line ()) != null) {
if (current_line.ascii_down ().has_prefix ("file:///")) {
uri_list += File.new_for_uri (current_line);
}
else if (FileUtils.test (current_line, FileTest.IS_DIR)) {
uri_list += File.new_for_path (current_line);
}
else {
debug ("Unknown line: " + current_line);
}
}
}

public void save_playlist (ListStore queue) requires (playlist_file != null) {
FileOutputStream fos = playlist_file.replace (null, false, GLib.FileCreateFlags.REPLACE_DESTINATION);
DataOutputStream dos = new DataOutputStream (fos);

for (uint i = 0; i < queue.n_items; i++) {
AudioObject track = (AudioObject)queue.get_item (i);
dos.put_string (track.uri);
dos.put_string ("\n");
}
}
}
2 changes: 1 addition & 1 deletion src/Widgets/SearchBar.vala
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public class Music.SearchBar : Granite.Bin {
filter_model = new Gtk.FilterListModel (list_model, filter);

search_entry = new Gtk.SearchEntry () {
placeholder_text = _("Search titles in playlist")
placeholder_text = _("Search titles in queue")
};

child = search_entry;
Expand Down
3 changes: 2 additions & 1 deletion src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ sources = [
'Application.vala',
'AudioObject.vala',
'MainWindow.vala',
'PlaybackManager.vala',
'MetadataDiscoverer.vala',
'PlaybackManager.vala',
'PlaylistObject.vala',
'DBus/MprisPlayer.vala',
'DBus/MprisRoot.vala',
'Views/NowPlayingView.vala',
Expand Down