Something went wrong. Try again.
🎥 Command line media player
Something went wrong. Try again.
C
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271/* * 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 <stdlib.h>#include <stdbool.h>#include <string.h>#include <inttypes.h>
#include "mpv_talloc.h"
#include "misc/bstr.h"#include "common/common.h"#include "common/tags.h"
#include "cue.h"
#define SECS_PER_CUE_FRAME (1.0/75.0)
enum cue_command { CUE_ERROR = -1, // not a valid CUE command, or an unknown extension CUE_EMPTY, // line with whitespace only CUE_UNUSED, // valid CUE command, but ignored by this code CUE_FILE, CUE_TRACK, CUE_INDEX, CUE_TITLE, CUE_PERFORMER,};
static const struct { enum cue_command command; const char *text;} cue_command_strings[] = { { CUE_FILE, "FILE" }, { CUE_TRACK, "TRACK" }, { CUE_INDEX, "INDEX" }, { CUE_TITLE, "TITLE" }, { CUE_UNUSED, "CATALOG" }, { CUE_UNUSED, "CDTEXTFILE" }, { CUE_UNUSED, "FLAGS" }, { CUE_UNUSED, "ISRC" }, { CUE_PERFORMER, "PERFORMER" }, { CUE_UNUSED, "POSTGAP" }, { CUE_UNUSED, "PREGAP" }, { CUE_UNUSED, "REM" }, { CUE_UNUSED, "SONGWRITER" }, { CUE_UNUSED, "MESSAGE" }, { -1 },};
static const uint8_t spaces[] = {' ', '\f', '\n', '\r', '\t', '\v', 0xA0};
static struct bstr lstrip_whitespace(struct bstr data){ while (data.len) { bstr rest = data; int code = bstr_decode_utf8(data, &rest); if (code < 0) { // Tolerate Latin1 => probing works (which doesn't convert charsets). code = data.start[0]; rest.start += 1; rest.len -= 1; } for (size_t n = 0; n < MP_ARRAY_SIZE(spaces); n++) { if (spaces[n] == code) { data = rest; goto next; } } break; next: ; } return data;}
static enum cue_command read_cmd(struct bstr *data, struct bstr *out_params){ struct bstr line = bstr_strip_linebreaks(bstr_getline(*data, data)); line = lstrip_whitespace(line); if (line.len == 0) return CUE_EMPTY; for (int n = 0; cue_command_strings[n].command != -1; n++) { struct bstr name = bstr0(cue_command_strings[n].text); if (bstr_case_startswith(line, name)) { struct bstr rest = bstr_cut(line, name.len); struct bstr par = lstrip_whitespace(rest); if (rest.len && par.len == rest.len) continue; if (out_params) *out_params = par; return cue_command_strings[n].command; } } return CUE_ERROR;}
static bool eat_char(struct bstr *data, char ch){ if (data->len && data->start[0] == ch) { *data = bstr_cut(*data, 1); return true; } else { return false; }}
static char *read_quoted(void *talloc_ctx, struct bstr *data){ *data = lstrip_whitespace(*data); if (!eat_char(data, '"')) return NULL; int end = bstrchr(*data, '"'); if (end < 0) return NULL; struct bstr res = bstr_splice(*data, 0, end); *data = bstr_cut(*data, end + 1); return bstrto0(talloc_ctx, res);}
static struct bstr strip_quotes(struct bstr data){ bstr s = data; if (bstr_eatstart0(&s, "\"") && bstr_eatend0(&s, "\"")) return s; return data;}
// Read an unsigned decimal integer.// Optionally check if it is 2 digit.// Return -1 on failure.static int read_int(struct bstr *data, bool two_digit){ *data = lstrip_whitespace(*data); if (data->len && data->start[0] == '-') return -1; struct bstr s = *data; int res = (int)bstrtoll(s, &s, 10); if (data->len == s.len || (two_digit && data->len - s.len > 2)) return -1; *data = s; return res;}
static double read_time(struct bstr *data){ struct bstr s = *data; bool ok = true; double t1 = read_int(&s, false); ok = eat_char(&s, ':') && ok; double t2 = read_int(&s, true); ok = eat_char(&s, ':') && ok; double t3 = read_int(&s, true); ok = ok && t1 >= 0 && t2 >= 0 && t3 >= 0; return ok ? t1 * 60.0 + t2 + t3 * SECS_PER_CUE_FRAME : 0;}
static struct bstr skip_utf8_bom(struct bstr data){ return bstr_startswith0(data, "\xEF\xBB\xBF") ? bstr_cut(data, 3) : data;}
// Check if the text in data is most likely CUE data. This is used by the// demuxer code to check the file type.// data is the start of the probed file, possibly cut off at a random point.bool mp_probe_cue(struct bstr data){ bool valid = false; data = skip_utf8_bom(data); for (;;) { enum cue_command cmd = read_cmd(&data, NULL); // End reached. Since the line was most likely cut off, don't use the // result of the last parsing call. if (data.len == 0) break; if (cmd == CUE_ERROR) return false; if (cmd != CUE_EMPTY) valid = true; } return valid;}
struct cue_file *mp_parse_cue(struct bstr data){ struct cue_file *f = talloc_zero(NULL, struct cue_file); f->tags = talloc_zero(f, struct mp_tags);
data = skip_utf8_bom(data);
char *filename = NULL; // Global metadata, and copied into new tracks. struct cue_track proto_track = {0}; struct cue_track *cur_track = NULL;
while (data.len) { struct bstr param; int cmd = read_cmd(&data, ¶m); switch (cmd) { case CUE_ERROR: talloc_free(f); return NULL; case CUE_TRACK: { MP_TARRAY_GROW(f, f->tracks, f->num_tracks); f->num_tracks += 1; cur_track = &f->tracks[f->num_tracks - 1]; *cur_track = proto_track; cur_track->tags = talloc_zero(f, struct mp_tags); break; } case CUE_TITLE: case CUE_PERFORMER: { static const char *metanames[] = { [CUE_TITLE] = "title", [CUE_PERFORMER] = "performer", }; struct mp_tags *tags = cur_track ? cur_track->tags : f->tags; mp_tags_set_bstr(tags, bstr0(metanames[cmd]), strip_quotes(param)); break; } case CUE_INDEX: { int type = read_int(¶m, true); double time = read_time(¶m); if (cur_track) { if (type == 1) { cur_track->start = time; cur_track->filename = filename; } else if (type == 0) { cur_track->pregap_start = time; } } break; } case CUE_FILE: // NOTE: FILE comes before TRACK, so don't use cur_track->filename filename = read_quoted(f, ¶m); break; } }
return f;}
int mp_check_embedded_cue(struct cue_file *f){ char *fn0 = f->tracks[0].filename; for (int n = 1; n < f->num_tracks; n++) { char *fn = f->tracks[n].filename; // both filenames have the same address (including NULL) if (fn0 == fn) continue; // only one filename is NULL, or the strings don't match if (!fn0 || !fn || strcmp(fn0, fn) != 0) return -1; } return 0;}