-- Copyright (C) 2011-2013 Anton Burdinuk -- clark15b@gmail.com -- https://tsdemuxer.googlecode.com/svn/trunk/xupnpd -- sysmer add support api v3 (need install curl with ssl) -- 20150524 AnLeAl changes: -- in url fetch, added docs section -- 20150527 AnLeAl changes: -- fixed for video get when less than 50 -- returned ui config for user amount video -- add possibility get more than 50 videos -- 20150527 MejGun changes: -- code refactoring for feed update -- 20150530 AnLeAl changes: -- small code cleanup -- added 'channel/mostpopular' for youtube mostpopular videos (it's only 30 from api), also region code from ui working -- added favorites/username to get favorites -- added search function -- 20150531 AnLeAl changes: -- fixed error when only first feed can get all videos for cfg.youtube_video_count and other no more 50 -- ui help updated -- curl settings from cycles was moved to variables -- 20170321 vidok changes: -- added vlc descrambler -- added youtube_preferred_resolution parameter -- README -- This is YouTube api v3 plugin for xupnpd. -- For now only search username supported. -- Quickstart: -- 1. Place this file into xupnpd plugin directory. -- 2. Go to google developers console: https://developers.google.com/youtube/registering_an_application?hl=ru -- 3. You need API Key, choose Browser key: https://developers.google.com/youtube/registering_an_application?hl=ru#Create_API_Keys -- 4. Don't use option: only allow referrals from domains. -- 5. Replace '***' with your new key in section '&key=***' in this file. Save file. -- 6. Restart xupnpd, remove any old feeds that was made for youtube earlier. Add new one based on username. -- 7. Enjoy! -- 18 - 360p (MP4,h.264/AVC) -- 22 - 720p (MP4,h.264/AVC) hd -- 37 - 1080p (MP4,h.264/AVC) hd -- 82 - 360p (MP4,h.264/AVC) stereo3d -- 83 - 480p (MP4,h.264/AVC) hq stereo3d -- 84 - 720p (MP4,h.264/AVC) hd stereo3d -- 85 - 1080p (MP4,h.264/AVC) hd stereo3d cfg.youtube_preferred_resolution=1080 cfg.youtube_fmt=37 cfg.youtube_region='*' cfg.youtube_video_count=100 -- cfg.youtube_api_key=123 youtube_api_url='https://www.googleapis.com/youtube/v3/' function youtube_updatefeed(feed,friendly_name) local function isempty(s) return s == nil or s == '' end local rc=false local feed_url=nil local feed_urn=nil local tfeed=split_string(feed,'/') local feed_name='youtube_'..string.lower(string.gsub(feed,"[/ :\'\"]",'_')) local feed_m3u_path=cfg.feeds_path..feed_name..'.m3u' local tmp_m3u_path=cfg.tmp_path..feed_name..'.m3u' local dfd=io.open(tmp_m3u_path,'w+') if dfd then dfd:write('#EXTM3U name=\"',friendly_name or feed_name,'\" type=mp4 plugin=youtube\n') -------------------------------------------------------------------------------------------------- --local getopt = '/mnt/sda1/iptv/curl/curl -k ' local count = 0 local totalres = 0 local numA = 50 -- show 50 videos per page 0..50 from youtube api v3 local pagetokenA = '' local nextpageA = '' local allpages = math.ceil(cfg.youtube_video_count/numA) -- check how much pages per 50 videos there can be local lastpage = allpages - 1 local maxA = '&maxResults=' .. numA if cfg.youtube_video_count < numA then maxA = '&maxResults=' .. cfg.youtube_video_count numA = cfg.youtube_video_count end local keyA = '&key=***' -- change *** to your youtube api key from: https://console.developers.google.com local cA = '' local iA = '' local userA = tfeed[1] local uploads = '' local region = '' local enough = false -- Get what exactly user wants to get. if tfeed[1]=='channel' and tfeed[2]=='mostpopular' then if cfg.youtube_region and cfg.youtube_region~='*' then uploads='®ionCode='..cfg.youtube_region end iA = youtube_api_url..'videos?part=snippet&chart=mostPopular' elseif tfeed[1]=='favorites' then cA = youtube_api_url..'channels?part=contentDetails&forUsername=' iA = youtube_api_url..'playlistItems?part=snippet&playlistId=' userA = tfeed[2] local jsonA = cA .. userA .. keyA --local url_data = io.popen(getopt .. jsonA) --local user_data = url_data:read('*all') --url_data:close() local user_data = http.download(jsonA) local x=json.decode(user_data) uploads = x['items'][1]['contentDetails']['relatedPlaylists']['favorites'] x=nil elseif tfeed[1]=='playlist' then uploads = tfeed[2] iA = youtube_api_url..'playlistItems?part=snippet&playlistId=' elseif tfeed[1]=='channel' then cA = youtube_api_url..'channels?part=contentDetails&id=' iA = youtube_api_url..'playlistItems?part=snippet&playlistId=' local channel_id = tfeed[2] local jsonA = cA .. channel_id .. keyA --local url_data = io.popen(getopt .. jsonA) --local user_data = url_data:read('*all') --url_data:close() local user_data = http.download(jsonA) local x=json.decode(user_data) uploads = x['items'][1]['contentDetails']['relatedPlaylists']['uploads'] x=nil elseif tfeed[1]=='search' then -- feed_urn='videos?vq='..util.urlencode(tfeed[2])..'&alt=json' if cfg.youtube_region and cfg.youtube_region~='*' then region='®ionCode='..cfg.youtube_region end iA = youtube_api_url..'search?type=video&part=snippet&order=date&q=' .. util.urlencode(tfeed[2]) .. '&videoDefinition=high&videoDimension=2d' .. region uploads = '' else cA = youtube_api_url..'channels?part=contentDetails&forUsername=' iA = youtube_api_url..'playlistItems?part=snippet&playlistId=' userA = tfeed[1] local jsonA = cA .. userA .. keyA --local url_data = io.popen(getopt .. jsonA) --local user_data = url_data:read('*all') --url_data:close() local user_data = http.download(jsonA) local x=json.decode(user_data) uploads = x['items'][1]['contentDetails']['relatedPlaylists']['uploads'] x=nil end while true do local jsonA = iA .. uploads .. maxA .. pagetokenA .. keyA --local url_data = io.popen(getopt .. jsonA) --local item_data = url_data:read('*all') --url_data:close() local item_data = http.download(jsonA) if item_data == nil then if cfg.debug>0 then print('YouTube feed \''..feed_name..'\' NOT updated') end return rc end local x=json.decode(item_data) if isempty(x['pageInfo']) then break end totalres = x['pageInfo']['totalResults'] local realpages = math.ceil(totalres/numA) local prelastpage = realpages - 1 local items = nil local title = nil local url = nil local img = nil local countx = 0 for key,value in pairs(x['items']) do count = count + 1 if count > cfg.youtube_video_count then enough = true break end if tfeed[1]=='channel' and tfeed[2]=='mostpopular' then items = value['id'] elseif tfeed[1]=='search' then items = value['id']['videoId'] else items = value['snippet']['resourceId']['videoId'] end title = value['snippet']['title'] url = 'https://www.youtube.com/watch?v=' .. items .. '&feature=youtube_gdata' img = 'http://i.ytimg.com/vi/' .. items .. '/mqdefault.jpg' dfd:write('#EXTINF:0 logo=',img,' ,',title,'\n',url,'\n') end if isempty(x['nextPageToken']) or enough then break else nextpageA = x['nextPageToken'] pagetokenA = '&pageToken=' .. nextpageA end x=nil -- enough=nil end dfd:close() --------------------------------------------------------------------------------------------------------- if util.md5(tmp_m3u_path)~=util.md5(feed_m3u_path) then if os.execute(string.format('mv %s %s',tmp_m3u_path,feed_m3u_path))==0 then if cfg.debug>0 then print('YouTube feed \''..feed_name..'\' updated') end rc=true end else util.unlink(tmp_m3u_path) end end return rc end function youtube_sendurl(youtube_url,range) local url=nil if plugin_sendurl_from_cache(youtube_url,range) then return end url=youtube_get_video_url(youtube_url) if url then if cfg.debug>0 then print('YouTube Real URL: '..url) end plugin_sendurl(youtube_url,url,range) else if cfg.debug>0 then print('YouTube clip is not found') end plugin_sendfile('www/corrupted.mp4') end end -- Helper to search and extract code from javascript stream function js_extract( js, pattern ) --js.i = 0 -- Reset to beginning --for line in buf_iter, js do for line in string.gmatch(js.stream,"(.-};)\r?\n" ) do local ex = string.match( line, pattern ) if ex then return ex end end if cfg.debug>0 then print("Youtube.js_extract(pattern="..pattern.."). Couldn't process youtube video URL." ) end return nil end -- Descramble the URL signature using the javascript code that does that -- in the web page function js_descramble( sig, js_url ) --print("Youtube.js_descramble stream="..stream) -- Fetch javascript code local js = { stream = plugin_download(js_url) } --print("Youtube.js_descramble js.stream="..js.stream) if not js.stream then if cfg.debug>0 then print("Youtube.js_descramble("..sig..", "..js_url.."). Couldn't process youtube video JS URL." ) end return sig end -- Look for the descrambler function's name -- c&&a.set("signature",br(c)); local descrambler = js_extract( js, "%.set%(\"signature\",([^)]-)%(" ) --print("Youtube.js_descramble descrambler = "..descrambler) if not descrambler then if cfg.debug>0 then print ( "Youtube.js_descramble. ("..js.."). Couldn't extract youtube video URL signature descrambling function name" ) end js.stream=nil return sig end -- Fetch the code of the descrambler function -- Go=function(a){a=a.split("");Fo.sH(a,2);Fo.TU(a,28);Fo.TU(a,44);Fo.TU(a,26);Fo.TU(a,40);Fo.TU(a,64);Fo.TR(a,26);Fo.sH(a,1);return a.join("")}; local rules = js_extract( js, "^"..descrambler.."=function%([^)]*%){(.-)};" ) --print("Youtube.js_descramble rules = "..rules) if not rules then if cfg.debug>0 then print ( "Youtube.js_descramble. Couldn't extract youtube video URL signature descrambling rules" ) end js.stream=nil return sig end -- Get the name of the helper object providing transformation definitions local helper = string.match( rules, ";(..)%...%(" ) --print("Youtube.js_descramble helper = "..helper) if not helper then if cfg.debug>0 then print ( "Youtube.js_descramble. Couldn't extract youtube video URL signature transformation helper name" ) end js.stream=nil return sig end -- Fetch the helper object code -- var Fo={TR:function(a){a.reverse()},TU:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b]=c},sH:function(a,b){a.splice(0,b)}}; local transformations = js_extract( js, "[ ,]"..helper.."={(.-)};" ) --print("Youtube.js_descramble transformations = "..transformations) js.stream=nil if not transformations then if cfg.debug>0 then print ( "Youtube.js_descramble. Couldn't extract youtube video URL signature transformation code" ) end return sig end -- Parse the helper object to map available transformations local trans = {} for meth,code in string.gmatch( transformations, "(..):function%([^)]*%){([^}]*)}" ) do -- a=a.reverse() if string.match( code, "%.reverse%(" ) then trans[meth] = "reverse" -- a.splice(0,b) elseif string.match( code, "%.splice%(") then trans[meth] = "slice" -- var c=a[0];a[0]=a[b%a.length];a[b]=c elseif string.match( code, "var c=" ) then trans[meth] = "swap" else if cfg.debug>0 then print ( "Youtube.js_descramble. Couldn't parse unknown youtube video URL signature transformation") end end end -- Parse descrambling rules, map them to known transformations -- and apply them on the signature local missing = false for meth,idx in string.gmatch( rules, "..%.(..)%([^,]+,(%d+)%)" ) do idx = tonumber( idx ) if trans[meth] == "reverse" then sig = string.reverse( sig ) elseif trans[meth] == "slice" then sig = string.sub( sig, idx + 1 ) elseif trans[meth] == "swap" then if idx > 1 then sig = string.gsub( sig, "^(.)("..string.rep( ".", idx - 1 )..")(.)(.*)$", "%3%2%1%4" ) elseif idx == 1 then sig = string.gsub( sig, "^(.)(.)", "%2%1" ) end else if cfg.debug>0 then print ( "Youtube.js_descramble. Couldn't apply unknown youtube video URL signature transformation. missing = true") end missing = true end end if missing then if cfg.debug>0 then print ( "Youtube.js_descramble. Couldn't process youtube video URL. missing=true" ) end end --print("Youtube.js_descramble sig = "..sig) return sig end -- decode URL function unescape (s) s = string.gsub(s,"%%(%x%x)", function (h) return string.char(tonumber(h, 16)) end) return s end -- Parse and pick our video URL function pick_url( url_map, fmt, js_url ) local path = nil for stream in string.gmatch( url_map, "[^,]+" ) do -- Apparently formats are listed in quality order, -- so we can afford to simply take the first one local itag = string.match( stream, "itag=(%d+)" ) if not fmt or not itag or tonumber( itag ) == tonumber( fmt ) then local url = string.match( stream, "url=([^&,]+)" ) if url then -- url = vlc.strings.decode_uri( url ) url = unescape (url) --print( "Youtube.pick_url unescape url="..url) local sig = string.match( stream, "sig=([^&,]+)" ) if not sig then -- Scrambled signature sig = string.match( stream, "s=([^&,]+)" ) if sig then if cfg.debug>0 then print( "Youtube.pick_url Found "..string.len( sig ).."-character scrambled signature for youtube video URL, attempting to descramble... " ) end if js_url then sig = js_descramble( sig, js_url ) else if cfg.debug>0 then print("Youtube.pick_url "..js_url..". Couldn't process youtube video URL" ) end end end end local signature = "" if sig then signature = "&signature="..sig end --print( "Youtube.pick_url signature="..signature) path = url..signature --print( "Youtube.pick_url path="..path) break end end end return path end function youtube_get_best_fmt(urls,fmt) if fmt>81 and fmt<86 then -- 3d local i=fmt while(i>81) do if urls[i] then return urls[i] end i=i-1 end local t={ [82]=18, [83]=18, [84]=22, [85]=37 } fmt=t[fmt] end local t={ 37,22,18 } local t2={ [18]=true, [22]=true, [37]=true } for i=1,3,1 do local u=urls[ t[i] ] if u and t2[fmt] and t[i]<=fmt then return u end end return urls[18] end -- Pick the most suited format available function get_fmt( fmt_list ) local prefres = cfg.youtube_preferred_resolution if prefres < 0 then return nil end local fmt = nil for itag,height in string.gmatch( fmt_list, "(%d+)/%d+x(%d+)/[^,]+" ) do -- Apparently formats are listed in quality -- order, so we take the first one that works, -- or fallback to the lowest quality fmt = itag -- remove WebM itag=43 if tonumber(height) <= prefres and fmt~='43' then break end end return fmt end function youtube_get_video_url(youtube_url) local clip_page=plugin_download(youtube_url) if clip_page then local line=string.match(clip_page,'ytplayer.config%s*=%s*({.-});') clip_page=nil local js_url = string.match( line, "\"js\": *\"(.-)\"" ) if js_url then js_url = string.gsub( js_url, "\\/", "/" ) -- Resolve JS URL if string.match( js_url, "^/[^/]" ) then local authority = string.match( youtube_url, "://([^/]*)/" ) js_url = "//"..authority..js_url end js_url = string.gsub( js_url, "^//", string.match( youtube_url, ".-://" ) ) end fmt_list = string.match( line, "\"fmt_list\": *\"(.-)\"" ) if fmt_list then fmt_list = string.gsub( fmt_list, "\\/", "/" ) fmt = get_fmt( fmt_list ) end -- print ("fmt\r\n"..fmt) url_map = string.match( line, "\"url_encoded_fmt_stream_map\": *\"(.-)\"" ) if url_map then -- FIXME: do this properly url_map = string.gsub( url_map, "\\u0026", "&" ) path = pick_url( url_map, fmt, js_url ) end -- print ("path\r\n"..path) if not path then -- If this is a live stream, the URL map will be empty -- and we get the URL from this field instead local hlsvp = string.match( line, "\"hlsvp\": *\"(.-)\"" ) if hlsvp then hlsvp = string.gsub( hlsvp, "\\/", "/" ) path = hlsvp end end else if cfg.debug>0 then print('YouTube clip is not found') end return nil end return path; end function youtube_old_get_video_url(youtube_url) local url=nil local clip_page=plugin_download(youtube_url) if clip_page then local s=json.decode(string.match(clip_page,'ytplayer.config%s*=%s*({.-});')) clip_page=nil local stream_map=nil -- s.args.adaptive_fmts -- itag 137: 1080p -- itag 136: 720p -- itag 135: 480p -- itag 134: 360p -- itag 133: 240p -- itag 160: 144 -- local player_url=nil if s.assets then player_url=s.assets.js end if player_url and string.sub(player_url,1,2)=='//' then player_url='http:'..player_url end if s.args then stream_map=s.args.url_encoded_fmt_stream_map end local fmt=string.match(youtube_url,'&fmt=(%w+)$') if not fmt then fmt=cfg.youtube_fmt end if stream_map then local urls={} for i in string.gmatch(stream_map,'([^,]+)') do local item={} for j in string.gmatch(i,'([^&]+)') do local name,value=string.match(j,'(%w+)=(.+)') if name then --print(name,util.urldecode(value)) item[name]=util.urldecode(value) end end local sig=item['sig'] or item['s'] local u=item['url'] if sig then u=u..'&signature='..sig end --print(item['itag'],u) urls[tonumber(item['itag'])]=u --print('\n') end url=youtube_get_best_fmt(urls,tonumber(fmt)) --print('old url='..url) end return url else if cfg.debug>0 then print('YouTube clip is not found') end return nil end end plugins['youtube']={} plugins.youtube.name="YouTube" plugins.youtube.desc="username, favorites/username, playlist/idplaylist, search/search_string".. "
YouTube channels: channel/mostpopular, channel/idchannel" plugins.youtube.sendurl=youtube_sendurl plugins.youtube.updatefeed=youtube_updatefeed plugins.youtube.getvideourl=youtube_get_video_url plugins.youtube.ui_config_vars= { { "select", "youtube_fmt", "int" }, { "select", "youtube_region" }, { "input", "youtube_video_count", "int" } -- { "input", "youtube_api_key", "int" } } --youtube_updatefeed('channel/top_rated','')