diff --git a/DOCS/interface-changes/autocreate-playlist.txt b/DOCS/interface-changes/autocreate-playlist.txt new file mode 100644 index 0000000000..f2d1d1113a --- /dev/null +++ b/DOCS/interface-changes/autocreate-playlist.txt @@ -0,0 +1,4 @@ +add `--autocreate-playlist` +add `--autocreate-playlist-video-exts` +add `--autocreate-playlist-audio-exts` +add `--autocreate-playlist-image-exts` diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index ae4662c95d..37383f8de0 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -4139,6 +4139,33 @@ Demuxer all. The default is ``auto``, which behaves like ``recursive`` with ``--shuffle``, and like ``lazy`` otherwise. +``--autocreate-playlist=`` + When opening a local file, act as if the parent directory is opened and + create a playlist automatically. + + :no: Load a single file (default). + :any: Create a playlist from the parent directory with any file type. + :exts: Create a playlist from the parent directory with files matching + extensions from any list ``autocreate-playlist-*-exts``. + :same: Create a playlist from the parent directory with files matching the + same category as the currently loaded file. One of the + ``autocreate-playlist-*-exts`` is selected based on the input file + and only files with matching extensions are added to the playlist. + If the input file itself is not matched to any extension list, + the playlist is not autogenerated. + +``--autocreate-playlist-video-exts=`` + Video file extension list that is used to match files when using + ``--autocreate-playlist=``. + +``--autocreate-playlist-audio-exts=`` + Audio file extension list that is used to match files when using + ``--autocreate-playlist=``. + +``--autocreate-playlist-image-exts=`` + Image file extension list that is used to match files when using + ``--autocreate-playlist=``. + Input ----- diff --git a/common/playlist.h b/common/playlist.h index d14d8f3fa5..d1d36e9236 100644 --- a/common/playlist.h +++ b/common/playlist.h @@ -72,6 +72,7 @@ struct playlist { bool current_was_replaced; bool playlist_completed; bool playlist_started; + bool autocreated; uint64_t id_alloc; }; diff --git a/demux/demux.c b/demux/demux.c index b6f2c2efae..842d8e7731 100644 --- a/demux/demux.c +++ b/demux/demux.c @@ -57,6 +57,7 @@ extern const demuxer_desc_t demuxer_desc_mf; extern const demuxer_desc_t demuxer_desc_matroska; extern const demuxer_desc_t demuxer_desc_lavf; extern const demuxer_desc_t demuxer_desc_playlist; +extern const demuxer_desc_t demuxer_desc_directory; extern const demuxer_desc_t demuxer_desc_disc; extern const demuxer_desc_t demuxer_desc_rar; extern const demuxer_desc_t demuxer_desc_libarchive; @@ -64,10 +65,10 @@ extern const demuxer_desc_t demuxer_desc_null; extern const demuxer_desc_t demuxer_desc_timeline; static const demuxer_desc_t *const demuxer_list[] = { + &demuxer_desc_directory, &demuxer_desc_disc, &demuxer_desc_edl, &demuxer_desc_cue, - &demuxer_desc_playlist, &demuxer_desc_rawaudio, &demuxer_desc_rawvideo, &demuxer_desc_matroska, @@ -76,6 +77,7 @@ static const demuxer_desc_t *const demuxer_list[] = { #endif &demuxer_desc_lavf, &demuxer_desc_mf, + &demuxer_desc_playlist, &demuxer_desc_null, NULL }; @@ -117,7 +119,8 @@ const struct m_sub_options demux_conf = { {"demuxer-backward-playback-step", OPT_DOUBLE(back_seek_size), M_RANGE(0, DBL_MAX)}, {"metadata-codepage", OPT_STRING(meta_cp)}, - {"autocreate-playlist", OPT_BOOL(autocreate_playlist)}, + {"autocreate-playlist", OPT_CHOICE(autocreate_playlist, + {"no", 0}, {"any", 1}, {"exts", 2}, {"same", 3})}, {0} }, .size = sizeof(struct demux_opts), diff --git a/demux/demux.h b/demux/demux.h index d9cd9f7ea4..e0a3e556f0 100644 --- a/demux/demux.h +++ b/demux/demux.h @@ -86,7 +86,7 @@ struct demux_opts { double back_seek_size; char *meta_cp; bool force_retry_eof; - bool autocreate_playlist; + int autocreate_playlist; }; #define SEEK_FACTOR (1 << 1) // argument is in range [0,1] @@ -212,7 +212,7 @@ struct demuxer_params { bool stream_record; // if true, enable stream recording if option is set int stream_flags; struct stream *external_stream; // if set, use this, don't open or close streams - bool has_playlist; + bool allow_playlist_create; // result bool demuxer_failed; }; diff --git a/demux/demux_playlist.c b/demux/demux_playlist.c index c90e69bfd5..cf1a0499b5 100644 --- a/demux/demux_playlist.c +++ b/demux/demux_playlist.c @@ -42,9 +42,20 @@ enum dir_mode { DIR_IGNORE, }; +enum autocreate_mode { + AUTO_NONE = 0, + AUTO_VIDEO = 1 << 0, + AUTO_AUDIO = 1 << 1, + AUTO_IMAGE = 1 << 2, + AUTO_ANY = 1 << 3, +}; + #define OPT_BASE_STRUCT struct demux_playlist_opts struct demux_playlist_opts { int dir_mode; + char **autocreate_playlist_vid_exts; + char **autocreate_playlist_aud_exts; + char **autocreate_playlist_img_exts; }; struct m_sub_options demux_playlist_conf = { @@ -54,11 +65,29 @@ struct m_sub_options demux_playlist_conf = { {"lazy", DIR_LAZY}, {"recursive", DIR_RECURSIVE}, {"ignore", DIR_IGNORE})}, + {"autocreate-playlist-video-exts", + OPT_STRINGLIST(autocreate_playlist_vid_exts)}, + {"autocreate-playlist-audio-exts", + OPT_STRINGLIST(autocreate_playlist_aud_exts)}, + {"autocreate-playlist-image-exts", + OPT_STRINGLIST(autocreate_playlist_img_exts)}, {0} }, .size = sizeof(struct demux_playlist_opts), .defaults = &(const struct demux_playlist_opts){ .dir_mode = DIR_AUTO, + .autocreate_playlist_vid_exts = (char *[]){ + "3g2", "3gp", "avi", "flv", "m2ts", "m4v", "mj2", "mkv", "mov", + "mp4", "mpeg", "mpg", "ogv", "rmvb", "webm", "wmv", "y4m", NULL + }, + .autocreate_playlist_aud_exts = (char *[]){ + "aiff", "ape", "au", "flac", "m4a", "mka", "mp3", "oga", "ogg", + "ogm", "opus", "wav", "wma", NULL + }, + .autocreate_playlist_img_exts = (char *[]){ + "avif", "bmp", "gif", "j2k", "jp2", "jpeg", "jpg", "jxl", "png", + "svg", "tga", "tif", "tiff", "webp", NULL + }, }, }; @@ -85,7 +114,7 @@ struct pl_parser { bool force; bool add_base; bool line_allocated; - bool create_dir_playlist; + int autocreate_playlist; enum demux_check check_level; struct stream *real_stream; char *format; @@ -395,9 +424,36 @@ static int cmp_dir_entry(const void *a, const void *b) } } +static bool has_ext(bstr ext, char **list) +{ + if (!list) + return false; + while (*list) { + if (!bstrcasecmp0(ext, *list++)) + return true; + } + return false; +} + +static bool test_autocreate_path(struct pl_parser *p, char *path, int autocreate) +{ + if (autocreate & AUTO_ANY) + return true; + + bstr ext = bstr_get_ext(bstr0(path)); + if (autocreate & AUTO_VIDEO && has_ext(ext, p->opts->autocreate_playlist_vid_exts)) + return true; + if (autocreate & AUTO_AUDIO && has_ext(ext, p->opts->autocreate_playlist_aud_exts)) + return true; + if (autocreate & AUTO_IMAGE && has_ext(ext, p->opts->autocreate_playlist_img_exts)) + return true; + + return false; +} + // Return true if this was a readable directory. static bool scan_dir(struct pl_parser *p, char *path, - struct stat *dir_stack, int num_dir_stack) + struct stat *dir_stack, int num_dir_stack, int autocreate) { if (strlen(path) >= 8192 || num_dir_stack == MAX_DIR_STACK) return false; // things like mount bind loops @@ -452,10 +508,11 @@ static bool scan_dir(struct pl_parser *p, char *path, if (dir_mode == DIR_RECURSIVE && dir_entries[n].is_dir) { dir_stack[num_dir_stack] = dir_entries[n].st; char *file = dir_entries[n].path; - scan_dir(p, file, dir_stack, num_dir_stack + 1); + scan_dir(p, file, dir_stack, num_dir_stack + 1, autocreate); } else { - playlist_append_file(p->pl, dir_entries[n].path); + if (autocreate == AUTO_NONE || test_autocreate_path(p, dir_entries[n].path, autocreate)) + playlist_append_file(p->pl, dir_entries[n].path); } } @@ -466,11 +523,32 @@ static int parse_dir(struct pl_parser *p) { int ret = -1; struct stream *stream = p->real_stream; - if (p->create_dir_playlist && p->real_stream->is_local_file && !p->real_stream->is_directory) { + int autocreate = AUTO_NONE; + if (p->autocreate_playlist && p->real_stream->is_local_file && !p->real_stream->is_directory) { + bstr ext = bstr_get_ext(bstr0(p->real_stream->url)); + switch (p->autocreate_playlist) { + case 1: // any + autocreate = AUTO_ANY; + break; + case 2: // exts + autocreate = AUTO_VIDEO | AUTO_AUDIO | AUTO_IMAGE; + break; + case 3: // same + if (has_ext(ext, p->opts->autocreate_playlist_vid_exts)) { + autocreate = AUTO_VIDEO; + } else if (has_ext(ext, p->opts->autocreate_playlist_aud_exts)) { + autocreate = AUTO_AUDIO; + } else if (has_ext(ext, p->opts->autocreate_playlist_img_exts)) { + autocreate = AUTO_IMAGE; + } + break; + } int flags = STREAM_ORIGIN_DIRECT | STREAM_READ | STREAM_LOCAL_FS_ONLY | STREAM_LESS_NOISE; bstr dir = mp_dirname(p->real_stream->url); - if (dir.len) + if (!dir.len) + autocreate = AUTO_NONE; + if (autocreate != AUTO_NONE) stream = stream_create(bstrdup0(p, dir), flags, NULL, p->global); } if (!stream->is_directory) @@ -492,9 +570,10 @@ static int parse_dir(struct pl_parser *p) talloc_free(opts); } - scan_dir(p, path, dir_stack, 0); + scan_dir(p, path, dir_stack, 0, autocreate); p->add_base = false; + p->pl->autocreated = autocreate != AUTO_NONE; ret = p->pl->num_entries > 0 ? 0 : -1; done: @@ -512,7 +591,12 @@ struct pl_format { const char *const *mime_types; }; -static const struct pl_format formats[] = { +static const struct pl_format dir_formats[] = { + {"directory", parse_dir}, + {0}, +}; + +static const struct pl_format playlist_formats[] = { {"m3u", parse_m3u, MIME_TYPES("audio/mpegurl", "audio/x-mpegurl", "application/x-mpegurl")}, {"ini", parse_ref_init}, @@ -520,14 +604,14 @@ static const struct pl_format formats[] = { MIME_TYPES("audio/x-scpls")}, {"url", parse_url}, {"txt", parse_txt}, - {"directory", parse_dir}, + {0}, }; -static const struct pl_format *probe_pl(struct pl_parser *p) +static const struct pl_format *probe_pl(struct pl_parser *p, const struct pl_format *fmts) { int64_t start = stream_tell(p->s); - for (int n = 0; n < MP_ARRAY_SIZE(formats); n++) { - const struct pl_format *fmt = &formats[n]; + const struct pl_format *fmt = fmts; + while (fmt->name) { stream_seek(p->s, start); if (check_mimetype(p->s, fmt->mime_types)) { MP_VERBOSE(p, "forcing format by mime-type.\n"); @@ -536,10 +620,14 @@ static const struct pl_format *probe_pl(struct pl_parser *p) } if (fmt->parse(p) >= 0) return fmt; + fmt++; } return NULL; } +extern const demuxer_desc_t demuxer_desc_playlist; +extern const demuxer_desc_t demuxer_desc_directory; + static int open_file(struct demuxer *demuxer, enum demux_check check) { if (!demuxer->access_references) @@ -565,9 +653,14 @@ static int open_file(struct demuxer *demuxer, enum demux_check check) p->force = force; p->check_level = check; p->probing = true; - p->create_dir_playlist = !demuxer->params->has_playlist && opts->autocreate_playlist; + p->autocreate_playlist = demuxer->params->allow_playlist_create ? opts->autocreate_playlist : 0; + p->opts = mp_get_config_group(demuxer, demuxer->global, &demux_playlist_conf); - const struct pl_format *fmt = probe_pl(p); + const struct pl_format *fmts = playlist_formats; + if (demuxer->desc == &demuxer_desc_directory) + fmts = dir_formats; + + const struct pl_format *fmt = probe_pl(p, fmts); free_stream(p->s); playlist_clear(p->pl); if (!fmt) { @@ -579,7 +672,6 @@ static int open_file(struct demuxer *demuxer, enum demux_check check) p->error = false; p->s = demuxer->stream; p->utf16 = stream_skip_bom(p->s); - p->opts = mp_get_config_group(demuxer, demuxer->global, &demux_playlist_conf); bool ok = fmt->parse(p) >= 0 && !p->error; if (p->add_base) { bstr proto = mp_split_proto(bstr0(demuxer->filename), NULL); @@ -600,6 +692,12 @@ static int open_file(struct demuxer *demuxer, enum demux_check check) return ok ? 0 : -1; } +const demuxer_desc_t demuxer_desc_directory = { + .name = "directory", + .desc = "Playlist dir", + .open = open_file, +}; + const demuxer_desc_t demuxer_desc_playlist = { .name = "playlist", .desc = "Playlist file", diff --git a/player/loadfile.c b/player/loadfile.c index 53500f4c30..e5791321b3 100644 --- a/player/loadfile.c +++ b/player/loadfile.c @@ -881,6 +881,7 @@ int mp_add_external_file(struct MPContext *mpctx, char *filename, struct demuxer_params params = { .is_top_level = true, .stream_flags = STREAM_ORIGIN_DIRECT, + .allow_playlist_create = false, }; switch (filter) { @@ -1041,11 +1042,9 @@ void prepare_playlist(struct MPContext *mpctx, struct playlist *pl) if (opts->playlist_pos >= 0) pl->current = playlist_entry_from_index(pl, opts->playlist_pos); - for (int i = 0; i < pl->num_entries; ++i) { + for (int i = 0; i < pl->num_entries && pl->autocreated; ++i) { if (!pl->entries[i]->playlist_path) continue; - // If playlist_path exists as an element in the playlist itself, it means - // playlist was autogenerated. if (!strcmp(pl->entries[i]->filename, pl->entries[i]->playlist_path)) { pl->current = pl->entries[i]; break; @@ -1080,6 +1079,7 @@ static void transfer_playlist(struct MPContext *mpctx, struct playlist *pl, playlist_remove(mpctx->playlist, mpctx->playlist->current); if (new) mpctx->playlist->current = new; + mpctx->playlist->autocreated = pl->autocreated; } else { MP_WARN(mpctx, "Empty playlist!\n"); } @@ -1155,7 +1155,8 @@ static MP_THREAD_VOID open_demux_thread(void *ctx) .stream_flags = mpctx->open_url_flags, .stream_record = true, .is_top_level = true, - .has_playlist = mpctx->playlist->num_entries > 1, + .allow_playlist_create = mpctx->playlist->num_entries <= 1 && + !mpctx->playlist->autocreated, }; struct demuxer *demux = demux_open_url(mpctx->open_url, &p, mpctx->open_cancel, mpctx->global);