Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- local remoteExe = 'https://github.com/yt-dlp/yt-dlp/releases/download/2025.03.27/yt-dlp.exe'
- ---Finds optimal format from list of formats returned by yt-dlp. Prefers something with both audio and video,
- ---looking for the largest file.
- local function findOptimalQuality(data)
- ac.log('Finding optimal quality', data)
- local ret = tostring((table.maxEntry(data:split('\n'), function (format)
- if not string.match(string.sub(format, 1, 1), '[0-9]') then return -1e9 end
- if string.match(format, 'audio only') then return -1e9 end
- local w = 0
- if string.match(format, 'video only') then w = w - 20 end
- if string.match(format, 'mp4_dash') then w = w + 10 end
- if string.match(format, '3gp') then w = w - 5 end
- local v, u = string.match(format, ' ([0-9.]+)([MKG])iB')
- if v then
- v = tonumber(v) or 0
- if u == 'M' then v = v * 1e3
- elseif u == 'G' then v = v * 1e6 end
- else
- v = string.match(format, ' ([0-9.]+)k') or 0
- end
- w = w + v / 1e6
- return w
- end) or ''):sub(1, 3):trim())
- if ret == '' or tonumber(ret) == nil then
- ac.debug('Failed to find quality', data)
- end
- return ret
- end
- ---Gets URL of a video stream from video URL using yt-dlp.
- local function findVideoStreamURL(videoURL, callback, progressCallback, failCounter)
- if progressCallback then progressCallback('Getting list of available formats…') end
- ac.debug('Youtube Stream', 'Listing formats…')
- os.runConsoleProcess({ filename = remoteExe, arguments = { '-F', videoURL }, separateStderr = true }, function (err, data)
- ac.debug('Youtube Stream 1', string.format('Formats list: error=%s, data=%s, err=%s', err, data and data.stdout, data and data.stderr))
- if (err or data and data.stdout:trim() == '') and (failCounter or 0) < 3 then
- ac.warn('Second attempt')
- findVideoStreamURL(videoURL, callback, progressCallback, (failCounter or 0) + 1)
- return
- end
- if err then return callback(err) end
- local quality = findOptimalQuality(data.stdout)
- ac.debug('Quality.source', data.stdout)
- ac.debug('Quality.value', quality)
- ac.log('Formats', data.stdout, data.stderr)
- if quality == nil then callback('Couldn’t find optimal quality') end
- if progressCallback then progressCallback('Getting a stream URL…') end
- ac.debug('Youtube quality', quality)
- ac.debug('Youtube video URL', videoURL)
- os.runConsoleProcess({ filename = remoteExe, arguments = { '-f', quality, '--get-url', videoURL }, separateStderr = true }, function (err, data)
- ac.debug('Youtube Stream 2', string.format('URL: error=%s, data=%s, err=%s', err, data and data.stdout, data and data.stderr))
- if data and data.stdout:trim() == '' then
- ac.warn('Second attempt')
- os.runConsoleProcess({ filename = remoteExe, arguments = { '-f', quality, '--get-url', videoURL }, separateStderr = true }, function (err, data)
- ac.debug('Youtube Stream 3', string.format('URL: error=%s, data=%s, err=%s', err, data and data.stdout, data and data.stderr))
- callback(err, data and data.stdout)
- end)
- else
- callback(err, data and data.stdout)
- end
- end)
- end)
- end
- ---Helper library to deal with YouTube. Might not work for long.
- Youtube = {}
- ---@class YoutubeVideo
- ---@field id string
- ---@field thumbnail string
- ---@field title string
- ---@field published string
- ---@field views string
- ---@field loadingState string
- ---@field durationText string
- ---@field channelName string
- ---@field channelThumbnail string
- ---@field channelVerified string
- ---@field channelURL string
- ---@field channelSubscribers string @Only present in .channelMode
- ---@field streamError string
- ---@field streamURL string
- local YoutubeVideo = class('YoutubeVideo', function (data) return data end)
- function YoutubeVideo:getURL()
- return 'https://www.youtube.com/watch?v='..self.id
- end
- ---@param callback fun(err: string, url: string)
- function YoutubeVideo:getStreamURL(callback)
- if self.streamError ~= nil or self.streamURL ~= nil then return callback(self.streamError, self.streamURL) end
- ac.debug('Last URL', self:getURL())
- return findVideoStreamURL(self:getURL(),
- function (err, url)
- if err == nil and string.sub(url, 1, 4) ~= 'http' then
- ac.debug('Not a URL', url)
- err, url = 'Unknown error', nil
- end
- self.streamError, self.streamURL = err and 'Failed to get video URL: '..err or nil, url
- callback(self.streamError, self.streamURL)
- end,
- function (state)
- self.loadingState = state
- end)
- end
- ---@param html string
- ---@param pattern string
- local function findString(html, pattern)
- local r = html:match(pattern)
- if r == nil or r:sub(#r, #r) ~= '\\' then return r end
- r = r:sub(1, #r - 1) .. '"'
- local _, i2 = html:find(pattern)
- local forceNext = false
- for i = i2 + 1, #html do
- local c = html:sub(i, i)
- if c == '\\' and not forceNext then
- forceNext = true
- else
- if c == '"' and not forceNext then break end
- forceNext = false
- r = r..c
- end
- end
- return r
- end
- local function decodeString(str)
- if not str then return nil end
- str = string.gsub(str, '\\"', '"')
- str = string.gsub(str, '\\u0026', '&')
- return str
- end
- local function parseYoutubeMainPageInner(html, separator)
- local ret = {}
- local index = html:find(separator)
- ac.debug('html', html)
- while index ~= nil do
- local nextIndex = html:find(separator, index + 30)
- local piece = nextIndex == nil and html:sub(index) or html:sub(index, nextIndex)
- local id = piece:match('"videoId":"(.-)"')
- local thumbnail = piece:match('"thumbnails":%[{"url":"(.-)"')
- local title = findString(piece, '"title":{"runs":%[{"text":"(.-)"}')
- local published = piece:match('"publishedTimeText":{"simpleText":"(.-)"')
- local views = piece:match('"viewCountText":{"simpleText":"(.-)"')
- if not views then
- views = piece:match('"viewCountText":{"runs":%[{"text":"(.-)"')
- if views then views = views .. ' watching' end
- end
- local durationText = piece:match('"lengthText":{.-"simpleText":"(.-)"')
- local channelName = findString(piece, '"ownerText":{"runs":%[{"text":"(.-)"')
- local channelThumbnail = piece:match('{"channelThumbnailWithLinkRenderer":{.-{"url":"(.-)"')
- local channelURL = piece:match('(/user/.-)"') or piece:match('(/c/.-)"') or piece:match('(/channel/.-)"')
- local channelVerified = channelName ~= nil and piece:match('"BADGE_STYLE_TYPE_VERIFIED"') ~= nil
- if id and thumbnail and title then
- table.insert(ret, YoutubeVideo{
- id = id,
- thumbnail = thumbnail,
- title = decodeString(title),
- published = published,
- views = views,
- durationText = durationText,
- channelName = decodeString(channelName),
- channelThumbnail = channelThumbnail,
- channelURL = channelURL,
- channelVerified = true or channelVerified
- })
- end
- index = nextIndex
- end
- return ret
- end
- local function parseYoutubeChannelInfo(html)
- return {
- channelName = findString(html, '"header":.-"title":"(.-)"'),
- channelThumbnail = html:match('"avatar":.-"url":"(.-)"'),
- channelVerified = html:match('BADGE_STYLE_TYPE_VERIFIED'),
- channelSubscribers = html:match('"subscriberCountText":{.-"simpleText":"(.-)"'),
- channelURL = 'https://m.youtube.com'..(html:match('(/user/.-)"') or html:match('(/c/.-)"') or html:match('(/channel/.-)"')),
- }
- end
- ---Very simple parsing of a youtube page.
- ---@param html string
- ---@return YoutubeVideo[]
- local function parseYoutubeMainPage(html)
- local ret = parseYoutubeMainPageInner(html, '"videoRenderer"')
- if #ret == 0 then
- ret = parseYoutubeMainPageInner(html, '"gridVideoRenderer"')
- end
- if #ret > 0 and table.every(ret, function (item, index, firstItem)
- return item.channelName == firstItem.channelName
- end, ret[1]) then
- ret.channelMode = parseYoutubeChannelInfo(html)
- end
- return ret
- end
- ---@param searchQuery string
- local function buildURL(searchQuery)
- searchQuery = searchQuery and searchQuery:trim() or nil
- if not searchQuery then return 'https://m.youtube.com' end
- if searchQuery:sub(1, 1) == '/' then return 'https://m.youtube.com'..searchQuery..'/videos' end
- return 'https://m.youtube.com/results?search_query='..searchQuery
- end
- ---@param searchQuery string|nil
- ---@param callback fun(err: string, videos: YoutubeVideo[])
- function Youtube.getVideos(searchQuery, callback)
- if not searchQuery then
- -- YouTube no longer populates default page with videos
- searchQuery = table.random({'racing', 'motorsport', 'assetto corsa', 'cats', 'cars', 'automotive', 'music',
- 'driving', 'f1', 'gt3', 'rally', 'top gear'})
- end
- local cacheKey = searchQuery == nil and 'cache' or nil -- 'cache:'..tostring(searchQuery)
- local cached = ac.load(cacheKey)
- if not searchQuery and cached then
- return callback(nil, parseYoutubeMainPage(cached))
- end
- web.get(buildURL(searchQuery), {
- ['Accept-Language'] = 'en-US',
- -- ['User-Agent'] = 'com.google.ios.youtubemusic/6.33.3 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)',
- -- ['X-YouTube-Client-Version'] = '6.33.3',
- -- ['X-YouTube-Client-Name'] = 'IOS_MUSIC',
- -- [ 'Origin'] = 'https://youtubei.googleapis.com',
- }, function (err, response)
- if err then return callback('Failed to load YouTube: '..err, nil) end
- if cacheKey then ac.store(cacheKey, response.body) end
- local videos = try(function () return parseYoutubeMainPage(response.body) end,
- function (err) callback('Failed to parse YouTube: '..err, nil) end)
- if videos then callback(nil, videos) end
- end)
- end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement