Skip to content

Instantly share code, notes, and snippets.

@Daniel-Mendes
Last active May 8, 2024 13:27
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Daniel-Mendes/21cff1d94b584769fa05a4905bd272b5 to your computer and use it in GitHub Desktop.
Save Daniel-Mendes/21cff1d94b584769fa05a4905bd272b5 to your computer and use it in GitHub Desktop.
VLC: Youtube Playlist Parser
--[[
Youtube playlist parser for VLC media player v1.0.2
Copyright © 2021 Daniel Mendes
Author: Daniel Mendes
Contact: contact@daniel-mendes.ch
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 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 General Public License for more details.
You should have received a copy of the GNU 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.
--]]
-- Helper function to get a parameter's value in a URL
function get_url_param( url, name )
local _, _, res = string.find( url, "[&?]"..name.."=([^&]*)" )
return res
end
-- Helper emulating vlc.readline() to work around its failure on
-- very long lines (see #24957)
function read_long_line()
local eol
local pos = 0
local len = 32768
repeat
len = len * 2
local line = vlc.peek( len )
if not line then return nil end
eol = string.find( line, "\n", pos + 1 )
pos = len
until eol or len >= 1024 * 1024 -- No EOF detection, loop until limit
return vlc.read( eol or len )
end
-- Unescape Hexadecimal representation
function unescape (s)
s = string.gsub(s or '', "\\x(%x%x)", function (h) return string.char(tonumber(h,16)) end)
return s
end
-- convert a duration to seconds
function durationInSeconds(duration)
local hour, min, sec = string.match(duration, "(%d?%d?):?(%d?%d):(%d%d)")
if hour == "" then
hour = 0
end
if min == "" then
min = 0
end
if sec == "" then
sec = 0
end
return hour*60*60 + min*60 + sec
end
-- Probe function.
function probe()
if vlc.access == "http" and vlc.access == "https" then
return false
end
return ( ( vlc.access == "http" or vlc.access == "https" ) and (
((
string.match( vlc.path, "^www%.youtube%.com/" )
or string.match( vlc.path, "^music%.youtube%.com/" ) -- out of use
) and (
string.match(vlc.path, "[?&]list=") -- video page with playlist
or string.match( vlc.path, "/playlist%?" ) -- playlist page
)) or
string.match( vlc.path, "^consent%.youtube%.com/" )
) )
end
-- Parse function.
function parse()
if string.match( vlc.path, "^consent%.youtube%.com/" ) then
vlc.msg.info("type: consent.youtube.com")
-- Cookie consent redirection
-- Location: https://consent.youtube.com/m?continue=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DXXXXXXXXXXX&gl=FR&m=0&pc=yt&uxe=23983172&hl=fr&src=1
-- Set-Cookie: CONSENT=PENDING+355; expires=Fri, 01-Jan-2038 00:00:00 GMT; path=/; domain=.youtube.com
local url = get_url_param( vlc.path, "continue" )
if not url then
vlc.msg.err( "Couldn't handle YouTube cookie consent redirection, please check for updates to this script or try disabling HTTP cookie forwarding" )
return { }
end
return { { path = vlc.strings.decode_uri( url ), options = { ":no-http-forward-cookies" } } }
elseif string.match( vlc.path, "^www.youtube.com/playlist%?list=" ) then
vlc.msg.info("type: youtube.com/playlist?list=")
local playlist = {}
local item, lines, line
local index = 1
local playlistID = get_url_param( vlc.path, "list" )
while true do
lines = ""
line = ""
while line do
lines = lines..line
line = read_long_line()
if not line then break end
end
if string.match(lines, 'var ytInitialData = ') then
local index_start, index_end, group1, group2, group3 = string.find(lines, '(var ytInitialData = )(.*)(;</script><link rel=)')
local json = require('dkjson')
local jsonParsed = json.decode (group2)
for key, video in ipairs(jsonParsed.contents.twoColumnBrowseResultsRenderer.tabs[1].tabRenderer.content.sectionListRenderer.contents[1].itemSectionRenderer.contents[1].playlistVideoListRenderer.contents) do
item = nil
item = {}
-- If continuation playlist
if video.continuationItemRenderer then
else
item.path = "https://www.youtube.com/watch?v="..video.playlistVideoRenderer.videoId
item.title = video.playlistVideoRenderer.title.runs[1].text
item.duration = video.playlistVideoRenderer.lengthSeconds
item.author = video.playlistVideoRenderer.shortBylineText.runs[1].text
item.arturl = video.playlistVideoRenderer.thumbnail.thumbnails[3].url
table.insert (playlist, item)
end
end
return playlist
end
end
elseif string.match(vlc.path, "(www.youtube.com/watch?).*([?&]list=)") then
vlc.msg.info("type: youtube.com/watch?list=")
local playlist = {}
local item, lines, line
local index = 1
local playlistID = get_url_param( vlc.path, "list" )
local videoID = get_url_param( vlc.path, "v" )
while true do
lines = ""
line = ""
while line do
lines = lines..line
line = read_long_line()
if not line then break end
end
if string.match(lines, 'var ytInitialData = ') then
local index_start, index_end, group1, group2, group3 = string.find(lines, '(var ytInitialData = )(.*)(;</script><script nonce=")')
local json = require('dkjson')
local jsonParsed = json.decode (group2)
for key, video in ipairs(jsonParsed.contents.twoColumnWatchNextResults.playlist.playlist.contents) do
item = nil
item = {}
item.path = "https://www.youtube.com/watch?v="..video.playlistPanelVideoRenderer.videoId
item.title = video.playlistPanelVideoRenderer.title.simpleText
item.duration = durationInSeconds(video.playlistPanelVideoRenderer.lengthText.simpleText)
item.author = video.playlistPanelVideoRenderer.shortBylineText.runs[1].text
item.arturl = video.playlistPanelVideoRenderer.thumbnail.thumbnails[4].url
table.insert (playlist, item)
end
return playlist
end
end
elseif string.match( vlc.path, "^music%.youtube%.com/playlist%?list=" ) then
vlc.msg.info("type: music.youtube.com/playlist?list=")
elseif string.match(vlc.path, "(music.youtube.com/watch?).*([?&]list=)") then
vlc.msg.info("type: music.youtube.com/watch?list=")
end
end
@Seneral
Copy link

Seneral commented Jun 8, 2021

Looks good! Haven't tested but since you only load once, you only support up to 200 videos correct?
If you ever want to load more, I got some javascript code that parses the continuationToken from the page to successively load full playlists here:
https://github.com/Seneral/FlagPlayer/blob/951df5917c1e3c50af96fcaccf12d684d4ed60bf/page/page.js#L1889
A lot of the actual code is abstracted in here since that same continuation logic is used for a bunch of other stuff on the youtube page: https://github.com/Seneral/FlagPlayer/blob/951df5917c1e3c50af96fcaccf12d684d4ed60bf/page/page.js#L1825

@NeuroCPP
Copy link

NeuroCPP commented Jun 18, 2021

Looks good! Haven't tested but since you only load once, you only support up to 200 videos correct?
If you ever want to load more, I got some javascript code that parses the continuationToken from the page to successively load full playlists here:
https://github.com/Seneral/FlagPlayer/blob/951df5917c1e3c50af96fcaccf12d684d4ed60bf/page/page.js#L1889
A lot of the actual code is abstracted in here since that same continuation logic is used for a bunch of other stuff on the youtube page: https://github.com/Seneral/FlagPlayer/blob/951df5917c1e3c50af96fcaccf12d684d4ed60bf/page/page.js#L1825

Hello Seneral , can I ask you to compile this and fork this up please
or make a revision(if possible) or post it in your gist

@Seneral
Copy link

Seneral commented Jun 18, 2021

Sorry I'm not using this myself, and I really don't have the time for it rn

@Daniel-Mendes
Copy link
Author

Daniel-Mendes commented Jun 21, 2021

Looks good! Haven't tested but since you only load once, you only support up to 200 videos correct?
If you ever want to load more, I got some javascript code that parses the continuationToken from the page to successively load full playlists here:
https://github.com/Seneral/FlagPlayer/blob/951df5917c1e3c50af96fcaccf12d684d4ed60bf/page/page.js#L1889
A lot of the actual code is abstracted in here since that same continuation logic is used for a bunch of other stuff on the youtube page: https://github.com/Seneral/FlagPlayer/blob/951df5917c1e3c50af96fcaccf12d684d4ed60bf/page/page.js#L1825

Firstly, thanks for your feedback.

That's true, I tried and my code only loads the 100 first videos.

I tried to implement the query to load the next videos, but with vlc.stream() you can only pass an url in params and can't pass a request body with the continuationToken and clickTrackingParams, so I don't know how I can do it ? (do you have an idea ?)

@NeuroCPP
Copy link

Sorry I'm not using this myself, and I really don't have the time for it rn

No Problem Brother you might be busy in studying... Stay Strong..

@Seneral
Copy link

Seneral commented Jun 22, 2021

Firstly, thanks for your feedback.

That's true, I tried and my code only loads the 100 first videos.

I tried to implement the query to load the next videos, but with vlc.stream() you can only pass an url in params and can't pass a request body with the continuationToken and clickTrackingParams, so I don't know how I can do it ? (do you have an idea ?)

Ahh I do remember having problems with the VLC Api before...
Previous discussion: https://gist.github.com/Seneral/bd64281ae30a6010e97d956ecd63238e#gistcomment-3631488
Apparently POST is also not supported. So no way to use the YouTube API directly from within VLC, the new unified browse API is completely based on POST requests with data in the body. So unless you recompile VLC or they add more control to the web requests interface, a proper YouTube plugin without external programs is impossible.
That's why the other version had to use youtube-dl

@Daniel-Mendes
Copy link
Author

Daniel-Mendes commented Jun 23, 2021

I tried using &index=200 in the url to load the next videos.

But the vlc.stream() don't seems to work and get the source code of the page, so I don't know.

Code inside function extractAllPlaylistVideos(url)

--[[
 Youtube playlist parser for VLC media player v1.0.2
 Copyright © 2021 Daniel Mendes

 Author: Daniel Mendes
 Contact: contact@daniel-mendes.ch

 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 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 General Public License for more details.

 You should have received a copy of the GNU 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.
--]]
 
-- Helper function to get a parameter's value in a URL
function get_url_param( url, name )
    local _, _, res = string.find( url, "[&?]"..name.."=([^&]*)" )
    return res
end

-- Helper emulating vlc.readline() to work around its failure on
-- very long lines (see #24957)
function read_long_line()
    local eol
    local pos = 0
    local len = 32768
    repeat
        len = len * 2
        local line = vlc.peek( len )
        if not line then return nil end
        eol = string.find( line, "\n", pos + 1 )
        pos = len
    until eol or len >= 1024 * 1024 -- No EOF detection, loop until limit
    return vlc.read( eol or len )
end

-- Unescape Hexadecimal representation
function unescape (s)
    s = string.gsub(s or '', "\\x(%x%x)", function (h) return string.char(tonumber(h,16)) end)
    return s
end

-- convert a duration to seconds
function durationToSeconds(duration)
    local min, sec = string.match(duration, "(%d+):(%d+)")
    return min*60 + sec
end

-- extract all the playlist videos
function extractAllPlaylistVideos(url)

    local playlist = {}
    local item, lines, line, totalVideos, playlistSize
    local videoID, playlistID, index

    while true do
        lines = ""
        line = ""
        local s = nil

        playlistID = get_url_param( url, "list" )
        videoID = get_url_param( url, "v" )
        index = get_url_param( url, "index" )

        s = vlc.stream('http://'..url)

        while line do
            lines = lines..line
            line = s:readline()
            if not line then break end
        end

        vlc.msg.info("THE LINES:"..lines)

        if string.match(lines, 'var ytInitialData = ') then
            local index_start, index_end, group1, group2, group3 = string.find(lines, '(var ytInitialData = )(.*)(;</script><script nonce=")')

            local json = require('dkjson')
            local inspect = require('inspect')

            local jsonParsed = json.decode (group2)

            if(tonumber(index) == 1) then
                totalVideos = tonumber(jsonParsed.contents.twoColumnWatchNextResults.playlist.playlist.totalVideos)
            end

            for key, video in ipairs(jsonParsed.contents.twoColumnWatchNextResults.playlist.playlist.contents) do
                item = nil
                item = {}

                if (video.playlistPanelVideoRenderer.unplayableText) then
                    totalVideos = totalVideos - 1
                else
                    item.path = "https://www.youtube.com/watch?v="..video.playlistPanelVideoRenderer.videoId
                    item.title = video.playlistPanelVideoRenderer.title.simpleText
                    item.duration =  durationToSeconds(video.playlistPanelVideoRenderer.lengthText.simpleText)
                    item.author = video.playlistPanelVideoRenderer.shortBylineText.runs[1].text
                    item.arturl = vlc.strings.resolve_xml_special_chars(video.playlistPanelVideoRenderer.thumbnail.thumbnails[4].url)
                    item.meta = {}
                    item.meta.videoID = video.playlistPanelVideoRenderer.navigationEndpoint.watchEndpoint.videoId
                    item.meta.index = video.playlistPanelVideoRenderer.navigationEndpoint.watchEndpoint.index + 1

                    playlist[item.meta.index] = item
                end
            end

            vlc.msg.info("I TRY:"..inspect(playlist))

            playlistSize = 0
            for _ in pairs(playlist) do playlistSize = playlistSize + 1 end

            vlc.msg.info("playlist:"..playlistSize)
            vlc.msg.info("totalVideos:"..totalVideos)

            if (playlistSize == totalVideos) then
                return playlist
            end

            -- load continuation
            url = 'www.youtube.com/watch?v='..playlist[#playlist].meta.videoID..'&list='..playlistID..'&index='..playlist[#playlist].meta.index
        end
    end
end

-- Probe function.
function probe()
    if vlc.access == "http" and vlc.access == "https" then
        return false
    end

    return ( ( vlc.access == "http" or vlc.access == "https" ) and (
        ((
            string.match( vlc.path, "^www%.youtube%.com/" )
        or string.match( vlc.path, "^music%.youtube%.com/" ) -- out of use
         ) and (
            string.match(vlc.path, "[?&]list=") -- video page with playlist
        or string.match( vlc.path, "/playlist%?" ) -- playlist page
         )) or
            string.match( vlc.path, "^consent%.youtube%.com/" )
     ) )
end

-- Parse function.
function parse()
    if string.match( vlc.path, "^consent%.youtube%.com/" ) then
        vlc.msg.info("type consent.youtube.com")

        -- Cookie consent redirection
        -- Location: https://consent.youtube.com/m?continue=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DXXXXXXXXXXX&gl=FR&m=0&pc=yt&uxe=23983172&hl=fr&src=1
        -- Set-Cookie: CONSENT=PENDING+355; expires=Fri, 01-Jan-2038 00:00:00 GMT; path=/; domain=.youtube.com
        
        local url = get_url_param( vlc.path, "continue" )

        if not url then
            vlc.msg.err( "Couldn't handle YouTube cookie consent redirection, please check for updates to this script or try disabling HTTP cookie forwarding" )
            return { }
        end

        return { { path = vlc.strings.decode_uri( url ), options = { ":no-http-forward-cookies" } } }
    elseif string.match( vlc.path, "^www.youtube.com/playlist%?list=" ) then
        vlc.msg.info("type: youtube.com/playlist?list=")

        local playlist = {}
        local item, lines, line
        local index = 1
        local playlistID = get_url_param( vlc.path, "list" )

        while true do
            lines = ""
            line = ""

            while line do
                lines = lines..line
                line = read_long_line()
                if not line then break end
            end
           
            if string.match(lines, 'var ytInitialData = ') then
                local index_start, index_end, group1, group2, group3 = string.find(lines, '(var ytInitialData = )(.*)(;</script><link rel=)')

                local inspect = require('inspect')
                local json = require('dkjson')

                local jsonParsed = json.decode (group2)

                for key, video in ipairs(jsonParsed.contents.twoColumnBrowseResultsRenderer.tabs[1].tabRenderer.content.sectionListRenderer.contents[1].itemSectionRenderer.contents[1].playlistVideoListRenderer.contents) do
                    item = nil
                    item = {}

                     -- If continuation playlist
                    if video.continuationItemRenderer then
                       
                    else
                        item.path = "https://www.youtube.com/watch?v="..video.playlistVideoRenderer.videoId
                        item.title = video.playlistVideoRenderer.title.runs[1].text
                        item.duration = video.playlistVideoRenderer.lengthSeconds
                        item.author = video.playlistVideoRenderer.shortBylineText.runs[1].text
                        item.arturl = video.playlistVideoRenderer.thumbnail.thumbnails[3].url
                        
                        table.insert (playlist, item)
                    end
                end

                return playlist
            end
        end
    elseif string.match(vlc.path, "(www.youtube.com/watch?).*([?&]list=)") then
        vlc.msg.info("type: youtube.com/watch?v&list=")

        local videoID, playlistID, index, url

        if(get_url_param(vlc.path, 'index') == '1') then
            return extractAllPlaylistVideos(vlc.path)
        end

        return extractAllPlaylistVideos(vlc.path)
    elseif string.match( vlc.path, "^music%.youtube%.com/playlist%?list=" ) then
        vlc.msg.info("type: music.youtube.com/playlist?list=")

    elseif string.match(vlc.path, "(music.youtube.com/watch?).*([?&]list=)") then
        vlc.msg.info("type: music.youtube.com/watch?list=")
    end
end

@MauriceAvecWii
Copy link

So is there a version for Playlists over 100/200 videos?

@Daniel-Mendes
Copy link
Author

So is there a version for Playlists over 100/200 videos?

I did not find how to do it, sorry

@xquilt
Copy link

xquilt commented Sep 7, 2022

Thanks! Your script was the one that worked for me after hassling with other generic plain yt-vlc integration scripts, as it started to raise the lua stream error: Couldn't extract youtube video URL, please check for updates to this script error message. However, resolution seems to be statically fixed at lower settings. Presumably at 240/144.

@Daniel-Mendes
Copy link
Author

Thanks! Your script was the one that worked for me after hassling with other generic plain yt-vlc integration scripts, as it started to raise the lua stream error: Couldn't extract youtube video URL, please check for updates to this script error message. However, resolution seems to be statically fixed at lower settings. Presumably at 240/144.

Hello, this message doesn't come from my script but from the vlc youtube.lua

@xquilt
Copy link

xquilt commented Sep 7, 2022

this message doesn't come from my script but from the vlc youtube.lua

That's exactly what i'm saying. Maybe by sentence structure caused a misinterpretation for you. Still, The resolution part, is of this script tho.

@Jolubeat
Copy link

Jolubeat commented Jan 4, 2023

It's working!
Thanks!

@LuisFilipeLP
Copy link

After so many hours of downloading and testing old codes from 2016, I finally found one that adds YouTube playlists to VLC, thanks.

@LuisFilipeLP
Copy link

From what I can see, this code can't identify Youtube Music links, if anyone is wanting this, I came up with a silly "trick" that worked for me, just remove the "music." from the beginning of the link.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment