diff options
Diffstat (limited to 'player/configfiles.c')
-rw-r--r-- | player/configfiles.c | 472 |
1 files changed, 472 insertions, 0 deletions
diff --git a/player/configfiles.c b/player/configfiles.c new file mode 100644 index 0000000..9441638 --- /dev/null +++ b/player/configfiles.c @@ -0,0 +1,472 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <errno.h> +#include <stddef.h> +#include <stdbool.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> +#include <utime.h> + +#include <libavutil/md5.h> + +#include "mpv_talloc.h" + +#include "osdep/io.h" + +#include "common/global.h" +#include "common/encode.h" +#include "common/msg.h" +#include "misc/ctype.h" +#include "options/path.h" +#include "options/m_config.h" +#include "options/m_config_frontend.h" +#include "options/parse_configfile.h" +#include "common/playlist.h" +#include "options/options.h" +#include "options/m_property.h" + +#include "stream/stream.h" + +#include "core.h" +#include "command.h" + +static void load_all_cfgfiles(struct MPContext *mpctx, char *section, + char *filename) +{ + char **cf = mp_find_all_config_files(NULL, mpctx->global, filename); + for (int i = 0; cf && cf[i]; i++) + m_config_parse_config_file(mpctx->mconfig, mpctx->global, cf[i], section, 0); + talloc_free(cf); +} + +// This name is used in builtin.conf to force encoding defaults (like ao/vo). +#define SECT_ENCODE "encoding" + +void mp_parse_cfgfiles(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + + mp_mk_user_dir(mpctx->global, "home", ""); + + char *p1 = mp_get_user_path(NULL, mpctx->global, "~~home/"); + char *p2 = mp_get_user_path(NULL, mpctx->global, "~~old_home/"); + if (strcmp(p1, p2) != 0 && mp_path_exists(p2)) { + MP_WARN(mpctx, "Warning, two config dirs found:\n %s (main)\n" + " %s (bogus)\nYou should merge or delete the second one.\n", + p1, p2); + } + talloc_free(p1); + talloc_free(p2); + + char *section = NULL; + bool encoding = opts->encode_opts && + opts->encode_opts->file && opts->encode_opts->file[0]; + // In encoding mode, we don't want to apply normal config options. + // So we "divert" normal options into a separate section, and the diverted + // section is never used - unless maybe it's explicitly referenced from an + // encoding profile. + if (encoding) + section = "playback-default"; + + load_all_cfgfiles(mpctx, NULL, "encoding-profiles.conf"); + + load_all_cfgfiles(mpctx, section, "mpv.conf|config"); + + if (encoding) + m_config_set_profile(mpctx->mconfig, SECT_ENCODE, 0); +} + +static int try_load_config(struct MPContext *mpctx, const char *file, int flags, + int msgl) +{ + if (!mp_path_exists(file)) + return 0; + MP_MSG(mpctx, msgl, "Loading config '%s'\n", file); + m_config_parse_config_file(mpctx->mconfig, mpctx->global, file, NULL, flags); + return 1; +} + +// Set options file-local, and don't set them if the user set them via the +// command line. +#define FILE_LOCAL_FLAGS (M_SETOPT_BACKUP | M_SETOPT_PRESERVE_CMDLINE) + +static void mp_load_per_file_config(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + char *confpath; + char cfg[512]; + const char *file = mpctx->filename; + + if (opts->use_filedir_conf) { + if (snprintf(cfg, sizeof(cfg), "%s.conf", file) >= sizeof(cfg)) { + MP_VERBOSE(mpctx, "Filename is too long, can not load file or " + "directory specific config files\n"); + return; + } + + char *name = mp_basename(cfg); + + bstr dir = mp_dirname(cfg); + char *dircfg = mp_path_join_bstr(NULL, dir, bstr0("mpv.conf")); + try_load_config(mpctx, dircfg, FILE_LOCAL_FLAGS, MSGL_INFO); + talloc_free(dircfg); + + if (try_load_config(mpctx, cfg, FILE_LOCAL_FLAGS, MSGL_INFO)) + return; + + if ((confpath = mp_find_config_file(NULL, mpctx->global, name))) { + try_load_config(mpctx, confpath, FILE_LOCAL_FLAGS, MSGL_INFO); + + talloc_free(confpath); + } + } +} + +static void mp_auto_load_profile(struct MPContext *mpctx, char *category, + bstr item) +{ + if (!item.len) + return; + + char t[512]; + snprintf(t, sizeof(t), "%s.%.*s", category, BSTR_P(item)); + m_profile_t *p = m_config_get_profile0(mpctx->mconfig, t); + if (p) { + MP_INFO(mpctx, "Auto-loading profile '%s'\n", t); + m_config_set_profile(mpctx->mconfig, t, FILE_LOCAL_FLAGS); + } +} + +void mp_load_auto_profiles(struct MPContext *mpctx) +{ + mp_auto_load_profile(mpctx, "protocol", + mp_split_proto(bstr0(mpctx->filename), NULL)); + mp_auto_load_profile(mpctx, "extension", + bstr0(mp_splitext(mpctx->filename, NULL))); + + mp_load_per_file_config(mpctx); +} + +#define MP_WATCH_LATER_CONF "watch_later" + +static bool check_mtime(const char *f1, const char *f2) +{ + struct stat st1, st2; + if (stat(f1, &st1) != 0 || stat(f2, &st2) != 0) + return false; + return st1.st_mtime == st2.st_mtime; +} + +static bool copy_mtime(const char *f1, const char *f2) +{ + struct stat st1, st2; + + if (stat(f1, &st1) != 0 || stat(f2, &st2) != 0) + return false; + + struct utimbuf ut = { + .actime = st2.st_atime, // we want to pass this through intact + .modtime = st1.st_mtime, + }; + + if (utime(f2, &ut) != 0) + return false; + + return true; +} + +static char *mp_get_playback_resume_dir(struct MPContext *mpctx) +{ + char *wl_dir = mpctx->opts->watch_later_dir; + if (wl_dir && wl_dir[0]) { + wl_dir = mp_get_user_path(mpctx, mpctx->global, wl_dir); + } else { + wl_dir = mp_find_user_file(mpctx, mpctx->global, "state", MP_WATCH_LATER_CONF); + } + return wl_dir; +} + +static char *mp_get_playback_resume_config_filename(struct MPContext *mpctx, + const char *fname) +{ + struct MPOpts *opts = mpctx->opts; + char *res = NULL; + void *tmp = talloc_new(NULL); + const char *realpath = fname; + bstr bfname = bstr0(fname); + if (!mp_is_url(bfname)) { + if (opts->ignore_path_in_watch_later_config) { + realpath = mp_basename(fname); + } else { + char *cwd = mp_getcwd(tmp); + if (!cwd) + goto exit; + realpath = mp_path_join(tmp, cwd, fname); + } + } + uint8_t md5[16]; + av_md5_sum(md5, realpath, strlen(realpath)); + char *conf = talloc_strdup(tmp, ""); + for (int i = 0; i < 16; i++) + conf = talloc_asprintf_append(conf, "%02X", md5[i]); + + char *wl_dir = mp_get_playback_resume_dir(mpctx); + if (wl_dir && wl_dir[0]) + res = mp_path_join(NULL, wl_dir, conf); + +exit: + talloc_free(tmp); + return res; +} + +// Should follow what parser-cfg.c does/needs +static bool needs_config_quoting(const char *s) +{ + if (s[0] == '%') + return true; + for (int i = 0; s[i]; i++) { + unsigned char c = s[i]; + if (!mp_isprint(c) || mp_isspace(c) || c == '#' || c == '\'' || c == '"') + return true; + } + return false; +} + +static void write_filename(struct MPContext *mpctx, FILE *file, char *filename) +{ + if (mpctx->opts->write_filename_in_watch_later_config) { + char write_name[1024] = {0}; + for (int n = 0; filename[n] && n < sizeof(write_name) - 1; n++) + write_name[n] = (unsigned char)filename[n] < 32 ? '_' : filename[n]; + fprintf(file, "# %s\n", write_name); + } +} + +static void write_redirect(struct MPContext *mpctx, char *path) +{ + char *conffile = mp_get_playback_resume_config_filename(mpctx, path); + if (conffile) { + FILE *file = fopen(conffile, "wb"); + if (file) { + fprintf(file, "# redirect entry\n"); + write_filename(mpctx, file, path); + fclose(file); + } + + if (mpctx->opts->position_check_mtime && + !mp_is_url(bstr0(path)) && !copy_mtime(path, conffile)) + MP_WARN(mpctx, "Can't copy mtime from %s to %s\n", path, conffile); + + talloc_free(conffile); + } +} + +static void write_redirects_for_parent_dirs(struct MPContext *mpctx, char *path) +{ + if (mp_is_url(bstr0(path))) + return; + + // Write redirect entries for the file's parent directories to allow + // resuming playback when playing parent directories whose entries are + // expanded only the first time they are "played". For example, if + // "/a/b/c.mkv" is the current entry, also create resume files for /a/b and + // /a, so that "mpv --directory-mode=lazy /a" resumes playback from + // /a/b/c.mkv even when b isn't the first directory in /a. + bstr dir = mp_dirname(path); + // There is no need to write a redirect entry for "/". + while (dir.len > 1 && dir.len < strlen(path)) { + path[dir.len] = '\0'; + mp_path_strip_trailing_separator(path); + write_redirect(mpctx, path); + dir = mp_dirname(path); + } +} + +void mp_write_watch_later_conf(struct MPContext *mpctx) +{ + struct playlist_entry *cur = mpctx->playing; + char *conffile = NULL; + void *ctx = talloc_new(NULL); + + if (!cur) + goto exit; + + char *path = mp_normalize_path(ctx, cur->filename); + + struct demuxer *demux = mpctx->demuxer; + + conffile = mp_get_playback_resume_config_filename(mpctx, path); + if (!conffile) + goto exit; + + char *wl_dir = mp_get_playback_resume_dir(mpctx); + mp_mkdirp(wl_dir); + + MP_INFO(mpctx, "Saving state.\n"); + + FILE *file = fopen(conffile, "wb"); + if (!file) { + MP_WARN(mpctx, "Can't open %s for writing\n", conffile); + goto exit; + } + + write_filename(mpctx, file, path); + + bool write_start = true; + double pos = get_current_time(mpctx); + + if ((demux && (!demux->seekable || demux->partially_seekable)) || + pos == MP_NOPTS_VALUE) + { + write_start = false; + MP_INFO(mpctx, "Not seekable, or time unknown - not saving position.\n"); + } + char **watch_later_options = mpctx->opts->watch_later_options; + for (int i = 0; watch_later_options && watch_later_options[i]; i++) { + char *pname = watch_later_options[i]; + // Always save start if we have it in the array. + if (write_start && strcmp(pname, "start") == 0) { + fprintf(file, "%s=%f\n", pname, pos); + continue; + } + // Only store it if it's different from the initial value. + if (m_config_watch_later_backup_opt_changed(mpctx->mconfig, pname)) { + char *val = NULL; + mp_property_do(pname, M_PROPERTY_GET_STRING, &val, mpctx); + if (needs_config_quoting(val)) { + // e.g. '%6%STRING' + fprintf(file, "%s=%%%d%%%s\n", pname, (int)strlen(val), val); + } else { + fprintf(file, "%s=%s\n", pname, val); + } + talloc_free(val); + } + } + fclose(file); + + if (mpctx->opts->position_check_mtime && !mp_is_url(bstr0(path)) && + !copy_mtime(path, conffile)) + { + MP_WARN(mpctx, "Can't copy mtime from %s to %s\n", cur->filename, + conffile); + } + + write_redirects_for_parent_dirs(mpctx, path); + + // Also write redirect entries for a playlist that mpv expanded if the + // current entry is a URL, this is mostly useful for playing multiple + // archives of images, e.g. with mpv 1.zip 2.zip and quit-watch-later + // on 2.zip, write redirect entries for 2.zip, not just for the archive:// + // URL. + if (cur->playlist_path && mp_is_url(bstr0(path))) { + char *playlist_path = mp_normalize_path(ctx, cur->playlist_path); + write_redirect(mpctx, playlist_path); + write_redirects_for_parent_dirs(mpctx, playlist_path); + } + +exit: + talloc_free(conffile); + talloc_free(ctx); +} + +void mp_delete_watch_later_conf(struct MPContext *mpctx, const char *file) +{ + if (!file) { + struct playlist_entry *cur = mpctx->playing; + if (!cur) + return; + file = cur->filename; + if (!file) + return; + } + + char *fname = mp_get_playback_resume_config_filename(mpctx, file); + if (fname) { + unlink(fname); + talloc_free(fname); + } + + if (mp_is_url(bstr0(file))) + return; + + void *ctx = talloc_new(NULL); + char *path = mp_normalize_path(ctx, file); + + bstr dir = mp_dirname(path); + while (dir.len > 1 && dir.len < strlen(path)) { + path[dir.len] = '\0'; + mp_path_strip_trailing_separator(path); + fname = mp_get_playback_resume_config_filename(mpctx, path); + if (fname) { + unlink(fname); + talloc_free(fname); + } + dir = mp_dirname(path); + } + + talloc_free(ctx); +} + +bool mp_load_playback_resume(struct MPContext *mpctx, const char *file) +{ + bool resume = false; + if (!mpctx->opts->position_resume) + return resume; + char *fname = mp_get_playback_resume_config_filename(mpctx, file); + if (fname && mp_path_exists(fname)) { + if (mpctx->opts->position_check_mtime && + !mp_is_url(bstr0(file)) && !check_mtime(file, fname)) + { + talloc_free(fname); + return resume; + } + + // Never apply the saved start position to following files + m_config_backup_opt(mpctx->mconfig, "start"); + MP_INFO(mpctx, "Resuming playback. This behavior can " + "be disabled with --no-resume-playback.\n"); + try_load_config(mpctx, fname, M_SETOPT_PRESERVE_CMDLINE, MSGL_V); + resume = true; + } + talloc_free(fname); + return resume; +} + +// Returns the first file that has a resume config. +// Compared to hashing the playlist file or contents and managing separate +// resume file for them, this is simpler, and also has the nice property +// that appending to a playlist doesn't interfere with resuming (especially +// if the playlist comes from the command line). +struct playlist_entry *mp_check_playlist_resume(struct MPContext *mpctx, + struct playlist *playlist) +{ + if (!mpctx->opts->position_resume) + return NULL; + for (int n = 0; n < playlist->num_entries; n++) { + struct playlist_entry *e = playlist->entries[n]; + char *conf = mp_get_playback_resume_config_filename(mpctx, e->filename); + bool exists = conf && mp_path_exists(conf); + talloc_free(conf); + if (exists) + return e; + } + return NULL; +} + |