You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

448 lines
11 KiB

/*****************************************************************************
* ytdl.c:
*****************************************************************************
* Copyright (C) 2019-2020 Rémi Denis-Courmont
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include <errno.h>
#include <math.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include "json/json.h"
#include <vlc_common.h>
#include <vlc_demux.h>
#include <vlc_stream.h>
#include <vlc_fs.h>
#include <vlc_input_item.h>
#include <vlc_plugin.h>
#include <vlc_spawn.h>
#include <vlc_interrupt.h>
struct ytdl_json {
struct vlc_logger *logger;
int fd;
};
void json_parse_error(void *data, const char *msg)
{
struct ytdl_json *sys = data;
vlc_error(sys->logger, "%s", msg);
}
size_t json_read(void *data, void *buf, size_t size)
{
struct ytdl_json *sys = data;
while (!vlc_killed()) {
ssize_t val = vlc_read_i11e(sys->fd, buf, size);
if (val >= 0)
return val;
}
return 0;
}
static int ytdl_popen(pid_t *restrict pid, const char *argv[])
{
int fds[2];
if (vlc_pipe(fds))
return -1;
int fdv[] = { -1, fds[1], 2, -1 };
int val = vlc_spawn(pid, argv[0], fdv, argv);
vlc_close(fds[1]);
if (val) {
vlc_close(fds[0]);
errno = val;
return -1;
}
return fds[0];
}
struct ytdl_playlist {
struct json_object json;
stream_t *source;
};
static int CompareFormats(const struct json_object *f_a,
const struct json_object *f_b, double pref_height)
{
double h_a = json_get_num(f_a, "height");
double abr_a = json_get_num(f_a, "abr");
double h_b = json_get_num(f_b, "height");
double abr_b = json_get_num(f_b, "abr");
/* Prefer non-mute formats */
if (!isnan(abr_a) != !isnan(abr_b))
return isnan(abr_a) ? -1 : +1;
/* Prefer non-blind formats */
if (!isnan(h_a) != !isnan(h_b))
return isnan(h_a) ? -1 : +1;
if (islessequal(h_a, pref_height)) {
if (!islessequal(h_b, pref_height))
return +1;
if (h_a > h_b)
return -1;
if (h_a < h_b)
return +1;
} else {
if (islessequal(h_b, pref_height))
return -1;
if (isless(h_a, h_b))
return -1;
if (isgreater(h_a, h_b))
return +1;
}
if (isgreater(abr_a, abr_b))
return +1;
if (isgreater(abr_b, abr_a))
return -1;
return 0;
}
static const struct json_object *PickFormat(stream_t *s,
const struct json_object *entry)
{
const struct json_value *fmts = json_get(entry, "formats");
if (fmts == NULL)
return entry; /* only one format */
if (fmts->type != JSON_ARRAY)
return NULL;
const struct json_object *best_fmt = NULL;
double pref_height = var_InheritInteger(s, "preferred-resolution");
if (isless(pref_height, 0.))
pref_height = NAN;
for (size_t i = 0; i < fmts->array.size; i++) {
const struct json_value *v = &fmts->array.entries[i];
if (v->type != JSON_OBJECT)
continue;
const struct json_object *fmt = &v->object;
if (best_fmt == NULL) {
best_fmt = fmt;
continue;
}
if (CompareFormats(fmt, best_fmt, pref_height) > 0)
best_fmt = fmt;
}
return best_fmt;
}
static const char *PickArt(const struct json_object *entry)
{
const struct json_value *v = json_get(entry, "thumbnails");
if (v == NULL || v->type != JSON_ARRAY || v->array.size == 0)
return NULL;
v = &v->array.entries[0];
if (v->type != JSON_OBJECT)
return NULL;
return json_get_str(&v->object, "url");
}
static void GetMeta(vlc_meta_t *meta, const struct json_object *json)
{
const char *title = json_get_str(json, "title");
if (title != NULL)
vlc_meta_Set(meta, vlc_meta_Title, title);
const char *desc = json_get_str(json, "description");
if (desc != NULL)
vlc_meta_Set(meta, vlc_meta_Description, desc);
const char *author = json_get_str(json, "uploader");
if (author != NULL)
vlc_meta_Set(meta, vlc_meta_Artist, author);
const char *arturl = PickArt(json);
if (arturl != NULL)
vlc_meta_Set(meta, vlc_meta_ArtworkURL, arturl);
}
static int ReadItem(stream_t *s, input_item_node_t *node,
const struct json_object *json)
{
const struct json_object *fmt = PickFormat(s, json);
if (fmt == NULL)
return VLC_EGENERIC;
const char *url = json_get_str(fmt, "url");
if (url == NULL)
return VLC_EGENERIC;
const char *title = json_get_str(json, "title");
double duration = json_get_num(json, "duration");
vlc_tick_t ticks = isnan(duration) ? INPUT_DURATION_UNSET
: lround(duration * CLOCK_FREQ);
if (title == NULL)
title = url;
input_item_t *item = input_item_NewStream(url, title, ticks);
if (unlikely(item == NULL))
return VLC_ENOMEM;
/* Don't care to lock, the item is still private. */
GetMeta(item->p_meta, json);
input_item_AddOption(item, "no-ytdl", 0);
input_item_node_AppendItem(node, item);
input_item_Release(item);
return VLC_SUCCESS;
}
static int ReadDir(stream_t *s, input_item_node_t *node)
{
struct ytdl_playlist *sys = s->p_sys;
const struct json_value *v = json_get(&sys->json, "entries");
if (v == NULL) /* Single item */
return ReadItem(s, node, &sys->json);
/* Playlist: parse each entry */
if (v->type != JSON_ARRAY)
return VLC_EGENERIC;
for (size_t i = 0; i < v->array.size; i++) {
const struct json_value *e = &v->array.entries[i];
if (e->type == JSON_OBJECT)
ReadItem(s, node, &e->object);
}
return VLC_SUCCESS;
}
static int Control(stream_t *s, int query, va_list args)
{
switch (query)
{
case STREAM_CAN_SEEK:
case STREAM_CAN_FASTSEEK:
case STREAM_CAN_PAUSE:
case STREAM_CAN_CONTROL_PACE:
*va_arg(args, bool *) = false;
break;
case STREAM_GET_TYPE:
*va_arg(args, int *) = ITEM_TYPE_PLAYLIST;
break;
case STREAM_GET_PTS_DELAY:
*va_arg(args, vlc_tick_t *) =
VLC_TICK_FROM_MS(var_InheritInteger(s, "network-caching"));
break;
default:
return VLC_EGENERIC;
}
return VLC_SUCCESS;
}
static int DemuxNested(stream_t *s)
{
struct ytdl_playlist *sys = s->p_sys;
return demux_Demux(sys->source);
}
static int ControlNested(stream_t *s, int query, va_list args)
{
struct ytdl_playlist *sys = s->p_sys;
switch (query) {
case DEMUX_GET_META: {
vlc_meta_t *meta = va_arg(args, vlc_meta_t *);
GetMeta(meta, &sys->json);
return demux_Control(sys->source, query, meta);
}
default:
return demux_vaControl(sys->source, query, args);
}
}
static stream_t *vlc_demux_NewURL(vlc_object_t *obj, const char *url,
es_out_t *out)
{
stream_t *stream = vlc_stream_NewURL(obj, url);
if (stream != NULL) {
demux_t *demux = demux_New(obj, "any", url, stream, out);
if (demux != NULL)
return demux;
vlc_stream_Delete(stream);
}
return NULL;
}
static void Close(vlc_object_t *obj)
{
stream_t *s = (stream_t *)obj;
struct ytdl_playlist *sys = s->p_sys;
if (sys->source != NULL)
vlc_stream_Delete(sys->source);
json_free(&sys->json);
}
static int OpenCommon(vlc_object_t *obj)
{
stream_t *s = (stream_t *)obj;
struct ytdl_playlist *sys = vlc_obj_malloc(obj, sizeof (*sys));
if (unlikely(sys == NULL))
return VLC_EGENERIC;
char *path = config_GetSysPath(VLC_PKG_DATA_DIR, "ytdl-extract.py");
if (unlikely(path == NULL))
return VLC_EGENERIC;
struct ytdl_json jsdata;
pid_t pid;
const char *argv[] = { path, s->psz_url, NULL };
jsdata.logger = s->obj.logger;
jsdata.fd = ytdl_popen(&pid, argv);
if (jsdata.fd == -1) {
msg_Dbg(obj, "cannot start %s: %s", path, vlc_strerror_c(errno));
free(path);
return VLC_EGENERIC;
}
free(path);
int val = json_parse(&jsdata, &sys->json);
kill(pid, SIGTERM);
vlc_close(jsdata.fd);
vlc_waitpid(pid);
if (val) {
/* Location not handled */
msg_Dbg(s, "cannot extract infos");
return VLC_EGENERIC;
}
s->p_sys = sys;
sys->source = NULL;
if (json_get(&sys->json, "entries") != NULL) {
/* Playlist */
s->pf_readdir = ReadDir;
s->pf_control = Control;
return VLC_SUCCESS;
}
/* Redirect if there is a single URL, so that we can refresh it every
* time it is opened.
*/
const struct json_object *fmt = PickFormat(s, &sys->json);
stream_t *demux = NULL;
if (fmt != NULL) {
const char *url = json_get_str(fmt, "url");
if (url != NULL) {
var_Create(obj, "ytdl", VLC_VAR_BOOL);
demux = vlc_demux_NewURL(obj, url, s->out);
if (demux == NULL)
msg_Err(s, "cannot open URL: %s", url);
else
msg_Dbg(s, "redirecting to: %s", url);
}
}
if (demux == NULL) {
json_free(&sys->json);
return VLC_EGENERIC;
}
s->pf_demux = DemuxNested;
s->pf_control = ControlNested;
sys->source = demux;
return VLC_SUCCESS;
}
static int OpenFilter(vlc_object_t *obj)
{
stream_t *s = (stream_t *)obj;
if (s->psz_url == NULL)
return VLC_EGENERIC;
if (strncasecmp(s->psz_url, "http:", 5)
&& strncasecmp(s->psz_url, "https:", 6))
return VLC_EGENERIC;
if (!var_InheritBool(obj, "ytdl"))
return VLC_EGENERIC;
return OpenCommon(obj);
}
vlc_module_begin()
set_shortname("YT-DL")
set_description("YT-DL extractor")
set_subcategory(SUBCAT_INPUT_STREAM_FILTER)
set_capability("demux", 5)
set_callbacks(OpenFilter, Close)
add_bool("ytdl", true, N_("Enable YT-DL"), NULL)
change_safe()
add_submodule()
set_capability("access", 0)
add_shortcut("ytdl")
set_callbacks(OpenCommon, Close)
vlc_module_end()