diff --git a/spec/helpers_spec.cr b/spec/helpers_spec.cr index 41a55a9cd..dd12a9e64 100644 --- a/spec/helpers_spec.cr +++ b/spec/helpers_spec.cr @@ -27,7 +27,19 @@ describe "Helper" do it "correctly produces token for searching a specific channel" do produce_channel_search_url("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100).should eq("/browse_ajax?continuation=4qmFsgI-EhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FqZ0JZQUZxQUxnQkFIb0RNVEF3WgA%3D&gl=US&hl=en") - produce_channel_search_url("UCXuqSBlHAE6Xw-yeJA0Tunw", "По ожиशुपतिरपि子而時ஸ்றீனி", 0).should eq("/browse_ajax?continuation=4qmFsgJZEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaJEVnWnpaV0Z5WTJnd0FqZ0JZQUZxQUxnQkFIb0JNQSUzRCUzRFoX0J_QviDQvtC20LjgpLbgpYHgpKrgpKTgpL_gpLDgpKrgpL_lrZDogIzmmYLgrrjgr43grrHgr4Dgrqngrr8%3D&gl=US&hl=en") + produce_channel_search_url("UCXuqSBlHAE6Xw-yeJA0Tunw", "По ожиशुपतिरपि子而時ஸ்றீனி", 0).should eq("/browse_ajax?continuation=4qmFsgKAARIYVUNYdXFTQmxIQUU2WHcteWVKQTBUdW53GiRFZ1p6WldGeVkyZ3dBamdCWUFGcUFMZ0JBSG9CTUElM0QlM0RaPtCf0L4g0L7QttC44KS24KWB4KSq4KSk4KS_4KSw4KSq4KS_5a2Q6ICM5pmC4K644K-N4K6x4K-A4K6p4K6_&gl=US&hl=en") + end + end + + describe "#produce_channel_playlists_url" do + it "correctly produces a /browse_ajax URL with the given UCID and cursor" do + produce_channel_playlists_url("UCCj956IF62FbT7Gouszaj9w", "AIOkY9EQpi_gyn1_QrFuZ1reN81_MMmI1YmlBblw8j7JHItEFG5h7qcJTNd4W9x5Quk_CVZ028gW").should eq("/browse_ajax?continuation=4qmFsgLRARIYVUNDajk1NklGNjJGYlQ3R291c3phajl3GrQBRWdsd2JHRjViR2x6ZEhNWUF5QUJNQUk0QVdBQmFnQjZabEZWYkZCaE1XczFVbFpHZDJGV09XNWxWelI0V0RGR2VWSnVWbUZOV0Vwc1ZHcG5lRmd3TVU1aVZXdDRWMWN4YzFGdFNuTmtlbWh4VGpCd1NWTllVa1pTYTJNeFlVUmtlRmt3Y0ZWVWJWRXdWbnBzTkU1V1JqRmhNVGxFVm14dmQwMXFhRzVXZDdnQkFBJTNEJTNE&gl=US&hl=en") + end + end + + describe "#extract_channel_playlists_cursor" do + it "correctly extracts a playlists cursor from the given URL" do + extract_channel_playlists_cursor("/browse_ajax?continuation=4qmFsgLRARIYVUNDajk1NklGNjJGYlQ3R291c3phajl3GrQBRWdsd2JHRjViR2x6ZEhNWUF5QUJNQUk0QVdBQmFnQjZabEZWYkZCaE1XczFVbFpHZDJGV09XNWxWelI0V0RGR2VWSnVWbUZOV0Vwc1ZHcG5lRmd3TVU1aVZXdDRWMWN4YzFGdFNuTmtlbWh4VGpCd1NWTllVa1pTYTJNeFlVUmtlRmt3Y0ZWVWJWRXdWbnBzTkU1V1JqRmhNVGxFVm14dmQwMXFhRzVXZDdnQkFBJTNEJTNE&gl=US&hl=en", false).should eq("AIOkY9EQpi_gyn1_QrFuZ1reN81_MMmI1YmlBblw8j7JHItEFG5h7qcJTNd4W9x5Quk_CVZ028gW") end end @@ -83,6 +95,26 @@ describe "Helper" do end end + describe "#produce_channel_community_continuation" do + it "correctly produces a continuation token for a channel community" do + produce_channel_community_continuation("UCCj956IF62FbT7Gouszaj9w", "Egljb21tdW5pdHm4").should eq("4qmFsgIsEhhVQ0NqOTU2SUY2MkZiVDdHb3VzemFqOXcaEEVnbGpiMjF0ZFc1cGRIbTQ%3D") + produce_channel_community_continuation("UCCj956IF62FbT7Gouszaj9w", "Egljb21tdW5pdHm4AQCqAyQaIBIaVWd3cE9NQmVwWEdjclhsUHg2WjRBYUFCQ1FIZGgDKAA%3D").should eq("4qmFsgJoEhhVQ0NqOTU2SUY2MkZiVDdHb3VzemFqOXcaTEVnbGpiMjF0ZFc1cGRIbTRBUUNxQXlRYUlCSWFWV2QzY0U5TlFtVndXRWRqY2xoc1VIZzJXalJCWVVGQ1ExRklaR2dES0FBJTI1M0Q%3D") + + produce_channel_community_continuation("UC-lHJZR3Gqxm24_Vd_AJ5Yw", "Egljb21tdW5pdHm4AQCqAyQaIBIaVWd5RTI2NW1rUkk2cE9uS21nbDRBYUFCQ1FIZGgDKAA%3D").should eq("4qmFsgJoEhhVQy1sSEpaUjNHcXhtMjRfVmRfQUo1WXcaTEVnbGpiMjF0ZFc1cGRIbTRBUUNxQXlRYUlCSWFWV2Q1UlRJMk5XMXJVa2syY0U5dVMyMW5iRFJCWVVGQ1ExRklaR2dES0FBJTI1M0Q%3D") + produce_channel_community_continuation("UC-lHJZR3Gqxm24_Vd_AJ5Yw", "Egljb21tdW5pdHm4AQCqA-cOCsAOUVVSVFNsOXBNWEYxYlVablFXaGFiWFJNTW5WM1ZHSXdPVU5EWTNoeFJWWlVjRWRGVTBOa1prTktjVUoyWjBZemNEZHRPV2cwV1hWbVJtaFVPWFJwVjJaUU4xTXlNRWRaYlZwSVFUa3dlak5pTUV0dll6QkRVMlpsWHpoVFdUbHFSR0o1YkRkM1kydEhMVTVwWDFCdFdXOUhjR0Z6ZEMxbldVcEhUMjkzUm1saGRXSkViVmR6ZFhwd1QxTnpOME54TW5KUloxQlBkME5QU1VWVWMybHlNbFZvUVV0NlVIZFVhMVV5UzNWUmJHRldkRmszU1dKd1pVUllVMkZFVG1aV1ZsRnVUMGhsZFd0T01sVndTbGd3TkhweVdDMVBTRUphV25GNk5Yb3dYMWRCVTFnMlltODBPRmhIV205WlQwNW1YMjV1UlVKTWNucHNNSGR5Y1hKaFltUkVkblJYZG1Kc1FVaHFUV3BwTkc5R1pUQkVlbGw2ZHpSM2FISlBTSFJoYjJGbVMwNTBiV1pxV2pCSVNWWnZTalpRT0RoclVGVmhia1p5VFhsaWFTMVBjREZZV1dSTFdERkZjSHB0ZUhseWFtRXdNR1JmTkhOWmFEVlZTbVZ1ZUVkRU1XRlFhbU4xVERabk4wdDVSSGxHU2xsT1VEQlJXR1ZLTUhGM1UwWkJTSE5oWkRWQ2NXZHNaMFpqYW1ST1YxZFlhMDVOVUZSSFZWVktRekZSYVhodlUxTm1SV1EwTUdsdWNEWXlPV1YwUjNkcGFVcEVTM040YUZadmRXbHJhblkyZFdFelNHWXpUV3hMYURCa2JIRTFSblJ4Wms4NU1XbGtOM0pHYjBGeU4xZFJNMU5qYkZCd05rZE9jV1JqT1hGRGIyNU5Xak5TUlhkemFsUXRObGt4UWxkUE16ZGFaRTlxVGtaZlIweEhRbXRNWXpCWE9GUjNOMHBsYVhwS2RtSlZkMmxGTVhCbVNIWkdkVTFJY0MxbFdYSkVZM0V0ZFROWWRtVlFlV3hhYlVKMmVreGZUMGxOU2xaSlRFTlBZMVpEUjFwd1RHZFhZMmhIYVVKakxUSmFabXd0U1RNeFJEWkhlSGhYTkhOMU1GZGhOMjFCVlVnNGNFTlJXSGx2WW5ScWNUaHZXWGxKT1d0TVRXc3lRMWc0Um5wU2JEVjBlRGxpTW5vMVRYaEtkelExY201S1JHSmZkamhmTlhOWmRGYzRjak5FVVdkMlpXVnNRWEJyZW5OdFpHcEljVGhWYzFsZkxWa3dRVTkyTVZVMmIyMTNVeTFLVEUxeFIwUldRbmc0VEdsTlpGVktjVmxzTkZGa1UwazFabE0wZUhsRk5WZ3lWR0ZaYzJadlYyaHRPRFpzTjNCT1dHRnBiMHhUVDBkMmRuZFVOMlptVm05dWIwRTFZVkZuYldKNmIwMUNaMng2VGkxSk56bHhXV3BJVGt4RFYwVllUM05pTVcwemRHc3lUVWN6TVVKcVRHdElNVWg1YmtKQmVrbFNVMnczZEVKUlJGOUlNVWRyZERsbFJraHVYekJXZUhGbE1rTTBlVE40YVU1T1pFcGpVMkpFZFMxWVdITjNTMnhWVjJwYVgzVXRXbGcwZG5OSE1qUXpYMlJHTVhSV1kxWkZRMlZwU25OdVlXTkdVek5wVUd4b2FUbDVSRVp4YVhsbFRqbG1aRWxYVFZCMVFWbG9OMEl3TW5KV1JUVjRkREJLZG5obmJGZHhSVlY1ZWpjMFIyeGlZemRIVmkxeFpESmlaMnhFZGxkcVRuSjZNVEZWUkRWamVIQlFkRk5DVmtSU2RITlRaSGhWZG05WE9VUkNhWEYwTm1kSFRtb3RNV1pNYlhSeVJWTnJhRWhIVDB0SU0yVkxUbFZ2V1VGNlJTMDJialJZYkRKdFFUVnJhRVJ4WmpjeFptcERNR001UmpkM2QwNW1VRXd5YUZCZlEwWjFSbEUzY0doRk5ISkZZMWxTTWs5d2RXRnhiRzFrYjBVMmIxWkJaRzkyU2xneFZWOXNiMDVWWkUxRFJ6QjBjWGhpVjBVMldYY3pTUzF4UVcxa1RuZEJRVGRvWVZFNGNsSTBaVUl0UmxacVdETnJXazVLY21aRk9HVndRbWxqUjB0blRFZEZVR3N6YzJOclkwSTNlVlZZVEdkcE1YQkdiMHAyZVU1aGRVZFdVblJQYVhaQlZtdHZSa0UzTFU1Sk1XaFJRMUpMV2kxSWJ6WkxjWEkxZGtSTWJsOVdUa0ZFVmpKZmMwUlFWV3gwUTJ0TFRsbDJaM2gxZFVOSVkzbEVORUpRZVUxMVREQnpOMVowWDI1MWRrVmlUMU54TkRkUk5rVjViMEpRTUZGNmR6RlJSR2RxY1U1eVgwNTBjMDkxWm14R2NUVjBlRkJGT1dGVmFXeFJTMEZYYldwQlVVbHNOVmgwZERZdGFFRlViMWxmUjFWc1EycG1WVkJQV0hkcFVRPT0aIBIaVWd5RTI2NW1rUkk2cE9uS21nbDRBYUFCQ1FIZGgDKGM%3D").should eq("4qmFsgKZFBIYVUMtbEhKWlIzR3F4bTI0X1ZkX0FKNVl3GvwTRWdsamIyMXRkVzVwZEhtNEFRQ3FBLWNPQ3NBT1VWVlNWRk5zT1hCTldFWXhZbFZhYmxGWGFHRmlXRkpOVFc1V00xWkhTWGRQVlU1RVdUTm9lRkpXV2xWalJXUkdWVEJPYTFwclRrdGpWVW95V2pCWmVtTkVaSFJQVjJjd1YxaFdiVkp0YUZWUFdGSndWakphVVU0eFRYbE5SV1JhWWxad1NWRlVhM2RsYWs1cFRVVjBkbGw2UWtSVk1scHNXSHBvVkZkVWJIRlNSMG8xWWtSa00xa3lkRWhNVlRWd1dERkNkRmRYT1VoalIwWjZaRU14YmxkVmNFaFVNamt6VW0xc2FHUlhTa1ZpVm1SNlpGaHdkMVF4VG5wT01FNTRUVzVLVWxveFFsQmtNRTVRVTFWV1ZXTXliSGxOYkZadlVWVjBObFZJWkZWaE1WVjVVek5XVW1KSFJsZGtSbXN6VTFkS2QxcFZVbGxWTWtaRlZHMWFWMVpzUm5WVU1HaHNaRmQwVDAxc1ZuZFRiR2QzVGtod2VWZERNVkJUUlVwaFYyNUdOazVZYjNkWU1XUkNWVEZuTWxsdE9EQlBSbWhJVjIwNVdsUXdOVzFZTWpWMVVsVktUV051Y0hOTlNHUjVZMWhLYUZsdFVrVmtibEpZWkcxS2MxRlZhSEZVVjNCd1RrYzVSMXBVUWtWbGJHdzJaSHBTTTJGSVNsQlRTRkpvWWpKR2JWTXdOVEJpVjFweFYycENTVk5XV25aVGFscFJUMFJvY2xWR1ZtaGlhMXA1VkZoc2FXRlRNVkJqUkVaWlYxZFNURmRFUmtaalNIQjBaVWhzZVdGdFJYZE5SMUptVGtoT1dtRkVWbFpUYlZaMVpVVmtSVTFYUmxGaGJVNHhWRVJhYms0d2REVlNTR3hIVTJ4c1QxVkVRbEpYUjFaTFRVaEdNMVV3V2tKVFNFNW9Xa1JXUTJOWFpITmFNRnBxWVcxU1QxWXhaRmxoTURWT1ZVWlNTRlpXVmt0UmVrWlNZVmhvZGxVeFRtMVNWMUV3VFVkc2RXTkVXWGxQVjFZd1VqTmtjR0ZWY0VWVE0wNDBZVVphZG1SWGJISmhibGt5WkZkRmVsTkhXWHBVVjNoTVlVUkNhMkpJUlRGU2JsSjRXbXM0TlUxWGJHdE9NMHBIWWpCR2VVNHhaRkpOTVU1cVlrWkNkMDVyWkU5alYxSnFUMWhHUkdJeU5VNVhhazVUVWxoa2VtRnNVWFJPYkd0NFVXeGtVRTE2WkdGYVJUbHhWR3RhWmxJd2VFaFJiWFJOV1hwQ1dFOUdVak5PTUhCc1lWaHdTMlJ0U2xaa01teEdUVmhDYlZOSVdrZGtWVEZKWTBNeGJGZFlTa1ZaTTBWMFpGUk9XV1J0VmxGbFYzaGhZbFZLTW1WcmVHWlVNR3hPVTJ4YVNsUkZUbEJaTVZwRVVqRndkMVJIWkZoWk1taElZVlZLYWt4VVNtRmFiWGQwVTFSTmVGSkVXa2hsU0doWVRraE9NVTFHWkdoT01qRkNWbFZuTkdORlRsSlhTR3gyV1c1U2NXTlVhSFpYV0d4S1QxZDBUVlJYYzNsUk1XYzBVbTV3VTJKRVZqQmxSR3hwVFc1dk1WUllhRXRrZWxFeFkyMDFTMUpIU21aa2FtaG1UbGhPV21SR1l6UmphazVGVlZka01scFhWbk5SV0VKeVpXNU9kRnBIY0VsalZHaFdZekZzWmt4V2EzZFJWVGt5VFZaVk1tSXlNVE5WZVRGTFZFVXhlRkl3VWxkUmJtYzBWRWRzVGxwR1ZrdGpWbXh6VGtaR2ExVXdhekZhYkUwd1pVaHNSazVXWjNsV1IwWmFZekphZGxZeWFIUlBSRnB6VGpOQ1QxZEhSbkJpTUhoVVZEQmtNbVJ1WkZWT01scHRWbTA1ZFdJd1JURlpWa1p1WWxkS05tSXdNVU5hTW5nMlZHa3hTazU2YkhoWFYzQkpWR3Q0UkZZd1ZsbFVNMDVwVFZjd2VtUkhjM2xVVldONlRWVktjVlJIZEVsTlZXZzFZbXRLUW1WcmJGTlZNbmN6WkVWS1VsSkdPVWxOVldSeVpFUnNiRkpyYUhWWWVrSlhaVWhHYkUxclRUQmxWRTQwWVZVMVQxcEZjR3BWTWtwRlpGTXhXVmRJVGpOVE1uaFdWakp3WVZnelZYUlhiR2N3Wkc1T1NFMXFVWHBZTWxKSFRWaFNWMWt4V2taUk1sWndVMjVPZFZsWFRrZFZlazV3VlVkNGIyRlViRFZTUlZwNFlWaHNiRlJxYkcxYVJXeFlWRlpDTVZGV2JHOU9NRWwzVFc1S1YxSlVWalJrUkVKTFpHNW9ibUpHWkhoU1ZsWTFaV3BqTUZJeWVHbFplbVJJVm1reGVGcEVTbWxhTW5oRlpHeGtjVlJ1U2paTlZFWldVa1JXYW1WSVFsRmtSazVEVm10U1UyUklUbFJhU0doV1pHMDVXRTlWVWtOaFdFWXdUbTFrU0ZSdGIzUk5WMXBOWWxoU2VWSldUbkpoUldoSVZEQjBTVTB5Vmt4VWJGWjJWMVZHTmxKVE1ESmlhbEpaWWtSS2RGRlVWbkpoUlZKNFdtcGplRnB0Y0VSTlIwMDFVbXBrTTJRd05XMVZSWGQ1WVVaQ1psRXdXakZTYkVVelkwZG9SazVJU2taWk1XeFRUV3M1ZDJSWFJuaGlSekZyWWpCVk1tSXhXa0phUnpreVUyeG5lRlpXT1hOaU1EVldXa1V4UkZKNlFqQmpXR2hwVmpCVk1sZFlZM3BUVXpGNFVWY3hhMVJ1WkVKUlZHUnZXVlpGTkdOc1NUQmFWVWwwVW14YWNWZEVUbkpYYXpWTFkyMWFSazlIVm5kUmJXeHFVakIwYmxSRlpFWlZSM042WXpKT2Nsa3dTVE5sVmxaWlZFZGtjRTFZUWtkaU1IQXlaVlUxYUdSVlpGZFZibEpRWVZoYVFsWnRkSFpTYTBVelRGVTFTazFYYUZKUk1VcE1WMmt4U1dKNldreGpXRWt4Wkd0U1RXSnNPVmRVYTBaRlZtcEtabU13VWxGV1YzZ3dVVEowVEZSc2JESmFNMmd4WkZWT1NWa3piRVZPUlVwUlpWVXhNVlJFUW5wT01Wb3dXREkxTVdSclZtbFVNVTU0VGtSa1VrNXJWalZpTUVwUlRVWkdObVI2UmxKU1IyUnhZMVUxZVZnd05UQmpNRGt4V20xNFIyTlVWakJsUmtKR1QxZEdWbUZYZUZKVE1FWllZbGR3UWxWVmJITk9WbWd3WkVSWmRHRkZSbFZpTVd4bVVqRldjMUV5Y0cxV1ZrSlFWMGhrY0ZWUlBUMGFJQklhVldkNVJUSTJOVzFyVWtrMmNFOXVTMjFuYkRSQllVRkNRMUZJWkdnREtHTSUyNTNE") + end + end + + describe "#extract_channel_community_cursor" do + it "correctly extracts a community cursor from a given continuation" do + extract_channel_community_cursor("4qmFsgIsEhhVQ0NqOTU2SUY2MkZiVDdHb3VzemFqOXcaEEVnbGpiMjF0ZFc1cGRIbTQ%3D").should eq("Egljb21tdW5pdHm4") + extract_channel_community_cursor("4qmFsgJoEhhVQ0NqOTU2SUY2MkZiVDdHb3VzemFqOXcaTEVnbGpiMjF0ZFc1cGRIbTRBUUNxQXlRYUlCSWFWV2QzY0U5TlFtVndXRWRqY2xoc1VIZzJXalJCWVVGQ1ExRklaR2dES0FBJTI1M0Q%3D").should eq("Egljb21tdW5pdHm4AQCqAyQaIBIaVWd3cE9NQmVwWEdjclhsUHg2WjRBYUFCQ1FIZGgDKAA%3D") + + extract_channel_community_cursor("4qmFsgJoEhhVQy1sSEpaUjNHcXhtMjRfVmRfQUo1WXcaTEVnbGpiMjF0ZFc1cGRIbTRBUUNxQXlRYUlCSWFWV2Q1UlRJMk5XMXJVa2syY0U5dVMyMW5iRFJCWVVGQ1ExRklaR2dES0FBJTI1M0Q%3D").should eq("Egljb21tdW5pdHm4AQCqAyQaIBIaVWd5RTI2NW1rUkk2cE9uS21nbDRBYUFCQ1FIZGgDKAA%3D") + extract_channel_community_cursor("4qmFsgKZFBIYVUMtbEhKWlIzR3F4bTI0X1ZkX0FKNVl3GvwTRWdsamIyMXRkVzVwZEhtNEFRQ3FBLWNPQ3NBT1VWVlNWRk5zT1hCTldFWXhZbFZhYmxGWGFHRmlXRkpOVFc1V00xWkhTWGRQVlU1RVdUTm9lRkpXV2xWalJXUkdWVEJPYTFwclRrdGpWVW95V2pCWmVtTkVaSFJQVjJjd1YxaFdiVkp0YUZWUFdGSndWakphVVU0eFRYbE5SV1JhWWxad1NWRlVhM2RsYWs1cFRVVjBkbGw2UWtSVk1scHNXSHBvVkZkVWJIRlNSMG8xWWtSa00xa3lkRWhNVlRWd1dERkNkRmRYT1VoalIwWjZaRU14YmxkVmNFaFVNamt6VW0xc2FHUlhTa1ZpVm1SNlpGaHdkMVF4VG5wT01FNTRUVzVLVWxveFFsQmtNRTVRVTFWV1ZXTXliSGxOYkZadlVWVjBObFZJWkZWaE1WVjVVek5XVW1KSFJsZGtSbXN6VTFkS2QxcFZVbGxWTWtaRlZHMWFWMVpzUm5WVU1HaHNaRmQwVDAxc1ZuZFRiR2QzVGtod2VWZERNVkJUUlVwaFYyNUdOazVZYjNkWU1XUkNWVEZuTWxsdE9EQlBSbWhJVjIwNVdsUXdOVzFZTWpWMVVsVktUV051Y0hOTlNHUjVZMWhLYUZsdFVrVmtibEpZWkcxS2MxRlZhSEZVVjNCd1RrYzVSMXBVUWtWbGJHdzJaSHBTTTJGSVNsQlRTRkpvWWpKR2JWTXdOVEJpVjFweFYycENTVk5XV25aVGFscFJUMFJvY2xWR1ZtaGlhMXA1VkZoc2FXRlRNVkJqUkVaWlYxZFNURmRFUmtaalNIQjBaVWhzZVdGdFJYZE5SMUptVGtoT1dtRkVWbFpUYlZaMVpVVmtSVTFYUmxGaGJVNHhWRVJhYms0d2REVlNTR3hIVTJ4c1QxVkVRbEpYUjFaTFRVaEdNMVV3V2tKVFNFNW9Xa1JXUTJOWFpITmFNRnBxWVcxU1QxWXhaRmxoTURWT1ZVWlNTRlpXVmt0UmVrWlNZVmhvZGxVeFRtMVNWMUV3VFVkc2RXTkVXWGxQVjFZd1VqTmtjR0ZWY0VWVE0wNDBZVVphZG1SWGJISmhibGt5WkZkRmVsTkhXWHBVVjNoTVlVUkNhMkpJUlRGU2JsSjRXbXM0TlUxWGJHdE9NMHBIWWpCR2VVNHhaRkpOTVU1cVlrWkNkMDVyWkU5alYxSnFUMWhHUkdJeU5VNVhhazVUVWxoa2VtRnNVWFJPYkd0NFVXeGtVRTE2WkdGYVJUbHhWR3RhWmxJd2VFaFJiWFJOV1hwQ1dFOUdVak5PTUhCc1lWaHdTMlJ0U2xaa01teEdUVmhDYlZOSVdrZGtWVEZKWTBNeGJGZFlTa1ZaTTBWMFpGUk9XV1J0VmxGbFYzaGhZbFZLTW1WcmVHWlVNR3hPVTJ4YVNsUkZUbEJaTVZwRVVqRndkMVJIWkZoWk1taElZVlZLYWt4VVNtRmFiWGQwVTFSTmVGSkVXa2hsU0doWVRraE9NVTFHWkdoT01qRkNWbFZuTkdORlRsSlhTR3gyV1c1U2NXTlVhSFpYV0d4S1QxZDBUVlJYYzNsUk1XYzBVbTV3VTJKRVZqQmxSR3hwVFc1dk1WUllhRXRrZWxFeFkyMDFTMUpIU21aa2FtaG1UbGhPV21SR1l6UmphazVGVlZka01scFhWbk5SV0VKeVpXNU9kRnBIY0VsalZHaFdZekZzWmt4V2EzZFJWVGt5VFZaVk1tSXlNVE5WZVRGTFZFVXhlRkl3VWxkUmJtYzBWRWRzVGxwR1ZrdGpWbXh6VGtaR2ExVXdhekZhYkUwd1pVaHNSazVXWjNsV1IwWmFZekphZGxZeWFIUlBSRnB6VGpOQ1QxZEhSbkJpTUhoVVZEQmtNbVJ1WkZWT01scHRWbTA1ZFdJd1JURlpWa1p1WWxkS05tSXdNVU5hTW5nMlZHa3hTazU2YkhoWFYzQkpWR3Q0UkZZd1ZsbFVNMDVwVFZjd2VtUkhjM2xVVldONlRWVktjVlJIZEVsTlZXZzFZbXRLUW1WcmJGTlZNbmN6WkVWS1VsSkdPVWxOVldSeVpFUnNiRkpyYUhWWWVrSlhaVWhHYkUxclRUQmxWRTQwWVZVMVQxcEZjR3BWTWtwRlpGTXhXVmRJVGpOVE1uaFdWakp3WVZnelZYUlhiR2N3Wkc1T1NFMXFVWHBZTWxKSFRWaFNWMWt4V2taUk1sWndVMjVPZFZsWFRrZFZlazV3VlVkNGIyRlViRFZTUlZwNFlWaHNiRlJxYkcxYVJXeFlWRlpDTVZGV2JHOU9NRWwzVFc1S1YxSlVWalJrUkVKTFpHNW9ibUpHWkhoU1ZsWTFaV3BqTUZJeWVHbFplbVJJVm1reGVGcEVTbWxhTW5oRlpHeGtjVlJ1U2paTlZFWldVa1JXYW1WSVFsRmtSazVEVm10U1UyUklUbFJhU0doV1pHMDVXRTlWVWtOaFdFWXdUbTFrU0ZSdGIzUk5WMXBOWWxoU2VWSldUbkpoUldoSVZEQjBTVTB5Vmt4VWJGWjJWMVZHTmxKVE1ESmlhbEpaWWtSS2RGRlVWbkpoUlZKNFdtcGplRnB0Y0VSTlIwMDFVbXBrTTJRd05XMVZSWGQ1WVVaQ1psRXdXakZTYkVVelkwZG9SazVJU2taWk1XeFRUV3M1ZDJSWFJuaGlSekZyWWpCVk1tSXhXa0phUnpreVUyeG5lRlpXT1hOaU1EVldXa1V4UkZKNlFqQmpXR2hwVmpCVk1sZFlZM3BUVXpGNFVWY3hhMVJ1WkVKUlZHUnZXVlpGTkdOc1NUQmFWVWwwVW14YWNWZEVUbkpYYXpWTFkyMWFSazlIVm5kUmJXeHFVakIwYmxSRlpFWlZSM042WXpKT2Nsa3dTVE5sVmxaWlZFZGtjRTFZUWtkaU1IQXlaVlUxYUdSVlpGZFZibEpRWVZoYVFsWnRkSFpTYTBVelRGVTFTazFYYUZKUk1VcE1WMmt4U1dKNldreGpXRWt4Wkd0U1RXSnNPVmRVYTBaRlZtcEtabU13VWxGV1YzZ3dVVEowVEZSc2JESmFNMmd4WkZWT1NWa3piRVZPUlVwUlpWVXhNVlJFUW5wT01Wb3dXREkxTVdSclZtbFVNVTU0VGtSa1VrNXJWalZpTUVwUlRVWkdObVI2UmxKU1IyUnhZMVUxZVZnd05UQmpNRGt4V20xNFIyTlVWakJsUmtKR1QxZEdWbUZYZUZKVE1FWllZbGR3UWxWVmJITk9WbWd3WkVSWmRHRkZSbFZpTVd4bVVqRldjMUV5Y0cxV1ZrSlFWMGhrY0ZWUlBUMGFJQklhVldkNVJUSTJOVzFyVWtrMmNFOXVTMjFuYkRSQllVRkNRMUZJWkdnREtHTSUyNTNE").should eq("Egljb21tdW5pdHm4AQCqA-cOCsAOUVVSVFNsOXBNWEYxYlVablFXaGFiWFJNTW5WM1ZHSXdPVU5EWTNoeFJWWlVjRWRGVTBOa1prTktjVUoyWjBZemNEZHRPV2cwV1hWbVJtaFVPWFJwVjJaUU4xTXlNRWRaYlZwSVFUa3dlak5pTUV0dll6QkRVMlpsWHpoVFdUbHFSR0o1YkRkM1kydEhMVTVwWDFCdFdXOUhjR0Z6ZEMxbldVcEhUMjkzUm1saGRXSkViVmR6ZFhwd1QxTnpOME54TW5KUloxQlBkME5QU1VWVWMybHlNbFZvUVV0NlVIZFVhMVV5UzNWUmJHRldkRmszU1dKd1pVUllVMkZFVG1aV1ZsRnVUMGhsZFd0T01sVndTbGd3TkhweVdDMVBTRUphV25GNk5Yb3dYMWRCVTFnMlltODBPRmhIV205WlQwNW1YMjV1UlVKTWNucHNNSGR5Y1hKaFltUkVkblJYZG1Kc1FVaHFUV3BwTkc5R1pUQkVlbGw2ZHpSM2FISlBTSFJoYjJGbVMwNTBiV1pxV2pCSVNWWnZTalpRT0RoclVGVmhia1p5VFhsaWFTMVBjREZZV1dSTFdERkZjSHB0ZUhseWFtRXdNR1JmTkhOWmFEVlZTbVZ1ZUVkRU1XRlFhbU4xVERabk4wdDVSSGxHU2xsT1VEQlJXR1ZLTUhGM1UwWkJTSE5oWkRWQ2NXZHNaMFpqYW1ST1YxZFlhMDVOVUZSSFZWVktRekZSYVhodlUxTm1SV1EwTUdsdWNEWXlPV1YwUjNkcGFVcEVTM040YUZadmRXbHJhblkyZFdFelNHWXpUV3hMYURCa2JIRTFSblJ4Wms4NU1XbGtOM0pHYjBGeU4xZFJNMU5qYkZCd05rZE9jV1JqT1hGRGIyNU5Xak5TUlhkemFsUXRObGt4UWxkUE16ZGFaRTlxVGtaZlIweEhRbXRNWXpCWE9GUjNOMHBsYVhwS2RtSlZkMmxGTVhCbVNIWkdkVTFJY0MxbFdYSkVZM0V0ZFROWWRtVlFlV3hhYlVKMmVreGZUMGxOU2xaSlRFTlBZMVpEUjFwd1RHZFhZMmhIYVVKakxUSmFabXd0U1RNeFJEWkhlSGhYTkhOMU1GZGhOMjFCVlVnNGNFTlJXSGx2WW5ScWNUaHZXWGxKT1d0TVRXc3lRMWc0Um5wU2JEVjBlRGxpTW5vMVRYaEtkelExY201S1JHSmZkamhmTlhOWmRGYzRjak5FVVdkMlpXVnNRWEJyZW5OdFpHcEljVGhWYzFsZkxWa3dRVTkyTVZVMmIyMTNVeTFLVEUxeFIwUldRbmc0VEdsTlpGVktjVmxzTkZGa1UwazFabE0wZUhsRk5WZ3lWR0ZaYzJadlYyaHRPRFpzTjNCT1dHRnBiMHhUVDBkMmRuZFVOMlptVm05dWIwRTFZVkZuYldKNmIwMUNaMng2VGkxSk56bHhXV3BJVGt4RFYwVllUM05pTVcwemRHc3lUVWN6TVVKcVRHdElNVWg1YmtKQmVrbFNVMnczZEVKUlJGOUlNVWRyZERsbFJraHVYekJXZUhGbE1rTTBlVE40YVU1T1pFcGpVMkpFZFMxWVdITjNTMnhWVjJwYVgzVXRXbGcwZG5OSE1qUXpYMlJHTVhSV1kxWkZRMlZwU25OdVlXTkdVek5wVUd4b2FUbDVSRVp4YVhsbFRqbG1aRWxYVFZCMVFWbG9OMEl3TW5KV1JUVjRkREJLZG5obmJGZHhSVlY1ZWpjMFIyeGlZemRIVmkxeFpESmlaMnhFZGxkcVRuSjZNVEZWUkRWamVIQlFkRk5DVmtSU2RITlRaSGhWZG05WE9VUkNhWEYwTm1kSFRtb3RNV1pNYlhSeVJWTnJhRWhIVDB0SU0yVkxUbFZ2V1VGNlJTMDJialJZYkRKdFFUVnJhRVJ4WmpjeFptcERNR001UmpkM2QwNW1VRXd5YUZCZlEwWjFSbEUzY0doRk5ISkZZMWxTTWs5d2RXRnhiRzFrYjBVMmIxWkJaRzkyU2xneFZWOXNiMDVWWkUxRFJ6QjBjWGhpVjBVMldYY3pTUzF4UVcxa1RuZEJRVGRvWVZFNGNsSTBaVUl0UmxacVdETnJXazVLY21aRk9HVndRbWxqUjB0blRFZEZVR3N6YzJOclkwSTNlVlZZVEdkcE1YQkdiMHAyZVU1aGRVZFdVblJQYVhaQlZtdHZSa0UzTFU1Sk1XaFJRMUpMV2kxSWJ6WkxjWEkxZGtSTWJsOVdUa0ZFVmpKZmMwUlFWV3gwUTJ0TFRsbDJaM2gxZFVOSVkzbEVORUpRZVUxMVREQnpOMVowWDI1MWRrVmlUMU54TkRkUk5rVjViMEpRTUZGNmR6RlJSR2RxY1U1eVgwNTBjMDkxWm14R2NUVjBlRkJGT1dGVmFXeFJTMEZYYldwQlVVbHNOVmgwZERZdGFFRlViMWxmUjFWc1EycG1WVkJQV0hkcFVRPT0aIBIaVWd5RTI2NW1rUkk2cE9uS21nbDRBYUFCQ1FIZGgDKGM%3D") + end + end + describe "#sign_token" do it "correctly signs a given hash" do token = { diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index 129ff1923..988b39f44 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -441,53 +441,57 @@ def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = " switch = 0x00 end - meta = IO::Memory.new - meta.write(Bytes[0x12, 0x06]) - meta.print("videos") + data = IO::Memory.new + data.write_byte 0x12 + data.write_byte 0x06 + data.print "videos" - meta.write(Bytes[0x30, 0x02]) - meta.write(Bytes[0x38, 0x01]) - meta.write(Bytes[0x60, 0x01]) - meta.write(Bytes[0x6a, 0x00]) - meta.write(Bytes[0xb8, 0x01, 0x00]) + data.write Bytes[0x30, 0x02] + data.write Bytes[0x38, 0x01] + data.write Bytes[0x60, 0x01] + data.write Bytes[0x6a, 0x00] + data.write Bytes[0xb8, 0x01, 0x00] - meta.write(Bytes[0x20, switch]) - meta.write(Bytes[0x7a, page.size]) - meta.print(page) + data.write Bytes[0x20, switch] + data.write_byte 0x7a + VarInt.to_io(data, page.bytesize) + data.print page case sort_by when "newest" # Empty tags can be omitted # meta.write(Bytes[0x18,0x00]) when "popular" - meta.write(Bytes[0x18, 0x01]) + data.write Bytes[0x18, 0x01] when "oldest" - meta.write(Bytes[0x18, 0x02]) + data.write Bytes[0x18, 0x02] end - meta.rewind - meta = Base64.urlsafe_encode(meta.to_slice) - meta = URI.escape(meta) + data = Base64.urlsafe_encode(data) + cursor = URI.escape(data) - continuation = IO::Memory.new - continuation.write(Bytes[0x12, ucid.size]) - continuation.print(ucid) + data = IO::Memory.new - continuation.write(Bytes[0x1a, meta.size]) - continuation.print(meta) + data.write_byte 0x12 + VarInt.to_io(data, ucid.bytesize) + data.print ucid - continuation.rewind - continuation = continuation.gets_to_end + data.write_byte 0x1a + VarInt.to_io(data, cursor.bytesize) + data.print cursor - wrapper = IO::Memory.new - wrapper.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02, continuation.size]) - wrapper.print(continuation) - wrapper.rewind + data.rewind - wrapper = Base64.urlsafe_encode(wrapper.to_slice) - wrapper = URI.escape(wrapper) + buffer = IO::Memory.new + buffer.write Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02] + VarInt.to_io(buffer, data.bytesize) - url = "/browse_ajax?continuation=#{wrapper}&gl=US&hl=en" + IO.copy data, buffer + + continuation = Base64.urlsafe_encode(buffer) + continuation = URI.escape(continuation) + + url = "/browse_ajax?continuation=#{continuation}&gl=US&hl=en" return url end @@ -497,117 +501,108 @@ def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated cursor = Base64.urlsafe_encode(cursor, false) end - meta = IO::Memory.new + data = IO::Memory.new if auto_generated - meta.write(Bytes[0x08, 0x0a]) + data.write Bytes[0x08, 0x0a] end - meta.write(Bytes[0x12, 0x09]) - meta.print("playlists") + data.write Bytes[0x12, 0x09] + data.print "playlists" if auto_generated - meta.write(Bytes[0x20, 0x32]) + data.write Bytes[0x20, 0x32] else # TODO: Look at 0x01, 0x00 case sort when "oldest", "oldest_created" - meta.write(Bytes[0x18, 0x02]) + data.write Bytes[0x18, 0x02] when "newest", "newest_created" - meta.write(Bytes[0x18, 0x03]) + data.write Bytes[0x18, 0x03] when "last", "last_added" - meta.write(Bytes[0x18, 0x04]) + data.write Bytes[0x18, 0x04] end - meta.write(Bytes[0x20, 0x01]) + data.write Bytes[0x20, 0x01] end - meta.write(Bytes[0x30, 0x02]) - meta.write(Bytes[0x38, 0x01]) - meta.write(Bytes[0x60, 0x01]) - meta.write(Bytes[0x6a, 0x00]) + data.write Bytes[0x30, 0x02] + data.write Bytes[0x38, 0x01] + data.write Bytes[0x60, 0x01] + data.write Bytes[0x6a, 0x00] - meta.write(Bytes[0x7a, cursor.size]) - meta.print(cursor) + data.write_byte 0x7a + VarInt.to_io(data, cursor.bytesize) + data.print cursor - meta.write(Bytes[0xb8, 0x01, 0x00]) + data.write Bytes[0xb8, 0x01, 0x00] - meta.rewind - meta = Base64.urlsafe_encode(meta.to_slice) - meta = URI.escape(meta) + data.rewind + data = Base64.urlsafe_encode(data) + continuation = URI.escape(data) - continuation = IO::Memory.new - continuation.write(Bytes[0x12, ucid.size]) - continuation.print(ucid) + data = IO::Memory.new - continuation.write(Bytes[0x1a]) - continuation.write(write_var_int(meta.size)) - continuation.print(meta) + data.write_byte 0x12 + VarInt.to_io(data, ucid.bytesize) + data.print ucid - continuation.rewind - continuation = continuation.gets_to_end + data.write_byte 0x1a + VarInt.to_io(data, continuation.bytesize) + data.print continuation - wrapper = IO::Memory.new - wrapper.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02]) - wrapper.write(write_var_int(continuation.size)) - wrapper.print(continuation) - wrapper.rewind + data.rewind - wrapper = Base64.urlsafe_encode(wrapper.to_slice) - wrapper = URI.escape(wrapper) + buffer = IO::Memory.new + buffer.write Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02] + VarInt.to_io(buffer, data.bytesize) - url = "/browse_ajax?continuation=#{wrapper}&gl=US&hl=en" + IO.copy data, buffer + + continuation = Base64.urlsafe_encode(buffer) + continuation = URI.escape(continuation) + + url = "/browse_ajax?continuation=#{continuation}&gl=US&hl=en" return url end def extract_channel_playlists_cursor(url, auto_generated) - wrapper = HTTP::Params.parse(URI.parse(url).query.not_nil!)["continuation"] + continuation = HTTP::Params.parse(URI.parse(url).query.not_nil!)["continuation"] - wrapper = URI.unescape(wrapper) - wrapper = Base64.decode(wrapper) + continuation = URI.unescape(continuation) + data = IO::Memory.new(Base64.decode(continuation)) # 0xe2 0xa9 0x85 0xb2 0x02 - wrapper += 5 + data.pos += 5 - continuation_size = read_var_int(wrapper[0, 4]) - wrapper += write_var_int(continuation_size).size - continuation = wrapper[0, continuation_size] + continuation = Bytes.new(data.read_bytes(VarInt)) + data.read continuation + data = IO::Memory.new(continuation) - # 0x12 - continuation += 1 - ucid_size = continuation[0] - continuation += 1 - ucid = continuation[0, ucid_size] - continuation += ucid_size + data.read_byte # => 0x12 + ucid = Bytes.new(data.read_bytes(VarInt)) + data.read ucid - # 0x1a - continuation += 1 - meta_size = read_var_int(continuation[0, 4]) - continuation += write_var_int(meta_size).size - meta = continuation[0, meta_size] - continuation += meta_size + data.read_byte # => 0x1a + inner_continuation = Bytes.new(data.read_bytes(VarInt)) + data.read inner_continuation - meta = String.new(meta) - meta = URI.unescape(meta) - meta = Base64.decode(meta) + continuation = String.new(inner_continuation) + continuation = URI.unescape(continuation) + data = IO::Memory.new(Base64.decode(continuation)) # 0x12 0x09 playlists - meta += 11 + data.pos += 11 - until meta[0] == 0x7a - tag = read_var_int(meta[0, 4]) - meta += write_var_int(tag).size - value = meta[0] - meta += 1 + until data.peek[0] == 0x7a + key = data.read_bytes(VarInt) + value = data.read_bytes(VarInt) end - # 0x7a - meta += 1 - cursor_size = meta[0] - meta += 1 - cursor = meta[0, cursor_size] - + data.pos += 1 # => 0x7a + cursor = Bytes.new(data.read_bytes(VarInt)) + data.read cursor cursor = String.new(cursor) if !auto_generated @@ -874,20 +869,26 @@ end def produce_channel_community_continuation(ucid, cursor) cursor = URI.escape(cursor) - continuation = IO::Memory.new - continuation.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02]) - continuation.write(write_var_int(3 + ucid.size + write_var_int(cursor.size).size + cursor.size)) + data = IO::Memory.new - continuation.write(Bytes[0x12, ucid.size]) - continuation.print(ucid) + data.write_byte 0x12 + VarInt.to_io(data, ucid.bytesize) + data.print ucid - continuation.write(Bytes[0x1a]) - continuation.write(write_var_int(cursor.size)) - continuation.print(cursor) - continuation.rewind + data.write_byte 0x1a + VarInt.to_io(data, cursor.bytesize) + data.print cursor - continuation = Base64.urlsafe_encode(continuation.to_slice) + data.rewind + + buffer = IO::Memory.new + buffer.write Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02] + VarInt.to_io(buffer, data.size) + + IO.copy data, buffer + + continuation = Base64.urlsafe_encode(buffer) continuation = URI.escape(continuation) return continuation @@ -895,28 +896,25 @@ end def extract_channel_community_cursor(continuation) continuation = URI.unescape(continuation) - continuation = Base64.decode(continuation) + data = IO::Memory.new(Base64.decode(continuation)) # 0xe2 0xa9 0x85 0xb2 0x02 - continuation += 5 + data.pos += 5 - total_size = read_var_int(continuation[0, 4]) - continuation += write_var_int(total_size).size + continuation = Bytes.new(data.read_bytes(VarInt)) + data.read continuation + data = IO::Memory.new(continuation) - # 0x12 - continuation += 1 - ucid_size = continuation[0] - continuation += 1 - ucid = continuation[0, ucid_size] - continuation += ucid_size + data.read_byte # => 0x12 + ucid = Bytes.new(data.read_bytes(VarInt)) + data.read ucid - # 0x1a - continuation += 1 - until continuation[0] == 'E'.ord - continuation += 1 + data.read_byte # => 0x1a + until data.peek[0] == 'E'.ord + data.read_byte end - return String.new(continuation) + return URI.unescape(data.gets_to_end) end def get_about_info(ucid, locale) diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index b72f8ec7f..e060fe460 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -564,108 +564,105 @@ def content_to_comment_html(content) end def produce_comment_continuation(video_id, cursor = "", sort_by = "top") - continuation = IO::Memory.new + data = IO::Memory.new - continuation.write(Bytes[0x12, 0x26]) + data.write Bytes[0x12, 0x26] - continuation.write(Bytes[0x12, video_id.size]) - continuation.print(video_id) + data.write_byte 0x12 + VarInt.to_io(data, video_id.bytesize) + data.print video_id - continuation.write(Bytes[0xc0, 0x01, 0x01]) - continuation.write(Bytes[0xc8, 0x01, 0x01]) - continuation.write(Bytes[0xe0, 0x01, 0x01]) + data.write Bytes[0xc0, 0x01, 0x01] + data.write Bytes[0xc8, 0x01, 0x01] + data.write Bytes[0xe0, 0x01, 0x01] - continuation.write(Bytes[0xa2, 0x02, 0x0d]) - continuation.write(Bytes[0x28, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]) + data.write Bytes[0xa2, 0x02, 0x0d] + data.write Bytes[0x28, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01] - continuation.write(Bytes[0x40, 0x00]) - continuation.write(Bytes[0x18, 0x06]) + data.write Bytes[0x40, 0x00] + data.write Bytes[0x18, 0x06] if cursor.empty? - continuation.write(Bytes[0x32]) - continuation.write(write_var_int(video_id.size + 8)) + data.write Bytes[0x32] + VarInt.to_io(data, cursor.bytesize + video_id.bytesize + 8) - continuation.write(Bytes[0x22, video_id.size + 4]) - continuation.write(Bytes[0x22, video_id.size]) - continuation.print(video_id) + data.write Bytes[0x22, video_id.bytesize + 4] + data.write Bytes[0x22, video_id.bytesize] + data.print video_id case sort_by when "top" - continuation.write(Bytes[0x30, 0x00]) + data.write Bytes[0x30, 0x00] when "new", "newest" - continuation.write(Bytes[0x30, 0x01]) + data.write Bytes[0x30, 0x01] end - continuation.write(Bytes[0x78, 0x02]) + data.write(Bytes[0x78, 0x02]) else - continuation.write(Bytes[0x32]) - continuation.write(write_var_int(cursor.size + video_id.size + 11)) + data.write Bytes[0x32] + VarInt.to_io(data, cursor.bytesize + video_id.bytesize + 11) - continuation.write(Bytes[0x0a]) - continuation.write(write_var_int(cursor.size)) - continuation.print(cursor) + data.write_byte 0x0a + VarInt.to_io(data, cursor.bytesize) + data.print cursor - continuation.write(Bytes[0x22, video_id.size + 4]) - continuation.write(Bytes[0x22, video_id.size]) - continuation.print(video_id) + data.write Bytes[0x22, video_id.bytesize + 4] + data.write Bytes[0x22, video_id.bytesize] + data.print video_id case sort_by when "top" - continuation.write(Bytes[0x30, 0x00]) + data.write Bytes[0x30, 0x00] when "new", "newest" - continuation.write(Bytes[0x30, 0x01]) + data.write Bytes[0x30, 0x01] end - continuation.write(Bytes[0x28, 0x14]) + data.write Bytes[0x28, 0x14] end - continuation.rewind - continuation = continuation.gets_to_end - - continuation = Base64.urlsafe_encode(continuation.to_slice) + continuation = Base64.urlsafe_encode(data) continuation = URI.escape(continuation) return continuation end def produce_comment_reply_continuation(video_id, ucid, comment_id) - continuation = IO::Memory.new + data = IO::Memory.new - continuation.write(Bytes[0x12, 0x26]) + data.write Bytes[0x12, 0x26] - continuation.write(Bytes[0x12, video_id.size]) - continuation.print(video_id) + data.write_byte 0x12 + VarInt.to_io(data, video_id.size) + data.print video_id - continuation.write(Bytes[0xc0, 0x01, 0x01]) - continuation.write(Bytes[0xc8, 0x01, 0x01]) - continuation.write(Bytes[0xe0, 0x01, 0x01]) + data.write Bytes[0xc0, 0x01, 0x01] + data.write Bytes[0xc8, 0x01, 0x01] + data.write Bytes[0xe0, 0x01, 0x01] - continuation.write(Bytes[0xa2, 0x02, 0x0d]) - continuation.write(Bytes[0x28, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]) + data.write Bytes[0xa2, 0x02, 0x0d] + data.write Bytes[0x28, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01] - continuation.write(Bytes[0x40, 0x00]) - continuation.write(Bytes[0x18, 0x06]) + data.write Bytes[0x40, 0x00] + data.write Bytes[0x18, 0x06] - continuation.write(Bytes[0x32, ucid.size + video_id.size + comment_id.size + 16]) - continuation.write(Bytes[0x1a, ucid.size + video_id.size + comment_id.size + 14]) + data.write(Bytes[0x32, ucid.size + video_id.size + comment_id.size + 16]) + data.write(Bytes[0x1a, ucid.size + video_id.size + comment_id.size + 14]) - continuation.write(Bytes[0x12, comment_id.size]) - continuation.print(comment_id) + data.write_byte 0x12 + VarInt.to_io(data, comment_id.size) + data.print comment_id - continuation.write(Bytes[0x22, 0x02, 0x08, 0x00]) # ?? + data.write(Bytes[0x22, 0x02, 0x08, 0x00]) # ?? - continuation.write(Bytes[ucid.size + video_id.size + 7]) - continuation.write(Bytes[ucid.size]) - continuation.print(ucid) - continuation.write(Bytes[0x32, video_id.size]) - continuation.print(video_id) - continuation.write(Bytes[0x40, 0x01]) - continuation.write(Bytes[0x48, 0x0a]) + data.write(Bytes[ucid.size + video_id.size + 7]) + data.write(Bytes[ucid.size]) + data.print(ucid) + data.write(Bytes[0x32, video_id.size]) + data.print(video_id) + data.write(Bytes[0x40, 0x01]) + data.write(Bytes[0x48, 0x0a]) - continuation.rewind - continuation = continuation.gets_to_end - - continuation = Base64.urlsafe_encode(continuation.to_slice) + continuation = Base64.urlsafe_encode(data.to_slice) continuation = URI.escape(continuation) return continuation diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index b7deae764..69aae839d 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -266,50 +266,40 @@ def get_referer(env, fallback = "/", unroll = true) return referer end -def read_var_int(bytes) - num_read = 0 - result = 0 +struct VarInt + def self.from_io(io : IO, format = IO::ByteFormat::BigEndian) : Int32 + result = 0_i32 + num_read = 0 - read = bytes[num_read] - - if bytes.size == 1 - result = bytes[0].to_i32 - else - while ((read & 0b10000000) != 0) - read = bytes[num_read].to_u64 - value = (read & 0b01111111) - result |= (value << (7 * num_read)) + loop do + byte = io.read_byte + raise "Invalid VarInt" if !byte + value = byte & 0x7f + result |= value.to_i32 << (7 * num_read) num_read += 1 - if num_read > 5 - raise "VarInt is too big" - end + + break if byte & 0x80 == 0 + raise "Invalid VarInt" if num_read > 5 end + + result end - return result -end + def self.to_io(io : IO, value : Int32) + io.write_byte 0x00 if value == 0x00 -def write_var_int(value : Int) - bytes = [] of UInt8 - value = value.to_u32 - - if value == 0 - bytes = [0_u8] - else while value != 0 - temp = (value & 0b01111111).to_u8 - value = value >> 7 + byte = (value & 0x7f).to_u8 + value >>= 7 if value != 0 - temp |= 0b10000000 + byte |= 0x80 end - bytes << temp + io.write_byte byte end end - - return Slice.new(bytes.to_unsafe, bytes.size) end def sha256(text) diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index 373d1fbaf..d28a41497 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -157,37 +157,44 @@ def produce_playlist_url(id, index) end ucid = "VL" + id - meta = IO::Memory.new - meta.write(Bytes[0x08]) - meta.write(write_var_int(index)) + data = IO::Memory.new + data.write_byte 0x08 + VarInt.to_io(data, index) - meta.rewind - meta = Base64.urlsafe_encode(meta.to_slice, false) - meta = "PT:#{meta}" + data.rewind + data = Base64.urlsafe_encode(data, false) + data = "PT:#{data}" continuation = IO::Memory.new - continuation.write(Bytes[0x7a, meta.size]) - continuation.print(meta) + continuation.write_byte 0x7a + VarInt.to_io(continuation, data.bytesize) + continuation.print data - continuation.rewind - meta = Base64.urlsafe_encode(continuation.to_slice) - meta = URI.escape(meta) + data = Base64.urlsafe_encode(continuation) + cursor = URI.escape(data) - continuation = IO::Memory.new - continuation.write(Bytes[0x12, ucid.size]) - continuation.print(ucid) - continuation.write(Bytes[0x1a, meta.size]) - continuation.print(meta) + data = IO::Memory.new - wrapper = IO::Memory.new - wrapper.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02, continuation.size]) - wrapper.print(continuation) - wrapper.rewind + data.write_byte 0x12 + VarInt.to_io(data, ucid.bytesize) + data.print ucid - wrapper = Base64.urlsafe_encode(wrapper.to_slice) - wrapper = URI.escape(wrapper) + data.write_byte 0x1a + VarInt.to_io(data, cursor.bytesize) + data.print cursor - url = "/browse_ajax?continuation=#{wrapper}&gl=US&hl=en" + data.rewind + + buffer = IO::Memory.new + buffer.write Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02] + VarInt.to_io(buffer, data.bytesize) + + IO.copy data, buffer + + continuation = Base64.urlsafe_encode(buffer) + continuation = URI.escape(continuation) + + url = "/browse_ajax?continuation=#{continuation}&gl=US&hl=en" return url end diff --git a/src/invidious/search.cr b/src/invidious/search.cr index ebeb2236a..79bfd55aa 100644 --- a/src/invidious/search.cr +++ b/src/invidious/search.cr @@ -374,45 +374,51 @@ end def produce_channel_search_url(ucid, query, page) page = "#{page}" - meta = IO::Memory.new - meta.write(Bytes[0x12, 0x06]) - meta.print("search") + data = IO::Memory.new + data.write_byte 0x12 + data.write_byte 0x06 + data.print "search" - meta.write(Bytes[0x30, 0x02]) - meta.write(Bytes[0x38, 0x01]) - meta.write(Bytes[0x60, 0x01]) - meta.write(Bytes[0x6a, 0x00]) - meta.write(Bytes[0xb8, 0x01, 0x00]) + data.write Bytes[0x30, 0x02] + data.write Bytes[0x38, 0x01] + data.write Bytes[0x60, 0x01] + data.write Bytes[0x6a, 0x00] + data.write Bytes[0xb8, 0x01, 0x00] - meta.write(Bytes[0x7a, page.size]) - meta.print(page) + data.write_byte 0x7a + VarInt.to_io(data, page.bytesize) + data.print page - meta.rewind - meta = Base64.urlsafe_encode(meta.to_slice) - meta = URI.escape(meta) + data.rewind + data = Base64.urlsafe_encode(data) + continuation = URI.escape(data) - continuation = IO::Memory.new - continuation.write(Bytes[0x12, ucid.size]) - continuation.print(ucid) + data = IO::Memory.new - continuation.write(Bytes[0x1a, meta.size]) - continuation.print(meta) + data.write_byte 0x12 + VarInt.to_io(data, ucid.bytesize) + data.print ucid - continuation.write(Bytes[0x5a, query.size]) - continuation.print(query) + data.write_byte 0x1a + VarInt.to_io(data, continuation.bytesize) + data.print continuation - continuation.rewind - continuation = continuation.gets_to_end + data.write_byte 0x5a + VarInt.to_io(data, query.bytesize) + data.print query - wrapper = IO::Memory.new - wrapper.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02, continuation.size]) - wrapper.print(continuation) - wrapper.rewind + data.rewind - wrapper = Base64.urlsafe_encode(wrapper.to_slice) - wrapper = URI.escape(wrapper) + buffer = IO::Memory.new + buffer.write Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02] + VarInt.to_io(buffer, data.bytesize) - url = "/browse_ajax?continuation=#{wrapper}&gl=US&hl=en" + IO.copy data, buffer + + continuation = Base64.urlsafe_encode(buffer) + continuation = URI.escape(continuation) + + url = "/browse_ajax?continuation=#{continuation}&gl=US&hl=en" return url end