Coverage Report

Created: 2026-03-27 06:40

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mpv/player/configfiles.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 <errno.h>
19
#include <stddef.h>
20
#include <stdbool.h>
21
#include <sys/types.h>
22
#include <sys/stat.h>
23
#include <fcntl.h>
24
25
#ifdef _WIN32
26
#include <sys/utime.h>
27
#else
28
#include <utime.h>
29
#endif
30
31
#include <libavutil/md5.h>
32
33
#include "mpv_talloc.h"
34
35
#include "osdep/io.h"
36
37
#include "common/encode.h"
38
#include "common/msg.h"
39
#include "misc/ctype.h"
40
#include "misc/hash.h"
41
#include "misc/io_utils.h"
42
#include "options/path.h"
43
#include "options/m_config.h"
44
#include "options/m_config_frontend.h"
45
#include "options/parse_configfile.h"
46
#include "common/playlist.h"
47
#include "options/options.h"
48
#include "options/m_property.h"
49
#include "input/input.h"
50
51
#include "stream/stream.h"
52
53
#include "core.h"
54
#include "command.h"
55
56
static void load_all_cfgfiles(struct MPContext *mpctx, char *section,
57
                              char *filename)
58
260k
{
59
260k
    char **cf = mp_find_all_config_files(NULL, mpctx->global, filename);
60
260k
    for (int i = 0; cf && cf[i]; i++)
61
0
        m_config_parse_config_file(mpctx->mconfig, mpctx->global, cf[i], section, 0);
62
260k
    talloc_free(cf);
63
260k
}
64
65
// This name is used in builtin.conf to force encoding defaults (like ao/vo).
66
397
#define SECT_ENCODE "encoding"
67
68
void mp_parse_cfgfiles(struct MPContext *mpctx)
69
130k
{
70
130k
    struct MPOpts *opts = mpctx->opts;
71
72
130k
    mp_mk_user_dir(mpctx->global, "home", "");
73
74
130k
    char *p1 = mp_get_user_path(NULL, mpctx->global, "~~home/");
75
130k
    char *p2 = mp_get_user_path(NULL, mpctx->global, "~~old_home/");
76
130k
    if (strcmp(p1, p2) != 0 && mp_path_exists(p2)) {
77
0
        MP_WARN(mpctx, "Warning, two config dirs found:\n   %s (main)\n"
78
0
                "   %s (bogus)\nYou should merge or delete the second one.\n",
79
0
                p1, p2);
80
0
    }
81
130k
    talloc_free(p1);
82
130k
    talloc_free(p2);
83
84
130k
    char *section = NULL;
85
130k
    bool encoding = opts->encode_opts->file && opts->encode_opts->file[0];
86
    // In encoding mode, we don't want to apply normal config options.
87
    // So we "divert" normal options into a separate section, and the diverted
88
    // section is never used - unless maybe it's explicitly referenced from an
89
    // encoding profile.
90
130k
    if (encoding)
91
397
        section = "playback-default";
92
93
130k
    load_all_cfgfiles(mpctx, NULL, "encoding-profiles.conf");
94
95
130k
    load_all_cfgfiles(mpctx, section, "mpv.conf|config");
96
97
130k
    if (encoding) {
98
397
        m_config_set_profile(mpctx->mconfig, SECT_ENCODE, 0);
99
397
        mp_input_enable_section(mpctx->input, "encode", MP_INPUT_EXCLUSIVE);
100
397
    }
101
130k
}
102
103
static int try_load_config(struct MPContext *mpctx, const char *file, int flags,
104
                           int msgl)
105
2
{
106
2
    if (!mp_path_exists(file))
107
2
        return 0;
108
0
    MP_MSG(mpctx, msgl, "Loading config '%s'\n", file);
109
0
    m_config_parse_config_file(mpctx->mconfig, mpctx->global, file, NULL, flags);
110
0
    return 1;
111
2
}
112
113
// Set options file-local, and don't set them if the user set them via the
114
// command line.
115
2
#define FILE_LOCAL_FLAGS (M_SETOPT_BACKUP | M_SETOPT_PRESERVE_CMDLINE)
116
117
static void mp_load_per_file_config(struct MPContext *mpctx)
118
99.0k
{
119
99.0k
    struct MPOpts *opts = mpctx->opts;
120
99.0k
    char *confpath;
121
99.0k
    char cfg[512];
122
99.0k
    const char *file = mpctx->filename;
123
124
99.0k
    if (opts->use_filedir_conf) {
125
1
        if (snprintf(cfg, sizeof(cfg), "%s.conf", file) >= sizeof(cfg)) {
126
0
            MP_VERBOSE(mpctx, "Filename is too long, can not load file or "
127
0
                              "directory specific config files\n");
128
0
            return;
129
0
        }
130
131
1
        const char *name = mp_basename(cfg);
132
133
1
        bstr dir = mp_dirname(cfg);
134
1
        char *dircfg = mp_path_join_bstr(NULL, dir, bstr0("mpv.conf"));
135
1
        try_load_config(mpctx, dircfg, FILE_LOCAL_FLAGS, MSGL_INFO);
136
1
        talloc_free(dircfg);
137
138
1
        if (try_load_config(mpctx, cfg, FILE_LOCAL_FLAGS, MSGL_INFO))
139
0
            return;
140
141
1
        if ((confpath = mp_find_config_file(NULL, mpctx->global, name))) {
142
0
            try_load_config(mpctx, confpath, FILE_LOCAL_FLAGS, MSGL_INFO);
143
144
0
            talloc_free(confpath);
145
0
        }
146
1
    }
147
99.0k
}
148
149
static void mp_auto_load_profile(struct MPContext *mpctx, char *category,
150
                                 bstr item)
151
198k
{
152
198k
    if (!item.len)
153
65.5k
        return;
154
155
132k
    char t[512];
156
132k
    snprintf(t, sizeof(t), "%s.%.*s", category, BSTR_P(item));
157
132k
    m_profile_t *p = m_config_get_profile0(mpctx->mconfig, t);
158
132k
    if (p) {
159
0
        MP_INFO(mpctx, "Auto-loading profile '%s'\n", t);
160
0
        m_config_set_profile(mpctx->mconfig, t, FILE_LOCAL_FLAGS);
161
0
    }
162
132k
}
163
164
void mp_load_auto_profiles(struct MPContext *mpctx)
165
99.0k
{
166
99.0k
    mp_auto_load_profile(mpctx, "protocol",
167
99.0k
                         mp_split_proto(bstr0(mpctx->filename), NULL));
168
99.0k
    mp_auto_load_profile(mpctx, "extension",
169
99.0k
                         bstr0(mp_splitext(mpctx->filename, NULL)));
170
171
99.0k
    mp_load_per_file_config(mpctx);
172
99.0k
}
173
174
501k
#define MP_WATCH_LATER_CONF "watch_later"
175
176
static bool check_mtime(const char *f1, const char *f2)
177
0
{
178
0
    struct stat st1, st2;
179
0
    if (stat(f1, &st1) != 0 || stat(f2, &st2) != 0)
180
0
        return false;
181
0
    return st1.st_mtime == st2.st_mtime;
182
0
}
183
184
static bool copy_mtime(const char *f1, const char *f2)
185
0
{
186
0
    struct stat st1, st2;
187
188
0
    if (stat(f1, &st1) != 0 || stat(f2, &st2) != 0)
189
0
        return false;
190
191
0
    struct utimbuf ut = {
192
0
        .actime = st2.st_atime,  // we want to pass this through intact
193
0
        .modtime = st1.st_mtime,
194
0
    };
195
196
0
    if (utime(f2, &ut) != 0)
197
0
        return false;
198
199
0
    return true;
200
0
}
201
202
char *mp_get_playback_resume_dir(struct MPContext *mpctx)
203
510k
{
204
510k
    char *wl_dir = mpctx->opts->watch_later_dir;
205
510k
    if (wl_dir && wl_dir[0]) {
206
8.96k
        wl_dir = mp_get_user_path(NULL, mpctx->global, wl_dir);
207
501k
    } else {
208
501k
        wl_dir = mp_find_user_file(NULL, mpctx->global, "state", MP_WATCH_LATER_CONF);
209
501k
    }
210
510k
    return wl_dir;
211
510k
}
212
213
static char *mp_get_playback_resume_config_filename(struct MPContext *mpctx,
214
                                                    const char *path)
215
509k
{
216
509k
    struct MPOpts *opts = mpctx->opts;
217
509k
    char *res = NULL;
218
509k
    void *tmp = talloc_new(NULL);
219
509k
    if (opts->ignore_path_in_watch_later_config && !mp_is_url(bstr0(path)))
220
0
        path = mp_basename(path);
221
222
509k
    bstr hashstr = mp_hash_to_bstr(tmp, path, strlen(path), "MD5");
223
509k
    char *wl_dir = mp_get_playback_resume_dir(mpctx);
224
509k
    if (wl_dir && wl_dir[0])
225
16.8k
        res = mp_path_join_bstr(NULL, bstr0(wl_dir), hashstr);
226
509k
    talloc_free(wl_dir);
227
509k
    talloc_free(tmp);
228
509k
    return res;
229
509k
}
230
231
// Should follow what parser-cfg.c does/needs
232
static bool needs_config_quoting(const char *s)
233
0
{
234
0
    if (s[0] == '%')
235
0
        return true;
236
0
    for (int i = 0; s[i]; i++) {
237
0
        unsigned char c = s[i];
238
0
        if (!mp_isprint(c) || mp_isspace(c) || c == '#' || c == '\'' || c == '"')
239
0
            return true;
240
0
    }
241
0
    return false;
242
0
}
243
244
static void write_filename(struct MPContext *mpctx, void *talloc_ctx, bstr *s, const char *filename)
245
0
{
246
0
    if (mpctx->opts->ignore_path_in_watch_later_config && !mp_is_url(bstr0(filename)))
247
0
        filename = mp_basename(filename);
248
249
0
    if (mpctx->opts->write_filename_in_watch_later_config) {
250
0
        char write_name[1024] = {0};
251
0
        for (int n = 0; filename[n] && n < sizeof(write_name) - 1; n++)
252
0
            write_name[n] = (unsigned char)filename[n] < 32 ? '_' : filename[n];
253
0
        bstr_xappend_asprintf(talloc_ctx, s, "# %s\n", write_name);
254
0
    }
255
0
}
256
257
static void write_redirect(struct MPContext *mpctx, char *path)
258
0
{
259
0
    char *conffile = mp_get_playback_resume_config_filename(mpctx, path);
260
0
    if (conffile) {
261
0
        bstr data = {0};
262
0
        bstr_xappend0(conffile, &data, "# redirect entry\n");
263
0
        write_filename(mpctx, conffile, &data, path);
264
0
        bool ok = mp_save_to_file(conffile, data.start, data.len);
265
0
        if (!ok)
266
0
            MP_WARN(mpctx, "Can't save redirect to %s\n", conffile);
267
268
0
        if (ok && mpctx->opts->position_check_mtime &&
269
0
            !mp_is_url(bstr0(path)) && !copy_mtime(path, conffile))
270
0
            MP_WARN(mpctx, "Can't copy mtime from %s to %s\n", path, conffile);
271
272
0
        talloc_free(conffile);
273
0
    }
274
0
}
275
276
static void write_redirects_for_parent_dirs(struct MPContext *mpctx, char *path)
277
0
{
278
0
    if (mp_is_url(bstr0(path)) || mpctx->opts->ignore_path_in_watch_later_config)
279
0
        return;
280
281
    // Write redirect entries for the file's parent directories to allow
282
    // resuming playback when playing parent directories whose entries are
283
    // expanded only the first time they are "played". For example, if
284
    // "/a/b/c.mkv" is the current entry, also create resume files for /a/b and
285
    // /a, so that "mpv --directory-mode=lazy /a" resumes playback from
286
    // /a/b/c.mkv even when b isn't the first directory in /a.
287
0
    char* path_copy = talloc_strdup(NULL, path);
288
0
    bstr dir = mp_dirname(path);
289
    // There is no need to write a redirect entry for "/".
290
0
    while (dir.len > 1 && dir.len < strlen(path_copy)) {
291
0
        path_copy[dir.len] = '\0';
292
0
        mp_path_strip_trailing_separator(path_copy);
293
0
        write_redirect(mpctx, path_copy);
294
0
        dir = mp_dirname(path_copy);
295
0
    }
296
0
    talloc_free(path_copy);
297
0
}
298
299
void mp_write_watch_later_conf(struct MPContext *mpctx)
300
2
{
301
2
    struct playlist_entry *cur = mpctx->playing;
302
2
    char *conffile = NULL;
303
304
2
    if (!cur)
305
2
        goto exit;
306
307
0
    struct demuxer *demux = mpctx->demuxer;
308
309
0
    conffile = mp_get_playback_resume_config_filename(mpctx, cur->filename);
310
0
    if (!conffile)
311
0
        goto exit;
312
313
0
    char *wl_dir = mp_get_playback_resume_dir(mpctx);
314
0
    mp_mkdirp(wl_dir);
315
0
    talloc_free(wl_dir);
316
317
0
    MP_INFO(mpctx, "Saving state.\n");
318
319
0
    bstr data = {0};
320
0
    write_filename(mpctx, conffile, &data, cur->filename);
321
322
0
    bool write_start = true;
323
0
    double pos = get_playback_time(mpctx);
324
325
0
    if ((demux && (!demux->seekable || demux->partially_seekable)) ||
326
0
        pos == MP_NOPTS_VALUE)
327
0
    {
328
0
        write_start = false;
329
0
        MP_INFO(mpctx, "Not seekable, or time unknown - not saving position.\n");
330
0
    }
331
0
    char **watch_later_options = mpctx->opts->watch_later_options;
332
0
    for (int i = 0; watch_later_options && watch_later_options[i]; i++) {
333
0
        char *pname = watch_later_options[i];
334
        // Always save start if we have it in the array.
335
0
        if (write_start && strcmp(pname, "start") == 0) {
336
0
            bstr_xappend_asprintf(conffile, &data, "%s=%f\n", pname, pos);
337
0
            continue;
338
0
        }
339
        // Only store it if it's different from the initial value.
340
0
        if (m_config_watch_later_backup_opt_changed(mpctx->mconfig, pname)) {
341
0
            char *val = NULL;
342
0
            mp_property_do(pname, M_PROPERTY_GET_STRING, &val, mpctx);
343
0
            if (!val) {
344
0
                MP_VERBOSE(mpctx, "Option %s unavailable while "
345
0
                    "writing watch-later file\n", pname);
346
0
            } else if (needs_config_quoting(val)) {
347
                // e.g. '%6%STRING'
348
0
                bstr_xappend_asprintf(conffile, &data, "%s=%%%d%%%s\n", pname, (int)strlen(val), val);
349
0
            } else {
350
0
                bstr_xappend_asprintf(conffile, &data, "%s=%s\n", pname, val);
351
0
            }
352
0
            talloc_free(val);
353
0
        }
354
0
    }
355
0
    bool ok = mp_save_to_file(conffile, data.start, data.len);
356
0
    if (!ok) {
357
0
        MP_WARN(mpctx, "Can't save state to %s\n", conffile);
358
0
        goto exit;
359
0
    }
360
361
0
    if (mpctx->opts->position_check_mtime && !mp_is_url(bstr0(cur->filename)) &&
362
0
        !copy_mtime(cur->filename, conffile))
363
0
    {
364
0
        MP_WARN(mpctx, "Can't copy mtime from %s to %s\n", cur->filename,
365
0
                conffile);
366
0
    }
367
368
0
    write_redirects_for_parent_dirs(mpctx, cur->filename);
369
370
    // Also write redirect entries for a playlist that mpv expanded if the
371
    // current entry is a URL, this is mostly useful for playing multiple
372
    // archives of images, e.g. with mpv 1.zip 2.zip and quit-watch-later
373
    // on 2.zip, write redirect entries for 2.zip, not just for the archive://
374
    // URL.
375
0
    if (cur->playlist_path && mp_is_url(bstr0(cur->filename))) {
376
0
        write_redirect(mpctx, cur->playlist_path);
377
0
        write_redirects_for_parent_dirs(mpctx, cur->playlist_path);
378
0
    }
379
380
2
exit:
381
2
    talloc_free(conffile);
382
2
}
383
384
void mp_delete_watch_later_conf(struct MPContext *mpctx, const char *file)
385
0
{
386
0
    char *path = file ? mp_normalize_path(NULL, file)
387
0
                      : talloc_strdup(NULL, mpctx->filename);
388
0
    if (!path)
389
0
        goto exit;
390
391
0
    char *fname = mp_get_playback_resume_config_filename(mpctx, path);
392
0
    if (!fname)
393
0
        goto exit;
394
395
0
    unlink(fname);
396
0
    talloc_free(fname);
397
398
0
    if (mp_is_url(bstr0(path)) || mpctx->opts->ignore_path_in_watch_later_config)
399
0
        goto exit;
400
401
0
    bstr dir = mp_dirname(path);
402
0
    while (dir.len > 1 && dir.len < strlen(path)) {
403
0
        path[dir.len] = '\0';
404
0
        mp_path_strip_trailing_separator(path);
405
0
        fname = mp_get_playback_resume_config_filename(mpctx, path);
406
0
        if (!fname)
407
0
            break;
408
0
        unlink(fname);
409
0
        talloc_free(fname);
410
0
        dir = mp_dirname(path);
411
0
    }
412
413
0
exit:
414
0
    talloc_free(path);
415
0
}
416
417
bool mp_load_playback_resume(struct MPContext *mpctx, const char *file)
418
99.0k
{
419
99.0k
    bool resume = false;
420
99.0k
    if (!mpctx->opts->position_resume)
421
1
        return resume;
422
99.0k
    char *fname = mp_get_playback_resume_config_filename(mpctx, file);
423
99.0k
    if (fname && mp_path_exists(fname)) {
424
0
        if (mpctx->opts->position_check_mtime &&
425
0
            !mp_is_url(bstr0(file)) && !check_mtime(file, fname))
426
0
        {
427
0
            talloc_free(fname);
428
0
            return resume;
429
0
        }
430
431
        // Never apply the saved start position to following files
432
0
        m_config_backup_opt(mpctx->mconfig, "start");
433
0
        MP_INFO(mpctx, "Resuming playback. This behavior can "
434
0
               "be disabled with --no-resume-playback.\n");
435
0
        try_load_config(mpctx, fname, M_SETOPT_PRESERVE_CMDLINE, MSGL_V);
436
0
        resume = true;
437
0
    }
438
99.0k
    talloc_free(fname);
439
99.0k
    return resume;
440
99.0k
}
441
442
// Returns the first file that has a resume config.
443
// Compared to hashing the playlist file or contents and managing separate
444
// resume file for them, this is simpler, and also has the nice property
445
// that appending to a playlist doesn't interfere with resuming (especially
446
// if the playlist comes from the command line).
447
struct playlist_entry *mp_check_playlist_resume(struct MPContext *mpctx,
448
                                                struct playlist *playlist)
449
136k
{
450
136k
    if (!mpctx->opts->position_resume)
451
313
        return NULL;
452
547k
    for (int n = 0; n < playlist->num_entries; n++) {
453
410k
        struct playlist_entry *e = playlist->entries[n];
454
410k
        char *conf = mp_get_playback_resume_config_filename(mpctx, e->filename);
455
410k
        bool exists = conf && mp_path_exists(conf);
456
410k
        talloc_free(conf);
457
410k
        if (exists)
458
0
            return e;
459
410k
    }
460
136k
    return NULL;
461
136k
}