Skip to content

Commit c899675

Browse files
authored
Merge branch 'main' into jeremypw/queue/sort-folder-list
2 parents 27feb35 + 9738895 commit c899675

7 files changed

Lines changed: 180 additions & 7 deletions

File tree

src/Application.vala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public class Music.Application : Gtk.Application {
1111
public const string ACTION_SHUFFLE = "action-shuffle";
1212
public const string ACTION_FIND = "action-find";
1313
public const string ACTION_CLEAR_QUEUE = "action-clear-queue";
14+
public const string ACTION_SAVE_TO_PLAYLIST = "action-save-to-playlist";
1415
public const string ACTION_QUIT = "action-quit";
1516

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

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

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

149151
continue;
150152
}
153+
else if (PlaylistObject.is_playlist (file)) {
154+
PlaylistObject playlist = new PlaylistObject (file);
155+
playlist.load_playlist ();
156+
157+
foreach (File playlist_file in playlist.get_uri_list ()) {
158+
elements += playlist_file;
159+
}
160+
161+
// Don't add the playlist file itself
162+
continue;
163+
}
151164

152165
elements += file;
153166
}

src/MainWindow.vala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,15 @@ public class Music.MainWindow : Gtk.ApplicationWindow {
8080
};
8181
music_files_filter.add_mime_type ("audio/*");
8282

83+
var playlist_files_filter = new Gtk.FileFilter () {
84+
name = _("Playlist files"),
85+
};
86+
playlist_files_filter.add_mime_type ("audio/x-mpegurl");
87+
8388
var filter_model = new ListStore (typeof (Gtk.FileFilter));
8489
filter_model.append (all_files_filter);
8590
filter_model.append (music_files_filter);
91+
filter_model.append (playlist_files_filter);
8692

8793
var file_dialog = new Gtk.FileDialog () {
8894
accept_label = _("Open"),

src/PlaybackManager.vala

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* SPDX-License-Identifier: LGPL-3.0-or-later
3-
* SPDX-FileCopyrightText: 2021-2022 elementary, Inc. (https://elementary.io)
3+
* SPDX-FileCopyrightText: 2021-2026 elementary, Inc. (https://elementary.io)
44
*/
55

66
public class Music.PlaybackManager : Object {
@@ -14,6 +14,7 @@ public class Music.PlaybackManager : Object {
1414
}
1515
public int64 playback_position { get; private set; }
1616
public signal void invalids_found (int count);
17+
public signal void duplicates_found (int count);
1718

1819
private static GLib.Once<PlaybackManager> instance;
1920
public static unowned PlaybackManager get_default () {
@@ -39,6 +40,7 @@ public class Music.PlaybackManager : Object {
3940
private SimpleAction play_pause_action;
4041
private SimpleAction previous_action;
4142
private SimpleAction shuffle_action;
43+
private SimpleAction save_playlist_action;
4244

4345
private PlaybackManager () {}
4446

@@ -75,19 +77,25 @@ public class Music.PlaybackManager : Object {
7577
shuffle_action = new SimpleAction (Application.ACTION_SHUFFLE, null);
7678
shuffle_action.activate.connect (shuffle);
7779

80+
save_playlist_action = new SimpleAction (Application.ACTION_SAVE_TO_PLAYLIST, null);
81+
save_playlist_action.activate.connect (save_queue_to_playlist);
82+
7883
next_action.set_enabled (false);
7984
play_pause_action.set_enabled (false);
8085
previous_action.set_enabled (false);
8186
shuffle_action.set_enabled (false);
87+
save_playlist_action.set_enabled (false);
8288

8389
unowned var app = GLib.Application.get_default ();
8490
app.add_action (clear_action);
8591
app.add_action (next_action);
8692
app.add_action (play_pause_action);
8793
app.add_action (previous_action);
8894
app.add_action (shuffle_action);
95+
app.add_action (save_playlist_action);
8996

9097
bind_property ("has-items", clear_action, "enabled", SYNC_CREATE);
98+
bind_property ("has-items", save_playlist_action, "enabled", SYNC_CREATE);
9199
}
92100

93101
public void seek_to_progress (double percent) {
@@ -99,10 +107,26 @@ public class Music.PlaybackManager : Object {
99107
// AudioObjects start discovering metadata asynchronously on creation
100108
public void queue_files (File[] files) {
101109
int invalids = 0;
110+
int duplicates = 0;
111+
int added_tracks = 0;
102112
foreach (unowned var file in files) {
103113
if (file.query_exists () && "audio" in ContentType.guess (file.get_uri (), null, null)) {
104-
var audio_object = new AudioObject (file.get_uri ());
105-
queue_liststore.append (audio_object);
114+
uint pos = 0;
115+
bool found = false;
116+
Object? item = queue_liststore.get_item (pos++);
117+
while (item != null && !found) {
118+
if (((AudioObject)item).uri == file.get_uri ()) {
119+
found = true;
120+
}
121+
122+
item = queue_liststore.get_item (pos++);
123+
}
124+
if (!found) {
125+
queue_liststore.append (new AudioObject (file.get_uri ()));
126+
added_tracks++;
127+
} else {
128+
duplicates++;
129+
}
106130
} else {
107131
invalids++;
108132
continue;
@@ -112,6 +136,9 @@ public class Music.PlaybackManager : Object {
112136
if (invalids > 0) {
113137
invalids_found (invalids);
114138
}
139+
if (duplicates > 0) {
140+
duplicates_found (duplicates);
141+
}
115142

116143
if (current_audio == null) {
117144
var audio_object = (AudioObject) queue_liststore.get_object (0);
@@ -121,7 +148,6 @@ public class Music.PlaybackManager : Object {
121148
} else {
122149
// Don't notify on app startup or if the app is focused
123150
var application = (Gtk.Application) GLib.Application.get_default ();
124-
var added_tracks = files.length - invalids;
125151
if (
126152
!application.get_active_window ().is_active &&
127153
added_tracks > 0
@@ -417,4 +443,56 @@ public class Music.PlaybackManager : Object {
417443
current_audio = (AudioObject) queue_liststore.get_item (position);
418444
}
419445
}
446+
447+
public void save_queue_to_playlist () {
448+
var all_files_filter = new Gtk.FileFilter () {
449+
name = _("All files"),
450+
};
451+
all_files_filter.add_pattern ("*");
452+
453+
var playlist_filter = new Gtk.FileFilter () {
454+
name = _("Playlist files"),
455+
};
456+
playlist_filter.add_mime_type ("audio/x-mpegurl");
457+
458+
var filter_model = new ListStore (typeof (Gtk.FileFilter));
459+
filter_model.append (all_files_filter);
460+
filter_model.append (playlist_filter);
461+
462+
var save_dialog = new Gtk.FileDialog () {
463+
accept_label = _("Save"),
464+
default_filter = playlist_filter,
465+
filters = filter_model,
466+
modal = true,
467+
title = _("Save queue to playlist"),
468+
initial_name = "%s.m3u".printf (_("New Playlist")),
469+
};
470+
471+
save_dialog.save.begin (null, null, (obj, res) => {
472+
try {
473+
File? file;
474+
file = save_dialog.save.end (res);
475+
476+
PlaylistObject playlist = new PlaylistObject (file);
477+
playlist.save_playlist (queue_liststore);
478+
} catch (Error err) {
479+
if (err.matches (Gtk.DialogError.quark (), Gtk.DialogError.DISMISSED)) {
480+
return;
481+
}
482+
483+
warning ("Failed to save playlist: %s", err.message);
484+
485+
var dialog = new Granite.MessageDialog (
486+
_("Could not save playlist"),
487+
err.message,
488+
new ThemedIcon ("audio-x-playlist")
489+
) {
490+
badge_icon = new ThemedIcon ("dialog-error"),
491+
modal = true
492+
};
493+
dialog.present ();
494+
dialog.response.connect (dialog.destroy);
495+
}
496+
});
497+
}
420498
}

src/PlaylistObject.vala

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* SPDX-License-Identifier: LGPL-3.0-or-later
3+
* SPDX-FileCopyrightText: 2026 elementary, Inc. (https://elementary.io)
4+
*/
5+
6+
public class Music.PlaylistObject : Object {
7+
public File playlist_file { get; construct; }
8+
private File[] uri_list = {};
9+
10+
public PlaylistObject (File playlist) {
11+
Object (playlist_file: playlist);
12+
}
13+
14+
public static bool is_playlist (File playlist) {
15+
FileInfo info;
16+
17+
try {
18+
info = playlist.query_info (GLib.FileAttribute.STANDARD_CONTENT_TYPE, GLib.FileQueryInfoFlags.NONE);
19+
} catch (Error e) {
20+
warning (e.message);
21+
22+
return false;
23+
}
24+
25+
var mimetype = info.get_content_type ();
26+
if (mimetype == null) {
27+
warning ("Failed to get content type");
28+
29+
return false;
30+
}
31+
32+
return mimetype == "audio/x-mpegurl";
33+
}
34+
35+
public File[]? get_uri_list () {
36+
return uri_list;
37+
}
38+
39+
public void load_playlist () requires (playlist_file != null) {
40+
FileInputStream fis = playlist_file.read ();
41+
DataInputStream dis = new DataInputStream (fis);
42+
string current_line;
43+
44+
while ((current_line = dis.read_line ()) != null) {
45+
if (current_line.ascii_down ().has_prefix ("file:///")) {
46+
uri_list += File.new_for_uri (current_line);
47+
}
48+
else if (FileUtils.test (current_line, FileTest.IS_DIR)) {
49+
uri_list += File.new_for_path (current_line);
50+
}
51+
else {
52+
debug ("Unknown line: " + current_line);
53+
}
54+
}
55+
}
56+
57+
public void save_playlist (ListStore queue) requires (playlist_file != null) {
58+
FileOutputStream fos = playlist_file.replace (null, false, GLib.FileCreateFlags.REPLACE_DESTINATION);
59+
DataOutputStream dos = new DataOutputStream (fos);
60+
61+
for (uint i = 0; i < queue.n_items; i++) {
62+
AudioObject track = (AudioObject)queue.get_item (i);
63+
dos.put_string (track.uri);
64+
dos.put_string ("\n");
65+
}
66+
}
67+
}

src/Views/QueueView.vala

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* SPDX-License-Identifier: LGPL-3.0-or-later
3-
* SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io)
3+
* SPDX-FileCopyrightText: 2025-2026 elementary, Inc. (https://elementary.io)
44
*/
55

66
public class Music.QueueView : Granite.Bin {
@@ -169,6 +169,14 @@ public class Music.QueueView : Granite.Bin {
169169
error_toast.send_notification ();
170170
});
171171

172+
playback_manager.duplicates_found.connect ((count) => {
173+
error_toast.title = ngettext (
174+
"%d file was already in the queue and was not re-added",
175+
"%d files were already in the queue and were not re-added",
176+
count).printf (count);
177+
error_toast.send_notification ();
178+
});
179+
172180
repeat_button.clicked.connect (() => {
173181
var enum_step = settings.get_enum ("repeat-mode");
174182
if (enum_step < 2) {

src/Widgets/SearchBar.vala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public class Music.SearchBar : Granite.Bin {
3333
filter_model = new Gtk.FilterListModel (list_model, filter);
3434

3535
search_entry = new Gtk.SearchEntry () {
36-
placeholder_text = _("Search titles in playlist")
36+
placeholder_text = _("Search titles in queue")
3737
};
3838

3939
child = search_entry;

src/meson.build

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ sources = [
22
'Application.vala',
33
'AudioObject.vala',
44
'MainWindow.vala',
5-
'PlaybackManager.vala',
65
'MetadataDiscoverer.vala',
6+
'PlaybackManager.vala',
7+
'PlaylistObject.vala',
78
'DBus/MprisPlayer.vala',
89
'DBus/MprisRoot.vala',
910
'Views/NowPlayingView.vala',

0 commit comments

Comments
 (0)