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 <dirent.h>
19 #include <string.h>
20 #include <strings.h>
21 #include <stdlib.h>
22 #include <assert.h>
23
24 #include "osdep/io.h"
25
26 #include "common/common.h"
27 #include "common/global.h"
28 #include "common/msg.h"
29 #include "misc/ctype.h"
30 #include "misc/charset_conv.h"
31 #include "options/options.h"
32 #include "options/path.h"
33 #include "external_files.h"
34
35 static const char *const sub_exts[] = {"utf", "utf8", "utf-8", "idx", "sub",
36 "srt", "rt", "ssa", "ass", "mks", "vtt",
37 "sup", "scc", "smi", "lrc", "pgs",
38 NULL};
39
40 static const char *const audio_exts[] = {"mp3", "aac", "mka", "dts", "flac",
41 "ogg", "m4a", "ac3", "opus", "wav",
42 "wv", "eac3",
43 NULL};
44
45 static const char *const image_exts[] = {"jpg", "jpeg", "png", "gif", "bmp",
46 "webp",
47 NULL};
48
49 // Stolen from: vlc/-/blob/master/modules/meta_engine/folder.c#L40
50 // sorted by priority (descending)
51 static const char *const cover_files[] = {
52 "AlbumArt.jpg",
53 "Album.jpg",
54 "cover.jpg",
55 "cover.png",
56 "front.jpg",
57 "front.png",
58
59 "AlbumArtSmall.jpg",
60 "Folder.jpg",
61 "Folder.png",
62 ".folder.png",
63 "thumb.jpg",
64
65 "front.bmp",
66 "front.gif",
67 "cover.gif",
68 NULL
69 };
70
test_ext_list(bstr ext,const char * const * list)71 static bool test_ext_list(bstr ext, const char *const *list)
72 {
73 for (int n = 0; list[n]; n++) {
74 if (bstrcasecmp(bstr0(list[n]), ext) == 0)
75 return true;
76 }
77 return false;
78 }
79
test_ext(bstr ext)80 static int test_ext(bstr ext)
81 {
82 if (test_ext_list(ext, sub_exts))
83 return STREAM_SUB;
84 if (test_ext_list(ext, audio_exts))
85 return STREAM_AUDIO;
86 if (test_ext_list(ext, image_exts))
87 return STREAM_VIDEO;
88 return -1;
89 }
90
test_cover_filename(bstr fname)91 static int test_cover_filename(bstr fname)
92 {
93 for (int n = 0; cover_files[n]; n++) {
94 if (bstrcasecmp(bstr0(cover_files[n]), fname) == 0) {
95 return MP_ARRAY_SIZE(cover_files) - n;
96 }
97 }
98 return 0;
99 }
100
mp_might_be_subtitle_file(const char * filename)101 bool mp_might_be_subtitle_file(const char *filename)
102 {
103 return test_ext(bstr_get_ext(bstr0(filename))) == STREAM_SUB;
104 }
105
compare_sub_filename(const void * a,const void * b)106 static int compare_sub_filename(const void *a, const void *b)
107 {
108 const struct subfn *s1 = a;
109 const struct subfn *s2 = b;
110 return strcoll(s1->fname, s2->fname);
111 }
112
compare_sub_priority(const void * a,const void * b)113 static int compare_sub_priority(const void *a, const void *b)
114 {
115 const struct subfn *s1 = a;
116 const struct subfn *s2 = b;
117 if (s1->priority > s2->priority)
118 return -1;
119 if (s1->priority < s2->priority)
120 return 1;
121 return strcoll(s1->fname, s2->fname);
122 }
123
guess_lang_from_filename(struct bstr name,int * fn_start)124 static struct bstr guess_lang_from_filename(struct bstr name, int *fn_start)
125 {
126 if (name.len < 2)
127 return (struct bstr){NULL, 0};
128
129 int n = 0;
130 int i = name.len - 1;
131
132 char thing = '.';
133 if (name.start[i] == ')') {
134 thing = '(';
135 i--;
136 }
137 if (name.start[i] == ']') {
138 thing = '[';
139 i--;
140 }
141
142 while (i >= 0 && mp_isalpha(name.start[i])) {
143 n++;
144 if (n > 3)
145 return (struct bstr){NULL, 0};
146 i--;
147 }
148
149 if (n < 2 || i == 0 || name.start[i] != thing)
150 return (struct bstr){NULL, 0};
151
152 *fn_start = i;
153 return (struct bstr){name.start + i + 1, n};
154 }
155
append_dir_subtitles(struct mpv_global * global,struct MPOpts * opts,struct subfn ** slist,int * nsub,struct bstr path,const char * fname,int limit_fuzziness,int limit_type)156 static void append_dir_subtitles(struct mpv_global *global, struct MPOpts *opts,
157 struct subfn **slist, int *nsub,
158 struct bstr path, const char *fname,
159 int limit_fuzziness, int limit_type)
160 {
161 void *tmpmem = talloc_new(NULL);
162 struct mp_log *log = mp_log_new(tmpmem, global->log, "find_files");
163
164 struct bstr f_fbname = bstr0(mp_basename(fname));
165 struct bstr f_fname = mp_iconv_to_utf8(log, f_fbname,
166 "UTF-8-MAC", MP_NO_LATIN1_FALLBACK);
167 struct bstr f_fname_noext = bstrdup(tmpmem, bstr_strip_ext(f_fname));
168 bstr_lower(f_fname_noext);
169 struct bstr f_fname_trim = bstr_strip(f_fname_noext);
170
171 if (f_fbname.start != f_fname.start)
172 talloc_steal(tmpmem, f_fname.start);
173
174 char *path0 = bstrdup0(tmpmem, path);
175
176 if (mp_is_url(bstr0(path0)))
177 goto out;
178
179 DIR *d = opendir(path0);
180 if (!d)
181 goto out;
182 mp_verbose(log, "Loading external files in %.*s\n", BSTR_P(path));
183 struct dirent *de;
184 while ((de = readdir(d))) {
185 void *tmpmem2 = talloc_new(tmpmem);
186 struct bstr den = bstr0(de->d_name);
187 struct bstr dename = mp_iconv_to_utf8(log, den,
188 "UTF-8-MAC", MP_NO_LATIN1_FALLBACK);
189 // retrieve various parts of the filename
190 struct bstr tmp_fname_noext = bstrdup(tmpmem2, bstr_strip_ext(dename));
191 bstr_lower(tmp_fname_noext);
192 struct bstr tmp_fname_ext = bstr_get_ext(dename);
193 struct bstr tmp_fname_trim = bstr_strip(tmp_fname_noext);
194
195 if (den.start != dename.start)
196 talloc_steal(tmpmem2, dename.start);
197
198 // check what it is (most likely)
199 int type = test_ext(tmp_fname_ext);
200 char **langs = NULL;
201 int fuzz = -1;
202 switch (type) {
203 case STREAM_SUB:
204 langs = opts->stream_lang[type];
205 fuzz = opts->sub_auto;
206 break;
207 case STREAM_AUDIO:
208 langs = opts->stream_lang[type];
209 fuzz = opts->audiofile_auto;
210 break;
211 case STREAM_VIDEO:
212 fuzz = opts->coverart_auto;
213 break;
214 }
215
216 if (fuzz < 0 || (limit_type >= 0 && limit_type != type))
217 goto next_sub;
218
219 // we have a (likely) subtitle file
220 // higher prio -> auto-selection may prefer it (0 = not loaded)
221 int prio = 0;
222
223 if (bstrcmp(tmp_fname_trim, f_fname_trim) == 0)
224 prio |= 32; // exact movie name match
225
226 bstr lang = {0};
227 if (bstr_startswith(tmp_fname_trim, f_fname_trim)) {
228 int start = 0;
229 lang = guess_lang_from_filename(tmp_fname_trim, &start);
230
231 if (lang.len && start == f_fname_trim.len)
232 prio |= 16; // exact movie name + followed by lang
233 }
234
235 for (int n = 0; langs && langs[n]; n++) {
236 if (lang.len && bstr_case_startswith(lang, bstr0(langs[n]))) {
237 if (fuzz >= 1)
238 prio |= 8; // known language -> boost priority
239 break;
240 }
241 }
242
243 if (lang.len && fuzz >= 1)
244 prio |= 4; // matches the movie name + a language was matched
245
246 if (bstr_find(tmp_fname_trim, f_fname_trim) >= 0 && fuzz >= 1)
247 prio |= 2; // contains the movie name
248
249 if (type == STREAM_VIDEO && fuzz >= 1 && prio == 0)
250 prio = test_cover_filename(dename);
251
252 // doesn't contain the movie name
253 // don't try in the mplayer subtitle directory
254 if (!limit_fuzziness && fuzz >= 2)
255 prio |= 1;
256
257 mp_dbg(log, "Potential external file: \"%s\" Priority: %d\n",
258 de->d_name, prio);
259
260 if (prio) {
261 char *subpath = mp_path_join_bstr(*slist, path, dename);
262 if (mp_path_exists(subpath)) {
263 MP_TARRAY_GROW(NULL, *slist, *nsub);
264 struct subfn *sub = *slist + (*nsub)++;
265
266 // annoying and redundant
267 if (strncmp(subpath, "./", 2) == 0)
268 subpath += 2;
269
270 sub->type = type;
271 sub->priority = prio;
272 sub->fname = subpath;
273 sub->lang = lang.len ? bstrdup0(*slist, lang) : NULL;
274 } else
275 talloc_free(subpath);
276 }
277
278 next_sub:
279 talloc_free(tmpmem2);
280 }
281 closedir(d);
282
283 out:
284 talloc_free(tmpmem);
285 }
286
case_endswith(const char * s,const char * end)287 static bool case_endswith(const char *s, const char *end)
288 {
289 size_t len = strlen(s);
290 size_t elen = strlen(end);
291 return len >= elen && strcasecmp(s + len - elen, end) == 0;
292 }
293
294 // Drop .sub file if .idx file exists.
295 // Assumes slist is sorted by compare_sub_filename.
filter_subidx(struct subfn ** slist,int * nsub)296 static void filter_subidx(struct subfn **slist, int *nsub)
297 {
298 const char *prev = NULL;
299 for (int n = 0; n < *nsub; n++) {
300 const char *fname = (*slist)[n].fname;
301 if (case_endswith(fname, ".idx")) {
302 prev = fname;
303 } else if (case_endswith(fname, ".sub")) {
304 if (prev && strncmp(prev, fname, strlen(fname) - 4) == 0)
305 (*slist)[n].priority = -1;
306 }
307 }
308 for (int n = *nsub - 1; n >= 0; n--) {
309 if ((*slist)[n].priority < 0)
310 MP_TARRAY_REMOVE_AT(*slist, *nsub, n);
311 }
312 }
313
load_paths(struct mpv_global * global,struct MPOpts * opts,struct subfn ** slist,int * nsubs,const char * fname,char ** paths,char * cfg_path,int type)314 static void load_paths(struct mpv_global *global, struct MPOpts *opts,
315 struct subfn **slist, int *nsubs, const char *fname,
316 char **paths, char *cfg_path, int type)
317 {
318 for (int i = 0; paths && paths[i]; i++) {
319 char *expanded_path = mp_get_user_path(NULL, global, paths[i]);
320 char *path = mp_path_join_bstr(
321 *slist, mp_dirname(fname),
322 bstr0(expanded_path ? expanded_path : paths[i]));
323 append_dir_subtitles(global, opts, slist, nsubs, bstr0(path),
324 fname, 0, type);
325 talloc_free(expanded_path);
326 }
327
328 // Load subtitles in ~/.mpv/sub (or similar) limiting sub fuzziness
329 char *mp_subdir = mp_find_config_file(NULL, global, cfg_path);
330 if (mp_subdir) {
331 append_dir_subtitles(global, opts, slist, nsubs, bstr0(mp_subdir),
332 fname, 1, type);
333 }
334 talloc_free(mp_subdir);
335 }
336
337 // Return a list of subtitles and audio files found, sorted by priority.
338 // Last element is terminated with a fname==NULL entry.
find_external_files(struct mpv_global * global,const char * fname,struct MPOpts * opts)339 struct subfn *find_external_files(struct mpv_global *global, const char *fname,
340 struct MPOpts *opts)
341 {
342 struct subfn *slist = talloc_array_ptrtype(NULL, slist, 1);
343 int n = 0;
344
345 // Load subtitles from current media directory
346 append_dir_subtitles(global, opts, &slist, &n, mp_dirname(fname), fname, 0, -1);
347
348 // Load subtitles in dirs specified by sub-paths option
349 if (opts->sub_auto >= 0) {
350 load_paths(global, opts, &slist, &n, fname, opts->sub_paths, "sub",
351 STREAM_SUB);
352 }
353
354 if (opts->audiofile_auto >= 0) {
355 load_paths(global, opts, &slist, &n, fname, opts->audiofile_paths,
356 "audio", STREAM_AUDIO);
357 }
358
359 // Sort by name for filter_subidx()
360 qsort(slist, n, sizeof(*slist), compare_sub_filename);
361
362 filter_subidx(&slist, &n);
363
364 // Sort subs by priority and append them
365 qsort(slist, n, sizeof(*slist), compare_sub_priority);
366
367 struct subfn z = {0};
368 MP_TARRAY_APPEND(NULL, slist, n, z);
369
370 return slist;
371 }
372