Coverage Report

Created: 2026-01-26 07:36

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mpv/common/playlist.c
Line
Count
Source
1
/*
2
 * This file is part of mpv.
3
 *
4
 * mpv is free software; you can redistribute it and/or
5
 * modify it under the terms of the GNU Lesser General Public
6
 * License as published by the Free Software Foundation; either
7
 * version 2.1 of the License, or (at your option) any later version.
8
 *
9
 * mpv is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU Lesser General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU Lesser General Public
15
 * License along with mpv.  If not, see <http://www.gnu.org/licenses/>.
16
 */
17
18
#include <assert.h>
19
#include "playlist.h"
20
#include "common/common.h"
21
#include "common/global.h"
22
#include "common/msg.h"
23
#include "misc/random.h"
24
#include "mpv_talloc.h"
25
#include "options/path.h"
26
27
#include "demux/demux.h"
28
#include "stream/stream.h"
29
30
struct playlist_entry *playlist_entry_new(const char *filename)
31
400k
{
32
400k
    struct playlist_entry *e = talloc_zero(NULL, struct playlist_entry);
33
400k
    char *local_filename = mp_file_url_to_filename(e, bstr0(filename));
34
400k
    e->filename = mp_normalize_path(e, local_filename ? local_filename : filename);
35
400k
    talloc_free(local_filename);
36
400k
    e->stream_flags = STREAM_ORIGIN_DIRECT;
37
400k
    e->original_index = -1;
38
400k
    return e;
39
400k
}
40
41
void playlist_entry_add_param(struct playlist_entry *e, bstr name, bstr value)
42
5.30k
{
43
5.30k
    struct playlist_param p = {bstrdup(e, name), bstrdup(e, value)};
44
5.30k
    MP_TARRAY_APPEND(e, e->params, e->num_params, p);
45
5.30k
}
46
47
void playlist_entry_add_params(struct playlist_entry *e,
48
                               struct playlist_param *params,
49
                               int num_params)
50
986
{
51
6.29k
    for (int n = 0; n < num_params; n++)
52
5.30k
        playlist_entry_add_param(e, params[n].name, params[n].value);
53
986
}
54
55
static void playlist_update_indexes(struct playlist *pl, int start, int end)
56
434k
{
57
434k
    start = MPMAX(start, 0);
58
434k
    end = end < 0 ? pl->num_entries : MPMIN(end, pl->num_entries);
59
60
1.08M
    for (int n = start; n < end; n++)
61
652k
        pl->entries[n]->pl_index = n;
62
434k
}
63
64
// Inserts the entry so that it takes "at"'s place, shifting "at" and all
65
// further entries to the right (or append to end, if at==NULL).
66
void playlist_insert_at(struct playlist *pl, struct playlist_entry *add,
67
                        struct playlist_entry *at)
68
400k
{
69
400k
    mp_assert(add->filename);
70
400k
    mp_assert(!at || at->pl == pl);
71
72
400k
    int index = at ? at->pl_index : pl->num_entries;
73
400k
    MP_TARRAY_INSERT_AT(pl, pl->entries, pl->num_entries, index, add);
74
75
400k
    add->pl = pl;
76
400k
    add->pl_index = index;
77
400k
    add->id = ++pl->id_alloc;
78
79
400k
    playlist_update_indexes(pl, index, pl->num_entries);
80
81
400k
    talloc_steal(pl, add);
82
400k
}
83
84
void playlist_entry_unref(struct playlist_entry *e)
85
122k
{
86
122k
    e->reserved--;
87
122k
    if (e->reserved < 0) {
88
23.8k
        mp_assert(!e->pl);
89
23.8k
        talloc_free(e);
90
23.8k
    }
91
122k
}
92
93
void playlist_remove(struct playlist *pl, struct playlist_entry *entry)
94
23.8k
{
95
23.8k
    mp_assert(pl && entry->pl == pl);
96
97
23.8k
    if (pl->current == entry) {
98
10.4k
        pl->current = playlist_entry_get_rel(entry, 1);
99
10.4k
        pl->current_was_replaced = true;
100
10.4k
    }
101
102
23.8k
    MP_TARRAY_REMOVE_AT(pl->entries, pl->num_entries, entry->pl_index);
103
23.8k
    playlist_update_indexes(pl, entry->pl_index, -1);
104
105
23.8k
    entry->pl = NULL;
106
23.8k
    entry->pl_index = -1;
107
23.8k
    ta_set_parent(entry, NULL);
108
109
23.8k
    entry->removed = true;
110
23.8k
    playlist_entry_unref(entry);
111
23.8k
}
112
113
void playlist_clear(struct playlist *pl)
114
251k
{
115
265k
    for (int n = pl->num_entries - 1; n >= 0; n--)
116
13.4k
        playlist_remove(pl, pl->entries[n]);
117
251k
    mp_assert(!pl->current);
118
251k
    pl->current_was_replaced = false;
119
251k
    pl->playlist_completed = false;
120
251k
    pl->playlist_started = false;
121
251k
    TA_FREEP(&pl->playlist_dir);
122
251k
}
123
124
void playlist_clear_except_current(struct playlist *pl)
125
0
{
126
0
    for (int n = pl->num_entries - 1; n >= 0; n--) {
127
0
        if (pl->entries[n] != pl->current)
128
0
            playlist_remove(pl, pl->entries[n]);
129
0
    }
130
0
    pl->playlist_completed = false;
131
0
    pl->playlist_started = false;
132
0
}
133
134
// Moves the entry so that it takes "at"'s place (or move to end, if at==NULL).
135
void playlist_move(struct playlist *pl, struct playlist_entry *entry,
136
                   struct playlist_entry *at)
137
0
{
138
0
    if (entry == at)
139
0
        return;
140
141
0
    mp_assert(entry && entry->pl == pl);
142
0
    mp_assert(!at || at->pl == pl);
143
144
0
    int index = at ? at->pl_index : pl->num_entries;
145
0
    MP_TARRAY_INSERT_AT(pl, pl->entries, pl->num_entries, index, entry);
146
147
0
    int old_index = entry->pl_index;
148
0
    if (old_index >= index)
149
0
        old_index += 1;
150
0
    MP_TARRAY_REMOVE_AT(pl->entries, pl->num_entries, old_index);
151
152
0
    playlist_update_indexes(pl, MPMIN(index - 1, old_index - 1),
153
0
                                MPMAX(index + 1, old_index + 1));
154
0
}
155
156
void playlist_append_file(struct playlist *pl, const char *filename)
157
312k
{
158
312k
    playlist_insert_at(pl, playlist_entry_new(filename), NULL);
159
312k
}
160
161
void playlist_populate_playlist_path(struct playlist *pl, const char *path)
162
10.8k
{
163
10.8k
    char *playlist_path = talloc_strdup(pl, path);
164
244k
    for (int n = 0; n < pl->num_entries; n++) {
165
233k
        struct playlist_entry *e = pl->entries[n];
166
233k
        e->playlist_path = playlist_path;
167
233k
    }
168
10.8k
}
169
170
void playlist_shuffle(struct playlist *pl)
171
149
{
172
18.9k
    for (int n = 0; n < pl->num_entries; n++)
173
18.8k
        pl->entries[n]->original_index = n;
174
149
    mp_rand_state s = mp_rand_seed(0);
175
18.8k
    for (int n = 0; n < pl->num_entries - 1; n++) {
176
18.6k
        size_t j = mp_rand_in_range32(&s, n, pl->num_entries);
177
18.6k
        MPSWAP(struct playlist_entry *, pl->entries[n], pl->entries[j]);
178
18.6k
    }
179
149
    playlist_update_indexes(pl, 0, -1);
180
149
}
181
182
0
#define CMP_INT(a, b) ((a) == (b) ? 0 : ((a) > (b) ? 1 : -1))
183
184
static int cmp_unshuffle(const void *a, const void *b)
185
0
{
186
0
    struct playlist_entry *ea = *(struct playlist_entry **)a;
187
0
    struct playlist_entry *eb = *(struct playlist_entry **)b;
188
189
0
    if (ea->original_index >= 0 && ea->original_index != eb->original_index)
190
0
        return CMP_INT(ea->original_index, eb->original_index);
191
0
    return CMP_INT(ea->pl_index, eb->pl_index);
192
0
}
193
194
void playlist_unshuffle(struct playlist *pl)
195
0
{
196
0
    if (pl->num_entries)
197
0
        qsort(pl->entries, pl->num_entries, sizeof(pl->entries[0]), cmp_unshuffle);
198
0
    playlist_update_indexes(pl, 0, -1);
199
0
}
200
201
// (Explicitly ignores current_was_replaced.)
202
struct playlist_entry *playlist_get_first(struct playlist *pl)
203
144k
{
204
144k
    return pl->num_entries ? pl->entries[0] : NULL;
205
144k
}
206
207
// (Explicitly ignores current_was_replaced.)
208
struct playlist_entry *playlist_get_last(struct playlist *pl)
209
232k
{
210
232k
    return pl->num_entries ? pl->entries[pl->num_entries - 1] : NULL;
211
232k
}
212
213
struct playlist_entry *playlist_get_next(struct playlist *pl, int direction)
214
326k
{
215
326k
    mp_assert(direction == -1 || direction == +1);
216
326k
    if (!pl->current && pl->playlist_completed && direction < 0) {
217
0
        return playlist_entry_from_index(pl, pl->num_entries - 1);
218
326k
    } else if (!pl->current && !pl->playlist_started && direction > 0) {
219
0
        return playlist_entry_from_index(pl, 0);
220
326k
    } else if (!pl->current) {
221
0
        return NULL;
222
0
    }
223
326k
    mp_assert(pl->current->pl == pl);
224
326k
    if (direction < 0)
225
0
        return playlist_entry_get_rel(pl->current, -1);
226
326k
    return pl->current_was_replaced ? pl->current :
227
326k
           playlist_entry_get_rel(pl->current, 1);
228
326k
}
229
230
// (Explicitly ignores current_was_replaced.)
231
struct playlist_entry *playlist_entry_get_rel(struct playlist_entry *e,
232
                                              int direction)
233
327k
{
234
327k
    mp_assert(direction == -1 || direction == +1);
235
327k
    if (!e->pl)
236
0
        return NULL;
237
327k
    return playlist_entry_from_index(e->pl, e->pl_index + direction);
238
327k
}
239
240
struct playlist_entry *playlist_get_first_in_next_playlist(struct playlist *pl,
241
                                                           int direction)
242
0
{
243
0
    struct playlist_entry *entry = playlist_get_next(pl, direction);
244
0
    if (!entry)
245
0
        return NULL;
246
247
0
    while (entry && entry->playlist_path && pl->current->playlist_path &&
248
0
           strcmp(entry->playlist_path, pl->current->playlist_path) == 0)
249
0
        entry = playlist_entry_get_rel(entry, direction);
250
251
0
    if (direction < 0)
252
0
        entry = playlist_get_first_in_same_playlist(entry,
253
0
                                                    pl->current->playlist_path);
254
255
0
    return entry;
256
0
}
257
258
struct playlist_entry *playlist_get_first_in_same_playlist(
259
    struct playlist_entry *entry, char *current_playlist_path)
260
0
{
261
0
    void *tmp = talloc_new(NULL);
262
263
0
    if (!entry || !entry->playlist_path)
264
0
        goto exit;
265
266
    // Don't go to the beginning of the playlist when the current playlist-path
267
    // starts with the previous playlist-path, e.g. with mpv --loop-playlist
268
    // archive_dir/, which expands to archive_dir/{1..9}.zip, the current
269
    // playlist path "archive_dir/1.zip" begins with the playlist-path
270
    // "archive_dir/" of {2..9}.zip, so go to 9.zip instead of 2.zip. But
271
    // playlist-prev-playlist from e.g. the directory "foobar" to the directory
272
    // "foo" should still go to the first entry in "foo/", and this should all
273
    // work whether mpv's arguments have trailing slashes or not, e.g. in the
274
    // first example:
275
    // mpv archive_dir results in the playlist-paths "archive_dir/1.zip" and
276
    // "archive_dir"
277
    // mpv archive_dir/ in "archive_dir/1.zip" and "archive_dir/"
278
    // mpv archive_dir// in "archive_dir//1.zip" and "archive_dir//"
279
    // Always adding a separator to entry->playlist_path to fix the foobar foo
280
    // case would break the previous 2 cases instead. Stripping the separator
281
    // from entry->playlist_path if present and appending it again makes this
282
    // work in all cases.
283
0
    char* playlist_path = talloc_strdup(tmp, entry->playlist_path);
284
0
    mp_path_strip_trailing_separator(playlist_path);
285
0
    if (bstr_startswith(bstr0(current_playlist_path),
286
0
                        bstr0(talloc_strdup_append(playlist_path, "/")))
287
#if HAVE_DOS_PATHS
288
        ||
289
        bstr_startswith(bstr0(current_playlist_path),
290
                        bstr0(talloc_strdup_append(playlist_path, "\\")))
291
#endif
292
0
       )
293
0
        goto exit;
294
295
0
    struct playlist_entry *prev = playlist_entry_get_rel(entry, -1);
296
297
0
    while (prev && prev->playlist_path &&
298
0
           strcmp(prev->playlist_path, entry->playlist_path) == 0) {
299
0
        entry = prev;
300
0
        prev = playlist_entry_get_rel(entry, -1);
301
0
    }
302
303
0
exit:
304
0
    talloc_free(tmp);
305
0
    return entry;
306
0
}
307
308
void playlist_set_stream_flags(struct playlist *pl, int flags)
309
10.8k
{
310
244k
    for (int n = 0; n < pl->num_entries; n++)
311
233k
        pl->entries[n]->stream_flags = flags;
312
10.8k
}
313
314
int64_t playlist_transfer_entries_to(struct playlist *pl, int dst_index,
315
                                     struct playlist *source_pl)
316
10.4k
{
317
10.4k
    mp_assert(pl != source_pl);
318
10.4k
    struct playlist_entry *first = playlist_get_first(source_pl);
319
320
10.4k
    int count = source_pl->num_entries;
321
10.4k
    MP_TARRAY_INSERT_N_AT(pl, pl->entries, pl->num_entries, dst_index, count);
322
323
244k
    for (int n = 0; n < count; n++) {
324
233k
        struct playlist_entry *e = source_pl->entries[n];
325
233k
        e->pl = pl;
326
233k
        e->pl_index = dst_index + n;
327
233k
        e->id = ++pl->id_alloc;
328
233k
        pl->entries[e->pl_index] = e;
329
233k
        talloc_steal(pl, e);
330
233k
        talloc_steal(pl, e->playlist_path);
331
233k
    }
332
333
10.4k
    playlist_update_indexes(pl, dst_index + count, -1);
334
10.4k
    source_pl->num_entries = 0;
335
336
10.4k
    pl->playlist_completed = source_pl->playlist_completed;
337
10.4k
    pl->playlist_started = source_pl->playlist_started;
338
339
10.4k
    return first ? first->id : 0;
340
10.4k
}
341
342
// Move all entries from source_pl to pl, appending them after the current entry
343
// of pl. source_pl will be empty, and all entries have changed ownership to pl.
344
// Return the new ID of the first added entry within pl (0 if source_pl was
345
// empty). The IDs of all added entries increase by 1 each entry (you can
346
// predict the ID of the last entry).
347
int64_t playlist_transfer_entries(struct playlist *pl, struct playlist *source_pl)
348
10.4k
{
349
350
10.4k
    int add_at = pl->num_entries;
351
10.4k
    if (pl->current) {
352
10.4k
        add_at = pl->current->pl_index + 1;
353
10.4k
        if (pl->current_was_replaced)
354
0
            add_at += 1;
355
10.4k
    }
356
10.4k
    mp_assert(add_at >= 0);
357
10.4k
    mp_assert(add_at <= pl->num_entries);
358
359
10.4k
    return playlist_transfer_entries_to(pl, add_at, source_pl);
360
10.4k
}
361
362
int64_t playlist_append_entries(struct playlist *pl, struct playlist *source_pl)
363
0
{
364
0
    return playlist_transfer_entries_to(pl, pl->num_entries, source_pl);
365
0
}
366
367
// Return number of entries between list start and e.
368
// Return -1 if e is not on the list, or if e is NULL.
369
int playlist_entry_to_index(struct playlist *pl, struct playlist_entry *e)
370
89.2k
{
371
89.2k
    if (!e || e->pl != pl)
372
582
        return -1;
373
88.6k
    return e->pl_index;
374
89.2k
}
375
376
int playlist_entry_count(struct playlist *pl)
377
4.72k
{
378
4.72k
    return pl->num_entries;
379
4.72k
}
380
381
// Return entry for which playlist_entry_to_index() would return index.
382
// Return NULL if not found.
383
struct playlist_entry *playlist_entry_from_index(struct playlist *pl, int index)
384
384k
{
385
384k
    return index >= 0 && index < pl->num_entries ? pl->entries[index] : NULL;
386
384k
}
387
388
struct playlist *playlist_parse_file(const char *file, struct mp_cancel *cancel,
389
                                     struct mpv_global *global)
390
0
{
391
0
    struct mp_log *log = mp_log_new(NULL, global->log, "!playlist_parser");
392
0
    mp_verbose(log, "Parsing playlist file %s...\n", file);
393
394
0
    char *path = mp_get_user_path(NULL, global, file);
395
0
    struct demuxer_params p = {
396
0
        .force_format = "playlist",
397
0
        .stream_flags = STREAM_ORIGIN_DIRECT,
398
0
    };
399
0
    struct demuxer *d = demux_open_url(path, &p, cancel, global);
400
0
    struct playlist *ret = NULL;
401
0
    if (!d)
402
0
        goto done;
403
404
0
    if (d && d->playlist) {
405
0
        ret = talloc_zero(NULL, struct playlist);
406
0
        playlist_populate_playlist_path(d->playlist, file);
407
0
        playlist_transfer_entries(ret, d->playlist);
408
0
        if (d->filetype && strcmp(d->filetype, "hls") == 0) {
409
0
            mp_warn(log, "This might be a HLS stream. For correct operation, "
410
0
                         "pass it to the player\ndirectly. Don't use --playlist.\n");
411
0
        }
412
0
    }
413
0
    demux_free(d);
414
415
0
    if (ret) {
416
0
        mp_verbose(log, "Playlist successfully parsed\n");
417
0
    } else {
418
0
        mp_err(log, "Error while parsing playlist\n");
419
0
    }
420
421
0
    if (ret && !ret->num_entries)
422
0
        mp_warn(log, "Warning: empty playlist\n");
423
424
0
done:
425
0
    talloc_free(log);
426
0
    talloc_free(path);
427
0
    return ret;
428
0
}
429
430
void playlist_set_current(struct playlist *pl)
431
0
{
432
0
    if (!pl->playlist_dir)
433
0
        return;
434
435
0
    for (int i = 0; i < pl->num_entries; ++i) {
436
0
        if (pl->entries[i]->playlist_path &&
437
0
            !strcmp(pl->entries[i]->filename, pl->entries[i]->playlist_path)) {
438
0
            pl->current = pl->entries[i];
439
0
            break;
440
0
        }
441
0
    }
442
0
}