[vlc-commits] ytdl: stream filter module for YoutubeDL

Rémi Denis-Courmont git at videolan.org
Sun Sep 27 15:16:40 CEST 2020


vlc | branch: master | Rémi Denis-Courmont <remi at remlab.net> | Sun Sep 20 21:46:41 2020 +0300| [4fa60bf98d9697c09a412c35a38a4a460044fa2a] | committer: Rémi Denis-Courmont

ytdl: stream filter module for YoutubeDL

This passes every HTTP(S) URL through YoutubeDL to extract playlists
or media.

> http://git.videolan.org/gitweb.cgi/vlc.git/?a=commit;h=4fa60bf98d9697c09a412c35a38a4a460044fa2a
---

 NEWS                      |   1 +
 modules/demux/Makefile.am |  10 +-
 modules/demux/ytdl.c      | 270 ++++++++++++++++++++++++++++++++++++++++++++++
 po/POTFILES.in            |   1 +
 4 files changed, 281 insertions(+), 1 deletion(-)

diff --git a/NEWS b/NEWS
index 9af6a77b8f..7eb1a72567 100644
--- a/NEWS
+++ b/NEWS
@@ -65,6 +65,7 @@ Access:
  * Audio CD data tracks are now correctly detected and skipped
  * Deprecates Audio CD CDDB lookups in favor of more accurate Musicbrainz
  * Improved CD-TEXT and added Shift-JIS encoding support
+ * Support for YoutubeDL (where available).
 
 Access output:
  * Added support for the RIST (Reliable Internet Stream Transport) Protocol
diff --git a/modules/demux/Makefile.am b/modules/demux/Makefile.am
index d7b8df6ece..88075b06a0 100644
--- a/modules/demux/Makefile.am
+++ b/modules/demux/Makefile.am
@@ -505,6 +505,14 @@ libadaptive_plugin_la_LIBADD += $(GCRYPT_LIBS)
 endif
 demux_LTLIBRARIES += libadaptive_plugin.la
 
+libytdl_plugin_la_SOURCES = demux/ytdl.c
+libytdl_plugin_la_LIBADD = libvlc_json.la
+if !HAVE_WIN32
+if !HAVE_ANDROID
+demux_LTLIBRARIES += libytdl_plugin.la
+endif
+endif
+
 libnoseek_plugin_la_SOURCES = demux/filter/noseek.c
 demux_LTLIBRARIES += libnoseek_plugin.la
 
@@ -519,6 +527,6 @@ libvlc_json_la_SOURCES = \
 	demux/json/grammar.y \
 	demux/json/json.c demux/json/json.h
 libvlc_json_la_CPPFLAGS = $(AM_CPPFLAGS) -I$(srcdir)/demux/json
-libvlc_json_la_LIBADD = $(LTLIBVLCCORE) ../compat/libcompat.la
+libvlc_json_la_LIBADD = $(LTLIBVLCCORE) ../compat/libcompat.la $(LIBM)
 libvlc_json_la_LDFLAGS = -static
 noinst_LTLIBRARIES += libvlc_json.la
diff --git a/modules/demux/ytdl.c b/modules/demux/ytdl.c
new file mode 100644
index 0000000000..d0066eda24
--- /dev/null
+++ b/modules/demux/ytdl.c
@@ -0,0 +1,270 @@
+/*****************************************************************************
+ * 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_stream.h>
+#include <vlc_fs.h>
+#include <vlc_input_item.h>
+#include <vlc_plugin.h>
+#include <vlc_spawn.h>
+
+void json_parse_error(void *data, const char *msg)
+{
+    struct vlc_logger *log = data;
+
+    vlc_error(log, "%s", msg);
+}
+
+static
+FILE *vlc_popen(pid_t *restrict pid, const char *argv[])
+{
+    int fds[2];
+
+    if (vlc_pipe(fds))
+        return NULL;
+
+    FILE *input = fdopen(fds[0], "rt");
+
+    if (input == NULL) {
+        vlc_close(fds[1]);
+        vlc_close(fds[0]);
+        return NULL;
+    }
+
+    int fdv[] = { -1, fds[1], 2, -1 };
+    int val = vlc_spawn(pid, argv[0], fdv, argv);
+
+    vlc_close(fds[1]);
+
+    if (val) {
+        fclose(input);
+        input = NULL;
+        errno = val;
+    }
+
+    return input;
+}
+
+struct ytdl_playlist {
+    struct json_object json;
+};
+
+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");
+     double best_height = -1.;
+     double best_abr = -1.;
+
+     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;
+         double height = json_get_num(fmt, "height");
+         double abr = json_get_num(fmt, "abr");
+
+         if (!isgreaterequal(height, best_height)
+          || (best_height < pref_height && pref_height < height))
+             continue;
+
+         if (!isgreaterequal(abr, best_abr))
+             continue;
+
+         best_fmt = fmt;
+         best_height = height;
+         best_abr = abr;
+     }
+
+     return best_fmt;
+}
+
+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 (likely(item != NULL)) {
+        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 void Close(vlc_object_t *obj)
+{
+    stream_t *s = (stream_t *)obj;
+    struct ytdl_playlist *sys = s->p_sys;
+
+    json_free(&sys->json);
+}
+
+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(s, "ytdl"))
+        return VLC_EGENERIC;
+
+    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;
+
+    pid_t pid;
+    const char *argv[] = { path, s->psz_url, NULL };
+    FILE *input = vlc_popen(&pid, argv);
+
+    if (input == NULL) {
+        msg_Dbg(obj, "cannot start %s: %s", path, vlc_strerror_c(errno));
+        free(path);
+        return VLC_EGENERIC;
+    }
+
+    free(path);
+
+    int val = json_parse(obj->logger, input, &sys->json);
+
+    kill(pid, SIGTERM);
+    fclose(input);
+    vlc_waitpid(pid);
+
+    if (val) {
+        /* Location not handled */
+        msg_Dbg(s, "cannot extract infos");
+        return VLC_EGENERIC;
+    }
+
+    s->p_sys = sys;
+    s->pf_readdir = ReadDir;
+    s->pf_control = Control;
+    return VLC_SUCCESS;
+}
+
+vlc_module_begin()
+    set_shortname("YT-DL")
+    set_description("YoutubeDL")
+    set_category(CAT_INPUT)
+    set_subcategory(SUBCAT_INPUT_STREAM_FILTER)
+    set_capability("stream_filter", 305)
+    set_callbacks(OpenFilter, Close)
+    /* TODO: convert to demux and enable by default */
+    add_bool("ytdl", false, N_("Enable YT-DL"), N_("Enable YT-DL"), true)
+        change_safe()
+vlc_module_end()
diff --git a/po/POTFILES.in b/po/POTFILES.in
index d5ce933408..0069a2b6db 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -435,6 +435,7 @@ modules/demux/xa.c
 modules/demux/xiph.h
 modules/demux/xiph_metadata.c
 modules/demux/xiph_metadata.h
+modules/demux/ytdl.c
 modules/gui/macosx/coreinteraction/VLCHotkeysController.h
 modules/gui/macosx/coreinteraction/VLCHotkeysController.m
 modules/gui/macosx/coreinteraction/VLCVideoFilterHelper.h



More information about the vlc-commits mailing list