Skip to content

Commit 1b11246

Browse files
committed
Use hydration JSON parsing to extract track info from Soundcloud
1 parent ab985d3 commit 1b11246

1 file changed

Lines changed: 124 additions & 121 deletions

File tree

cinema_modded/gamemode/modules/theater/services/sh_soundcloud.lua

Lines changed: 124 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -11,148 +11,110 @@ local Ignored = {
1111
["sets"] = true,
1212
}
1313

14-
1514
function SERVICE:Match(url)
1615
return url.host and url.host:match("soundcloud.com")
1716
end
1817

1918
if CLIENT then
20-
local EMBED_HTML = [[
21-
<!doctype html>
22-
<html>
23-
24-
<head>
25-
<script src="https://w.soundcloud.com/player/api.js"></script>
26-
</head>
27-
28-
<body>
29-
<script>
30-
(async () => {
31-
const audioTrack = "https://soundcloud.com/{@audioPath}"
32-
const shouldPlay = {@shouldPlay}
33-
34-
const response = await fetch(`https://soundcloud.com/oembed?format=json&url=${audioTrack}`)
35-
const json = await response.json()
36-
37-
if (!!json && !!json.html) {
38-
const container = document.createElement('div');
39-
container.innerHTML = json.html;
40-
41-
document.body.appendChild(container)
42-
document.body.style.overflow = 'hidden';
43-
44-
const frame = container.firstElementChild
45-
var player = SC.Widget(frame);
46-
player.bind(SC.Widget.Events.READY, function () {
47-
var totalDuration = 0
48-
var curVol = 0
49-
var curTime = 0
50-
51-
player.getDuration((duration) => {
52-
totalDuration = duration
53-
54-
if (shouldPlay) {
19+
function SERVICE:LoadProvider(Video, panel)
20+
local html = [[
21+
<!doctype html>
22+
<html>
23+
<head>
24+
<script src="https://w.soundcloud.com/player/api.js"></script>
25+
</head>
26+
<body>
27+
<script>
28+
(async () => {
29+
const audioTrack = "https://soundcloud.com/{@audioPath}"
30+
31+
const response = await fetch(`https://soundcloud.com/oembed?format=json&url=${audioTrack}`)
32+
const json = await response.json()
33+
34+
if (!!json && !!json.html) {
35+
const container = document.createElement('div');
36+
container.innerHTML = json.html;
37+
38+
document.body.appendChild(container)
39+
document.body.style.overflow = 'hidden';
40+
41+
const frame = container.firstElementChild
42+
var player = SC.Widget(frame);
43+
player.bind(SC.Widget.Events.READY, function () {
44+
var totalDuration = 0
45+
var curVol = 0
46+
var curTime = 0
47+
48+
player.getDuration((duration) => {
49+
totalDuration = duration
5550
frame.setAttribute("height", window.innerHeight)
5651
5752
setInterval(function () {
5853
player.getVolume((volume) => { curVol = volume });
5954
player.getPosition((currentTime) => { curTime = currentTime });
6055
}, 100);
6156
62-
{ // Native audio controll
63-
player.volume = 0;
64-
player.currentTime = 0;
65-
player.duration = 0;
66-
67-
Object.defineProperty(player, "volume", {
68-
get() {
69-
return curVol / 100;
70-
},
71-
set(volume) {
72-
player.setVolume(volume * 100);
73-
},
74-
});
75-
76-
Object.defineProperty(player, "currentTime", {
77-
get() {
78-
return curTime / 1000;
79-
},
80-
set(time) {
81-
player.seekTo(time * 1000);
82-
},
83-
});
84-
85-
Object.defineProperty(player, "duration", {
86-
get() {
87-
return totalDuration / 1000;
88-
},
89-
});
90-
91-
player.play()
92-
window.cinema_controller = player
93-
exTheater.controllerReady()
94-
}
95-
} else {
96-
var metadata = {
97-
duration: Math.round(totalDuration / 1000),
98-
thumbnail: json.thumbnail_url,
99-
title: json.title
100-
}
101-
102-
console.log("METADATA:" + JSON.stringify(metadata))
103-
}
57+
player.volume = 0;
58+
player.currentTime = 0;
59+
player.duration = 0;
60+
61+
Object.defineProperty(player, "volume", {
62+
get() {
63+
return curVol / 100;
64+
},
65+
set(volume) {
66+
player.setVolume(volume * 100);
67+
},
68+
});
69+
70+
Object.defineProperty(player, "currentTime", {
71+
get() {
72+
return curTime / 1000;
73+
},
74+
set(time) {
75+
player.seekTo(time * 1000);
76+
},
77+
});
78+
79+
Object.defineProperty(player, "duration", {
80+
get() {
81+
return totalDuration / 1000;
82+
},
83+
});
84+
85+
player.play()
86+
window.cinema_controller = player
87+
exTheater.controllerReady()
88+
});
10489
});
105-
});
106-
}
107-
})()
90+
}
91+
})()
92+
</script>
93+
</body>
94+
</html>
95+
]]
10896

109-
</script>
110-
</body>
111-
112-
</html>
113-
]]
114-
115-
local BROWSER_JS = [[
116-
setInterval(() => {
117-
var cookieBanner = document.querySelector("#onetrust-banner-sdk #onetrust-reject-all-handler")
118-
if (!!cookieBanner) {cookieBanner.click()}
119-
}, 500);
120-
]]
121-
122-
function SERVICE:LoadProvider(Video, panel)
123-
local html = EMBED_HTML
12497
html = html:Replace("{@audioPath}", Video:Data())
125-
html = html:Replace("{@shouldPlay}", "true")
126-
12798
panel:SetHTML(html)
12899

129100
panel.OnDocumentReady = function(pnl)
130-
self:LoadExFunctions( pnl )
131-
pnl:QueueJavascript(THEATER_JS)
101+
self:LoadExFunctions(pnl)
132102
end
133103
end
134104

135-
function SERVICE:GetMetadata( data, callback )
136-
137-
local panel = self:CreateWebCrawler(callback)
138-
139-
local html = EMBED_HTML
140-
html = html:Replace("{@audioPath}", data)
141-
html = html:Replace("{@shouldPlay}", "false")
142-
143-
panel:SetHTML(html)
144-
145-
end
146-
147-
function SERVICE:SearchFunctions( browser )
148-
if not IsValid( browser ) then return end
105+
function SERVICE:SearchFunctions(browser)
106+
if not IsValid(browser) then return end
149107

150-
browser:RunJavascript(BROWSER_JS)
108+
browser:RunJavascript([[
109+
setInterval(() => {
110+
var cookieBanner = document.querySelector("#onetrust-banner-sdk #onetrust-reject-all-handler")
111+
if (!!cookieBanner) {cookieBanner.click()}
112+
}, 500);
113+
]])
151114
end
152115
end
153116

154117
function SERVICE:GetURLInfo(url)
155-
156118
if url.path then
157119
local path1, path2 = url.path:match("/([%a%d-_]+)/([%a%d-_]+)$")
158120
if path1 and not Ignored[path1] and path2 then return { Data = ("%s/%s"):format(path1, path2)} end
@@ -161,23 +123,64 @@ function SERVICE:GetURLInfo(url)
161123
return false
162124
end
163125

126+
local function ParseSoundCloudHTML(body)
127+
local json = body:match('<script>window%.__sc_hydration = (.-);</script>')
128+
if not json then
129+
return nil, "Hydration JSON not found"
130+
end
131+
132+
local data = util.JSONToTable(json)
133+
if not data then
134+
return nil, "Failed to parse JSON"
135+
end
136+
137+
for i = 1, #data do
138+
if data[i]["hydratable"] and data[i]["hydratable"] == "sound" and data[i]["data"] then
139+
local metadata = data[i]["data"]
140+
141+
if metadata.embeddable_by ~= "all" or not metadata.public then
142+
return nil, "Track is not embeddable"
143+
end
144+
145+
local duration = metadata.duration or metadata.full_duration or 0
146+
duration = math.floor(duration / 1000)
147+
148+
return {
149+
title = metadata.title or "Unknown",
150+
thumbnail = metadata.artwork_url,
151+
duration = duration
152+
}
153+
end
154+
end
155+
156+
return nil, "Sound metadata not found in hydration data"
157+
end
158+
164159
function SERVICE:GetVideoInfo(data, onSuccess, onFailure)
160+
local onReceive = function(body, length, headers, code)
161+
local success, result = pcall(ParseSoundCloudHTML, body)
165162

166-
theater.FetchVideoMedata( data:GetOwner(), data, function(metadata)
163+
if not success then
164+
return onFailure(result or "Theater_RequestFailed")
165+
end
167166

168-
if metadata.err then
169-
return onFailure(metadata.err)
167+
local metadata, err = result
168+
if not metadata then
169+
return onFailure(err or "Theater_RequestFailed")
170170
end
171171

172172
local info = {}
173173
info.title = metadata.title
174-
info.duration = tonumber(metadata.duration)
174+
info.duration = metadata.duration
175175
info.thumbnail = metadata.thumbnail
176176

177177
if onSuccess then
178178
pcall(onSuccess, info)
179179
end
180-
end)
180+
end
181+
182+
local url = "https://soundcloud.com/" .. data:Data()
183+
self:Fetch(url, onReceive, onFailure)
181184
end
182185

183186
theater.RegisterService("soundcloud", SERVICE)

0 commit comments

Comments
 (0)