Merge remote-tracking branch 'upstream/dev' into feat/instagram-scraper

This commit is contained in:
Anish Sarkar 2026-07-11 04:31:11 +05:30
commit e38ca19b18
119 changed files with 5948 additions and 36 deletions

View file

@ -0,0 +1,559 @@
{
"AIGCDescription": "",
"CategoryType": 111,
"IsHDBitrate": false,
"anchors": [
{
"description": "CapCut · Video Editor",
"extraInfo": {
"subtype": ""
},
"icon": {
"urlList": [
"https://p16-common-sign.tiktokcdn-us.com/tiktok-obj/capcut_logo_64px_bk.png~tplv-tiktokx-origin.image?dr=9627&x-expires=1783544400&x-signature=44xwWHkyYJdE%2FIwqS3zAAjU4XQ4%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=9b759fb9&idc=useast5",
"https://p19-common-sign.tiktokcdn-us.com/tiktok-obj/capcut_logo_64px_bk.png~tplv-tiktokx-origin.image?dr=9627&x-expires=1783544400&x-signature=lMwgk8e0wLaM3WkpnsMqYcxZN6w%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=9b759fb9&idc=useast5",
"https://p16-common-sign.tiktokcdn-us.com/tiktok-obj/capcut_logo_64px_bk.png~tplv-tiktokx-origin.jpeg?dr=9627&x-expires=1783544400&x-signature=1EMaRlc0Snbs9Ltwr0s95%2BDvpFk%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=9b759fb9&idc=useast5"
]
},
"id": "0",
"keyword": "CapCut · Editing made easy",
"logExtra": "{\"anchor_id\":0,\"anchor_name\":\"CapCut · Editing made easy\",\"anchor_title_detail\":\"None\",\"anchor_title_id\":21502485763,\"anchor_type\":\"TT_CAPCUT\",\"capability_key\":\"default\",\"ccep_vip\":0,\"if_race_trigger\":0,\"is_two_line\":0,\"landing_page_style_id\":21502486275,\"maker_source\":\"\",\"publish_key\":\"default\",\"template_id\":\"none\",\"tutorial_id\":\"none\",\"viamaker_anchor_capability_key_weight\":1,\"viamaker_anchor_style_idx\":-1,\"viamaker_anchor_style_source\":3,\"video_source\":0,\"video_type_id\":1}",
"schema": "https://www.capcut.com/editor?scenario=tiktok&utm_source=tiktok_anchor_tool&utm_medium=tiktok_anchor&utm_campaign=70361230",
"thumbnail": {
"height": 64,
"urlList": [
"https://p19-common-sign.tiktokcdn-us.com/tiktok-obj/64x_Capcut3x.png~tplv-tiktokx-origin.image?dr=9627&x-expires=1783544400&x-signature=GOZUldk%2BrxINygKT0jwxeJz9VLQ%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=9b759fb9&idc=useast5",
"https://p16-common-sign.tiktokcdn-us.com/tiktok-obj/64x_Capcut3x.png~tplv-tiktokx-origin.image?dr=9627&x-expires=1783544400&x-signature=SJ6q86bjtCoRArlVNS1DjDDFWoc%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=9b759fb9&idc=useast5",
"https://p19-common-sign.tiktokcdn-us.com/tiktok-obj/64x_Capcut3x.png~tplv-tiktokx-origin.jpeg?dr=9627&x-expires=1783544400&x-signature=c7mUcXdyEOFc0SMcLAkkib3v9RU%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=9b759fb9&idc=useast5"
],
"width": 64
},
"type": 54
}
],
"author": {
"avatarLarger": "https://p19-common-sign.tiktokcdn-us.com/tos-useast8-avt-0068-tx2/b29bd2d826cffba1af22f99793a7d5dc~tplv-tiktokx-cropcenter:1080:1080.jpeg?dr=9640&refresh_token=c6d77c8a&x-expires=1783695600&x-signature=mUTuvDWLEGrAxXtWZlBE7gUxCl8%3D&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=useast5",
"avatarMedium": "https://p16-common-sign.tiktokcdn-us.com/tos-useast8-avt-0068-tx2/b29bd2d826cffba1af22f99793a7d5dc~tplv-tiktokx-cropcenter:720:720.jpeg?dr=9640&refresh_token=dfe18cb8&x-expires=1783695600&x-signature=cP14vkr0ndxwhW%2B1pWfefXfTsls%3D&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=useast5",
"avatarThumb": "https://p19-common-sign.tiktokcdn-us.com/tos-useast8-avt-0068-tx2/b29bd2d826cffba1af22f99793a7d5dc~tplv-tiktokx-cropcenter:100:100.jpeg?dr=9640&refresh_token=8c557171&x-expires=1783695600&x-signature=VaAysM3zFkbl%2FOT74d0h2kdkLqo%3D&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=useast5",
"commentSetting": 0,
"downloadSetting": 3,
"duetSetting": 0,
"ftc": false,
"id": "7501138183701103662",
"isADVirtual": false,
"isEmbedBanned": false,
"nickname": "Cookwithhali",
"openFavorite": false,
"privateAccount": false,
"relation": 0,
"secUid": "MS4wLjABAAAAywZ6lCXK3u4zKz8eQxxcjVblLiugz6zMysU98G7dyKVxu6IvMOJ97mpIfpDxUL4q",
"secret": false,
"shortDramaCreator": {},
"signature": "From my kitchen to your screen! 🍞\nFollow for more ✨\nIG: halikit25\n\n📩 Halikitchen25@gmail.com",
"stitchSetting": 0,
"uniqueId": "cookwithhali",
"verified": false
},
"authorStats": {
"diggCount": 223,
"followerCount": 372600,
"followingCount": 0,
"friendCount": 0,
"heart": 8000000,
"heartCount": 8000000,
"videoCount": 279
},
"authorStatsV2": {
"diggCount": "223",
"followerCount": "372600",
"followingCount": "0",
"friendCount": "0",
"heart": "8000000",
"heartCount": "8000000",
"videoCount": "279"
},
"backendSourceEventTracking": "",
"challenges": [
{
"coverLarger": "",
"coverMedium": "",
"coverThumb": "",
"desc": "",
"id": "6983",
"profileLarger": "",
"profileMedium": "",
"profileThumb": "",
"title": "bread"
},
{
"coverLarger": "",
"coverMedium": "",
"coverThumb": "",
"desc": "",
"id": "285169",
"profileLarger": "",
"profileMedium": "",
"profileThumb": "",
"title": "garlicbread"
},
{
"coverLarger": "",
"coverMedium": "",
"coverThumb": "",
"desc": "",
"id": "1643317566954502",
"profileLarger": "",
"profileMedium": "",
"profileThumb": "",
"title": "breadtok"
},
{
"coverLarger": "",
"coverMedium": "",
"coverThumb": "",
"desc": "Whether you're baking the perfect pie or just searching for a recipe, you've come to the right place for all things #Baking.",
"id": "7250",
"profileLarger": "https://p19-common-sign.tiktokcdn-us.com/musically-maliva-obj/1aa8a54136dc06390b83508fdd683160~tplv-tiktokx-origin.image?dr=9627&x-expires=1783544400&x-signature=nigLPQXvvDM%2BxFQezn3ELBH%2BLwQ%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=9b759fb9&idc=useast5",
"profileMedium": "https://p19-common-sign.tiktokcdn-us.com/musically-maliva-obj/1aa8a54136dc06390b83508fdd683160~tplv-tiktokx-origin.image?dr=9627&x-expires=1783544400&x-signature=nigLPQXvvDM%2BxFQezn3ELBH%2BLwQ%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=9b759fb9&idc=useast5",
"profileThumb": "https://p19-common-sign.tiktokcdn-us.com/musically-maliva-obj/1aa8a54136dc06390b83508fdd683160~tplv-tiktokx-origin.image?dr=9627&x-expires=1783544400&x-signature=nigLPQXvvDM%2BxFQezn3ELBH%2BLwQ%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=9b759fb9&idc=useast5",
"title": "baking"
},
{
"coverLarger": "",
"coverMedium": "",
"coverThumb": "",
"desc": "",
"id": "10488587",
"profileLarger": "",
"profileMedium": "",
"profileThumb": "",
"title": "homemadebread"
}
],
"collected": false,
"contents": [
{
"desc": "Pull Apart Garlic Bread "
},
{
"desc": ""
},
{
"desc": "Ingredients"
},
{
"desc": ""
},
{
"desc": "* 1 tsp yeast"
},
{
"desc": "* 1 tbsp sugar"
},
{
"desc": "* 270g milk (about 1 cup + 2 tbsp)"
},
{
"desc": "* 380g flour (about 3 cups)"
},
{
"desc": "* 1 tsp salt"
},
{
"desc": "* 43g softened butter (about 3 tbsp)"
},
{
"desc": ""
},
{
"desc": "Butter Mixture"
},
{
"desc": ""
},
{
"desc": "* 100g butter, softened"
},
{
"desc": "* 1 tbsp chopped cilantro"
},
{
"desc": "* 1 garlic clove, minced"
},
{
"desc": "* Shredded cheese"
},
{
"desc": ""
},
{
"desc": "Instructions"
},
{
"desc": ""
},
{
"desc": "1. In a bowl, mix the sugar and yeast. Add the milk, then add the flour and mix until combined."
},
{
"desc": "2. Add the salt and softened butter, then coil fold until the butter is fully incorporated."
},
{
"desc": "3. Cover and let the dough rest for 2 hours or until doubled."
},
{
"desc": "4. Mix the softened butter with the cilantro and minced garlic."
},
{
"desc": "5. Roll the dough into a rectangle and spread the garlic butter mixture evenly over it. Sprinkle with shredded cheddar cheese."
},
{
"desc": "6. Cut the dough in half, stack one half on top of the other, then cut into strips. Transfer the pieces into a loaf pan."
},
{
"desc": "7. Cover and let rest for 20 minutes."
},
{
"desc": "8. Bake at 370°F for 30 minutes, or until golden brown."
},
{
"desc": "9. Brush with the remaining garlic butter mixture while still warm."
},
{
"desc": ""
},
{
"desc": "#bread #garlicbread #breadtok #baking #homemadebread ",
"textExtra": [
{
"awemeId": "",
"end": 6,
"hashtagName": "bread",
"isCommerce": false,
"start": 0,
"subType": 0,
"type": 1
},
{
"awemeId": "",
"end": 19,
"hashtagName": "garlicbread",
"isCommerce": false,
"start": 7,
"subType": 0,
"type": 1
},
{
"awemeId": "",
"end": 29,
"hashtagName": "breadtok",
"isCommerce": false,
"start": 20,
"subType": 0,
"type": 1
},
{
"awemeId": "",
"end": 37,
"hashtagName": "baking",
"isCommerce": false,
"start": 30,
"subType": 0,
"type": 1
},
{
"awemeId": "",
"end": 52,
"hashtagName": "homemadebread",
"isCommerce": false,
"start": 38,
"subType": 0,
"type": 1
}
]
}
],
"createTime": 1782509863,
"desc": "Pull Apart Garlic Bread Ingredients * 1 tsp yeast * 1 tbsp sugar * 270g milk (about 1 cup + 2 tbsp) * 380g flour (about 3 cups) * 1 tsp salt * 43g softened butter (about 3 tbsp) Butter Mixture * 100g butter, softened * 1 tbsp chopped cilantro * 1 garlic clove, minced * Shredded cheese Instructions 1. In a bowl, mix the sugar and yeast. Add the milk, then add the flour and mix until combined. 2. Add the salt and softened butter, then coil fold until the butter is fully incorporated. 3. Cover and let the dough rest for 2 hours or until doubled. 4. Mix the softened butter with the cilantro and minced garlic. 5. Roll the dough into a rectangle and spread the garlic butter mixture evenly over it. Sprinkle with shredded cheddar cheese. 6. Cut the dough in half, stack one half on top of the other, then cut into strips. Transfer the pieces into a loaf pan. 7. Cover and let rest for 20 minutes. 8. Bake at 370°F for 30 minutes, or until golden brown. 9. Brush with the remaining garlic butter mixture while still warm. #bread #garlicbread #breadtok #baking #homemadebread ",
"digged": false,
"diversificationId": 10040,
"duetDisplay": 0,
"duetEnabled": true,
"forFriend": false,
"id": "7655821461772406047",
"isAd": false,
"isReviewing": false,
"itemCommentStatus": 0,
"item_control": {
"can_repost": true
},
"music": {
"authorName": "Cookwithhali",
"coverLarge": "https://p19-common-sign.tiktokcdn-us.com/tos-useast8-avt-0068-tx2/b29bd2d826cffba1af22f99793a7d5dc~tplv-tiktokx-cropcenter:1080:1080.jpeg?dr=9640&refresh_token=c6d77c8a&x-expires=1783695600&x-signature=mUTuvDWLEGrAxXtWZlBE7gUxCl8%3D&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=useast5",
"coverMedium": "https://p16-common-sign.tiktokcdn-us.com/tos-useast8-avt-0068-tx2/b29bd2d826cffba1af22f99793a7d5dc~tplv-tiktokx-cropcenter:720:720.jpeg?dr=9640&refresh_token=dfe18cb8&x-expires=1783695600&x-signature=cP14vkr0ndxwhW%2B1pWfefXfTsls%3D&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=useast5",
"coverThumb": "https://p19-common-sign.tiktokcdn-us.com/tos-useast8-avt-0068-tx2/b29bd2d826cffba1af22f99793a7d5dc~tplv-tiktokx-cropcenter:100:100.jpeg?dr=9640&refresh_token=8c557171&x-expires=1783695600&x-signature=VaAysM3zFkbl%2FOT74d0h2kdkLqo%3D&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=useast5",
"duration": 62,
"id": "7655821492566919966",
"isCopyrighted": false,
"is_commerce_music": true,
"is_unlimited_music": false,
"original": true,
"playUrl": "https://v16m.tiktokcdn-us.com/ecd83422a3e7bf5450bd92ebf07e6fce/6a4ec449/video/tos/useast8/tos-useast8-v-27dcd7-tx2/ocBIhANWILSemeAAINEGoTTPe58AJaRegg6eKS/?a=1233&bti=ODszNWYuMDE6&&bt=125&ft=GSDrKInz7Th2sbSGXq8Zmo&mime_type=audio_mpeg&rc=anY5cXA5cmRpOzMzaTU8NEBpanY5cXA5cmRpOzMzaTU8NEBpYzZrMmRjL3NhLS1kMTJzYSNpYzZrMmRjL3NhLS1kMTJzcw%3D%3D&vvpl=1&l=2026070815413036F7D0D0506FB569C78C&btag=e00050000",
"private": false,
"shoot_duration": 62,
"title": "original sound",
"tt2dsp": {}
},
"officalItem": false,
"originalItem": false,
"privateItem": false,
"secret": false,
"shareEnabled": true,
"stats": {
"collectCount": 396400,
"commentCount": 3720,
"diggCount": 754100,
"playCount": 8300000,
"shareCount": 144500
},
"statsV2": {
"collectCount": "396397",
"commentCount": "3720",
"diggCount": "754100",
"playCount": "8300000",
"repostCount": "0",
"shareCount": "144500"
},
"stickersOnItem": [
{
"stickerText": [
"Cheesy Pull Apart Garlic Bread! 🧄🧀\n",
"(Bakery-Style & No-Knead)"
],
"stickerType": 4
}
],
"stitchDisplay": 0,
"stitchEnabled": true,
"textExtra": [
{
"awemeId": "",
"end": 1030,
"hashtagName": "bread",
"isCommerce": false,
"start": 1024,
"subType": 0,
"type": 1
},
{
"awemeId": "",
"end": 1043,
"hashtagName": "garlicbread",
"isCommerce": false,
"start": 1031,
"subType": 0,
"type": 1
},
{
"awemeId": "",
"end": 1053,
"hashtagName": "breadtok",
"isCommerce": false,
"start": 1044,
"subType": 0,
"type": 1
},
{
"awemeId": "",
"end": 1061,
"hashtagName": "baking",
"isCommerce": false,
"start": 1054,
"subType": 0,
"type": 1
},
{
"awemeId": "",
"end": 1076,
"hashtagName": "homemadebread",
"isCommerce": false,
"start": 1062,
"subType": 0,
"type": 1
}
],
"textLanguage": "en",
"textTranslatable": true,
"video": {
"PlayAddrStruct": {
"DataSize": 13761060,
"FileCs": "c:0-51456-f8d1",
"FileHash": "485174c8492db027adbbf6dca7c7c201",
"Height": 1280,
"Uri": "v15044gf0000d8vf0bvog65kkvkd8ohg",
"UrlKey": "v15044gf0000d8vf0bvog65kkvkd8ohg_h264_720p_1773703",
"UrlList": [
"https://v16-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-pve-0068-tx2/oYDTerfAgGxvqFaEctpBAvVGEPIE8Q9ARoxPNB/?a=1988&bti=ODszNWYuMDE6&&bt=1732&ft=aEeq8qT0mIoPD12CGneI3wUcfzAbMeF~O5&mime_type=video_mp4&rc=NWZnZDVoNDQzN2c1ZGU0Z0BpanJneW45cmVpOzMzaTczNEBgMTBgYjQuNWExLmNgXmI2YSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698153&l=2026070815413036F7D0D0506FB569C78C&ply_type=2&policy=2&signature=aa5be80671c4a73d54517ee8e4a471ad&tk=tt_chain_token&btag=e00088000",
"https://v19-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-pve-0068-tx2/oYDTerfAgGxvqFaEctpBAvVGEPIE8Q9ARoxPNB/?a=1988&bti=ODszNWYuMDE6&&bt=1732&ft=aEeq8qT0mIoPD12CGneI3wUcfzAbMeF~O5&mime_type=video_mp4&rc=NWZnZDVoNDQzN2c1ZGU0Z0BpanJneW45cmVpOzMzaTczNEBgMTBgYjQuNWExLmNgXmI2YSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698153&l=2026070815413036F7D0D0506FB569C78C&ply_type=2&policy=2&signature=aa5be80671c4a73d54517ee8e4a471ad&tk=tt_chain_token&btag=e00088000",
"https://www.tiktok.com/aweme/v1/play/?faid=1988&file_id=9de1abcaf21d44d081117ee2ca3d4cc2&is_play_url=1&item_id=7655821461772406047&line=0&ply_type=2&signaturev3=dmlkZW9faWQ7ZmlsZV9pZDtpdGVtX2lkLjcwMGExZmI3NDg4YzFiZTIwZjczM2IyMmM0OTk2YTI2&tk=tt_chain_token&video_id=v15044gf0000d8vf0bvog65kkvkd8ohg"
],
"Width": 720
},
"VQScore": "69.69",
"bitrate": 1773703,
"bitrateInfo": [
{
"Bitrate": 1836180,
"BitrateFPS": 29,
"CodecType": "h265_hvc1",
"Format": "mp4",
"GearName": "adapt_lowest_1080_1",
"MVMAF": "{\"v2.0\": {\"srv1\": {\"v1080\": 96.83, \"v960\": 97.64, \"v864\": 98.256, \"v720\": 98.838}, \"ori\": {\"v1080\": 91.834, \"v960\": 93.423, \"v864\": 94.54, \"v720\": 95.872}}}",
"PlayAddr": {
"DataSize": 14245777,
"FileCs": "c:0-52754-ed06",
"FileHash": "38996a98cfeb2714fca1b94768090c37",
"Height": 1920,
"Uri": "v15044gf0000d8vf0bvog65kkvkd8ohg",
"UrlKey": "v15044gf0000d8vf0bvog65kkvkd8ohg_bytevc1_1080p_1836180",
"UrlList": [
"https://v16-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-pve-0068-tx2/oIygxEfEVAP1qvGVTIBARoAvb9QYFGEKDp5v9e/?a=1988&bti=ODszNWYuMDE6&&bt=1793&ft=aEeq8qT0mIoPD12CGneI3wUcfzAbMeF~O5&mime_type=video_mp4&rc=aDlmNGg7ZTdlaGY5ZzUzZ0BpanJneW45cmVpOzMzaTczNEBjXl5hMzAvXy8xNDAuYmIyYSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698153&l=2026070815413036F7D0D0506FB569C78C&ply_type=2&policy=2&signature=db26af0a98ff05e504d7121b25048527&tk=tt_chain_token&btag=e00088000",
"https://v19-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-pve-0068-tx2/oIygxEfEVAP1qvGVTIBARoAvb9QYFGEKDp5v9e/?a=1988&bti=ODszNWYuMDE6&&bt=1793&ft=aEeq8qT0mIoPD12CGneI3wUcfzAbMeF~O5&mime_type=video_mp4&rc=aDlmNGg7ZTdlaGY5ZzUzZ0BpanJneW45cmVpOzMzaTczNEBjXl5hMzAvXy8xNDAuYmIyYSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698153&l=2026070815413036F7D0D0506FB569C78C&ply_type=2&policy=2&signature=db26af0a98ff05e504d7121b25048527&tk=tt_chain_token&btag=e00088000",
"https://www.tiktok.com/aweme/v1/play/?faid=1988&file_id=5de0e21d367c4e2bb42b386ed1aca6fe&is_play_url=1&item_id=7655821461772406047&line=0&ply_type=2&signaturev3=dmlkZW9faWQ7ZmlsZV9pZDtpdGVtX2lkLjAzMmU1YzJlMTAwMDJiYjIwMmFiN2RhNTIzOWVhYWRk&tk=tt_chain_token&video_id=v15044gf0000d8vf0bvog65kkvkd8ohg"
],
"Width": 1080
},
"QualityType": 2,
"VideoExtra": "{\"PktOffsetMap\":\"[{\\\"time\\\": 1, \\\"offset\\\": 385342}, {\\\"time\\\": 2, \\\"offset\\\": 561014}, {\\\"time\\\": 3, \\\"offset\\\": 703814}, {\\\"time\\\": 4, \\\"offset\\\": 855867}, {\\\"time\\\": 5, \\\"offset\\\": 1019333}, {\\\"time\\\": 10, \\\"offset\\\": 2028412}]\",\"mvmaf\":\"{\\\"v2.0\\\": {\\\"srv1\\\": {\\\"v1080\\\": 96.83, \\\"v960\\\": 97.64, \\\"v864\\\": 98.256, \\\"v720\\\": 98.838}, \\\"ori\\\": {\\\"v1080\\\": 91.834, \\\"v960\\\": 93.423, \\\"v864\\\": 94.54, \\\"v720\\\": 95.872}}}\",\"ufq\":\"{\\\"enh\\\":89.317,\\\"playback\\\":{\\\"ori\\\":85.151,\\\"srv1\\\":90.147},\\\"src\\\":85.957,\\\"trans\\\":85.151,\\\"version\\\":\\\"v1.2\\\"}\",\"volume_info_json\":\"\",\"transcode_feature_id\":\"11b0345f3bf185ddbe7c48bd7301980f\",\"dec_info\":\"\",\"gearvqm\":\"\",\"audio_bit_rate\":96215,\"ps_info\":\"{\\\"vps\\\": \\\"AQwC//8BYAAAAwAAAwAAAwAAAwCWAACdzkgS\\\", \\\"sps\\\": \\\"AQMBYAAAAwAAAwAAAwAAAwCWAACgAhyAHgWWdzkySUQs0RJJJJify4d/1+bPl/1+M4gQ5qAgICCAAAADAIAAAA8E\\\", \\\"pps\\\": \\\"AcElPAiQ\\\"}\"}"
},
{
"Bitrate": 1773703,
"BitrateFPS": 29,
"CodecType": "h264",
"Format": "mp4",
"GearName": "normal_720_0",
"MVMAF": "{\"v2.0\": {\"srv1\": {\"v1080\": 84.77, \"v960\": 88.554, \"v864\": 88.982, \"v720\": 92.772}, \"ori\": {\"v1080\": 84.77, \"v960\": 88.554, \"v864\": 88.982, \"v720\": 92.772}}}",
"PlayAddr": {
"DataSize": 13761060,
"FileCs": "c:0-51456-f8d1",
"FileHash": "485174c8492db027adbbf6dca7c7c201",
"Height": 1280,
"Uri": "v15044gf0000d8vf0bvog65kkvkd8ohg",
"UrlKey": "v15044gf0000d8vf0bvog65kkvkd8ohg_h264_720p_1773703",
"UrlList": [
"https://v16-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-pve-0068-tx2/oYDTerfAgGxvqFaEctpBAvVGEPIE8Q9ARoxPNB/?a=1988&bti=ODszNWYuMDE6&&bt=1732&ft=aEeq8qT0mIoPD12CGneI3wUcfzAbMeF~O5&mime_type=video_mp4&rc=NWZnZDVoNDQzN2c1ZGU0Z0BpanJneW45cmVpOzMzaTczNEBgMTBgYjQuNWExLmNgXmI2YSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698153&l=2026070815413036F7D0D0506FB569C78C&ply_type=2&policy=2&signature=aa5be80671c4a73d54517ee8e4a471ad&tk=tt_chain_token&btag=e00088000",
"https://v19-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-pve-0068-tx2/oYDTerfAgGxvqFaEctpBAvVGEPIE8Q9ARoxPNB/?a=1988&bti=ODszNWYuMDE6&&bt=1732&ft=aEeq8qT0mIoPD12CGneI3wUcfzAbMeF~O5&mime_type=video_mp4&rc=NWZnZDVoNDQzN2c1ZGU0Z0BpanJneW45cmVpOzMzaTczNEBgMTBgYjQuNWExLmNgXmI2YSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698153&l=2026070815413036F7D0D0506FB569C78C&ply_type=2&policy=2&signature=aa5be80671c4a73d54517ee8e4a471ad&tk=tt_chain_token&btag=e00088000",
"https://www.tiktok.com/aweme/v1/play/?faid=1988&file_id=9de1abcaf21d44d081117ee2ca3d4cc2&is_play_url=1&item_id=7655821461772406047&line=0&ply_type=2&signaturev3=dmlkZW9faWQ7ZmlsZV9pZDtpdGVtX2lkLjcwMGExZmI3NDg4YzFiZTIwZjczM2IyMmM0OTk2YTI2&tk=tt_chain_token&video_id=v15044gf0000d8vf0bvog65kkvkd8ohg"
],
"Width": 720
},
"QualityType": 10,
"VideoExtra": "{\"PktOffsetMap\":\"[{\\\"time\\\": 1, \\\"offset\\\": 400712}, {\\\"time\\\": 2, \\\"offset\\\": 659243}, {\\\"time\\\": 3, \\\"offset\\\": 886968}, {\\\"time\\\": 4, \\\"offset\\\": 1101663}, {\\\"time\\\": 5, \\\"offset\\\": 1368565}, {\\\"time\\\": 10, \\\"offset\\\": 2375704}]\",\"mvmaf\":\"{\\\"v2.0\\\": {\\\"srv1\\\": {\\\"v1080\\\": 84.77, \\\"v960\\\": 88.554, \\\"v864\\\": 88.982, \\\"v720\\\": 92.772}, \\\"ori\\\": {\\\"v1080\\\": 84.77, \\\"v960\\\": 88.554, \\\"v864\\\": 88.982, \\\"v720\\\": 92.772}}}\",\"ufq\":\"\",\"volume_info_json\":\"\",\"transcode_feature_id\":\"65b88c41c0963253ad31b6f68981a242\",\"dec_info\":\"\",\"gearvqm\":\"\",\"audio_bit_rate\":64193}"
},
{
"Bitrate": 966380,
"BitrateFPS": 29,
"CodecType": "h265_hvc1",
"Format": "mp4",
"GearName": "adapt_lower_720_1",
"MVMAF": "{\"v2.0\": {\"srv1\": {\"v1080\": 90.05, \"v960\": 91.958, \"v864\": 93.608, \"v720\": 95.869}, \"ori\": {\"v1080\": 82.259, \"v960\": 85.339, \"v864\": 87.288, \"v720\": 90.851}}}",
"PlayAddr": {
"DataSize": 7497540,
"FileCs": "c:0-52786-7f65",
"FileHash": "1376df24c917669727b27273697fb826",
"Height": 1280,
"Uri": "v15044gf0000d8vf0bvog65kkvkd8ohg",
"UrlKey": "v15044gf0000d8vf0bvog65kkvkd8ohg_bytevc1_720p_966380",
"UrlList": [
"https://v16-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-pve-0068-tx2/og99rgOGqJED8xGeBpFEAADvEPARnbTIQTfoVv/?a=1988&bti=ODszNWYuMDE6&&bt=943&ft=aEeq8qT0mIoPD12CGneI3wUcfzAbMeF~O5&mime_type=video_mp4&rc=OTNkNDk2NzlmM2VkPGg7OkBpanJneW45cmVpOzMzaTczNEBeMmBiYWBfNWIxLzEyNF81YSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698153&l=2026070815413036F7D0D0506FB569C78C&ply_type=2&policy=2&signature=e3690d67b2ecae289320f5db6c338d05&tk=tt_chain_token&btag=e00088000",
"https://v19-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-pve-0068-tx2/og99rgOGqJED8xGeBpFEAADvEPARnbTIQTfoVv/?a=1988&bti=ODszNWYuMDE6&&bt=943&ft=aEeq8qT0mIoPD12CGneI3wUcfzAbMeF~O5&mime_type=video_mp4&rc=OTNkNDk2NzlmM2VkPGg7OkBpanJneW45cmVpOzMzaTczNEBeMmBiYWBfNWIxLzEyNF81YSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698153&l=2026070815413036F7D0D0506FB569C78C&ply_type=2&policy=2&signature=e3690d67b2ecae289320f5db6c338d05&tk=tt_chain_token&btag=e00088000",
"https://www.tiktok.com/aweme/v1/play/?faid=1988&file_id=87b87e594a2b40ec86b4c3d6e1ca50a6&is_play_url=1&item_id=7655821461772406047&line=0&ply_type=2&signaturev3=dmlkZW9faWQ7ZmlsZV9pZDtpdGVtX2lkLmRjNWNmZGVmMmFkNTIyNWQ1ZTA1NzExM2NjOTVlNzMy&tk=tt_chain_token&video_id=v15044gf0000d8vf0bvog65kkvkd8ohg"
],
"Width": 720
},
"QualityType": 14,
"VideoExtra": "{\"PktOffsetMap\":\"[{\\\"time\\\": 1, \\\"offset\\\": 284307}, {\\\"time\\\": 2, \\\"offset\\\": 357012}, {\\\"time\\\": 3, \\\"offset\\\": 438159}, {\\\"time\\\": 4, \\\"offset\\\": 532720}, {\\\"time\\\": 5, \\\"offset\\\": 636824}, {\\\"time\\\": 10, \\\"offset\\\": 1142427}]\",\"mvmaf\":\"{\\\"v2.0\\\": {\\\"srv1\\\": {\\\"v1080\\\": 90.05, \\\"v960\\\": 91.958, \\\"v864\\\": 93.608, \\\"v720\\\": 95.869}, \\\"ori\\\": {\\\"v1080\\\": 82.259, \\\"v960\\\": 85.339, \\\"v864\\\": 87.288, \\\"v720\\\": 90.851}}}\",\"ufq\":\"{\\\"enh\\\":89.767,\\\"playback\\\":{\\\"ori\\\":76.711,\\\"srv1\\\":84.502},\\\"src\\\":85.957,\\\"trans\\\":76.711,\\\"version\\\":\\\"v1.2\\\"}\",\"volume_info_json\":\"\",\"transcode_feature_id\":\"97eb57dffd9e7a0333979f6ca6c9418d\",\"dec_info\":\"\",\"gearvqm\":\"{\\\"v3.0\\\": {\\\"ori\\\": 91.625, \\\"sr20\\\": 97.355}}\",\"audio_bit_rate\":96215,\"ps_info\":\"{\\\"vps\\\": \\\"AQwC//8BYAAAAwAAAwAAAwAAAwC6AACIJElgSA==\\\", \\\"sps\\\": \\\"AQMBYAAAAwAAAwAAAwAAAwC6AACgBaIAUBZYgkSXJIkQTPCEREREREYR8xPOX+/+v82fy/+v8yh+S/3/1/mz+X/1/jOEAg5qAgICCAAAAwAIAAADAPBA\\\", \\\"pps\\\": \\\"AcElPAiQ\\\"}\"}"
},
{
"Bitrate": 799620,
"BitrateFPS": 29,
"CodecType": "h265_hvc1",
"Format": "mp4",
"GearName": "adapt_540_1",
"MVMAF": "{\"v2.0\": {\"srv1\": {\"v1080\": 87.061, \"v960\": 89.844, \"v864\": 91.813, \"v720\": 94.499}, \"ori\": {\"v1080\": 77.717, \"v960\": 80.972, \"v864\": 83.632, \"v720\": 87.945}}}",
"PlayAddr": {
"DataSize": 6203756,
"FileCs": "c:0-52786-7f6d",
"FileHash": "41f62f740ad13045000450f28b9ff2cb",
"Height": 1024,
"Uri": "v15044gf0000d8vf0bvog65kkvkd8ohg",
"UrlKey": "v15044gf0000d8vf0bvog65kkvkd8ohg_bytevc1_540p_799620",
"UrlList": [
"https://v16-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-ve-0068c001-tx2/oogvkDPHIAVvTxgEpE9EMoALxQRRA1FGqeB9Gf/?a=1988&bti=ODszNWYuMDE6&&bt=780&ft=aEeq8qT0mIoPD12CGneI3wUcfzAbMeF~O5&mime_type=video_mp4&rc=NThkNGRmNTo4MztpOTM3ZkBpanJneW45cmVpOzMzaTczNEAyNDMuNC0xNWExYjBiX2FgYSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698153&l=2026070815413036F7D0D0506FB569C78C&ply_type=2&policy=2&signature=05aaeb04a6cf8d012bba46bb131f00af&tk=tt_chain_token&btag=e00088000",
"https://v19-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-ve-0068c001-tx2/oogvkDPHIAVvTxgEpE9EMoALxQRRA1FGqeB9Gf/?a=1988&bti=ODszNWYuMDE6&&bt=780&ft=aEeq8qT0mIoPD12CGneI3wUcfzAbMeF~O5&mime_type=video_mp4&rc=NThkNGRmNTo4MztpOTM3ZkBpanJneW45cmVpOzMzaTczNEAyNDMuNC0xNWExYjBiX2FgYSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698153&l=2026070815413036F7D0D0506FB569C78C&ply_type=2&policy=2&signature=05aaeb04a6cf8d012bba46bb131f00af&tk=tt_chain_token&btag=e00088000",
"https://www.tiktok.com/aweme/v1/play/?faid=1988&file_id=ccd4b0e63fe840d587420c7a116a7552&is_play_url=1&item_id=7655821461772406047&line=0&ply_type=2&signaturev3=dmlkZW9faWQ7ZmlsZV9pZDtpdGVtX2lkLmRlMWFkM2IxY2JkODUxODVlZDJjMDFjYTJmZWE0NTEx&tk=tt_chain_token&video_id=v15044gf0000d8vf0bvog65kkvkd8ohg"
],
"Width": 576
},
"QualityType": 28,
"VideoExtra": "{\"PktOffsetMap\":\"[{\\\"time\\\": 1, \\\"offset\\\": 254747}, {\\\"time\\\": 2, \\\"offset\\\": 310409}, {\\\"time\\\": 3, \\\"offset\\\": 378126}, {\\\"time\\\": 4, \\\"offset\\\": 458158}, {\\\"time\\\": 5, \\\"offset\\\": 546764}, {\\\"time\\\": 10, \\\"offset\\\": 946117}]\",\"mvmaf\":\"{\\\"v2.0\\\": {\\\"srv1\\\": {\\\"v1080\\\": 87.061, \\\"v960\\\": 89.844, \\\"v864\\\": 91.813, \\\"v720\\\": 94.499}, \\\"ori\\\": {\\\"v1080\\\": 77.717, \\\"v960\\\": 80.972, \\\"v864\\\": 83.632, \\\"v720\\\": 87.945}}}\",\"ufq\":\"{\\\"enh\\\":89.767,\\\"playback\\\":{\\\"ori\\\":73.304,\\\"srv1\\\":82.648},\\\"src\\\":85.957,\\\"trans\\\":73.304,\\\"version\\\":\\\"v1.2\\\"}\",\"volume_info_json\":\"\",\"transcode_feature_id\":\"86fda13c754bde67159dea14b7d55d6a\",\"dec_info\":\"\",\"gearvqm\":\"{\\\"v3.0\\\": {\\\"ori\\\": 87.963, \\\"sr20\\\": 95.722}}\",\"audio_bit_rate\":64143,\"ps_info\":\"{\\\"vps\\\": \\\"AQwC//8BYAAAAwAAAwAAAwAAAwC6AACIJElgSA==\\\", \\\"sps\\\": \\\"AQMBYAAAAwAAAwAAAwAAAwC6AACgBIIAQBZYgkSXJIkQTPCEREREREYR8xPOX+/+v82fy/+v8yh+S/3/1/mz+X/1/jOEAg5qAgICCAAAAwAIAAADAPBA\\\", \\\"pps\\\": \\\"AcElPAiQ\\\"}\"}"
},
{
"Bitrate": 622546,
"BitrateFPS": 29,
"CodecType": "h264",
"Format": "mp4",
"GearName": "lowest_540_0",
"MVMAF": "{\"v2.0\": {\"srv1\": {\"v1080\": 72.483, \"v960\": 75.424, \"v864\": 79.843, \"v720\": 84.897}, \"ori\": {\"v1080\": 64.527, \"v960\": 67.93, \"v864\": 72.279, \"v720\": 74.495}}}",
"PlayAddr": {
"DataSize": 4829948,
"FileCs": "c:0-51513-3d83",
"FileHash": "a74fd8fabed8b36131a8e31bf78e9d1d",
"Height": 1024,
"Uri": "v15044gf0000d8vf0bvog65kkvkd8ohg",
"UrlKey": "v15044gf0000d8vf0bvog65kkvkd8ohg_h264_540p_622546",
"UrlList": [
"https://v16-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-ve-0068c001-tx2/o8vENAqIe9xD3BG8AvVUvfEqFEARhgPpHGoTJQ/?a=1988&bti=ODszNWYuMDE6&&bt=607&ft=aEeq8qT0mIoPD12CGneI3wUcfzAbMeF~O5&mime_type=video_mp4&rc=aGhoaDQ8ZTdlNGhmM2Q8ZkBpanJneW45cmVpOzMzaTczNEA1NjRgNTJjNi8xMl42NmIuYSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698153&l=2026070815413036F7D0D0506FB569C78C&ply_type=2&policy=2&signature=fa8ddc2bcbad8ebb4394c4a8b55b28df&tk=tt_chain_token&btag=e00088000",
"https://v19-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-ve-0068c001-tx2/o8vENAqIe9xD3BG8AvVUvfEqFEARhgPpHGoTJQ/?a=1988&bti=ODszNWYuMDE6&&bt=607&ft=aEeq8qT0mIoPD12CGneI3wUcfzAbMeF~O5&mime_type=video_mp4&rc=aGhoaDQ8ZTdlNGhmM2Q8ZkBpanJneW45cmVpOzMzaTczNEA1NjRgNTJjNi8xMl42NmIuYSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698153&l=2026070815413036F7D0D0506FB569C78C&ply_type=2&policy=2&signature=fa8ddc2bcbad8ebb4394c4a8b55b28df&tk=tt_chain_token&btag=e00088000",
"https://www.tiktok.com/aweme/v1/play/?faid=1988&file_id=1ce99a90ac5e412b94fb5981ce7e9e8e&is_play_url=1&item_id=7655821461772406047&line=0&ply_type=2&signaturev3=dmlkZW9faWQ7ZmlsZV9pZDtpdGVtX2lkLjU0ZWZiNDU4NWVmYzFlY2IyNmEyODAyMGY0ZWJjMGZj&tk=tt_chain_token&video_id=v15044gf0000d8vf0bvog65kkvkd8ohg"
],
"Width": 576
},
"QualityType": 25,
"VideoExtra": "{\"PktOffsetMap\":\"[{\\\"time\\\": 1, \\\"offset\\\": 176720}, {\\\"time\\\": 2, \\\"offset\\\": 268125}, {\\\"time\\\": 3, \\\"offset\\\": 342814}, {\\\"time\\\": 4, \\\"offset\\\": 414132}, {\\\"time\\\": 5, \\\"offset\\\": 511717}, {\\\"time\\\": 10, \\\"offset\\\": 841405}]\",\"mvmaf\":\"{\\\"v2.0\\\": {\\\"srv1\\\": {\\\"v1080\\\": 72.483, \\\"v960\\\": 75.424, \\\"v864\\\": 79.843, \\\"v720\\\": 84.897}, \\\"ori\\\": {\\\"v1080\\\": 64.527, \\\"v960\\\": 67.93, \\\"v864\\\": 72.279, \\\"v720\\\": 74.495}}}\",\"ufq\":\"\",\"volume_info_json\":\"\",\"transcode_feature_id\":\"25b3faebdda74a9e438e851988a6efcc\",\"dec_info\":\"\",\"gearvqm\":\"\",\"audio_bit_rate\":32099}"
}
],
"claInfo": {
"enableAutoCaption": true,
"hasOriginalAudio": true,
"noCaptionReason": 3
},
"codecType": "h264",
"cover": "https://p16-common-sign.tiktokcdn-us.com/tos-useast8-p-0068-tx2/oAiefnCIc6NNSgtgrKLRA8JGAMoVfgJSe8eiRS~tplv-tiktokx-origin.image?dr=9636&x-expires=1783695600&x-signature=ZIp3Rdhs6GNzMatSMsNWHFZX%2F7s%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=43f4a2f9&idc=useast5",
"definition": "720p",
"duration": 62,
"dynamicCover": "https://p19-common-sign.tiktokcdn-us.com/tos-useast8-p-0068-tx2/oYVoPG0RxQTGE89EEqvAuADpFvfGABVMxQBeza~tplv-tiktokx-origin.image?dr=9636&x-expires=1783695600&x-signature=JWF%2F5g2txpFXhNhJzOwleSJgKik%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=43f4a2f9&idc=useast5",
"encodeUserTag": "",
"encodedType": "normal",
"format": "mp4",
"height": 1280,
"id": "7655821461772406047",
"originCover": "https://p19-common-sign.tiktokcdn-us.com/tos-useast8-p-0068-tx2/oYVoQGFsU9VvLxieEDDAQAPpRTqA8B5EzvEGfF~tplv-tiktokx-origin.image?dr=9636&x-expires=1783695600&x-signature=yHDGczHGo3joyDeerZP6Dp4S1Jc%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=43f4a2f9&idc=useast5",
"playAddr": "https://v16-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-pve-0068-tx2/oYDTerfAgGxvqFaEctpBAvVGEPIE8Q9ARoxPNB/?a=1988&bti=ODszNWYuMDE6&&bt=1732&ft=aEeq8qT0mIoPD12CGneI3wUcfzAbMeF~O5&mime_type=video_mp4&rc=NWZnZDVoNDQzN2c1ZGU0Z0BpanJneW45cmVpOzMzaTczNEBgMTBgYjQuNWExLmNgXmI2YSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698153&l=2026070815413036F7D0D0506FB569C78C&ply_type=2&policy=2&signature=aa5be80671c4a73d54517ee8e4a471ad&tk=tt_chain_token&btag=e00088000",
"ratio": "720p",
"size": 13761060,
"videoID": "v15044gf0000d8vf0bvog65kkvkd8ohg",
"videoQuality": "normal",
"volumeInfo": {
"Loudness": -48.5,
"Peak": 0.18408
},
"width": 720,
"zoomCover": {
"240": "https://p19-common-sign.tiktokcdn-us.com/tos-useast8-p-0068-tx2/oAiefnCIc6NNSgtgrKLRA8JGAMoVfgJSe8eiRS~tplv-photomode-zoomcover:240:240.avif?dr=9616&x-expires=1783695600&x-signature=ZONw1V50ZmupszDX0NiC%2Bf8BimU%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=43f4a2f9&idc=useast5&ftpl=1",
"480": "https://p19-common-sign.tiktokcdn-us.com/tos-useast8-p-0068-tx2/oAiefnCIc6NNSgtgrKLRA8JGAMoVfgJSe8eiRS~tplv-photomode-zoomcover:480:480.avif?dr=9616&x-expires=1783695600&x-signature=LztWYGkvUC4%2BvhtQb0TO0l1ky0s%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=43f4a2f9&idc=useast5&ftpl=1",
"720": "https://p19-common-sign.tiktokcdn-us.com/tos-useast8-p-0068-tx2/oAiefnCIc6NNSgtgrKLRA8JGAMoVfgJSe8eiRS~tplv-photomode-zoomcover:720:720.avif?dr=9616&x-expires=1783695600&x-signature=X%2F3HQ%2BkiKqEAxnuk%2BdFfBfeOsko%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=43f4a2f9&idc=useast5&ftpl=1",
"960": "https://p19-common-sign.tiktokcdn-us.com/tos-useast8-p-0068-tx2/oAiefnCIc6NNSgtgrKLRA8JGAMoVfgJSe8eiRS~tplv-photomode-zoomcover:960:960.avif?dr=9616&x-expires=1783695600&x-signature=aJaYG%2BDAo%2BhO7nZ8t8XP7g0DArU%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=43f4a2f9&idc=useast5&ftpl=1"
}
}
}

View file

@ -0,0 +1,660 @@
{
"id": "7655821461772406047",
"desc": "Pull Apart Garlic Bread Ingredients * 1 tsp yeast * 1 tbsp sugar * 270g milk (about 1 cup + 2 tbsp) * 380g flour (about 3 cups) * 1 tsp salt * 43g softened butter (about 3 tbsp) Butter Mixture * 100g butter, softened * 1 tbsp chopped cilantro * 1 garlic clove, minced * Shredded cheese Instructions 1. In a bowl, mix the sugar and yeast. Add the milk, then add the flour and mix until combined. 2. Add the salt and softened butter, then coil fold until the butter is fully incorporated. 3. Cover and let the dough rest for 2 hours or until doubled. 4. Mix the softened butter with the cilantro and minced garlic. 5. Roll the dough into a rectangle and spread the garlic butter mixture evenly over it. Sprinkle with shredded cheddar cheese. 6. Cut the dough in half, stack one half on top of the other, then cut into strips. Transfer the pieces into a loaf pan. 7. Cover and let rest for 20 minutes. 8. Bake at 370°F for 30 minutes, or until golden brown. 9. Brush with the remaining garlic butter mixture while still warm. #bread #garlicbread #breadtok #baking #homemadebread ",
"createTime": "1782509863",
"scheduleTime": 0,
"video": {
"id": "7655821461772406047",
"height": 1280,
"width": 720,
"duration": 62,
"ratio": "720p",
"cover": "https://p16-common-sign.tiktokcdn-us.com/tos-useast8-p-0068-tx2/oAiefnCIc6NNSgtgrKLRA8JGAMoVfgJSe8eiRS~tplv-tiktokx-origin.image?dr=9636&x-expires=1783695600&x-signature=ZIp3Rdhs6GNzMatSMsNWHFZX%2F7s%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=43f4a2f9&idc=useast5",
"originCover": "https://p19-common-sign.tiktokcdn-us.com/tos-useast8-p-0068-tx2/oYVoQGFsU9VvLxieEDDAQAPpRTqA8B5EzvEGfF~tplv-tiktokx-origin.image?dr=9636&x-expires=1783695600&x-signature=yHDGczHGo3joyDeerZP6Dp4S1Jc%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=43f4a2f9&idc=useast5",
"dynamicCover": "https://p16-common-sign.tiktokcdn-us.com/tos-useast8-p-0068-tx2/oYVoPG0RxQTGE89EEqvAuADpFvfGABVMxQBeza~tplv-tiktokx-origin.image?dr=9636&x-expires=1783695600&x-signature=Jz%2BPtpSXdbjTV4qFbG4H7JFD0f8%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=43f4a2f9&idc=useast5",
"playAddr": "https://v16-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-pve-0068-tx2/oYDTerfAgGxvqFaEctpBAvVGEPIE8Q9ARoxPNB/?a=1988&bti=ODszNWYuMDE6&&bt=1732&ft=aEeq8qT0mIoPD12qGneI3wU6LRAbMeF~O5&mime_type=video_mp4&rc=NWZnZDVoNDQzN2c1ZGU0Z0BpanJneW45cmVpOzMzaTczNEBgMTBgYjQuNWExLmNgXmI2YSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698158&l=20260708154135271AF032BAC01F42EF1E&ply_type=2&policy=2&signature=1bb5fb748a97085b872e357d1cd5406e&tk=tt_chain_token&btag=e00090000",
"downloadAddr": "",
"shareCover": [
""
],
"reflowCover": "",
"bitrate": 1773703,
"encodedType": "normal",
"format": "mp4",
"videoQuality": "normal",
"encodeUserTag": "",
"codecType": "h264",
"definition": "720p",
"subtitleInfos": [],
"zoomCover": {
"240": "https://p16-common-sign.tiktokcdn-us.com/tos-useast8-p-0068-tx2/oAiefnCIc6NNSgtgrKLRA8JGAMoVfgJSe8eiRS~tplv-photomode-zoomcover:240:240.avif?dr=9616&x-expires=1783695600&x-signature=cBXoX16WOKJyUth9fnaohYv%2B%2BBs%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=43f4a2f9&idc=useast5&ftpl=1",
"480": "https://p16-common-sign.tiktokcdn-us.com/tos-useast8-p-0068-tx2/oAiefnCIc6NNSgtgrKLRA8JGAMoVfgJSe8eiRS~tplv-photomode-zoomcover:480:480.avif?dr=9616&x-expires=1783695600&x-signature=Ff5B1aV4teSPQc6JXMoidMXqJWk%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=43f4a2f9&idc=useast5&ftpl=1",
"720": "https://p19-common-sign.tiktokcdn-us.com/tos-useast8-p-0068-tx2/oAiefnCIc6NNSgtgrKLRA8JGAMoVfgJSe8eiRS~tplv-photomode-zoomcover:720:720.avif?dr=9616&x-expires=1783695600&x-signature=X%2F3HQ%2BkiKqEAxnuk%2BdFfBfeOsko%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=43f4a2f9&idc=useast5&ftpl=1",
"960": "https://p19-common-sign.tiktokcdn-us.com/tos-useast8-p-0068-tx2/oAiefnCIc6NNSgtgrKLRA8JGAMoVfgJSe8eiRS~tplv-photomode-zoomcover:960:960.avif?dr=9616&x-expires=1783695600&x-signature=aJaYG%2BDAo%2BhO7nZ8t8XP7g0DArU%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=43f4a2f9&idc=useast5&ftpl=1"
},
"volumeInfo": {
"Loudness": -48.5,
"Peak": 0.18408
},
"bitrateInfo": [
{
"Bitrate": 1836180,
"QualityType": 2,
"BitrateFPS": 29,
"GearName": "adapt_lowest_1080_1",
"PlayAddr": {
"DataSize": "14245777",
"Width": 1080,
"Height": 1920,
"Uri": "v15044gf0000d8vf0bvog65kkvkd8ohg",
"UrlList": [
"https://v16-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-pve-0068-tx2/oIygxEfEVAP1qvGVTIBARoAvb9QYFGEKDp5v9e/?a=1988&bti=ODszNWYuMDE6&&bt=1793&ft=aEeq8qT0mIoPD12qGneI3wU6LRAbMeF~O5&mime_type=video_mp4&rc=aDlmNGg7ZTdlaGY5ZzUzZ0BpanJneW45cmVpOzMzaTczNEBjXl5hMzAvXy8xNDAuYmIyYSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698158&l=20260708154135271AF032BAC01F42EF1E&ply_type=2&policy=2&signature=0648319cea95ee08fd7b4f93a95cb14c&tk=tt_chain_token&btag=e00090000",
"https://v19-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-pve-0068-tx2/oIygxEfEVAP1qvGVTIBARoAvb9QYFGEKDp5v9e/?a=1988&bti=ODszNWYuMDE6&&bt=1793&ft=aEeq8qT0mIoPD12qGneI3wU6LRAbMeF~O5&mime_type=video_mp4&rc=aDlmNGg7ZTdlaGY5ZzUzZ0BpanJneW45cmVpOzMzaTczNEBjXl5hMzAvXy8xNDAuYmIyYSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698158&l=20260708154135271AF032BAC01F42EF1E&ply_type=2&policy=2&signature=0648319cea95ee08fd7b4f93a95cb14c&tk=tt_chain_token&btag=e00090000",
"https://www.tiktok.com/aweme/v1/play/?faid=1988&file_id=5de0e21d367c4e2bb42b386ed1aca6fe&is_play_url=1&item_id=7655821461772406047&line=0&ply_type=2&signaturev3=dmlkZW9faWQ7ZmlsZV9pZDtpdGVtX2lkLjAzMmU1YzJlMTAwMDJiYjIwMmFiN2RhNTIzOWVhYWRk&tk=tt_chain_token&video_id=v15044gf0000d8vf0bvog65kkvkd8ohg"
],
"UrlKey": "v15044gf0000d8vf0bvog65kkvkd8ohg_bytevc1_1080p_1836180",
"FileHash": "38996a98cfeb2714fca1b94768090c37",
"FileCs": "c:0-52754-ed06"
},
"CodecType": "h265_hvc1",
"MVMAF": "{\"v2.0\": {\"srv1\": {\"v1080\": 96.83, \"v960\": 97.64, \"v864\": 98.256, \"v720\": 98.838}, \"ori\": {\"v1080\": 91.834, \"v960\": 93.423, \"v864\": 94.54, \"v720\": 95.872}}}",
"Format": "mp4",
"VideoExtra": "{\"PktOffsetMap\":\"[{\\\"time\\\": 1, \\\"offset\\\": 385342}, {\\\"time\\\": 2, \\\"offset\\\": 561014}, {\\\"time\\\": 3, \\\"offset\\\": 703814}, {\\\"time\\\": 4, \\\"offset\\\": 855867}, {\\\"time\\\": 5, \\\"offset\\\": 1019333}, {\\\"time\\\": 10, \\\"offset\\\": 2028412}]\",\"mvmaf\":\"{\\\"v2.0\\\": {\\\"srv1\\\": {\\\"v1080\\\": 96.83, \\\"v960\\\": 97.64, \\\"v864\\\": 98.256, \\\"v720\\\": 98.838}, \\\"ori\\\": {\\\"v1080\\\": 91.834, \\\"v960\\\": 93.423, \\\"v864\\\": 94.54, \\\"v720\\\": 95.872}}}\",\"ufq\":\"{\\\"enh\\\":89.317,\\\"playback\\\":{\\\"ori\\\":85.151,\\\"srv1\\\":90.147},\\\"src\\\":85.957,\\\"trans\\\":85.151,\\\"version\\\":\\\"v1.2\\\"}\",\"volume_info_json\":\"\",\"transcode_feature_id\":\"11b0345f3bf185ddbe7c48bd7301980f\",\"dec_info\":\"\",\"gearvqm\":\"\",\"audio_bit_rate\":96215,\"ps_info\":\"{\\\"vps\\\": \\\"AQwC//8BYAAAAwAAAwAAAwAAAwCWAACdzkgS\\\", \\\"sps\\\": \\\"AQMBYAAAAwAAAwAAAwAAAwCWAACgAhyAHgWWdzkySUQs0RJJJJify4d/1+bPl/1+M4gQ5qAgICCAAAADAIAAAA8E\\\", \\\"pps\\\": \\\"AcElPAiQ\\\"}\"}"
},
{
"Bitrate": 1773703,
"QualityType": 10,
"BitrateFPS": 29,
"GearName": "normal_720_0",
"PlayAddr": {
"DataSize": "13761060",
"Width": 720,
"Height": 1280,
"Uri": "v15044gf0000d8vf0bvog65kkvkd8ohg",
"UrlList": [
"https://v16-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-pve-0068-tx2/oYDTerfAgGxvqFaEctpBAvVGEPIE8Q9ARoxPNB/?a=1988&bti=ODszNWYuMDE6&&bt=1732&ft=aEeq8qT0mIoPD12qGneI3wU6LRAbMeF~O5&mime_type=video_mp4&rc=NWZnZDVoNDQzN2c1ZGU0Z0BpanJneW45cmVpOzMzaTczNEBgMTBgYjQuNWExLmNgXmI2YSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698158&l=20260708154135271AF032BAC01F42EF1E&ply_type=2&policy=2&signature=1bb5fb748a97085b872e357d1cd5406e&tk=tt_chain_token&btag=e00090000",
"https://v19-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-pve-0068-tx2/oYDTerfAgGxvqFaEctpBAvVGEPIE8Q9ARoxPNB/?a=1988&bti=ODszNWYuMDE6&&bt=1732&ft=aEeq8qT0mIoPD12qGneI3wU6LRAbMeF~O5&mime_type=video_mp4&rc=NWZnZDVoNDQzN2c1ZGU0Z0BpanJneW45cmVpOzMzaTczNEBgMTBgYjQuNWExLmNgXmI2YSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698158&l=20260708154135271AF032BAC01F42EF1E&ply_type=2&policy=2&signature=1bb5fb748a97085b872e357d1cd5406e&tk=tt_chain_token&btag=e00090000",
"https://www.tiktok.com/aweme/v1/play/?faid=1988&file_id=9de1abcaf21d44d081117ee2ca3d4cc2&is_play_url=1&item_id=7655821461772406047&line=0&ply_type=2&signaturev3=dmlkZW9faWQ7ZmlsZV9pZDtpdGVtX2lkLjcwMGExZmI3NDg4YzFiZTIwZjczM2IyMmM0OTk2YTI2&tk=tt_chain_token&video_id=v15044gf0000d8vf0bvog65kkvkd8ohg"
],
"UrlKey": "v15044gf0000d8vf0bvog65kkvkd8ohg_h264_720p_1773703",
"FileHash": "485174c8492db027adbbf6dca7c7c201",
"FileCs": "c:0-51456-f8d1"
},
"CodecType": "h264",
"MVMAF": "{\"v2.0\": {\"srv1\": {\"v1080\": 84.77, \"v960\": 88.554, \"v864\": 88.982, \"v720\": 92.772}, \"ori\": {\"v1080\": 84.77, \"v960\": 88.554, \"v864\": 88.982, \"v720\": 92.772}}}",
"Format": "mp4",
"VideoExtra": "{\"PktOffsetMap\":\"[{\\\"time\\\": 1, \\\"offset\\\": 400712}, {\\\"time\\\": 2, \\\"offset\\\": 659243}, {\\\"time\\\": 3, \\\"offset\\\": 886968}, {\\\"time\\\": 4, \\\"offset\\\": 1101663}, {\\\"time\\\": 5, \\\"offset\\\": 1368565}, {\\\"time\\\": 10, \\\"offset\\\": 2375704}]\",\"mvmaf\":\"{\\\"v2.0\\\": {\\\"srv1\\\": {\\\"v1080\\\": 84.77, \\\"v960\\\": 88.554, \\\"v864\\\": 88.982, \\\"v720\\\": 92.772}, \\\"ori\\\": {\\\"v1080\\\": 84.77, \\\"v960\\\": 88.554, \\\"v864\\\": 88.982, \\\"v720\\\": 92.772}}}\",\"ufq\":\"\",\"volume_info_json\":\"\",\"transcode_feature_id\":\"65b88c41c0963253ad31b6f68981a242\",\"dec_info\":\"\",\"gearvqm\":\"\",\"audio_bit_rate\":64193}"
},
{
"Bitrate": 966380,
"QualityType": 14,
"BitrateFPS": 29,
"GearName": "adapt_lower_720_1",
"PlayAddr": {
"DataSize": "7497540",
"Width": 720,
"Height": 1280,
"Uri": "v15044gf0000d8vf0bvog65kkvkd8ohg",
"UrlList": [
"https://v16-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-pve-0068-tx2/og99rgOGqJED8xGeBpFEAADvEPARnbTIQTfoVv/?a=1988&bti=ODszNWYuMDE6&&bt=943&ft=aEeq8qT0mIoPD12qGneI3wU6LRAbMeF~O5&mime_type=video_mp4&rc=OTNkNDk2NzlmM2VkPGg7OkBpanJneW45cmVpOzMzaTczNEBeMmBiYWBfNWIxLzEyNF81YSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698158&l=20260708154135271AF032BAC01F42EF1E&ply_type=2&policy=2&signature=3a0c7313f1a5d9908cc2f5b0dd24d048&tk=tt_chain_token&btag=e00090000",
"https://v19-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-pve-0068-tx2/og99rgOGqJED8xGeBpFEAADvEPARnbTIQTfoVv/?a=1988&bti=ODszNWYuMDE6&&bt=943&ft=aEeq8qT0mIoPD12qGneI3wU6LRAbMeF~O5&mime_type=video_mp4&rc=OTNkNDk2NzlmM2VkPGg7OkBpanJneW45cmVpOzMzaTczNEBeMmBiYWBfNWIxLzEyNF81YSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698158&l=20260708154135271AF032BAC01F42EF1E&ply_type=2&policy=2&signature=3a0c7313f1a5d9908cc2f5b0dd24d048&tk=tt_chain_token&btag=e00090000",
"https://www.tiktok.com/aweme/v1/play/?faid=1988&file_id=87b87e594a2b40ec86b4c3d6e1ca50a6&is_play_url=1&item_id=7655821461772406047&line=0&ply_type=2&signaturev3=dmlkZW9faWQ7ZmlsZV9pZDtpdGVtX2lkLmRjNWNmZGVmMmFkNTIyNWQ1ZTA1NzExM2NjOTVlNzMy&tk=tt_chain_token&video_id=v15044gf0000d8vf0bvog65kkvkd8ohg"
],
"UrlKey": "v15044gf0000d8vf0bvog65kkvkd8ohg_bytevc1_720p_966380",
"FileHash": "1376df24c917669727b27273697fb826",
"FileCs": "c:0-52786-7f65"
},
"CodecType": "h265_hvc1",
"MVMAF": "{\"v2.0\": {\"srv1\": {\"v1080\": 90.05, \"v960\": 91.958, \"v864\": 93.608, \"v720\": 95.869}, \"ori\": {\"v1080\": 82.259, \"v960\": 85.339, \"v864\": 87.288, \"v720\": 90.851}}}",
"Format": "mp4",
"VideoExtra": "{\"PktOffsetMap\":\"[{\\\"time\\\": 1, \\\"offset\\\": 284307}, {\\\"time\\\": 2, \\\"offset\\\": 357012}, {\\\"time\\\": 3, \\\"offset\\\": 438159}, {\\\"time\\\": 4, \\\"offset\\\": 532720}, {\\\"time\\\": 5, \\\"offset\\\": 636824}, {\\\"time\\\": 10, \\\"offset\\\": 1142427}]\",\"mvmaf\":\"{\\\"v2.0\\\": {\\\"srv1\\\": {\\\"v1080\\\": 90.05, \\\"v960\\\": 91.958, \\\"v864\\\": 93.608, \\\"v720\\\": 95.869}, \\\"ori\\\": {\\\"v1080\\\": 82.259, \\\"v960\\\": 85.339, \\\"v864\\\": 87.288, \\\"v720\\\": 90.851}}}\",\"ufq\":\"{\\\"enh\\\":89.767,\\\"playback\\\":{\\\"ori\\\":76.711,\\\"srv1\\\":84.502},\\\"src\\\":85.957,\\\"trans\\\":76.711,\\\"version\\\":\\\"v1.2\\\"}\",\"volume_info_json\":\"\",\"transcode_feature_id\":\"97eb57dffd9e7a0333979f6ca6c9418d\",\"dec_info\":\"\",\"gearvqm\":\"{\\\"v3.0\\\": {\\\"ori\\\": 91.625, \\\"sr20\\\": 97.355}}\",\"audio_bit_rate\":96215,\"ps_info\":\"{\\\"vps\\\": \\\"AQwC//8BYAAAAwAAAwAAAwAAAwC6AACIJElgSA==\\\", \\\"sps\\\": \\\"AQMBYAAAAwAAAwAAAwAAAwC6AACgBaIAUBZYgkSXJIkQTPCEREREREYR8xPOX+/+v82fy/+v8yh+S/3/1/mz+X/1/jOEAg5qAgICCAAAAwAIAAADAPBA\\\", \\\"pps\\\": \\\"AcElPAiQ\\\"}\"}"
},
{
"Bitrate": 799620,
"QualityType": 28,
"BitrateFPS": 29,
"GearName": "adapt_540_1",
"PlayAddr": {
"DataSize": "6203756",
"Width": 576,
"Height": 1024,
"Uri": "v15044gf0000d8vf0bvog65kkvkd8ohg",
"UrlList": [
"https://v16-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-ve-0068c001-tx2/oogvkDPHIAVvTxgEpE9EMoALxQRRA1FGqeB9Gf/?a=1988&bti=ODszNWYuMDE6&&bt=780&ft=aEeq8qT0mIoPD12qGneI3wU6LRAbMeF~O5&mime_type=video_mp4&rc=NThkNGRmNTo4MztpOTM3ZkBpanJneW45cmVpOzMzaTczNEAyNDMuNC0xNWExYjBiX2FgYSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698158&l=20260708154135271AF032BAC01F42EF1E&ply_type=2&policy=2&signature=a8516d4d8e79814753dc2cd0fde9f7bd&tk=tt_chain_token&btag=e00090000",
"https://v19-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-ve-0068c001-tx2/oogvkDPHIAVvTxgEpE9EMoALxQRRA1FGqeB9Gf/?a=1988&bti=ODszNWYuMDE6&&bt=780&ft=aEeq8qT0mIoPD12qGneI3wU6LRAbMeF~O5&mime_type=video_mp4&rc=NThkNGRmNTo4MztpOTM3ZkBpanJneW45cmVpOzMzaTczNEAyNDMuNC0xNWExYjBiX2FgYSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698158&l=20260708154135271AF032BAC01F42EF1E&ply_type=2&policy=2&signature=a8516d4d8e79814753dc2cd0fde9f7bd&tk=tt_chain_token&btag=e00090000",
"https://www.tiktok.com/aweme/v1/play/?faid=1988&file_id=ccd4b0e63fe840d587420c7a116a7552&is_play_url=1&item_id=7655821461772406047&line=0&ply_type=2&signaturev3=dmlkZW9faWQ7ZmlsZV9pZDtpdGVtX2lkLmRlMWFkM2IxY2JkODUxODVlZDJjMDFjYTJmZWE0NTEx&tk=tt_chain_token&video_id=v15044gf0000d8vf0bvog65kkvkd8ohg"
],
"UrlKey": "v15044gf0000d8vf0bvog65kkvkd8ohg_bytevc1_540p_799620",
"FileHash": "41f62f740ad13045000450f28b9ff2cb",
"FileCs": "c:0-52786-7f6d"
},
"CodecType": "h265_hvc1",
"MVMAF": "{\"v2.0\": {\"srv1\": {\"v1080\": 87.061, \"v960\": 89.844, \"v864\": 91.813, \"v720\": 94.499}, \"ori\": {\"v1080\": 77.717, \"v960\": 80.972, \"v864\": 83.632, \"v720\": 87.945}}}",
"Format": "mp4",
"VideoExtra": "{\"PktOffsetMap\":\"[{\\\"time\\\": 1, \\\"offset\\\": 254747}, {\\\"time\\\": 2, \\\"offset\\\": 310409}, {\\\"time\\\": 3, \\\"offset\\\": 378126}, {\\\"time\\\": 4, \\\"offset\\\": 458158}, {\\\"time\\\": 5, \\\"offset\\\": 546764}, {\\\"time\\\": 10, \\\"offset\\\": 946117}]\",\"mvmaf\":\"{\\\"v2.0\\\": {\\\"srv1\\\": {\\\"v1080\\\": 87.061, \\\"v960\\\": 89.844, \\\"v864\\\": 91.813, \\\"v720\\\": 94.499}, \\\"ori\\\": {\\\"v1080\\\": 77.717, \\\"v960\\\": 80.972, \\\"v864\\\": 83.632, \\\"v720\\\": 87.945}}}\",\"ufq\":\"{\\\"enh\\\":89.767,\\\"playback\\\":{\\\"ori\\\":73.304,\\\"srv1\\\":82.648},\\\"src\\\":85.957,\\\"trans\\\":73.304,\\\"version\\\":\\\"v1.2\\\"}\",\"volume_info_json\":\"\",\"transcode_feature_id\":\"86fda13c754bde67159dea14b7d55d6a\",\"dec_info\":\"\",\"gearvqm\":\"{\\\"v3.0\\\": {\\\"ori\\\": 87.963, \\\"sr20\\\": 95.722}}\",\"audio_bit_rate\":64143,\"ps_info\":\"{\\\"vps\\\": \\\"AQwC//8BYAAAAwAAAwAAAwAAAwC6AACIJElgSA==\\\", \\\"sps\\\": \\\"AQMBYAAAAwAAAwAAAwAAAwC6AACgBIIAQBZYgkSXJIkQTPCEREREREYR8xPOX+/+v82fy/+v8yh+S/3/1/mz+X/1/jOEAg5qAgICCAAAAwAIAAADAPBA\\\", \\\"pps\\\": \\\"AcElPAiQ\\\"}\"}"
},
{
"Bitrate": 622546,
"QualityType": 25,
"BitrateFPS": 29,
"GearName": "lowest_540_0",
"PlayAddr": {
"DataSize": "4829948",
"Width": 576,
"Height": 1024,
"Uri": "v15044gf0000d8vf0bvog65kkvkd8ohg",
"UrlList": [
"https://v16-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-ve-0068c001-tx2/o8vENAqIe9xD3BG8AvVUvfEqFEARhgPpHGoTJQ/?a=1988&bti=ODszNWYuMDE6&&bt=607&ft=aEeq8qT0mIoPD12qGneI3wU6LRAbMeF~O5&mime_type=video_mp4&rc=aGhoaDQ8ZTdlNGhmM2Q8ZkBpanJneW45cmVpOzMzaTczNEA1NjRgNTJjNi8xMl42NmIuYSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698158&l=20260708154135271AF032BAC01F42EF1E&ply_type=2&policy=2&signature=f44ac110e4573a7bc11c73155b9141d0&tk=tt_chain_token&btag=e00090000",
"https://v19-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-ve-0068c001-tx2/o8vENAqIe9xD3BG8AvVUvfEqFEARhgPpHGoTJQ/?a=1988&bti=ODszNWYuMDE6&&bt=607&ft=aEeq8qT0mIoPD12qGneI3wU6LRAbMeF~O5&mime_type=video_mp4&rc=aGhoaDQ8ZTdlNGhmM2Q8ZkBpanJneW45cmVpOzMzaTczNEA1NjRgNTJjNi8xMl42NmIuYSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698158&l=20260708154135271AF032BAC01F42EF1E&ply_type=2&policy=2&signature=f44ac110e4573a7bc11c73155b9141d0&tk=tt_chain_token&btag=e00090000",
"https://www.tiktok.com/aweme/v1/play/?faid=1988&file_id=1ce99a90ac5e412b94fb5981ce7e9e8e&is_play_url=1&item_id=7655821461772406047&line=0&ply_type=2&signaturev3=dmlkZW9faWQ7ZmlsZV9pZDtpdGVtX2lkLjU0ZWZiNDU4NWVmYzFlY2IyNmEyODAyMGY0ZWJjMGZj&tk=tt_chain_token&video_id=v15044gf0000d8vf0bvog65kkvkd8ohg"
],
"UrlKey": "v15044gf0000d8vf0bvog65kkvkd8ohg_h264_540p_622546",
"FileHash": "a74fd8fabed8b36131a8e31bf78e9d1d",
"FileCs": "c:0-51513-3d83"
},
"CodecType": "h264",
"MVMAF": "{\"v2.0\": {\"srv1\": {\"v1080\": 72.483, \"v960\": 75.424, \"v864\": 79.843, \"v720\": 84.897}, \"ori\": {\"v1080\": 64.527, \"v960\": 67.93, \"v864\": 72.279, \"v720\": 74.495}}}",
"Format": "mp4",
"VideoExtra": "{\"PktOffsetMap\":\"[{\\\"time\\\": 1, \\\"offset\\\": 176720}, {\\\"time\\\": 2, \\\"offset\\\": 268125}, {\\\"time\\\": 3, \\\"offset\\\": 342814}, {\\\"time\\\": 4, \\\"offset\\\": 414132}, {\\\"time\\\": 5, \\\"offset\\\": 511717}, {\\\"time\\\": 10, \\\"offset\\\": 841405}]\",\"mvmaf\":\"{\\\"v2.0\\\": {\\\"srv1\\\": {\\\"v1080\\\": 72.483, \\\"v960\\\": 75.424, \\\"v864\\\": 79.843, \\\"v720\\\": 84.897}, \\\"ori\\\": {\\\"v1080\\\": 64.527, \\\"v960\\\": 67.93, \\\"v864\\\": 72.279, \\\"v720\\\": 74.495}}}\",\"ufq\":\"\",\"volume_info_json\":\"\",\"transcode_feature_id\":\"25b3faebdda74a9e438e851988a6efcc\",\"dec_info\":\"\",\"gearvqm\":\"\",\"audio_bit_rate\":32099}"
}
],
"size": "13761060",
"VQScore": "69.69",
"claInfo": {
"hasOriginalAudio": true,
"enableAutoCaption": true,
"captionInfos": [],
"noCaptionReason": 3
},
"videoID": "v15044gf0000d8vf0bvog65kkvkd8ohg",
"PlayAddrStruct": {
"DataSize": "13761060",
"Width": 720,
"Height": 1280,
"Uri": "v15044gf0000d8vf0bvog65kkvkd8ohg",
"UrlList": [
"https://v16-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-pve-0068-tx2/oYDTerfAgGxvqFaEctpBAvVGEPIE8Q9ARoxPNB/?a=1988&bti=ODszNWYuMDE6&&bt=1732&ft=aEeq8qT0mIoPD12qGneI3wU6LRAbMeF~O5&mime_type=video_mp4&rc=NWZnZDVoNDQzN2c1ZGU0Z0BpanJneW45cmVpOzMzaTczNEBgMTBgYjQuNWExLmNgXmI2YSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698158&l=20260708154135271AF032BAC01F42EF1E&ply_type=2&policy=2&signature=1bb5fb748a97085b872e357d1cd5406e&tk=tt_chain_token&btag=e00090000",
"https://v19-webapp-prime.us.tiktok.com/video/tos/useast8/tos-useast8-pve-0068-tx2/oYDTerfAgGxvqFaEctpBAvVGEPIE8Q9ARoxPNB/?a=1988&bti=ODszNWYuMDE6&&bt=1732&ft=aEeq8qT0mIoPD12qGneI3wU6LRAbMeF~O5&mime_type=video_mp4&rc=NWZnZDVoNDQzN2c1ZGU0Z0BpanJneW45cmVpOzMzaTczNEBgMTBgYjQuNWExLmNgXmI2YSNlNWhoMmRzLXNhLS1kMTJzcw%3D%3D&expire=1783698158&l=20260708154135271AF032BAC01F42EF1E&ply_type=2&policy=2&signature=1bb5fb748a97085b872e357d1cd5406e&tk=tt_chain_token&btag=e00090000",
"https://www.tiktok.com/aweme/v1/play/?faid=1988&file_id=9de1abcaf21d44d081117ee2ca3d4cc2&is_play_url=1&item_id=7655821461772406047&line=0&ply_type=2&signaturev3=dmlkZW9faWQ7ZmlsZV9pZDtpdGVtX2lkLjcwMGExZmI3NDg4YzFiZTIwZjczM2IyMmM0OTk2YTI2&tk=tt_chain_token&video_id=v15044gf0000d8vf0bvog65kkvkd8ohg"
],
"UrlKey": "v15044gf0000d8vf0bvog65kkvkd8ohg_h264_720p_1773703",
"FileHash": "485174c8492db027adbbf6dca7c7c201",
"FileCs": "c:0-51456-f8d1"
}
},
"author": {
"id": "7501138183701103662",
"shortId": "",
"uniqueId": "cookwithhali",
"nickname": "Cookwithhali",
"avatarLarger": "https://p19-common-sign.tiktokcdn-us.com/tos-useast8-avt-0068-tx2/b29bd2d826cffba1af22f99793a7d5dc~tplv-tiktokx-cropcenter:1080:1080.jpeg?dr=9640&refresh_token=c6d77c8a&x-expires=1783695600&x-signature=mUTuvDWLEGrAxXtWZlBE7gUxCl8%3D&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=useast5",
"avatarMedium": "https://p16-common-sign.tiktokcdn-us.com/tos-useast8-avt-0068-tx2/b29bd2d826cffba1af22f99793a7d5dc~tplv-tiktokx-cropcenter:720:720.jpeg?dr=9640&refresh_token=dfe18cb8&x-expires=1783695600&x-signature=cP14vkr0ndxwhW%2B1pWfefXfTsls%3D&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=useast5",
"avatarThumb": "https://p16-common-sign.tiktokcdn-us.com/tos-useast8-avt-0068-tx2/b29bd2d826cffba1af22f99793a7d5dc~tplv-tiktokx-cropcenter:100:100.jpeg?dr=9640&refresh_token=84bba9f5&x-expires=1783695600&x-signature=Et06On6iQeXFbtycXoVodzl0fdM%3D&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=useast5",
"signature": "From my kitchen to your screen! 🍞\nFollow for more ✨\nIG: halikit25\n\n📩 Halikitchen25@gmail.com",
"createTime": 1746494976,
"verified": false,
"secUid": "MS4wLjABAAAAywZ6lCXK3u4zKz8eQxxcjVblLiugz6zMysU98G7dyKVxu6IvMOJ97mpIfpDxUL4q",
"ftc": false,
"relation": 0,
"openFavorite": false,
"commentSetting": 0,
"duetSetting": 0,
"stitchSetting": 0,
"privateAccount": false,
"secret": false,
"isADVirtual": false,
"roomId": "",
"uniqueIdModifyTime": 0,
"ttSeller": false,
"downloadSetting": 3,
"recommendReason": "",
"nowInvitationCardUrl": "",
"nickNameModifyTime": 0,
"isEmbedBanned": false,
"canExpPlaylist": false,
"suggestAccountBind": false,
"UserStoryStatus": 0,
"shortDramaCreator": {}
},
"music": {
"id": "7655821492566919966",
"title": "original sound",
"playUrl": "https://v16m.tiktokcdn-us.com/84ccc1629d7199ad68f976d419148401/6a4ec44e/video/tos/useast8/tos-useast8-v-27dcd7-tx2/ocBIhANWILSemeAAINEGoTTPe58AJaRegg6eKS/?a=1233&bti=ODszNWYuMDE6&&bt=125&ft=GSDrKInz7ThasbSGXq8Zmo&mime_type=audio_mpeg&rc=anY5cXA5cmRpOzMzaTU8NEBpanY5cXA5cmRpOzMzaTU8NEBpYzZrMmRjL3NhLS1kMTJzYSNpYzZrMmRjL3NhLS1kMTJzcw%3D%3D&vvpl=1&l=20260708154135271AF032BAC01F42EF1E&btag=e00050000",
"coverLarge": "https://p19-common-sign.tiktokcdn-us.com/tos-useast8-avt-0068-tx2/b29bd2d826cffba1af22f99793a7d5dc~tplv-tiktokx-cropcenter:1080:1080.jpeg?dr=9640&refresh_token=c6d77c8a&x-expires=1783695600&x-signature=mUTuvDWLEGrAxXtWZlBE7gUxCl8%3D&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=useast5",
"coverMedium": "https://p16-common-sign.tiktokcdn-us.com/tos-useast8-avt-0068-tx2/b29bd2d826cffba1af22f99793a7d5dc~tplv-tiktokx-cropcenter:720:720.jpeg?dr=9640&refresh_token=dfe18cb8&x-expires=1783695600&x-signature=cP14vkr0ndxwhW%2B1pWfefXfTsls%3D&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=useast5",
"coverThumb": "https://p16-common-sign.tiktokcdn-us.com/tos-useast8-avt-0068-tx2/b29bd2d826cffba1af22f99793a7d5dc~tplv-tiktokx-cropcenter:100:100.jpeg?dr=9640&refresh_token=84bba9f5&x-expires=1783695600&x-signature=Et06On6iQeXFbtycXoVodzl0fdM%3D&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=useast5",
"authorName": "Cookwithhali",
"original": true,
"private": false,
"duration": 62,
"scheduleSearchTime": 0,
"collected": false,
"preciseDuration": {
"preciseDuration": 62.093063,
"preciseShootDuration": 62.093063,
"preciseAuditionDuration": 62.093063,
"preciseVideoDuration": 62.093063
},
"isCopyrighted": false,
"tt2dsp": {
"tt_to_dsp_song_infos": []
},
"is_unlimited_music": false,
"is_commerce_music": true,
"shoot_duration": 62
},
"challenges": [
{
"id": "6983",
"title": "bread",
"desc": "",
"profileLarger": "",
"profileMedium": "",
"profileThumb": "",
"coverLarger": "",
"coverMedium": "",
"coverThumb": ""
},
{
"id": "285169",
"title": "garlicbread",
"desc": "",
"profileLarger": "",
"profileMedium": "",
"profileThumb": "",
"coverLarger": "",
"coverMedium": "",
"coverThumb": ""
},
{
"id": "1643317566954502",
"title": "breadtok",
"desc": "",
"profileLarger": "",
"profileMedium": "",
"profileThumb": "",
"coverLarger": "",
"coverMedium": "",
"coverThumb": ""
},
{
"id": "7250",
"title": "baking",
"desc": "Whether you're baking the perfect pie or just searching for a recipe, you've come to the right place for all things #Baking.",
"profileLarger": "https://p19-common-sign.tiktokcdn-us.com/musically-maliva-obj/1aa8a54136dc06390b83508fdd683160~tplv-tiktokx-origin.image?dr=9627&x-expires=1783544400&x-signature=nigLPQXvvDM%2BxFQezn3ELBH%2BLwQ%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=9b759fb9&idc=useast5",
"profileMedium": "https://p19-common-sign.tiktokcdn-us.com/musically-maliva-obj/1aa8a54136dc06390b83508fdd683160~tplv-tiktokx-origin.image?dr=9627&x-expires=1783544400&x-signature=nigLPQXvvDM%2BxFQezn3ELBH%2BLwQ%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=9b759fb9&idc=useast5",
"profileThumb": "https://p19-common-sign.tiktokcdn-us.com/musically-maliva-obj/1aa8a54136dc06390b83508fdd683160~tplv-tiktokx-origin.image?dr=9627&x-expires=1783544400&x-signature=nigLPQXvvDM%2BxFQezn3ELBH%2BLwQ%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=9b759fb9&idc=useast5",
"coverLarger": "",
"coverMedium": "",
"coverThumb": ""
},
{
"id": "10488587",
"title": "homemadebread",
"desc": "",
"profileLarger": "",
"profileMedium": "",
"profileThumb": "",
"coverLarger": "",
"coverMedium": "",
"coverThumb": ""
}
],
"stats": {
"diggCount": 754100,
"shareCount": 144500,
"commentCount": 3720,
"playCount": 8300000,
"collectCount": "396399"
},
"statsV2": {
"diggCount": "754100",
"shareCount": "144500",
"commentCount": "3720",
"playCount": "8300000",
"collectCount": "396399",
"repostCount": "0"
},
"warnInfo": [],
"originalItem": false,
"officalItem": false,
"textExtra": [
{
"awemeId": "",
"start": 1024,
"end": 1030,
"hashtagId": "6983",
"hashtagName": "bread",
"type": 1,
"subType": 0,
"isCommerce": false
},
{
"awemeId": "",
"start": 1031,
"end": 1043,
"hashtagId": "285169",
"hashtagName": "garlicbread",
"type": 1,
"subType": 0,
"isCommerce": false
},
{
"awemeId": "",
"start": 1044,
"end": 1053,
"hashtagId": "1643317566954502",
"hashtagName": "breadtok",
"type": 1,
"subType": 0,
"isCommerce": false
},
{
"awemeId": "",
"start": 1054,
"end": 1061,
"hashtagId": "7250",
"hashtagName": "baking",
"type": 1,
"subType": 0,
"isCommerce": false
},
{
"awemeId": "",
"start": 1062,
"end": 1076,
"hashtagId": "10488587",
"hashtagName": "homemadebread",
"type": 1,
"subType": 0,
"isCommerce": false
}
],
"secret": false,
"forFriend": false,
"digged": false,
"itemCommentStatus": 0,
"takeDown": 0,
"effectStickers": [],
"authorStats": {
"followerCount": 372600,
"followingCount": 0,
"heart": 8000000,
"heartCount": 8000000,
"videoCount": 279,
"diggCount": 223,
"friendCount": 0
},
"privateItem": false,
"duetEnabled": true,
"stitchEnabled": true,
"stickersOnItem": [
{
"stickerText": [
"Cheesy Pull Apart Garlic Bread! 🧄🧀\n",
"(Bakery-Style & No-Knead)"
],
"stickerType": 4
}
],
"isAd": false,
"shareEnabled": true,
"comments": [],
"duetDisplay": 0,
"stitchDisplay": 0,
"indexEnabled": true,
"diversificationLabels": [
"Cooking",
"Food & Drink",
"Lifestyle"
],
"locationCreated": "US",
"suggestedWords": [
"Garlic Bread",
"garlic bread recipe",
"pull apart bread",
"Sourdough Bread",
"How To Make Garlic Bread",
"Baking",
"food",
"bread",
"Baking Recipes",
"Garlic Pull Apart Bread Recipe"
],
"contents": [
{
"desc": "Pull Apart Garlic Bread ",
"textExtra": []
},
{
"desc": "",
"textExtra": []
},
{
"desc": "Ingredients",
"textExtra": []
},
{
"desc": "",
"textExtra": []
},
{
"desc": "* 1 tsp yeast",
"textExtra": []
},
{
"desc": "* 1 tbsp sugar",
"textExtra": []
},
{
"desc": "* 270g milk (about 1 cup + 2 tbsp)",
"textExtra": []
},
{
"desc": "* 380g flour (about 3 cups)",
"textExtra": []
},
{
"desc": "* 1 tsp salt",
"textExtra": []
},
{
"desc": "* 43g softened butter (about 3 tbsp)",
"textExtra": []
},
{
"desc": "",
"textExtra": []
},
{
"desc": "Butter Mixture",
"textExtra": []
},
{
"desc": "",
"textExtra": []
},
{
"desc": "* 100g butter, softened",
"textExtra": []
},
{
"desc": "* 1 tbsp chopped cilantro",
"textExtra": []
},
{
"desc": "* 1 garlic clove, minced",
"textExtra": []
},
{
"desc": "* Shredded cheese",
"textExtra": []
},
{
"desc": "",
"textExtra": []
},
{
"desc": "Instructions",
"textExtra": []
},
{
"desc": "",
"textExtra": []
},
{
"desc": "1. In a bowl, mix the sugar and yeast. Add the milk, then add the flour and mix until combined.",
"textExtra": []
},
{
"desc": "2. Add the salt and softened butter, then coil fold until the butter is fully incorporated.",
"textExtra": []
},
{
"desc": "3. Cover and let the dough rest for 2 hours or until doubled.",
"textExtra": []
},
{
"desc": "4. Mix the softened butter with the cilantro and minced garlic.",
"textExtra": []
},
{
"desc": "5. Roll the dough into a rectangle and spread the garlic butter mixture evenly over it. Sprinkle with shredded cheddar cheese.",
"textExtra": []
},
{
"desc": "6. Cut the dough in half, stack one half on top of the other, then cut into strips. Transfer the pieces into a loaf pan.",
"textExtra": []
},
{
"desc": "7. Cover and let rest for 20 minutes.",
"textExtra": []
},
{
"desc": "8. Bake at 370°F for 30 minutes, or until golden brown.",
"textExtra": []
},
{
"desc": "9. Brush with the remaining garlic butter mixture while still warm.",
"textExtra": []
},
{
"desc": "",
"textExtra": []
},
{
"desc": "#bread #garlicbread #breadtok #baking #homemadebread ",
"textExtra": [
{
"awemeId": "",
"start": 0,
"end": 6,
"hashtagId": "6983",
"hashtagName": "bread",
"type": 1,
"subType": 0,
"isCommerce": false
},
{
"awemeId": "",
"start": 7,
"end": 19,
"hashtagId": "285169",
"hashtagName": "garlicbread",
"type": 1,
"subType": 0,
"isCommerce": false
},
{
"awemeId": "",
"start": 20,
"end": 29,
"hashtagId": "1643317566954502",
"hashtagName": "breadtok",
"type": 1,
"subType": 0,
"isCommerce": false
},
{
"awemeId": "",
"start": 30,
"end": 37,
"hashtagId": "7250",
"hashtagName": "baking",
"type": 1,
"subType": 0,
"isCommerce": false
},
{
"awemeId": "",
"start": 38,
"end": 52,
"hashtagId": "10488587",
"hashtagName": "homemadebread",
"type": 1,
"subType": 0,
"isCommerce": false
}
]
}
],
"diversificationId": 10040,
"anchors": [
{
"id": "0",
"type": 54,
"keyword": "CapCut · Editing made easy",
"icon": {
"uri": "tiktok-obj/capcut_logo_64px_bk.png",
"urlList": [
"https://p19-common-sign.tiktokcdn-us.com/tiktok-obj/capcut_logo_64px_bk.png~tplv-tiktokx-origin.image?dr=9627&x-expires=1783544400&x-signature=lMwgk8e0wLaM3WkpnsMqYcxZN6w%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=9b759fb9&idc=useast5",
"https://p16-common-sign.tiktokcdn-us.com/tiktok-obj/capcut_logo_64px_bk.png~tplv-tiktokx-origin.image?dr=9627&x-expires=1783544400&x-signature=44xwWHkyYJdE%2FIwqS3zAAjU4XQ4%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=9b759fb9&idc=useast5",
"https://p19-common-sign.tiktokcdn-us.com/tiktok-obj/capcut_logo_64px_bk.png~tplv-tiktokx-origin.jpeg?dr=9627&x-expires=1783544400&x-signature=lzVxquDTAbut%2BvshGEhsFsYQc2s%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=9b759fb9&idc=useast5"
]
},
"schema": "https://www.capcut.com/tools/desktop-video-editor?channel=capcutpc_ttweb_anchor_edit1&download_channel=capcutpc_ttweb_anchor_edit1&enter_from=capcutpc_ttweb_anchor_edit1&force_dp=1&from_page=capcutpc_ttweb_anchor_edit1&type=tools",
"logExtra": "{\"anchor_id\":0,\"anchor_name\":\"CapCut · Editing made easy\",\"anchor_title_detail\":\"None\",\"anchor_title_id\":21502485763,\"anchor_type\":\"TT_CAPCUT\",\"capability_key\":\"default\",\"ccep_vip\":0,\"if_race_trigger\":0,\"is_two_line\":0,\"landing_page_style_id\":21502486275,\"maker_source\":\"\",\"publish_key\":\"default\",\"template_id\":\"none\",\"tutorial_id\":\"none\",\"viamaker_anchor_capability_key_weight\":1,\"viamaker_anchor_style_idx\":-1,\"viamaker_anchor_style_source\":3,\"video_source\":0,\"video_type_id\":1}",
"description": "CapCut · Video Editor",
"thumbnail": {
"uri": "tiktok-obj/64x_Capcut3x.png",
"urlList": [
"https://p16-common-sign.tiktokcdn-us.com/tiktok-obj/64x_Capcut3x.png~tplv-tiktokx-origin.image?dr=9627&x-expires=1783544400&x-signature=SJ6q86bjtCoRArlVNS1DjDDFWoc%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=9b759fb9&idc=useast5",
"https://p19-common-sign.tiktokcdn-us.com/tiktok-obj/64x_Capcut3x.png~tplv-tiktokx-origin.image?dr=9627&x-expires=1783544400&x-signature=GOZUldk%2BrxINygKT0jwxeJz9VLQ%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=9b759fb9&idc=useast5",
"https://p16-common-sign.tiktokcdn-us.com/tiktok-obj/64x_Capcut3x.png~tplv-tiktokx-origin.jpeg?dr=9627&x-expires=1783544400&x-signature=NBemEand1jjAoV81cQ1ANQzVqqc%3D&t=4d5b0474&ps=13740610&shp=81f88b70&shcp=9b759fb9&idc=useast5"
],
"width": 64,
"height": 64
},
"extraInfo": {
"subtype": ""
}
}
],
"collected": false,
"channelTags": [],
"item_control": {
"can_repost": true
},
"IsAigc": false,
"AIGCDescription": "",
"backendSourceEventTracking": "",
"CategoryType": 111,
"textLanguage": "en",
"textTranslatable": true,
"authorStatsV2": {
"followerCount": "372600",
"followingCount": "0",
"heart": "8000000",
"heartCount": "8000000",
"videoCount": "279",
"diggCount": "223",
"friendCount": "0"
},
"isReviewing": false,
"creatorAIComment": {
"hasAITopic": false,
"categoryList": [],
"eligibleVideo": false,
"notEligibleReason": 101
}
}

View file

@ -38,6 +38,7 @@ _EXPECTED_SUBAGENTS = frozenset(
"memory",
"onedrive",
"reddit",
"tiktok",
"web_crawler",
"youtube",
}

View file

@ -78,8 +78,14 @@ async def test_registry_becomes_one_tool_per_verb_plus_readers(isolate):
tools = isolate.module.build_capability_tools(workspace_id=7, capabilities=caps)
by_name = {t.name: t for t in tools}
# One tool per verb, plus the two shared run-reader tools.
assert set(by_name) == {"web_scrape", "web_discover", "read_run", "search_run"}
# One tool per verb, plus the shared run-reader tools.
assert set(by_name) == {
"web_scrape",
"web_discover",
"read_run",
"search_run",
"export_run",
}
assert by_name["web_scrape"].description == "web.scrape does a thing."
assert by_name["web_scrape"].args_schema is _EchoInput

View file

@ -0,0 +1,48 @@
"""``tiktok.comments`` executor: verb input → scraper args → typed comment items.
Boundary mocked: the proprietary comments actor (injected fake). NOT mocked: the
verb's own payload→args forwarding and the dict→CommentItem wrapping.
"""
from __future__ import annotations
import pytest
from app.capabilities.tiktok.comments.executor import build_comments_executor
from app.capabilities.tiktok.comments.schemas import CommentsInput, CommentsOutput
pytestmark = pytest.mark.unit
_VIDEO = "https://www.tiktok.com/@bob/video/123"
class _FakeComments:
"""Records the urls + kwargs it was called with; returns canned items."""
def __init__(self, items: list[dict]):
self._items = items
self.calls: list[tuple[list[str], int, int | None]] = []
async def __call__(
self, video_urls: list[str], *, per_video: int, limit: int | None = None
) -> list[dict]:
self.calls.append((video_urls, per_video, limit))
return self._items
async def test_forwards_urls_and_limits_and_wraps_items():
comments = _FakeComments([{"id": "1", "text": "hi"}])
execute = build_comments_executor(comments_fn=comments)
out = await execute(
CommentsInput(video_urls=[_VIDEO], comments_per_video=15, max_items=40)
)
assert isinstance(out, CommentsOutput)
assert len(out.items) == 1
assert out.items[0].text == "hi"
(urls, per_video, limit) = comments.calls[0]
assert urls == [_VIDEO]
assert per_video == 15
assert limit == 40

View file

@ -0,0 +1,48 @@
"""``tiktok.comments`` input guards and billing: a video URL is required, bounded,
and ErrorItems are surfaced free."""
from __future__ import annotations
import pytest
from pydantic import ValidationError
from app.capabilities.tiktok.comments.schemas import CommentsInput, CommentsOutput
from app.capabilities.tiktok.scrape.schemas import MAX_TIKTOK_ITEMS, MAX_TIKTOK_SOURCES
pytestmark = pytest.mark.unit
_VIDEO = "https://www.tiktok.com/@bob/video/123"
def test_rejects_input_with_no_url():
with pytest.raises(ValidationError):
CommentsInput(video_urls=[])
def test_defaults_and_bounds():
payload = CommentsInput(video_urls=[_VIDEO])
assert payload.max_items == 20
assert payload.comments_per_video == 20
assert payload.estimated_units == 20
with pytest.raises(ValidationError):
CommentsInput(video_urls=[_VIDEO], max_items=0)
with pytest.raises(ValidationError):
CommentsInput(video_urls=[_VIDEO], max_items=MAX_TIKTOK_ITEMS + 1)
def test_rejects_more_urls_than_the_cap():
too_many = [f"{_VIDEO}{i}" for i in range(MAX_TIKTOK_SOURCES + 1)]
with pytest.raises(ValidationError):
CommentsInput(video_urls=too_many)
def test_error_items_are_not_billed():
# Real comments count; ErrorItems (bad URL / empty video) are surfaced free.
out = CommentsOutput(
items=[
{"id": "1", "text": "hi"},
{"errorCode": "no_comments", "input": "123", "error": "empty"},
]
)
assert len(out.items) == 2
assert out.billable_units == 1

View file

@ -0,0 +1,79 @@
"""``tiktok.scrape`` executor: verb input → actor input mapping → typed items.
Boundary mocked: the proprietary scraper (injected fake). NOT mocked: the verb's
own payloadTikTokScrapeInput mapping and the dictTikTokVideoItem wrapping.
"""
from __future__ import annotations
import pytest
from app.capabilities.tiktok.scrape.executor import build_scrape_executor
from app.capabilities.tiktok.scrape.schemas import ScrapeInput, ScrapeOutput
from app.exceptions import ForbiddenError
from app.proprietary.platforms.tiktok import TikTokAccessBlockedError, TikTokScrapeInput
pytestmark = pytest.mark.unit
class _FakeScraper:
"""Records the actor input + limit it was called with; returns canned items."""
def __init__(self, items: list[dict]):
self._items = items
self.calls: list[tuple[TikTokScrapeInput, int | None]] = []
async def __call__(
self, actor_input: TikTokScrapeInput, *, limit: int | None = None
) -> list[dict]:
self.calls.append((actor_input, limit))
return self._items
async def test_maps_urls_to_start_urls_and_wraps_items():
scraper = _FakeScraper([{"id": "123", "text": "hello"}])
execute = build_scrape_executor(scrape_fn=scraper)
out = await execute(ScrapeInput(urls=["https://www.tiktok.com/@nasa/video/123"]))
assert isinstance(out, ScrapeOutput)
assert len(out.items) == 1
assert out.items[0].id == "123"
(actor_input, _limit) = scraper.calls[0]
assert [u.url for u in actor_input.startUrls] == [
"https://www.tiktok.com/@nasa/video/123"
]
async def test_forwards_typed_sources_and_limit():
scraper = _FakeScraper([])
execute = build_scrape_executor(scrape_fn=scraper)
await execute(
ScrapeInput(
profiles=["nasa"],
hashtags=["food"],
search_queries=["cats"],
results_per_page=7,
max_items=25,
)
)
(actor_input, limit) = scraper.calls[0]
assert actor_input.profiles == ["nasa"]
assert actor_input.hashtags == ["food"]
assert actor_input.searchQueries == ["cats"]
assert actor_input.resultsPerPage == 7
# The outer collection limit is the caller's total-item cap.
assert limit == 25
async def test_access_blocked_maps_to_forbidden():
async def _blocked(actor_input: TikTokScrapeInput, *, limit: int | None = None):
raise TikTokAccessBlockedError("all IPs refused")
execute = build_scrape_executor(scrape_fn=_blocked)
with pytest.raises(ForbiddenError):
await execute(ScrapeInput(hashtags=["food"]))

View file

@ -0,0 +1,58 @@
"""``tiktok.scrape`` input guards: a source is required and the batch is bounded."""
from __future__ import annotations
import pytest
from pydantic import ValidationError
from app.capabilities.tiktok.scrape.schemas import (
MAX_TIKTOK_ITEMS,
MAX_TIKTOK_SOURCES,
ScrapeInput,
ScrapeOutput,
)
pytestmark = pytest.mark.unit
def test_rejects_input_with_no_source():
with pytest.raises(ValidationError):
ScrapeInput()
def test_accepts_urls_only():
payload = ScrapeInput(urls=["https://www.tiktok.com/@nasa"])
assert payload.profiles == []
def test_accepts_hashtags_only():
payload = ScrapeInput(hashtags=["food"])
assert payload.hashtags == ["food"]
def test_defaults_and_bounds():
payload = ScrapeInput(hashtags=["food"])
assert payload.max_items == 10
assert payload.results_per_page == 10
with pytest.raises(ValidationError):
ScrapeInput(hashtags=["food"], max_items=0)
with pytest.raises(ValidationError):
ScrapeInput(hashtags=["food"], max_items=MAX_TIKTOK_ITEMS + 1)
def test_rejects_more_sources_than_the_cap():
too_many = [f"tag{i}" for i in range(MAX_TIKTOK_SOURCES + 1)]
with pytest.raises(ValidationError):
ScrapeInput(hashtags=too_many)
def test_error_items_are_not_billed():
# Real videos count; ErrorItems (blocked/empty targets) are surfaced free.
out = ScrapeOutput(
items=[
{"id": "1", "webVideoUrl": "https://tiktok.com/@a/video/1"},
{"errorCode": "no_items", "input": "nasa", "error": "blocked"},
]
)
assert len(out.items) == 2
assert out.billable_units == 1

View file

@ -0,0 +1,57 @@
"""The tiktok namespace registers its verb as one Capability the doors/agent read."""
from __future__ import annotations
import pytest
from app.capabilities import (
tiktok, # noqa: F401 — importing the namespace registers its verbs
)
from app.capabilities.core import BillingUnit
from app.capabilities.core.store import get_capability
from app.capabilities.tiktok.comments.schemas import CommentsInput, CommentsOutput
from app.capabilities.tiktok.scrape.schemas import ScrapeInput, ScrapeOutput
from app.capabilities.tiktok.trending.schemas import TrendingInput, TrendingOutput
from app.capabilities.tiktok.user_search.schemas import (
UserSearchInput,
UserSearchOutput,
)
pytestmark = pytest.mark.unit
def test_tiktok_scrape_is_registered_and_billed_per_video():
cap = get_capability("tiktok.scrape")
assert cap.name == "tiktok.scrape"
assert cap.input_schema is ScrapeInput
assert cap.output_schema is ScrapeOutput
assert cap.billing_unit is BillingUnit.TIKTOK_VIDEO
def test_tiktok_user_search_is_registered_and_billed_per_profile():
cap = get_capability("tiktok.user_search")
assert cap.name == "tiktok.user_search"
assert cap.input_schema is UserSearchInput
assert cap.output_schema is UserSearchOutput
assert cap.billing_unit is BillingUnit.TIKTOK_USER
def test_tiktok_comments_is_registered_and_billed_per_comment():
cap = get_capability("tiktok.comments")
assert cap.name == "tiktok.comments"
assert cap.input_schema is CommentsInput
assert cap.output_schema is CommentsOutput
assert cap.billing_unit is BillingUnit.TIKTOK_COMMENT
def test_tiktok_trending_is_registered_and_billed_per_video():
cap = get_capability("tiktok.trending")
assert cap.name == "tiktok.trending"
assert cap.input_schema is TrendingInput
assert cap.output_schema is TrendingOutput
# Trending returns videos, so it shares the per-video meter.
assert cap.billing_unit is BillingUnit.TIKTOK_VIDEO

View file

@ -0,0 +1,38 @@
"""``tiktok.trending`` executor: verb input → scraper count → typed video items.
Boundary mocked: the proprietary trending actor (injected fake). NOT mocked: the
verb's own count forwarding and the dict→TikTokVideoItem wrapping.
"""
from __future__ import annotations
import pytest
from app.capabilities.tiktok.trending.executor import build_trending_executor
from app.capabilities.tiktok.trending.schemas import TrendingInput, TrendingOutput
pytestmark = pytest.mark.unit
class _FakeTrending:
"""Records the count it was called with; returns canned items."""
def __init__(self, items: list[dict]):
self._items = items
self.calls: list[int] = []
async def __call__(self, *, count: int) -> list[dict]:
self.calls.append(count)
return self._items
async def test_forwards_count_and_wraps_items():
trending = _FakeTrending([{"id": "1", "text": "viral"}])
execute = build_trending_executor(trending_fn=trending)
out = await execute(TrendingInput(max_items=30))
assert isinstance(out, TrendingOutput)
assert len(out.items) == 1
assert out.items[0].id == "1"
assert trending.calls == [30]

View file

@ -0,0 +1,33 @@
"""``tiktok.trending`` input guards and billing: bounded count, ErrorItems free."""
from __future__ import annotations
import pytest
from pydantic import ValidationError
from app.capabilities.tiktok.scrape.schemas import MAX_TIKTOK_ITEMS
from app.capabilities.tiktok.trending.schemas import TrendingInput, TrendingOutput
pytestmark = pytest.mark.unit
def test_defaults_and_bounds():
payload = TrendingInput()
assert payload.max_items == 20
assert payload.estimated_units == 20
with pytest.raises(ValidationError):
TrendingInput(max_items=0)
with pytest.raises(ValidationError):
TrendingInput(max_items=MAX_TIKTOK_ITEMS + 1)
def test_error_items_are_not_billed():
# Real videos count; an ErrorItem (empty/withheld feed) is surfaced free.
out = TrendingOutput(
items=[
{"id": "1", "webVideoUrl": "https://tiktok.com/@a/video/1"},
{"errorCode": "no_items", "input": "explore", "error": "empty"},
]
)
assert len(out.items) == 2
assert out.billable_units == 1

View file

@ -0,0 +1,49 @@
"""``tiktok.user_search`` executor: verb input → search args → typed profile items.
Boundary mocked: the proprietary search actor (injected fake). NOT mocked: the
verb's own payload→args forwarding and the dict→TikTokProfileItem wrapping.
"""
from __future__ import annotations
import pytest
from app.capabilities.tiktok.user_search.executor import build_user_search_executor
from app.capabilities.tiktok.user_search.schemas import (
UserSearchInput,
UserSearchOutput,
)
pytestmark = pytest.mark.unit
class _FakeSearch:
"""Records the queries + kwargs it was called with; returns canned items."""
def __init__(self, items: list[dict]):
self._items = items
self.calls: list[tuple[list[str], int, int | None]] = []
async def __call__(
self, queries: list[str], *, per_query: int, limit: int | None = None
) -> list[dict]:
self.calls.append((queries, per_query, limit))
return self._items
async def test_forwards_queries_and_limits_and_wraps_items():
search = _FakeSearch([{"id": "1", "name": "nasa"}])
execute = build_user_search_executor(search_fn=search)
out = await execute(
UserSearchInput(queries=["nasa"], results_per_query=7, max_items=25)
)
assert isinstance(out, UserSearchOutput)
assert len(out.items) == 1
assert out.items[0].name == "nasa"
(queries, per_query, limit) = search.calls[0]
assert queries == ["nasa"]
assert per_query == 7
assert limit == 25

View file

@ -0,0 +1,49 @@
"""``tiktok.user_search`` input guards and billing: a query is required, bounded,
and ErrorItems are surfaced free."""
from __future__ import annotations
import pytest
from pydantic import ValidationError
from app.capabilities.tiktok.scrape.schemas import MAX_TIKTOK_ITEMS, MAX_TIKTOK_SOURCES
from app.capabilities.tiktok.user_search.schemas import (
UserSearchInput,
UserSearchOutput,
)
pytestmark = pytest.mark.unit
def test_rejects_input_with_no_query():
with pytest.raises(ValidationError):
UserSearchInput(queries=[])
def test_defaults_and_bounds():
payload = UserSearchInput(queries=["nasa"])
assert payload.max_items == 10
assert payload.results_per_query == 10
assert payload.estimated_units == 10
with pytest.raises(ValidationError):
UserSearchInput(queries=["nasa"], max_items=0)
with pytest.raises(ValidationError):
UserSearchInput(queries=["nasa"], max_items=MAX_TIKTOK_ITEMS + 1)
def test_rejects_more_queries_than_the_cap():
too_many = [f"q{i}" for i in range(MAX_TIKTOK_SOURCES + 1)]
with pytest.raises(ValidationError):
UserSearchInput(queries=too_many)
def test_error_items_are_not_billed():
# Real accounts count; ErrorItems (empty/withheld queries) are surfaced free.
out = UserSearchOutput(
items=[
{"id": "1", "name": "nasa"},
{"errorCode": "no_users", "input": "ghost", "error": "empty"},
]
)
assert len(out.items) == 2
assert out.billable_units == 1

View file

@ -0,0 +1,89 @@
"""Comments orchestration over a fake fetch (no network).
Drives ``scrape_tiktok_comments``: video URLs -> captured raw comments -> items.
"""
from __future__ import annotations
from typing import Any
from app.proprietary.platforms.tiktok import scrape_tiktok_comments
_VIDEO = "https://www.tiktok.com/@bob/video/123"
def _comment(cid: str, reply_id: str = "0") -> dict[str, Any]:
return {
"cid": cid,
"text": f"comment {cid}",
"digg_count": 7,
"reply_comment_total": 2,
"create_time": 1700000000,
"reply_id": reply_id,
"user": {
"uid": "u1",
"unique_id": "alice",
"nickname": "Alice",
"avatar_thumb": {"url_list": ["https://cdn/a.webp"]},
},
}
async def test_comments_parse_dedupe_and_cap():
async def fake_fetch(_url: str, _cap: int) -> list[dict]:
return [_comment("1"), _comment("1"), _comment("2", reply_id="1")]
items = await scrape_tiktok_comments(
[_VIDEO], per_video=2, fetch_comments_fn=fake_fetch
)
assert [i["id"] for i in items] == ["1", "2"]
first = items[0]
assert first["text"] == "comment 1"
assert first["videoWebUrl"] == _VIDEO
assert first["diggCount"] == 7
assert first["uniqueId"] == "alice"
assert first["avatar"] == "https://cdn/a.webp"
assert first["createTimeISO"] is not None
assert first["repliesToId"] is None # reply_id "0" == top-level
assert first["scrapedAt"] is not None
assert items[1]["repliesToId"] == "1" # a reply carries its parent id
async def test_empty_video_emits_error_item():
async def fake_fetch(_url: str, _cap: int) -> list[dict]:
return []
items = await scrape_tiktok_comments(
[_VIDEO], per_video=5, fetch_comments_fn=fake_fetch
)
assert len(items) == 1
assert items[0]["errorCode"] == "no_comments"
assert items[0]["input"] == "123"
async def test_non_video_url_emits_bad_url_error():
async def fake_fetch(_url: str, _cap: int) -> list[dict]:
raise AssertionError("should not fetch for a non-video URL")
items = await scrape_tiktok_comments(
["https://www.tiktok.com/@bob"], per_video=5, fetch_comments_fn=fake_fetch
)
assert len(items) == 1
assert items[0]["errorCode"] == "bad_url"
async def test_comments_honor_limit_across_videos():
async def fake_fetch(_url: str, _cap: int) -> list[dict]:
return [_comment("1"), _comment("2")]
items = await scrape_tiktok_comments(
[_VIDEO, "https://www.tiktok.com/@bob/video/456"],
per_video=5,
limit=3,
fetch_comments_fn=fake_fetch,
)
assert len(items) == 3

View file

@ -0,0 +1,135 @@
"""Fetch-seam resilience for the TikTok scraper (no network, fake sessions).
Fake sessions drive the cookie warm-up + rotate-on-block + backoff branches
deterministically; a live first IP normally warms and returns 200s.
"""
from __future__ import annotations
from app.proprietary.platforms.tiktok.session import (
TikTokAccessBlockedError,
client,
)
from app.proprietary.platforms.tiktok.session.proxy import _current_session
_HTML = "<html><body>ok</body></html>"
class _FakePage:
def __init__(self, status: int, *, cookies: dict | None = None, body: str = _HTML):
self.status = status
self.cookies = cookies or {}
self.body = body
@property
def text(self) -> str:
return self.body
class _FakeSession:
"""One 'IP': homepage warm mints ``ttwid`` per flag; page GETs return ``status``."""
def __init__(self, status: int = 200, *, warms: bool = True, body: str = _HTML):
self.status = status
self.warms = warms
self.body = body
self.page_calls = 0
self.warm_calls = 0
async def get(self, url, headers=None, cookies=None):
if url.rstrip("/") == "https://www.tiktok.com":
self.warm_calls += 1
return _FakePage(200, cookies={"ttwid": "x"} if self.warms else {})
self.page_calls += 1
return _FakePage(self.status, body=self.body)
class _FakeHolder:
def __init__(self, sessions: list[_FakeSession]) -> None:
self._sessions = sessions
self.session = sessions[0]
self.rotations = 0
self.warmed = False
async def rotate(self):
self.rotations += 1
self.session = self._sessions[min(self.rotations, len(self._sessions) - 1)]
self.warmed = False
return self.session
async def pace(self) -> None:
return None
async def close(self) -> None:
return None
def _no_sleep(monkeypatch) -> None:
async def _noop(_seconds):
return None
monkeypatch.setattr(client.asyncio, "sleep", _noop)
async def test_warms_then_returns_html():
holder = _FakeHolder([_FakeSession(200, warms=True)])
token = _current_session.set(holder)
try:
result = await client.fetch_html("https://www.tiktok.com/@scout2015")
finally:
_current_session.reset(token)
assert result == _HTML
assert holder.rotations == 0
assert holder.session.warm_calls == 1
async def test_rotates_when_warm_fails_then_succeeds():
holder = _FakeHolder([_FakeSession(200, warms=False), _FakeSession(200, warms=True)])
token = _current_session.set(holder)
try:
result = await client.fetch_html("https://www.tiktok.com/@scout2015")
finally:
_current_session.reset(token)
assert result == _HTML
assert holder.rotations == 1
async def test_404_returns_none_without_rotating():
holder = _FakeHolder([_FakeSession(404), _FakeSession(200)])
token = _current_session.set(holder)
try:
result = await client.fetch_html("https://www.tiktok.com/@missing")
finally:
_current_session.reset(token)
assert result is None
assert holder.rotations == 0
async def test_rotates_and_rewarms_on_403():
holder = _FakeHolder([_FakeSession(403), _FakeSession(200, warms=True)])
token = _current_session.set(holder)
try:
result = await client.fetch_html("https://www.tiktok.com/@scout2015")
finally:
_current_session.reset(token)
assert result == _HTML
assert holder.rotations == 1
assert holder.session.warm_calls == 1
async def test_persistent_403_raises_blocked(monkeypatch):
_no_sleep(monkeypatch)
holder = _FakeHolder(
[_FakeSession(403) for _ in range(client._MAX_ROTATIONS + 1)]
)
token = _current_session.set(holder)
try:
raised = False
try:
await client.fetch_html("https://www.tiktok.com/@scout2015")
except TikTokAccessBlockedError:
raised = True
finally:
_current_session.reset(token)
assert raised
assert holder.rotations == client._MAX_ROTATIONS

View file

@ -0,0 +1,32 @@
"""Rehydration-blob extraction from TikTok page HTML (pure, no network)."""
from __future__ import annotations
from app.proprietary.platforms.tiktok.extraction import extract_rehydration_data
_BLOB = (
'<html><body><script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" '
'type="application/json">'
'{"__DEFAULT_SCOPE__":{"webapp.video-detail":{"itemInfo":{"itemStruct":'
'{"id":"123"}}}}}'
"</script></body></html>"
)
def test_extracts_default_scope_from_blob():
data = extract_rehydration_data(_BLOB)
assert data is not None
item = data["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"]
assert item["id"] == "123"
def test_returns_none_when_blob_absent():
assert extract_rehydration_data("<html><body>no blob here</body></html>") is None
def test_returns_none_when_blob_json_malformed():
broken = (
'<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" '
'type="application/json">{not json}</script>'
)
assert extract_rehydration_data(broken) is None

View file

@ -0,0 +1,26 @@
"""Input surface for the TikTok scraper (anonymous, Apify-shaped)."""
from __future__ import annotations
from app.proprietary.platforms.tiktok.schemas import TikTokScrapeInput
def test_input_has_no_auth_fields():
forbidden = {"username", "password", "token", "login", "auth", "credentials"}
assert forbidden.isdisjoint(TikTokScrapeInput.model_fields)
def test_input_defaults():
model = TikTokScrapeInput()
assert model.resultsPerPage == 1
assert model.profileSorting == "latest"
assert model.proxyCountryCode == "None"
assert model.hashtags == []
assert model.profiles == []
assert model.searchQueries == []
assert model.postURLs == []
def test_input_allows_extra_inert_fields():
model = TikTokScrapeInput(shouldDownloadVideos=True, videoKvStoreIdOrName="x")
assert model.model_dump().get("shouldDownloadVideos") is True

View file

@ -0,0 +1,25 @@
"""Pulling item structs out of captured item_list / search API responses."""
from __future__ import annotations
from app.proprietary.platforms.tiktok.extraction import items_from_response
def test_reads_item_list_shape():
body = {"itemList": [{"id": "1"}, {"id": "2"}], "hasMore": True}
assert items_from_response(body) == [{"id": "1"}, {"id": "2"}]
def test_reads_search_data_shape():
body = {"data": [{"type": 1, "item": {"id": "9"}}, {"type": 4, "item": {}}]}
assert items_from_response(body) == [{"id": "9"}, {}]
def test_skips_malformed_entries():
body = {"data": [{"type": 1}, "junk", {"item": {"id": "7"}}]}
assert items_from_response(body) == [{"id": "7"}]
def test_returns_empty_for_unrelated_json():
assert items_from_response({"statusCode": 0}) == []
assert items_from_response("nope") == []

View file

@ -0,0 +1,60 @@
"""Retry-on-empty for the browser ``item_list`` seam (no browser, fake fetch).
``fetch_item_list`` re-fetches an empty capture up to ``TIKTOK_LISTING_MAX_ATTEMPTS``
so a flagged rotating exit IP on the first draw doesn't collapse straight to an
``ErrorItem``. These drive that loop deterministically by faking ``_fetch_sync``.
"""
from __future__ import annotations
from app.proprietary.platforms.tiktok.session import listing
class _Fake:
"""Returns each queued result once, repeating the last; counts calls."""
def __init__(self, results: list[list[dict]]):
self.results = results
self.calls = 0
def __call__(self, *args, **kwargs):
out = self.results[min(self.calls, len(self.results) - 1)]
self.calls += 1
return out
def _patch(monkeypatch, fake: _Fake, attempts: int) -> None:
monkeypatch.setattr(listing, "_fetch_sync", fake)
monkeypatch.setattr(listing.config, "TIKTOK_LISTING_MAX_ATTEMPTS", attempts)
async def test_returns_first_nonempty_without_retrying(monkeypatch):
fake = _Fake([[{"id": "1"}]])
_patch(monkeypatch, fake, 3)
items = await listing.fetch_item_list("https://tt/@x", 5)
assert items == [{"id": "1"}]
assert fake.calls == 1 # a draw with items never retries
async def test_retries_past_empty_draws_then_hits(monkeypatch):
fake = _Fake([[], [], [{"id": "9"}]])
_patch(monkeypatch, fake, 3)
items = await listing.fetch_item_list("https://tt/@x", 5)
assert items == [{"id": "9"}]
assert fake.calls == 3 # two empty (flagged-IP) draws retried, third lands
async def test_stops_at_attempt_ceiling_when_always_empty(monkeypatch):
fake = _Fake([[]])
_patch(monkeypatch, fake, 3)
items = await listing.fetch_item_list("https://tt/@x", 5)
assert items == []
assert fake.calls == 3 # capped; caller then emits the ErrorItem
async def test_single_attempt_config_disables_retry(monkeypatch):
fake = _Fake([[]])
_patch(monkeypatch, fake, 1)
items = await listing.fetch_item_list("https://tt/@x", 5)
assert items == []
assert fake.calls == 1 # static-IP setups opt out via attempts=1

View file

@ -0,0 +1,185 @@
"""End-to-end orchestration over a fake fetch (no network).
Drives the public collector: input -> target -> blob-first flow -> items.
"""
from __future__ import annotations
import json
from app.proprietary.platforms.tiktok import TikTokScrapeInput, scrape_tiktok
async def _no_html(_url: str) -> str:
"""Fetch stub that yields no rehydration blob (skips profile metadata)."""
return ""
def _profile_page(username: str, followers: int, videos: int) -> str:
blob = {
"__DEFAULT_SCOPE__": {
"webapp.user-detail": {
"userInfo": {
"user": {
"id": "u1",
"uniqueId": username,
"nickname": "Nick",
"verified": True,
},
"stats": {"followerCount": followers, "videoCount": videos},
}
}
}
}
return (
'<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" '
f'type="application/json">{json.dumps(blob)}</script>'
)
def _video_page(video_id: str, username: str) -> str:
blob = {
"__DEFAULT_SCOPE__": {
"webapp.video-detail": {
"itemInfo": {
"itemStruct": {
"id": video_id,
"desc": "hello",
"author": {"uniqueId": username},
"stats": {"diggCount": 5},
}
}
}
}
}
return (
'<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" '
f'type="application/json">{json.dumps(blob)}</script>'
)
async def test_scrape_video_url_returns_parsed_item():
url = "https://www.tiktok.com/@scout2015/video/123"
async def fake_fetch(_url: str) -> str:
return _video_page("123", "scout2015")
items = await scrape_tiktok(TikTokScrapeInput(postURLs=[url]), fetch=fake_fetch)
assert len(items) == 1
assert items[0]["id"] == "123"
assert items[0]["diggCount"] == 5
assert items[0]["webVideoUrl"] == "https://www.tiktok.com/@scout2015/video/123"
assert items[0]["scrapedAt"] is not None
async def test_scrape_honors_limit_across_targets():
urls = [
"https://www.tiktok.com/@a/video/1",
"https://www.tiktok.com/@b/video/2",
]
async def fake_fetch(url: str) -> str:
vid = url.rsplit("/", 1)[1]
user = url.split("@")[1].split("/")[0]
return _video_page(vid, user)
items = await scrape_tiktok(
TikTokScrapeInput(postURLs=urls), limit=1, fetch=fake_fetch
)
assert len(items) == 1
async def test_scrape_skips_unrecognized_urls():
async def fake_fetch(_url: str) -> str:
return ""
items = await scrape_tiktok(
TikTokScrapeInput(postURLs=["https://example.com/x"]), fetch=fake_fetch
)
assert items == []
async def test_scrape_profile_returns_listing_items():
async def fake_listing(_url: str, _count: int) -> list[dict]:
return [
{"id": "1", "author": {"uniqueId": "a"}},
{"id": "2", "author": {"uniqueId": "a"}},
]
items = await scrape_tiktok(
TikTokScrapeInput(profiles=["a"], resultsPerPage=5),
fetch=_no_html,
fetch_listing=fake_listing,
)
assert [i["id"] for i in items] == ["1", "2"]
assert items[0]["webVideoUrl"] == "https://www.tiktok.com/@a/video/1"
async def test_profile_emits_metadata_then_videos():
# The blob metadata item comes first and is billable; videos follow.
async def fake_fetch(_url: str) -> str:
return _profile_page("a", followers=100, videos=2)
async def fake_listing(_url: str, _count: int) -> list[dict]:
return [{"id": "1", "author": {"uniqueId": "a"}}]
items = await scrape_tiktok(
TikTokScrapeInput(profiles=["a"], resultsPerPage=5),
fetch=fake_fetch,
fetch_listing=fake_listing,
)
assert len(items) == 2
profile, video = items
assert "webVideoUrl" not in profile # metadata item, not a video
assert profile["name"] == "a"
assert profile["fans"] == 100
assert profile["verified"] is True
assert profile["scrapedAt"] is not None
assert video["id"] == "1"
async def test_profile_metadata_survives_blocked_listing():
# Videos withheld from anonymous access: we still return the profile metadata
# (not just an ErrorItem), so a blocked profile isn't a total loss.
async def fake_fetch(_url: str) -> str:
return _profile_page("a", followers=100, videos=9)
async def fake_listing(_url: str, _count: int) -> list[dict]:
return []
items = await scrape_tiktok(
TikTokScrapeInput(profiles=["a"], resultsPerPage=5),
fetch=fake_fetch,
fetch_listing=fake_listing,
)
assert len(items) == 2
assert items[0]["name"] == "a"
assert items[0]["fans"] == 100
assert items[1]["errorCode"] == "no_items"
async def test_listing_dedupes_then_caps_per_target():
async def fake_listing(_url: str, _count: int) -> list[dict]:
return [{"id": "1"}, {"id": "1"}, {"id": "2"}, {"id": "3"}]
items = await scrape_tiktok(
TikTokScrapeInput(hashtags=["x"], resultsPerPage=2), fetch_listing=fake_listing
)
assert [i["id"] for i in items] == ["1", "2"]
async def test_empty_listing_emits_error_item():
# A trust-gated/empty feed (0 videos) must surface one honest ErrorItem,
# tagged with errorCode, rather than vanishing silently.
async def fake_listing(_url: str, _count: int) -> list[dict]:
return []
items = await scrape_tiktok(
TikTokScrapeInput(profiles=["nasa"], resultsPerPage=5),
fetch=_no_html,
fetch_listing=fake_listing,
)
assert len(items) == 1
assert items[0]["errorCode"] == "no_items"
assert items[0]["input"] == "nasa"

View file

@ -0,0 +1,102 @@
"""Raw TikTok payload -> normalized item mapping (pure, no network)."""
from __future__ import annotations
from app.proprietary.platforms.tiktok.extraction import parse_author, parse_video
_ITEM_STRUCT = {
"id": "7534061113365859586",
"desc": "haha #comeramabanana",
"createTime": 1_700_000_000,
"author": {
"id": "6733",
"uniqueId": "bruniela_",
"nickname": "Bruni",
"verified": False,
"signature": "bio here",
"avatarLarger": "https://cdn/avatar.jpg",
},
"authorStats": {
"followerCount": 51200,
"followingCount": 269,
"heartCount": 3_000_000,
"videoCount": 259,
},
"stats": {
"diggCount": 5344,
"shareCount": 701,
"playCount": 55700,
"commentCount": 24,
"collectCount": 291,
},
"music": {
"id": "7529",
"title": "som original",
"authorName": "fox_rus0",
"original": True,
"playUrl": "https://cdn/music.mp3",
},
"video": {
"height": 1024,
"width": 576,
"duration": 16,
"cover": "https://cdn/cover.jpg",
"format": "mp4",
"definition": "540p",
},
"challenges": [{"id": "4982299", "title": "comeramabanana"}],
}
def test_parse_video_maps_core_and_derived_fields():
item = parse_video(_ITEM_STRUCT)
assert item["id"] == "7534061113365859586"
assert item["text"] == "haha #comeramabanana"
assert item["createTimeISO"] == "2023-11-14T22:13:20.000Z"
assert item["authorMeta"]["name"] == "bruniela_"
assert item["authorMeta"]["nickName"] == "Bruni"
assert item["authorMeta"]["profileUrl"] == "https://www.tiktok.com/@bruniela_"
assert item["authorMeta"]["fans"] == 51200
assert item["musicMeta"]["musicName"] == "som original"
assert item["videoMeta"]["duration"] == 16
assert item["diggCount"] == 5344
assert item["playCount"] == 55700
assert item["hashtags"] == [{"id": "4982299", "name": "comeramabanana"}]
assert (
item["webVideoUrl"]
== "https://www.tiktok.com/@bruniela_/video/7534061113365859586"
)
_USER_INFO = {
"user": {
"id": "6733",
"uniqueId": "bruniela_",
"nickname": "Bruni",
"verified": True,
"signature": "bio here",
"avatarLarger": "https://cdn/avatar.jpg",
"privateAccount": False,
},
"stats": {
"followerCount": 51200,
"followingCount": 269,
"heartCount": 3_000_000,
"videoCount": 259,
},
}
def test_parse_author_maps_user_and_stats():
author = parse_author(_USER_INFO)
assert author["name"] == "bruniela_"
assert author["nickName"] == "Bruni"
assert author["verified"] is True
assert author["profileUrl"] == "https://www.tiktok.com/@bruniela_"
assert author["fans"] == 51200
assert author["video"] == 259

View file

@ -0,0 +1,30 @@
"""Navigating the rehydration blob to its useful scopes (pure, no network)."""
from __future__ import annotations
from app.proprietary.platforms.tiktok.extraction import user_info, video_item_struct
def test_video_item_struct_navigates_video_detail_scope():
data = {
"__DEFAULT_SCOPE__": {
"webapp.video-detail": {"itemInfo": {"itemStruct": {"id": "123"}}}
}
}
item = video_item_struct(data)
assert item == {"id": "123"}
def test_user_info_navigates_user_detail_scope():
data = {
"__DEFAULT_SCOPE__": {
"webapp.user-detail": {"userInfo": {"user": {"uniqueId": "scout2015"}}}
}
}
info = user_info(data)
assert info == {"user": {"uniqueId": "scout2015"}}
def test_scopes_return_none_when_absent():
assert video_item_struct({}) is None
assert user_info({"__DEFAULT_SCOPE__": {}}) is None

View file

@ -0,0 +1,47 @@
"""URL classification for the TikTok scraper (pure, no network)."""
from __future__ import annotations
from app.proprietary.platforms.tiktok.targets import resolve_target
def test_resolve_video_carries_username_and_id():
target = resolve_target("https://www.tiktok.com/@scout2015/video/6718335390845095173")
assert target is not None
assert target.kind == "video"
assert target.value == "6718335390845095173"
assert target.username == "scout2015"
def test_resolve_profile():
target = resolve_target("https://www.tiktok.com/@scout2015")
assert target is not None
assert target.kind == "profile"
assert target.value == "scout2015"
def test_resolve_hashtag():
target = resolve_target("https://www.tiktok.com/tag/funny")
assert target is not None
assert target.kind == "hashtag"
assert target.value == "funny"
def test_resolve_search_top_video_and_user_sections():
top = resolve_target("https://www.tiktok.com/search?q=cats")
assert top is not None
assert top.kind == "search"
assert top.value == "cats"
assert top.section is None
videos = resolve_target("https://www.tiktok.com/search/video?q=cats")
assert videos is not None and videos.section == "video"
users = resolve_target("https://www.tiktok.com/search/user?q=cats")
assert users is not None and users.section == "user"
def test_resolve_rejects_non_tiktok_and_unknown_paths():
assert resolve_target("https://example.com/@scout2015") is None
assert resolve_target("https://www.tiktok.com/") is None
assert resolve_target("https://www.tiktok.com/foundation") is None

View file

@ -0,0 +1,35 @@
"""Trending orchestration over a fake fetch (no network).
Drives ``scrape_tiktok_trending``: the Explore feed -> captured itemStructs ->
video items, reusing the listing flow (parse/dedupe/cap/empty-ErrorItem).
"""
from __future__ import annotations
from app.proprietary.platforms.tiktok import scrape_tiktok_trending
async def test_trending_parses_dedupes_and_caps():
async def fake_fetch(url: str, _cap: int) -> list[dict]:
assert url == "https://www.tiktok.com/explore"
return [
{"id": "1", "author": {"uniqueId": "a"}},
{"id": "1", "author": {"uniqueId": "a"}},
{"id": "2", "author": {"uniqueId": "b"}},
]
items = await scrape_tiktok_trending(count=2, fetch_trending_fn=fake_fetch)
assert [i["id"] for i in items] == ["1", "2"]
assert items[0]["webVideoUrl"] == "https://www.tiktok.com/@a/video/1"
assert items[0]["scrapedAt"] is not None
async def test_trending_empty_feed_emits_error_item():
async def fake_fetch(_url: str, _cap: int) -> list[dict]:
return []
items = await scrape_tiktok_trending(count=5, fetch_trending_fn=fake_fetch)
assert len(items) == 1
assert items[0]["errorCode"] == "no_items"

View file

@ -0,0 +1,70 @@
"""User-search orchestration over a fake fetch (no network).
Drives ``search_tiktok_users``: queries -> captured ``user_info`` -> profile items.
"""
from __future__ import annotations
from typing import Any
from app.proprietary.platforms.tiktok import search_tiktok_users
def _user(uid: str, unique_id: str, followers: int = 10) -> dict[str, Any]:
return {
"uid": uid,
"unique_id": unique_id,
"nickname": unique_id.upper(),
"signature": "bio",
"follower_count": followers,
"total_favorited": 999,
"sec_uid": f"sec-{uid}",
"enterprise_verify_reason": "official" if uid == "1" else "",
"avatar_thumb": {"url_list": [f"https://cdn/{uid}.webp"]},
}
async def test_user_search_parses_dedupes_and_caps():
async def fake_fetch(_url: str, _cap: int) -> list[dict]:
return [_user("1", "nasa"), _user("1", "nasa"), _user("2", "nasa2")]
items = await search_tiktok_users(
["nasa"], per_query=2, fetch_users=fake_fetch
)
assert [i["id"] for i in items] == ["1", "2"]
first = items[0]
assert first["name"] == "nasa"
assert first["nickName"] == "NASA"
assert first["profileUrl"] == "https://www.tiktok.com/@nasa"
assert first["verified"] is True
assert first["fans"] == 10
assert first["avatar"] == "https://cdn/1.webp"
assert first["secUid"] == "sec-1"
assert first["scrapedAt"] is not None
assert items[1]["verified"] is False
async def test_user_search_empty_query_emits_error_item():
async def fake_fetch(_url: str, _cap: int) -> list[dict]:
return []
items = await search_tiktok_users(
["ghost"], per_query=5, fetch_users=fake_fetch
)
assert len(items) == 1
assert items[0]["errorCode"] == "no_users"
assert items[0]["input"] == "ghost"
async def test_user_search_honors_limit_across_queries():
async def fake_fetch(_url: str, _cap: int) -> list[dict]:
return [_user("1", "a"), _user("2", "b")]
items = await search_tiktok_users(
["q1", "q2"], per_query=5, limit=3, fetch_users=fake_fetch
)
# 2 from q1 + 1 from q2, then the cross-query limit stops it.
assert len(items) == 3