Last active
January 10, 2023 07:05
-
-
Save furious/b88f053f8c5c3b155e172d850276edbb to your computer and use it in GitHub Desktop.
VLC - Playlist Parser for Twitch streams/videos URLs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
Resolve Twitch channel and video URLs to the actual media URL | |
Author: furious | |
How to install? https://wiki.videolan.org/Documentation:Building_Lua_Playlist_Scripts | |
Don't forget to remove the old "twitch.luac" plugin | |
OBS: You can choose which quality you want just add ":option" in the url. | |
If the option isn't available it will fallback to the best quality as normal. | |
You can also check the console log to see all available quality options. | |
Common options: chunked (default), 1080p, 720p60, 720p30, 480p, 360, audio_only | |
Example: http://twitch.tv/furious:480 | |
--]] | |
-- Probe function | |
function probe() | |
return (vlc.access == "http" or vlc.access == "https") | |
and ( vlc.path:match("^www%.twitch%.tv/videos/.+") or | |
vlc.path:match("^www%.twitch%.tv/.+") or | |
vlc.path:match("^go%.twitch%.tv/.+") or | |
vlc.path:match("^go%.twitch%.tv/videos/.+") ) | |
end | |
-- Download and parse a JSON document from the specified URL | |
-- Returns: obj, pos, err (see dkjson docs) | |
function parse_json(url) | |
local json = require("dkjson") | |
local stream = vlc.stream(url) | |
local string = "" | |
local line = "" | |
if not stream then | |
return nil, nil, "Failed creating VLC stream" | |
end | |
while true do | |
line = stream:readline() | |
if not line then | |
break | |
end | |
string = string .. line | |
end | |
return json.decode(string) | |
end | |
-- Download and parse a M3U8 document from the specified URL | |
-- Returns: obj, pos, err | |
function parse_m3u8(url) | |
local json = require("dkjson") | |
local stream = vlc.stream(url) | |
local line = "" | |
local options = {} | |
if not stream then | |
return nil, nil, "Failed creating VLC stream" | |
end | |
while true do | |
line = stream:readline() | |
if not line then | |
break | |
end | |
if string.match(line, 'EXT--X--MEDIA:TYPE') ~= nil then | |
local option = {} | |
option.id, option.name = string.match(line, "GROUP--ID\=\"([^\"]+)\",NAME\=\"([^\"]+)\"") | |
vlc.msg.info("[Twitch] Quality Available: " .. option.id .. " - " .. option.name) | |
line = stream:readline() | |
option.resolution, option.codecs = string.match(line, "RESOLUTION\=([^,]+),CODECS\=\"([^\"]+)\"") | |
option.playlist = stream:readline() | |
if option.resolution then | |
options[option.id] = option | |
end | |
end | |
end | |
--if not next(options) then | |
-- return nil, nil, "No quality options available" | |
--end | |
return options | |
end | |
-- Make a request to the Twitch API endpoint given by url | |
-- Returns: obj, err | |
function twitch_api_req(url) | |
local obj, pos, err = parse_json(url .. "?client_id=1ht9oitznxzdo3agmdbn3dydbm06q2") | |
if err then | |
return nil, "Error getting JSON object: " .. err | |
end | |
-- In case of error, the API will return an object with | |
-- error and message given | |
if obj.error then | |
local err = "Twitch API error: " .. obj.error | |
if obj.message then | |
err = err .. " (" .. obj.message .. ")" | |
end | |
return nil, err | |
end | |
return obj, nil | |
end | |
-- Parser for twitch video urls | |
function parse_video() | |
local playlist = {} | |
local item = {} | |
local url, obj, err = nil | |
-- Parse video id out of url | |
local video_id = vlc.path:match("/videos/(%d+)") | |
local quality = vlc.path:match(":([a-zA-Z0-9_]+)") | |
if video_id == nil then | |
vlc.msg.err("Twitch: Failed to parse twitch url for video id") | |
return playlist | |
end | |
vlc.msg.dbg("Twitch: Loading video url for " .. video_id) | |
-- Request video token (required for the video stream) | |
url = "https://api.twitch.tv/api/vods/" .. video_id .. "/access_token" | |
obj, err = twitch_api_req(url) | |
if err then | |
vlc.msg.err("Error getting request token from Twitch: " .. err) | |
return playlist | |
end | |
-- Construct stream url | |
local stream_url = "http://usher.twitch.tv/vod/" .. video_id | |
stream_url = stream_url .. "?player=twitchweb" | |
stream_url = stream_url .. "&nauth=" .. vlc.strings.encode_uri_component(obj.token) | |
stream_url = stream_url .. "&nauthsig=" .. obj.sig | |
stream_url = stream_url .. "&allow_audio_only=true&allow_source=true" | |
local options, pos, err = parse_m3u8(stream_url) | |
if err then | |
vlc.msg.err("Error parsing m3u8 playlist: " .. err) | |
return playlist | |
end | |
item["path"] = stream_url | |
if quality then | |
for option, info in pairs(options) do | |
if string.find(info.id, quality) or string.find(info.name, quality) then | |
item["path"] = info.playlist | |
quality = info.name | |
end | |
end | |
else | |
info = next(options) | |
item["path"] = info.playlist | |
quality = info.name | |
end | |
-- Set base information | |
item["name"] = "Twitch: " .. video_id .. "[" .. quality .. "]" | |
-- Request video information | |
url = "https://api.twitch.tv/kraken/videos/v" .. video_id | |
obj, err = twitch_api_req(url) | |
if err then | |
vlc.msg.warn("Error getting video info from Twitch: " .. err) | |
table.insert(playlist, item) | |
return playlist | |
end | |
-- Overwrite with now obtained better info | |
item["name"] = "Twitch: " .. obj.title .. "[" .. quality .. "]" | |
item["artist"] = obj.channel.display_name | |
item["description"] = obj.description | |
item["url"] = vlc.path | |
table.insert(playlist, item) | |
return playlist | |
end | |
-- Parser for twitch stream urls | |
function parse_stream() | |
local playlist = {} | |
local item = {} | |
local url, obj, err = nil | |
-- Parse channel name out of url | |
local channel = vlc.path:match("/([a-zA-Z0-9_]+)") | |
local quality = vlc.path:match(":([a-zA-Z0-9_]+)") | |
if channel == nil then | |
vlc.msg.err("Twitch: Failed to parse twitch url for channel name") | |
return playlist | |
end | |
vlc.msg.dbg("Twitch: Loading stream url for " .. channel) | |
-- Request channel token (required for the stream m3u8) | |
url = "https://api.twitch.tv/api/channels/" .. channel .. "/access_token" | |
obj, err = twitch_api_req(url) | |
if err then | |
vlc.msg.err("Error getting request token from Twitch: " .. err) | |
return playlist | |
end | |
-- Construct stream url | |
local stream_url = "http://usher.twitch.tv/api/channel/hls/" .. channel .. ".m3u8" | |
stream_url = stream_url .. "?player=twitchweb" | |
stream_url = stream_url .. "&token=" .. vlc.strings.encode_uri_component(obj.token) | |
stream_url = stream_url .. "&sig=" .. obj.sig | |
stream_url = stream_url .. "&allow_audio_only=true&allow_source=true&type=any" | |
local options, pos, err = parse_m3u8(stream_url) | |
if err then | |
vlc.msg.err("Error parsing m3u8 playlist: " .. err) | |
return playlist | |
end | |
item["path"] = stream_url | |
if quality then | |
for option, info in pairs(options) do | |
if string.find(info.id, quality) or string.find(info.name, quality) then | |
item["path"] = info.playlist | |
quality = info.name | |
end | |
end | |
else | |
info = next(options) | |
item["path"] = info.playlist | |
quality = info.name | |
end | |
-- Set base information | |
item["name"] = "Twitch: Stream [" .. quality .. "]" | |
item["artist"] = channel | |
-- Request channel information | |
url = "https://api.twitch.tv/api/channels/" .. channel | |
obj, err = twitch_api_req(url) | |
if err then | |
vlc.msg.warn("Error getting channel info from Twitch: " .. err) | |
table.insert(playlist, item) | |
return playlist | |
end | |
-- Overwrite with now obtained better info | |
item["name"] = "Twitch: Stream [" .. quality .. "]" | |
item["nowplaying"] = obj.display_name .. " playing " .. obj.game | |
item["artist"] = obj.display_name | |
item["description"] = obj.status | |
item["url"] = vlc.path | |
table.insert(playlist, item) | |
return playlist | |
end | |
-- Parse function | |
function parse() | |
if vlc.path:match("/videos/.+") then | |
return parse_video() | |
else | |
return parse_stream() | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment