@@ -11,148 +11,110 @@ local Ignored = {
1111 [" sets" ] = true ,
1212}
1313
14-
1514function SERVICE :Match (url )
1615 return url .host and url .host :match (" soundcloud.com" )
1716end
1817
1918if 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
152115end
153116
154117function 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
162124end
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+
164159function 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 )
181184end
182185
183186theater .RegisterService (" soundcloud" , SERVICE )
0 commit comments