Browse Source

add: format

Ben 11 tháng trước cách đây
mục cha
commit
995bc67416
6 tập tin đã thay đổi với 243 bổ sung131 xóa
  1. 135 108
      js/info.js
  2. 2 2
      js/main.swift
  3. 8 2
      js/nginx.conf
  4. 9 19
      js/test.js
  5. 17 0
      test.py
  6. 72 0
      yt_dlp.json

+ 135 - 108
js/info.js

@@ -50,25 +50,24 @@ parseCodecs = (format) => {
     return {};
     return {};
 }
 }
 
 
-request = async (method, url, data = null, headers = {}, local) => {
-    if (local) {
+request = async (method, url, data = null, headers = {}, platform) => {
+    if (platform === "WEB") {
         url = url.replace("https://www.youtube.com", "http://127.0.0.1");
         url = url.replace("https://www.youtube.com", "http://127.0.0.1");
     }
     }
     console.log(`请求url:${url}`)
     console.log(`请求url:${url}`)
     console.log(`请求data:${data}`)
     console.log(`请求data:${data}`)
     console.log(`请求method:${method}`)
     console.log(`请求method:${method}`)
     console.log(`请求headers:${JSON.stringify((headers))}`)
     console.log(`请求headers:${JSON.stringify((headers))}`)
-    if (local) {
+    if (platform === "WEB") {
         return fetch(url, {
         return fetch(url, {
             "method": method,
             "method": method,
             "headers": headers,
             "headers": headers,
-            "body": data,
+            "body": data
         }).then(res => res.text())
         }).then(res => res.text())
     }
     }
     return new Promise((resolve, reject) => {
     return new Promise((resolve, reject) => {
         AF.request(url, method, data, headers, (data, err) => {
         AF.request(url, method, data, headers, (data, err) => {
             if (err) {
             if (err) {
-                console.log(`请求失败: ${err}`)
                 reject(err);
                 reject(err);
             } else {
             } else {
                 resolve(data);
                 resolve(data);
@@ -95,7 +94,7 @@ getDecipherFunction = (string) => {
 };
 };
 
 
 const cache = {};
 const cache = {};
-extractJSSignatureFunction = async (baseJsUrl, local) => {
+extractJSSignatureFunction = async (baseJsUrl, platform) => {
     console.log(`解析baseUrl: ${baseJsUrl}`);
     console.log(`解析baseUrl: ${baseJsUrl}`);
     const cacheKey = `js:${baseJsUrl}`;
     const cacheKey = `js:${baseJsUrl}`;
     if (cache[cacheKey]) {
     if (cache[cacheKey]) {
@@ -105,7 +104,7 @@ extractJSSignatureFunction = async (baseJsUrl, local) => {
     const headers = {
     const headers = {
         'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.101 Safari/537.36',
         'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.101 Safari/537.36',
     }
     }
-    const baseContent = await request('GET', baseJsUrl, null, headers, local);
+    const baseContent = await request('GET', baseJsUrl, null, headers, platform);
     const decipher = getDecipherFunction(baseContent);
     const decipher = getDecipherFunction(baseContent);
     if (decipher) {
     if (decipher) {
         cache[cacheKey] = decipher;
         cache[cacheKey] = decipher;
@@ -113,8 +112,8 @@ extractJSSignatureFunction = async (baseJsUrl, local) => {
     return decipher;
     return decipher;
 }
 }
 
 
-getUrlFromSignature = async (signatureCipher, baseJsUrl, local) => {
-    const decipher = await extractJSSignatureFunction(baseJsUrl, local);
+getUrlFromSignature = async (signatureCipher, baseJsUrl, platform) => {
+    const decipher = await extractJSSignatureFunction(baseJsUrl, platform);
     const searchParams = {}
     const searchParams = {}
     for (const item of signatureCipher.split("&")) {
     for (const item of signatureCipher.split("&")) {
         const [key, value] = item.split('=');
         const [key, value] = item.split('=');
@@ -125,55 +124,11 @@ getUrlFromSignature = async (signatureCipher, baseJsUrl, local) => {
     return `${url}&${sp}=${decipher(signature)}`;
     return `${url}&${sp}=${decipher(signature)}`;
 }
 }
 
 
-detail = async (url, local) => {
+detail = async (url, platform) => {
     try {
     try {
-        if (url.includes('?')) {
-            url = `${url}&bpctr=9999999999&has_verified=1`;
-        } else {
-            url = `${url}?bpctr=9999999999&has_verified=1`;
-        }
-        console.log(`接受到解析请求: ${url}`);
-        const headers = {
-            'User-Agent': 'com.google.ios.youtube/19.09.3 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)',
-        }
-        const html = await request('GET', url, null, headers, local);
-
-        // 尝试找到更好的
-        try {
-            let regex = /ytcfg\.set\s*\(\s*({.+?})\s*\)\s*;/
-            let match = html.match(regex);
-            if (match == null || match.length !== 2) {
-                console.log(`无法找到更好的format`);
-            } else {
-                const masterYtConfig = JSON.parse(match[1]);
-                const headers = {
-                    'Origin': 'https://www.youtube.com',
-                    'X-YouTube-Client-Name': `${masterYtConfig['INNERTUBE_CONTEXT_CLIENT_NAME']}`,
-                    'X-YouTube-Client-Version': `${masterYtConfig['INNERTUBE_CLIENT_VERSION']}`,
-                    'User-Agent': 'com.google.ios.youtube/19.09.3 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)',
-                    'Content-Type': 'application/json'
-                }
-                const apiKey = masterYtConfig['INNERTUBE_API_KEY']
-                const data = {
-                    'context': masterYtConfig['INNERTUBE_CONTEXT'],
-                    'videoId': url.replace('https://www.youtube.com/watch?v=', ''),
-                    'playbackContext': {
-                        'contentPlaybackContext': {
-                            'html5Preference': 'HTML5_PREF_WANTS',
-                            'signatureTimestamp': 0
-                        }
-                    },
-                    'contentCheckOk': true,
-                    'racyCheckOk': true
-                }
-                const jsUrl = `https://www.youtube.com/youtubei/v1/player?key=${apiKey}&prettyPrint=false`;
-                const res = await request('POST', jsUrl, JSON.stringify(data), headers, local);
-                console.log(`找到了更好的`);
-                console.log(JSON.stringify(res));
-            }
-        } catch (e) {
-            console.log(`无法找到更好的format,并且报错了: ${e}`);
-        }
+        let html = await request('GET', url, null, {
+            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36'
+        }, platform);
 
 
         let regex = /var ytInitialPlayerResponse\s*=\s*({.*?});/;
         let regex = /var ytInitialPlayerResponse\s*=\s*({.*?});/;
         let match = html.match(regex);
         let match = html.match(regex);
@@ -194,62 +149,133 @@ detail = async (url, local) => {
             })
             })
         }
         }
 
 
-        const formats = []
         const baseJsUrl = `https://www.youtube.com${JSON.parse(html.match(/set\(({.+?})\);/)[1])["PLAYER_JS_URL"]}`
         const baseJsUrl = `https://www.youtube.com${JSON.parse(html.match(/set\(({.+?})\);/)[1])["PLAYER_JS_URL"]}`
-        for (let format of [].concat(ytInitialPlayerResponse["streamingData"]["formats"]).concat(ytInitialPlayerResponse["streamingData"]["adaptiveFormats"])) {
-            console.log(`current format: ${JSON.stringify(format)}`);
-            if (!format["url"]) {
-                format["url"] = await getUrlFromSignature(format["signatureCipher"], baseJsUrl, local);
+        const formats = [];
+        try {
+            let regex = /ytcfg\.set\s*\(\s*({.+?})\s*\)\s*;/
+            let match = html.match(regex);
+            if (match != null && match.length === 2) {
+                const masterYtConfig = JSON.parse(match[1]);
+                const apiKey = masterYtConfig['INNERTUBE_API_KEY'] || 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39'
+                const data = {
+                    "context": {
+                        "client": {
+                            "clientName": "ANDROID",
+                            "clientVersion": "19.09.37",
+                            "androidSdkVersion": 30,
+                            "userAgent": "com.google.android.youtube/19.09.37 (Linux; U; Android 11) gzip",
+                        }
+                    },
+                    'videoId': url.replace('https://www.youtube.com/watch?v=', ''),
+                    "playbackContext": {
+                        "contentPlaybackContext": {
+                            "html5Preference": "HTML5_PREF_WANTS"
+                        }
+                    },
+                    "params": "CgIIAQ==",
+                    "contentCheckOk": true,
+                    "racyCheckOk": true
+                }
+                const apiUrl = `https://www.youtube.com/youtubei/v1/player?key=${apiKey}&prettyPrint=false`;
+                let res = await request('POST', apiUrl, JSON.stringify(data), {
+                    'X-YouTube-Client-Name': '5',
+                    'X-YouTube-Client-Version': '19.09.3',
+                    'User-Agent': 'com.google.ios.youtube/19.09.3 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)',
+                    'Content-Type': 'application/json'
+                }, platform);
+                console.log(`api结果: ${res}`);
+                res = JSON.parse(res);
+                console.log(res);
+                for (let format of [].concat(res["streamingData"]["formats"]).concat(res["streamingData"]["adaptiveFormats"])) {
+                    if (format) {
+                        console.log(`current format: ${JSON.stringify(format)}`);
+                        if (!format["url"]) {
+                            format["url"] = await getUrlFromSignature(format["signatureCipher"], baseJsUrl, platform);
+                        }
+                        if (format["url"]) {
+                            const {vcodec, acodec} = parseCodecs(format)
+                            if (vcodec && acodec) {
+                                formats.push({
+                                    "width": format["width"] + "",
+                                    "height": format["height"] + "",
+                                    "type": format["mimeType"],
+                                    "quality": format["quality"],
+                                    "itag": format["itag"],
+                                    "fps": format["fps"] + "",
+                                    "bitrate": format["bitrate"] + "",
+                                    "url": format["url"],
+                                    "ext": "mp4",
+                                    "vcodec": vcodec,
+                                    "acodec": acodec,
+                                    "vbr": "0",
+                                    "abr": "0",
+                                    "container": "mp4_dash"
+                                })
+                            }
+                        }
+                    }
+                }
             }
             }
-            if (format["url"]) {
-                const {vcodec, acodec} = parseCodecs(format)
-                if (vcodec && acodec) {
-                    formats.push({
-                        "width": format["width"] + "",
-                        "height": format["height"] + "",
-                        "type": format["mimeType"],
-                        "quality": format["quality"],
-                        "itag": format["itag"],
-                        "fps": format["fps"] + "",
-                        "bitrate": format["bitrate"] + "",
-                        "url": format["url"],
-                        "ext": "mp4",
-                        "vcodec": vcodec,
-                        "acodec": acodec,
-                        "vbr": "0",
-                        "abr": "0",
-                        "container": "mp4_dash"
-                    })
+        } catch (e) {
+            console.log(`无法从api中解析format,并且报错了: ${e}`);
+        }
+        if (formats.length === 0) {
+            for (let format of [].concat(ytInitialPlayerResponse["streamingData"]["formats"]).concat(ytInitialPlayerResponse["streamingData"]["adaptiveFormats"])) {
+                console.log(`current format: ${JSON.stringify(format)}`);
+                if (format) {
+                    if (!format["url"]) {
+                        format["url"] = await getUrlFromSignature(format["signatureCipher"], baseJsUrl, platform);
+                    }
+                    if (format["url"]) {
+                        const {vcodec, acodec} = parseCodecs(format)
+                        if (vcodec && acodec) {
+                            formats.push({
+                                "width": format["width"] + "",
+                                "height": format["height"] + "",
+                                "type": format["mimeType"],
+                                "quality": format["quality"],
+                                "itag": format["itag"],
+                                "fps": format["fps"] + "",
+                                "bitrate": format["bitrate"] + "",
+                                "url": format["url"],
+                                "ext": "mp4",
+                                "vcodec": vcodec,
+                                "acodec": acodec,
+                                "vbr": "0",
+                                "abr": "0",
+                                "container": "mp4_dash"
+                            })
+                        }
+                    }
                 }
                 }
             }
             }
         }
         }
 
 
-        regex = /var ytInitialData\s*=\s*({.*?});/;
-        match = html.match(regex);
-        if (!match || !match.length) {
-            console.log(`解析失败,无法找到 ytInitialData`);
-            throw new Error('JSON not found.');
-        }
-        const ytInitialData = JSON.parse(match[1]);
+        match = html.match(/var ytInitialData\s*=\s*({.*?});/);
         const recommendInfo = [];
         const recommendInfo = [];
-        for (const item of ytInitialData["contents"]["twoColumnWatchNextResults"]["secondaryResults"]["secondaryResults"]["results"]) {
-            if (item["compactVideoRenderer"]) {
-                const recommendVideo = item["compactVideoRenderer"];
-                console.log(`推荐视频: ${JSON.stringify(recommendVideo)}`);
-                if (recommendVideo["videoId"]) {
-                    recommendInfo.push({
-                        "type": "gridVideoRenderer",
-                        "videoId": recommendVideo["videoId"],
-                        "title": recommendVideo["title"]?.["simpleText"],
-                        "thumbnails": recommendVideo["thumbnail"]?.["thumbnails"],
-                        "channelName": recommendVideo["longBylineText"]?.["runs"]?.[0]?.["text"],
-                        "publishedTimeText": recommendVideo["publishedTimeText"]?.["simpleText"],
-                        "viewCountText": recommendVideo["viewCountText"]?.["simpleText"],
-                        "shortViewCountText": recommendVideo["shortViewCountText"]?.["simpleText"],
-                        "lengthText": recommendVideo["lengthText"]?.["simpleText"]
-                    })
+        if (match && match.length === 2) {
+            const ytInitialData = JSON.parse(match[1]);
+            for (const item of ytInitialData["contents"]?.["twoColumnWatchNextResults"]?.["secondaryResults"]?.["secondaryResults"]?.["results"] || []) {
+                if (item["compactVideoRenderer"]) {
+                    const recommendVideo = item["compactVideoRenderer"];
+                    console.log(`推荐视频: ${JSON.stringify(recommendVideo)}`);
+                    if (recommendVideo["videoId"]) {
+                        recommendInfo.push({
+                            "type": "gridVideoRenderer",
+                            "videoId": recommendVideo["videoId"],
+                            "title": recommendVideo["title"]?.["simpleText"],
+                            "thumbnails": recommendVideo["thumbnail"]?.["thumbnails"],
+                            "channelName": recommendVideo["longBylineText"]?.["runs"]?.[0]?.["text"],
+                            "publishedTimeText": recommendVideo["publishedTimeText"]?.["simpleText"],
+                            "viewCountText": recommendVideo["viewCountText"]?.["simpleText"],
+                            "shortViewCountText": recommendVideo["shortViewCountText"]?.["simpleText"],
+                            "lengthText": recommendVideo["lengthText"]?.["simpleText"]
+                        })
+                    }
                 }
                 }
             }
             }
+        } else {
+            console.log(`解析失败,无法找到 ytInitialData,无法获取推荐视频`);
         }
         }
 
 
         const videoDetails = {
         const videoDetails = {
@@ -272,7 +298,7 @@ detail = async (url, local) => {
             "data": {
             "data": {
                 "videoDetails": videoDetails,
                 "videoDetails": videoDetails,
                 "streamingData": {
                 "streamingData": {
-                    "formats": formats
+                    "formats": formats.reverse()
                 }
                 }
             },
             },
             "id": "MusicDetailViewModel_detail_url"
             "id": "MusicDetailViewModel_detail_url"
@@ -285,11 +311,12 @@ detail = async (url, local) => {
             "msg": e.toString()
             "msg": e.toString()
         }
         }
         console.log(`解析失败: ${JSON.stringify(ret)}`);
         console.log(`解析失败: ${JSON.stringify(ret)}`);
+        console.log(e);
         return ret;
         return ret;
     }
     }
 }
 }
 
 
-search = async (keyword, next, local) => {
+search = async (keyword, next, platform) => {
     try {
     try {
         console.log(`接受到搜索请求 keyword: ${keyword}`);
         console.log(`接受到搜索请求 keyword: ${keyword}`);
         console.log(`接收到搜索请求 next: ${next}`);
         console.log(`接收到搜索请求 next: ${next}`);
@@ -305,7 +332,7 @@ search = async (keyword, next, local) => {
                 },
                 },
                 continuation: nextObject["continuation"]
                 continuation: nextObject["continuation"]
             };
             };
-            let res = await request('POST', `https://www.youtube.com/youtubei/v1/search?key=${key}`, JSON.stringify(body), {}, local);
+            let res = await request('POST', `https://www.youtube.com/youtubei/v1/search?key=${key}`, JSON.stringify(body), {}, platform);
             res = JSON.parse(res);
             res = JSON.parse(res);
             const videos = [];
             const videos = [];
             for (const item of res["onResponseReceivedCommands"][0]["appendContinuationItemsAction"]["continuationItems"][0]["itemSectionRenderer"]["contents"]) {
             for (const item of res["onResponseReceivedCommands"][0]["appendContinuationItemsAction"]["continuationItems"][0]["itemSectionRenderer"]["contents"]) {
@@ -344,7 +371,7 @@ search = async (keyword, next, local) => {
         } else {
         } else {
             let url = `https://www.youtube.com/results?q=${encodeURIComponent(keyword)}&sp=EgIQAQ%253D%253D`;
             let url = `https://www.youtube.com/results?q=${encodeURIComponent(keyword)}&sp=EgIQAQ%253D%253D`;
 
 
-            const html = await request('GET', url, null, {}, local);
+            const html = await request('GET', url, null, {}, platform);
 
 
             let regex = /var ytInitialData\s*=\s*({.*?});/;
             let regex = /var ytInitialData\s*=\s*({.*?});/;
             let match = html.match(regex);
             let match = html.match(regex);

+ 2 - 2
js/main.swift

@@ -90,13 +90,13 @@ func testSearch(keyword: String, ctx: JSContext) -> Void {
 
 
 let ctx = createJSContext()
 let ctx = createJSContext()
 
 
-if let url = URL(string: "http://hubgit.cn/ben/be-ytb/raw/master/js/info.js") {
+if let url = URL(string: "file:///Users/ben/Desktop/app/be/be-ytb/js/info.js") {
     downloadJSFile(url: url) { result in
     downloadJSFile(url: url) { result in
         switch result {
         switch result {
         case .success(let jsString):
         case .success(let jsString):
             print("下载远程JS成功")
             print("下载远程JS成功")
             ctx.evaluateScript(jsString)
             ctx.evaluateScript(jsString)
-            testDetail(url: "https://www.youtube.com/watch?v=d0R-JyU4Btk", ctx: ctx)
+            testDetail(url: "https://www.youtube.com/watch?v=jniI5VUugCk", ctx: ctx)
 //            testSearch(keyword: "周杰伦", ctx: ctx)
 //            testSearch(keyword: "周杰伦", ctx: ctx)
         case .failure(let error):
         case .failure(let error):
             print("Download Error: \(error)")
             print("Download Error: \(error)")

+ 8 - 2
js/nginx.conf

@@ -22,11 +22,17 @@ http {
 
 
     server {
     server {
         listen       80;
         listen       80;
-        proxy_hide_header 'Access-Control-Allow-Origin';
+        proxy_hide_header 'Vary';
         add_header 'Access-Control-Allow-Origin' '*' always;
         add_header 'Access-Control-Allow-Origin' '*' always;
         add_header 'Access-Control-Allow-Methods' '*' always;
         add_header 'Access-Control-Allow-Methods' '*' always;
-        add_header 'Access-Control-Allow-Headers' '*';
+        add_header 'Access-Control-Allow-Headers' '*' always;
         location / {
         location / {
+            if ($request_method = OPTIONS) {
+                add_header 'Access-Control-Allow-Origin' '*' always;
+                add_header 'Access-Control-Allow-Methods' '*' always;
+                add_header 'Access-Control-Allow-Headers' '*' always;
+                return 204;
+            }
             proxy_pass https://www.youtube.com/;
             proxy_pass https://www.youtube.com/;
         }
         }
     }
     }

+ 9 - 19
js/test.js

@@ -1,5 +1,5 @@
-const url = `https://www.youtube.com/watch?v=S9bCLPwzSC0`
-detail(url, true)
+const url = `https://www.youtube.com/watch?v=8jXbBZEPiWk`
+detail(url, 'WEB')
     .then(res => {
     .then(res => {
         console.log(res);
         console.log(res);
     })
     })
@@ -7,20 +7,10 @@ detail(url, true)
         console.log(e);
         console.log(e);
     })
     })
 
 
-// search("周 杰伦", null, true)
-//     .then(res => {
-//         console.log(res);
-//
-//         const next = res["data"]["next"];
-//         console.log(next);
-//         search("周 杰伦", next, true)
-//             .then(nextRes => {
-//                 console.log(nextRes);
-//             })
-//             .catch(e => {
-//                 console.log(e);
-//             })
-//     })
-//     .catch(e => {
-//         console.log(e);
-//     })
+search("周 杰伦", null, "WEB")
+    .then(res => {
+        console.log(res);
+    })
+    .catch(e => {
+        console.log(e);
+    })

+ 17 - 0
test.py

@@ -0,0 +1,17 @@
+import json
+
+import yt_dlp
+
+with yt_dlp.YoutubeDL({
+    "flat-playlist": True,
+    "extract_flat": "flat-playlist",
+    'proxy': 'socks://127.0.0.1:8889',
+    'nocheckcertificate': True
+}) as ydl:
+    info = ydl.extract_info("https://www.youtube.com/watch?v=S9bCLPwzSC0", download=False)
+    formats = []
+    for item in info["formats"]:
+        if item.get("resolution") != "audio only" and item.get("url") and item.get("acodec") and item.get(
+                "acodec") != "none" and item.get("vcodec"):
+            formats.append(item)
+    print(json.dumps(formats, ensure_ascii=False))

+ 72 - 0
yt_dlp.json

@@ -0,0 +1,72 @@
+[
+  {
+    "asr": 22050,
+    "filesize": null,
+    "format_id": "18",
+    "format_note": "360p",
+    "source_preference": -1,
+    "fps": 24,
+    "audio_channels": 2,
+    "height": 360,
+    "quality": 6.0,
+    "has_drm": false,
+    "tbr": 268.987,
+    "filesize_approx": 8673922,
+    "url": "https://rr1---sn-i3b7knsl.googlevideo.com/videoplayback?expire=1715539168&ei=f7hAZrCyO9iX1d8PxLO90AE&ip=18.163.124.204&id=o-AJwI4MqhEMIZZsUwvQHgAab7ujnk1XP8mNGlhCaKN5ht&itag=18&source=youtube&requiressl=yes&xpc=EgVo2aDSNQ%3D%3D&mh=2l&mm=31%2C29&mn=sn-i3b7knsl%2Csn-i3belnl7&ms=au%2Crdu&mv=m&mvi=1&pl=16&gcr=hk&initcwndbps=2215000&vprv=1&svpuc=1&xtags=heaudio%3Dtrue&mime=video%2Fmp4&rqh=1&cnr=14&ratebypass=yes&dur=257.973&lmt=1705997836001178&mt=1715517169&fvip=2&c=ANDROID&txp=4538434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cgcr%2Cvprv%2Csvpuc%2Cxtags%2Cmime%2Crqh%2Ccnr%2Cratebypass%2Cdur%2Clmt&sig=AJfQdSswRQIgJ-eWUrAxB9diR7QM3GCN0tSl_pFih-tZmebTxzCpqi4CIQCLEValKNYnwYBw9ww5FKmYR1Ug8Z2EN5iAILCNazASVA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AHWaYeowRAIgUUqI3RF3Pp7LwR4sb44d-kvdf9gmOJ4ivbF6SVABWtICIA98ZHjOWjtjfUG93IgNgLcjLINjE9lwBXqR2GJskOlH",
+    "width": 640,
+    "language": null,
+    "language_preference": -1,
+    "preference": null,
+    "ext": "mp4",
+    "vcodec": "avc1.42001E",
+    "acodec": "mp4a.40.2",
+    "dynamic_range": "SDR",
+    "downloader_options": {
+      "http_chunk_size": 10485760
+    },
+    "protocol": "https",
+    "resolution": "640x360",
+    "aspect_ratio": 1.78,
+    "http_headers": {},
+    "video_ext": "mp4",
+    "audio_ext": "none",
+    "vbr": null,
+    "abr": null,
+    "format": "18 - 640x360 (360p)"
+  },
+  {
+    "asr": 44100,
+    "filesize": null,
+    "format_id": "22",
+    "format_note": "720p",
+    "source_preference": -5,
+    "fps": 24,
+    "audio_channels": 2,
+    "height": 720,
+    "quality": 8.0,
+    "has_drm": false,
+    "tbr": 731.988,
+    "filesize_approx": 23597829,
+    "url": "https://rr1---sn-i3b7knsl.googlevideo.com/videoplayback?expire=1715539168&ei=f7hAZrCyO9iX1d8PxLO90AE&ip=18.163.124.204&id=o-AJwI4MqhEMIZZsUwvQHgAab7ujnk1XP8mNGlhCaKN5ht&itag=22&source=youtube&requiressl=yes&xpc=EgVo2aDSNQ%3D%3D&mh=2l&mm=31%2C29&mn=sn-i3b7knsl%2Csn-i3belnl7&ms=au%2Crdu&mv=m&mvi=1&pl=16&gcr=hk&initcwndbps=2215000&vprv=1&svpuc=1&mime=video%2Fmp4&rqh=1&cnr=14&ratebypass=yes&dur=257.904&lmt=1706085849052968&mt=1715517169&fvip=2&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cgcr%2Cvprv%2Csvpuc%2Cmime%2Crqh%2Ccnr%2Cratebypass%2Cdur%2Clmt&sig=AJfQdSswRAIgGDveqKaqRKcEmdEyK5VWoVPHV8gXn8eaFxxtSidgf_4CICeImqNBUFH8ipaUZvhUYnddbtzSc00abFUWoHk_trPv&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AHWaYeowRAIgUUqI3RF3Pp7LwR4sb44d-kvdf9gmOJ4ivbF6SVABWtICIA98ZHjOWjtjfUG93IgNgLcjLINjE9lwBXqR2GJskOlH",
+    "width": 1280,
+    "language": null,
+    "language_preference": -1,
+    "preference": null,
+    "ext": "mp4",
+    "vcodec": "avc1.64001F",
+    "acodec": "mp4a.40.2",
+    "dynamic_range": "SDR",
+    "downloader_options": {
+      "http_chunk_size": 10485760
+    },
+    "protocol": "https",
+    "resolution": "1280x720",
+    "aspect_ratio": 1.78,
+    "http_headers": {},
+    "video_ext": "mp4",
+    "audio_ext": "none",
+    "vbr": null,
+    "abr": null,
+    "format": "22 - 1280x720 (720p)"
+  }
+]