From 0676f1435eecdb1a542cce3b8181d8f6cac6f1e9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Shadowsun=E2=84=A2?=
<12494967+CattoGamer@users.noreply.github.com>
Date: Sun, 4 Aug 2024 04:55:17 +0200
Subject: [PATCH] Update sh_youtube.lua
---
.../modules/theater/services/sh_youtube.lua | 264 +++++++++++++-----
1 file changed, 197 insertions(+), 67 deletions(-)
diff --git a/workshop/gamemodes/cinema_modded/gamemode/modules/theater/services/sh_youtube.lua b/workshop/gamemodes/cinema_modded/gamemode/modules/theater/services/sh_youtube.lua
index 9923734..e343e42 100644
--- a/workshop/gamemodes/cinema_modded/gamemode/modules/theater/services/sh_youtube.lua
+++ b/workshop/gamemodes/cinema_modded/gamemode/modules/theater/services/sh_youtube.lua
@@ -6,94 +6,83 @@ SERVICE.IsTimed = true
SERVICE.Dependency = DEPENDENCY_PARTIAL
SERVICE.ExtentedVideoInfo = true
-local METADATA_URL = "https://www.youtube.com/watch?v=%s"
-
---[[
- Credits to veitikka (https://github.com/veitikka) for fixing YouTube service and writing the
- Workaround with a Metadata parser.
---]]
-
--- Lua search patterns to find metadata from the html
-local patterns = {
- ["title"] = "",
- ["title_fallback"] = "
.-",
- ["duration"] = "",
- ["live"] = "",
- ["live_enddate"] = "",
- ["age_restriction"] = ""
-}
-
----
--- Function to parse video metadata straight from the html instead of using the API
---
-local function ParseMetaDataFromHTML( html )
- --MetaData table to return when we're done
- local metadata, html = {}, html
-
- -- Fetch title, with fallbacks if needed
- metadata.title = util.ParseElementAttribute(html:match(patterns["title"]), "content")
- or util.ParseElementContent(html:match(patterns["title_fallback"]))
-
- -- Parse HTML entities in the title into symbols
- metadata.title = url.htmlentities_decode(metadata.title)
-
- metadata.familyfriendly = util.ParseElementAttribute(html:match(patterns["age_restriction"]), "content") or ""
-
- -- See if the video is an ongoing live broadcast
- -- Set duration to 0 if it is, otherwise use the actual duration
- local isLiveBroadcast = tobool(util.ParseElementAttribute(html:match(patterns["live"]), "content"))
- local broadcastEndDate = html:match(patterns["live_enddate"])
- if isLiveBroadcast and not broadcastEndDate then
- -- Mark as live video
- metadata.duration = 0
- else
- local durationISO8601 = util.ParseElementAttribute(html:match(patterns["duration"]), "content")
- if isstring(durationISO8601) then
- metadata.duration = math.max(1, util.ISO_8601ToSeconds(durationISO8601))
- end
- end
-
- return metadata
-end
+local hostname = ""
function SERVICE:Match( url )
return url.host and url.host:match("youtu.?be[.com]?")
end
if (CLIENT) then
+ local THEATER_JS = [[
+ var checkerInterval = setInterval(function() {
+ var player = document.getElementsByTagName("VIDEO")[0]
+ if (!!player) {
+ if (player.paused) {player.play();}
+ if (player.paused === false && player.readyState === 4) {
+ clearInterval(checkerInterval);
+
+ window.cinema_controller = player;
+ player.style = "width:100%; height: 100%;";
+
+ exTheater.controllerReady();
+ }
+ }
+ }, 50);
+ ]]
+
+ local embedUrlParser = {
+ ["youtube"] = function( Video )
+ return ("https://" .. hostname .. "/embed/%s?autoplay=1&thin_mode=1&controls=0&quality=auto&volume=1&t=%s"):format(
+ Video:Data(),
+ math.Round(CurTime() - Video:StartTime()
+ )
+ )
+ end,
+ ["youtubelive"] = function( Video )
+ return ("https://" .. hostname .. "/embed/%s?autoplay=1&thin_mode=1&controls=0&quality=auto&volume=1"):format(
+ Video:Data()
+ )
+ end
+ }
function SERVICE:LoadProvider( Video, panel )
- local url = GetGlobal2String( "cinema_url", "" ) .. "youtube.html?v=%s"
- panel:OpenURL( url:format( Video:Data() ) ..
- (self.IsTimed and ("&t=%s"):format(
- math.Round(CurTime() - Video:StartTime())
- ) or "")
- )
-
+ panel:OpenURL( embedUrlParser[Video:Type()](Video) )
panel.OnDocumentReady = function(pnl)
- self:LoadExFunctions(pnl)
+ self:LoadExFunctions( pnl )
+ pnl:QueueJavascript(THEATER_JS)
end
+
end
function SERVICE:GetMetadata( data, callback )
+ local url = ("https://%s/api/v1/videos/%s"):format(hostname, data)
- http.Fetch(METADATA_URL:format(data), function(body, length, headers, code)
+ http.Fetch(url, function(body, length, headers, code)
if not body or code ~= 200 then
- callback({ err = ("Not expected response received from YouTube (Code: %d)"):format(code) })
+ callback({ err = ("Not expected response received from API (Code: %d, Try diffrent Instance)"):format(code) })
return
end
- local status, metadata = pcall(ParseMetaDataFromHTML, body)
- if not status then
+ local response = util.JSONToTable(body)
+ if not response then
callback({ err = "Failed to parse MetaData from YouTube" })
return
end
- callback(metadata)
+ callback({
+ title = response.title,
+ premium = response.premium,
+ lengthSeconds = response.lengthSeconds,
+ isListed = response.isListed,
+ liveNow = response.liveNow,
+ isUpcoming = response.isUpcoming,
+ isFamilyFriendly = response.isFamilyFriendly,
+ })
+
end, function(error)
- callback({ err = ("YouTube Error: %s"):format(error) })
+ callback({ err = ("YouTube Error: %s, Try diffrent Instance"):format(error) })
end, {})
end
end
@@ -140,19 +129,31 @@ function SERVICE:GetVideoInfo( data, onSuccess, onFailure )
return onFailure(metadata.err)
end
+ if not metadata.isListed then
+ return onFailure( "Service_EmbedDisabled" )
+ end
+
+ if metadata.premium then
+ return onFailure( "Service_PurchasableContent" )
+ end
+
+ if metadata.prisUpcomingemium then
+ return onFailure( "Service_StreamOffline" )
+ end
+
local info = {}
info.title = metadata.title
info.thumbnail = ("https://img.youtube.com/vi/(%s)/hqdefault.jpg"):format(data)
- if metadata.duration == 0 then
+ if (metadata.liveNow and metadata.lengthSeconds == 0) then
info.type = "youtubelive"
info.duration = 0
else
- if metadata.familyfriendly == "18+" then
+ if not metadata.isFamilyFriendly then
info.type = "youtubensfw"
end
- info.duration = metadata.duration
+ info.duration = metadata.lengthSeconds
end
if onSuccess then
@@ -173,4 +174,133 @@ theater.RegisterService( "youtubelive", {
Dependency = DEPENDENCY_COMPLETE,
Hidden = true,
LoadProvider = CLIENT and SERVICE.LoadProvider or function() end
-} )
\ No newline at end of file
+} )
+
+
+do
+ if not (CLIENT) then return end
+
+ local instances, description = {}, "Invidious is a de-googled alternative to YouTube, it allows you to watch videos without ads and restrictions. It reduces the data sent to Google when watching videos."
+ local cInstance = CreateClientConVar("cinema_invidious_instance", "invidious.fdn.fr", true, false)
+
+ hostname = cInstance:GetString()
+
+ cvars.AddChangeCallback(cInstance:GetName(), function(convar, oldValue, newValue)
+ hostname = newValue
+ end, cInstance:GetName())
+
+ do -- Invidious Switcher menu
+ local function createButton(parent, pos, size, text, callback )
+ local button = vgui.Create( "DButton", parent )
+ button:SetPos( pos[1], pos[2] )
+ button:SetSize( size[1], size[2] )
+ button:SetText(text)
+ button:SizeToContents()
+ button.DoClick = callback
+
+ return button
+ end
+
+ local function switcher()
+ local Frame = vgui.Create( "DFrame" )
+ Frame:SetTitle("(YouTube) Invidious Instance Switcher")
+ Frame:SetSize( 500, 500 )
+ Frame:Center()
+ Frame:MakePopup()
+
+ do -- Top Box
+ local SettingsBox = vgui.Create( "DPanel", Frame )
+ SettingsBox:Dock(TOP)
+ SettingsBox:SetHeight(50)
+ SettingsBox:SetBackgroundColor(Color(255,255,255, 0))
+
+ local Description = vgui.Create( "RichText", SettingsBox )
+ Description:Dock(FILL)
+ Description:SetText( description )
+ end
+
+ do -- Instance list
+ local InstanceList = vgui.Create( "DListView", Frame )
+ InstanceList:Dock( FILL )
+ InstanceList:SetMultiSelect( false )
+ InstanceList:SetSortable( true )
+
+ InstanceList:AddColumn( "Instance" )
+ InstanceList:AddColumn( "Users" )
+ InstanceList:AddColumn( "Location" )
+ InstanceList:AddColumn( "Health" )
+
+ function InstanceList:DoDoubleClick(lineID, line)
+ cInstance:SetString( line:GetColumnText(1) )
+
+ if theater.ActivePanel() then
+ theater.RefreshPanel(true)
+ end
+ end
+
+ local lines = {}
+ for host,tbl in pairs(instances) do
+ if tbl["api"] then
+ lines[host] = InstanceList:AddLine(host, tbl["users"], tbl["region"], tbl["uptime"])
+ end
+ end
+
+ InstanceList:SortByColumn( 2, true ) -- Sort by Users count
+
+ if IsValid(lines[hostname]) then
+ InstanceList:SelectItem( lines[hostname] )
+ end
+
+ end
+
+ end
+ concommand.Add("cinema_invidious_switch", switcher, nil, "Switch the Invidious instance")
+ end
+
+ do -- Instance fetcher & updater
+ local function fetchInstances()
+ local function onSuccess(body, length, headers, code)
+ if not body or code ~= 200 then return end
+
+ local response = util.JSONToTable(body)
+ if not response then return end
+
+ instances = {} -- Clear instance list
+
+ for k,v in pairs(response) do
+ local name, tbl = v[1], v[2]
+
+ if tbl.type ~= "https" then
+ continue
+ end
+
+ local api = tbl["api"]
+ local region = tbl["region"]
+ local users = (tbl["stats"] and tbl["stats"]["usage"] and tbl["stats"]["usage"]["users"] and tbl["stats"]["usage"]["users"]["total"] or "-")
+ local uptime = (tbl["monitor"] and tbl["monitor"]["uptime"] and tbl["monitor"]["uptime"] or "-")
+
+ instances[name] = {
+ api = api,
+ region = util.getCountryName(region),
+ users = users,
+ uptime = uptime,
+ }
+
+ end
+ end
+
+ local function onFailure(message)
+ print("[Invidious API]: " .. message)
+ end
+
+ http.Fetch("https://api.invidious.io/instances.json?sort_by=type,users", onSuccess, onFailure, {
+ ["Accept-Encoding"] = "gzip, deflate",
+ ["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36",
+ })
+ end
+ fetchInstances()
+
+ if timer.Exists("Invidious.Update") then timer.Remove("Invidious.Update") end
+ timer.Create("Invidious.Update", 300, 0, fetchInstances)
+ end
+end
\ No newline at end of file