1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
|
/*
* 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" },
{ CUE_UNUSED, "MCN" },
{ -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: {
if (bstr_find0(param, "AUDIO") == -1)
break;
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)
{
if (f->num_tracks == 0)
return -1;
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;
}
|