Advertisement
g32radasd

Untitled

Apr 26th, 2025
30
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 9.66 KB | None | 0 0
  1. local remoteExe = 'https://github.com/yt-dlp/yt-dlp/releases/download/2025.03.27/yt-dlp.exe'
  2.  
  3. ---Finds optimal format from list of formats returned by yt-dlp. Prefers something with both audio and video,
  4. ---looking for the largest file.
  5. local function findOptimalQuality(data)
  6.   ac.log('Finding optimal quality', data)
  7.   local ret = tostring((table.maxEntry(data:split('\n'), function (format)
  8.     if not string.match(string.sub(format, 1, 1), '[0-9]') then return -1e9 end
  9.     if string.match(format, 'audio only') then return -1e9 end
  10.     local w = 0
  11.     if string.match(format, 'video only') then w = w - 20 end
  12.     if string.match(format, 'mp4_dash') then w = w + 10 end
  13.     if string.match(format, '3gp') then w = w - 5 end
  14.     local v, u = string.match(format, ' ([0-9.]+)([MKG])iB')
  15.     if v then
  16.       v = tonumber(v) or 0
  17.       if u == 'M' then v = v * 1e3
  18.       elseif u == 'G' then v = v * 1e6 end
  19.     else
  20.       v = string.match(format, ' ([0-9.]+)k') or 0
  21.     end
  22.     w = w + v / 1e6
  23.     return w
  24.   end) or ''):sub(1, 3):trim())
  25.   if ret == '' or tonumber(ret) == nil then
  26.     ac.debug('Failed to find quality', data)
  27.   end
  28.   return ret
  29. end
  30.  
  31. ---Gets URL of a video stream from video URL using yt-dlp.
  32. local function findVideoStreamURL(videoURL, callback, progressCallback, failCounter)
  33.   if progressCallback then progressCallback('Getting list of available formats…') end
  34.   ac.debug('Youtube Stream', 'Listing formats…')
  35.   os.runConsoleProcess({ filename = remoteExe, arguments = { '-F', videoURL }, separateStderr = true }, function (err, data)
  36.     ac.debug('Youtube Stream 1', string.format('Formats list: error=%s, data=%s, err=%s', err, data and data.stdout, data and data.stderr))
  37.     if (err or data and data.stdout:trim() == '') and (failCounter or 0) < 3 then
  38.       ac.warn('Second attempt')
  39.       findVideoStreamURL(videoURL, callback, progressCallback, (failCounter or 0) + 1)
  40.       return
  41.     end
  42.     if err then return callback(err) end
  43.     local quality = findOptimalQuality(data.stdout)
  44.     ac.debug('Quality.source', data.stdout)
  45.     ac.debug('Quality.value', quality)
  46.     ac.log('Formats', data.stdout, data.stderr)
  47.     if quality == nil then callback('Couldn’t find optimal quality') end
  48.     if progressCallback then progressCallback('Getting a stream URL…') end
  49.     ac.debug('Youtube quality', quality)
  50.     ac.debug('Youtube video URL', videoURL)
  51.     os.runConsoleProcess({ filename = remoteExe, arguments = { '-f', quality, '--get-url', videoURL }, separateStderr = true }, function (err, data)
  52.       ac.debug('Youtube Stream 2', string.format('URL: error=%s, data=%s, err=%s', err, data and data.stdout, data and data.stderr))
  53.       if data and data.stdout:trim() == '' then
  54.         ac.warn('Second attempt')
  55.         os.runConsoleProcess({ filename = remoteExe, arguments = { '-f', quality, '--get-url', videoURL }, separateStderr = true }, function (err, data)
  56.           ac.debug('Youtube Stream 3', string.format('URL: error=%s, data=%s, err=%s', err, data and data.stdout, data and data.stderr))
  57.           callback(err, data and data.stdout)
  58.         end)
  59.       else
  60.         callback(err, data and data.stdout)
  61.       end
  62.     end)
  63.   end)
  64. end
  65.  
  66. ---Helper library to deal with YouTube. Might not work for long.
  67. Youtube = {}
  68.  
  69. ---@class YoutubeVideo
  70. ---@field id string
  71. ---@field thumbnail string
  72. ---@field title string
  73. ---@field published string
  74. ---@field views string
  75. ---@field loadingState string
  76. ---@field durationText string
  77. ---@field channelName string
  78. ---@field channelThumbnail string
  79. ---@field channelVerified string
  80. ---@field channelURL string
  81. ---@field channelSubscribers string @Only present in .channelMode
  82. ---@field streamError string
  83. ---@field streamURL string
  84. local YoutubeVideo = class('YoutubeVideo', function (data) return data end)
  85.  
  86. function YoutubeVideo:getURL()
  87.   return 'https://www.youtube.com/watch?v='..self.id
  88. end
  89.  
  90. ---@param callback fun(err: string, url: string)
  91. function YoutubeVideo:getStreamURL(callback)
  92.   if self.streamError ~= nil or self.streamURL ~= nil then return callback(self.streamError, self.streamURL) end
  93.   ac.debug('Last URL', self:getURL())
  94.   return findVideoStreamURL(self:getURL(),
  95.     function (err, url)
  96.       if err == nil and string.sub(url, 1, 4) ~= 'http' then
  97.         ac.debug('Not a URL', url)
  98.         err, url = 'Unknown error', nil
  99.       end
  100.       self.streamError, self.streamURL = err and 'Failed to get video URL: '..err or nil, url
  101.       callback(self.streamError, self.streamURL)
  102.     end,
  103.     function (state)
  104.       self.loadingState = state
  105.     end)
  106. end
  107.  
  108. ---@param html string
  109. ---@param pattern string
  110. local function findString(html, pattern)
  111.   local r = html:match(pattern)
  112.   if r == nil or r:sub(#r, #r) ~= '\\' then return r end
  113.  
  114.   r = r:sub(1, #r - 1) .. '"'
  115.   local _, i2 = html:find(pattern)
  116.   local forceNext = false
  117.   for i = i2 + 1, #html do
  118.     local c = html:sub(i, i)
  119.     if c == '\\' and not forceNext then
  120.       forceNext = true
  121.     else
  122.       if c == '"' and not forceNext then break end
  123.       forceNext = false
  124.       r = r..c
  125.     end
  126.   end
  127.   return r
  128. end
  129.  
  130. local function decodeString(str)
  131.   if not str then return nil end
  132.   str = string.gsub(str, '\\"', '"')
  133.   str = string.gsub(str, '\\u0026', '&')
  134.   return str
  135. end
  136.  
  137. local function parseYoutubeMainPageInner(html, separator)
  138.   local ret = {}
  139.   local index = html:find(separator)
  140.   ac.debug('html', html)
  141.   while index ~= nil do
  142.     local nextIndex = html:find(separator, index + 30)
  143.     local piece = nextIndex == nil and html:sub(index) or html:sub(index, nextIndex)
  144.     local id = piece:match('"videoId":"(.-)"')
  145.     local thumbnail = piece:match('"thumbnails":%[{"url":"(.-)"')
  146.     local title = findString(piece, '"title":{"runs":%[{"text":"(.-)"}')
  147.     local published = piece:match('"publishedTimeText":{"simpleText":"(.-)"')
  148.     local views = piece:match('"viewCountText":{"simpleText":"(.-)"')
  149.     if not views then
  150.       views = piece:match('"viewCountText":{"runs":%[{"text":"(.-)"')
  151.       if views then views = views .. ' watching' end
  152.     end
  153.     local durationText = piece:match('"lengthText":{.-"simpleText":"(.-)"')
  154.     local channelName = findString(piece, '"ownerText":{"runs":%[{"text":"(.-)"')
  155.     local channelThumbnail = piece:match('{"channelThumbnailWithLinkRenderer":{.-{"url":"(.-)"')
  156.     local channelURL = piece:match('(/user/.-)"') or piece:match('(/c/.-)"') or piece:match('(/channel/.-)"')
  157.     local channelVerified = channelName ~= nil and piece:match('"BADGE_STYLE_TYPE_VERIFIED"') ~= nil
  158.     if id and thumbnail and title then
  159.       table.insert(ret, YoutubeVideo{
  160.         id = id,
  161.         thumbnail = thumbnail,
  162.         title = decodeString(title),
  163.         published = published,
  164.         views = views,
  165.         durationText = durationText,
  166.         channelName = decodeString(channelName),
  167.         channelThumbnail = channelThumbnail,
  168.         channelURL = channelURL,
  169.         channelVerified = true or channelVerified
  170.       })
  171.     end
  172.     index = nextIndex
  173.   end
  174.   return ret
  175. end
  176.  
  177. local function parseYoutubeChannelInfo(html)
  178.   return {
  179.     channelName = findString(html, '"header":.-"title":"(.-)"'),
  180.     channelThumbnail = html:match('"avatar":.-"url":"(.-)"'),
  181.     channelVerified = html:match('BADGE_STYLE_TYPE_VERIFIED'),
  182.     channelSubscribers = html:match('"subscriberCountText":{.-"simpleText":"(.-)"'),
  183.     channelURL = 'https://m.youtube.com'..(html:match('(/user/.-)"') or html:match('(/c/.-)"') or html:match('(/channel/.-)"')),
  184.   }
  185. end
  186.  
  187. ---Very simple parsing of a youtube page.
  188. ---@param html string
  189. ---@return YoutubeVideo[]
  190. local function parseYoutubeMainPage(html)
  191.   local ret = parseYoutubeMainPageInner(html, '"videoRenderer"')
  192.   if #ret == 0 then
  193.     ret = parseYoutubeMainPageInner(html, '"gridVideoRenderer"')
  194.   end
  195.   if #ret > 0 and table.every(ret, function (item, index, firstItem)
  196.     return item.channelName == firstItem.channelName
  197.   end, ret[1]) then
  198.     ret.channelMode = parseYoutubeChannelInfo(html)
  199.   end
  200.   return ret
  201. end
  202.  
  203. ---@param searchQuery string
  204. local function buildURL(searchQuery)
  205.   searchQuery = searchQuery and searchQuery:trim() or nil
  206.   if not searchQuery then return 'https://m.youtube.com' end
  207.   if searchQuery:sub(1, 1) == '/' then return 'https://m.youtube.com'..searchQuery..'/videos' end
  208.   return 'https://m.youtube.com/results?search_query='..searchQuery
  209. end
  210.  
  211. ---@param searchQuery string|nil
  212. ---@param callback fun(err: string, videos: YoutubeVideo[])
  213. function Youtube.getVideos(searchQuery, callback)
  214.   if not searchQuery then
  215.     -- YouTube no longer populates default page with videos
  216.     searchQuery = table.random({'racing', 'motorsport', 'assetto corsa', 'cats', 'cars', 'automotive', 'music',
  217.       'driving', 'f1', 'gt3', 'rally', 'top gear'})
  218.   end
  219.  
  220.   local cacheKey = searchQuery == nil and 'cache' or nil -- 'cache:'..tostring(searchQuery)
  221.   local cached = ac.load(cacheKey)
  222.   if not searchQuery and cached then
  223.     return callback(nil, parseYoutubeMainPage(cached))
  224.   end
  225.  
  226.   web.get(buildURL(searchQuery), {
  227.     ['Accept-Language'] = 'en-US',
  228.     -- ['User-Agent'] = 'com.google.ios.youtubemusic/6.33.3 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)',
  229.     -- ['X-YouTube-Client-Version'] = '6.33.3',
  230.     -- ['X-YouTube-Client-Name'] = 'IOS_MUSIC',
  231.     -- [ 'Origin'] = 'https://youtubei.googleapis.com',
  232.   }, function (err, response)
  233.     if err then return callback('Failed to load YouTube: '..err, nil) end
  234.     if cacheKey then ac.store(cacheKey, response.body) end
  235.     local videos = try(function () return parseYoutubeMainPage(response.body) end,
  236.     function (err) callback('Failed to parse YouTube: '..err, nil) end)
  237.     if videos then callback(nil, videos) end
  238.   end)
  239. end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement