diff options
Diffstat (limited to 'spec')
| -rw-r--r-- | spec/helpers/vtt/builder_spec.cr | 87 | ||||
| -rw-r--r-- | spec/helpers_spec.cr | 141 | ||||
| -rw-r--r-- | spec/i18next_plurals_spec.cr | 231 | ||||
| -rw-r--r-- | spec/invidious/hashtag_spec.cr | 109 | ||||
| -rw-r--r-- | spec/invidious/helpers_spec.cr | 56 | ||||
| -rw-r--r-- | spec/invidious/search/iv_filters_spec.cr | 370 | ||||
| -rw-r--r-- | spec/invidious/search/query_spec.cr | 242 | ||||
| -rw-r--r-- | spec/invidious/search/yt_filters_spec.cr | 143 | ||||
| -rw-r--r-- | spec/invidious/user/imports_spec.cr | 51 | ||||
| -rw-r--r-- | spec/invidious/utils_spec.cr | 46 | ||||
| -rw-r--r-- | spec/invidious/videos/regular_videos_extract_spec.cr | 168 | ||||
| -rw-r--r-- | spec/invidious/videos/scheduled_live_extract_spec.cr | 111 | ||||
| -rw-r--r-- | spec/locales_spec.cr | 29 | ||||
| -rw-r--r-- | spec/parsers_helper.cr | 35 | ||||
| -rw-r--r-- | spec/spec_helper.cr | 18 |
15 files changed, 1667 insertions, 170 deletions
diff --git a/spec/helpers/vtt/builder_spec.cr b/spec/helpers/vtt/builder_spec.cr new file mode 100644 index 00000000..dc1f4613 --- /dev/null +++ b/spec/helpers/vtt/builder_spec.cr @@ -0,0 +1,87 @@ +require "../../spec_helper.cr" + +MockLines = ["Line 1", "Line 2"] +MockLinesWithEscapableCharacter = ["<Line 1>", "&Line 2>", '\u200E' + "Line\u200F 3", "\u00A0Line 4"] + +Spectator.describe "WebVTT::Builder" do + it "correctly builds a vtt file" do + result = WebVTT.build do |vtt| + 2.times do |i| + vtt.cue( + Time::Span.new(seconds: i), + Time::Span.new(seconds: i + 1), + MockLines[i] + ) + end + end + + expect(result).to eq([ + "WEBVTT", + "", + "00:00:00.000 --> 00:00:01.000", + "Line 1", + "", + "00:00:01.000 --> 00:00:02.000", + "Line 2", + "", + "", + ].join('\n')) + end + + it "correctly builds a vtt file with setting fields" do + setting_fields = { + "Kind" => "captions", + "Language" => "en", + } + + result = WebVTT.build(setting_fields) do |vtt| + 2.times do |i| + vtt.cue( + Time::Span.new(seconds: i), + Time::Span.new(seconds: i + 1), + MockLines[i] + ) + end + end + + expect(result).to eq([ + "WEBVTT", + "Kind: captions", + "Language: en", + "", + "00:00:00.000 --> 00:00:01.000", + "Line 1", + "", + "00:00:01.000 --> 00:00:02.000", + "Line 2", + "", + "", + ].join('\n')) + end + + it "properly escapes characters" do + result = WebVTT.build do |vtt| + 4.times do |i| + vtt.cue(Time::Span.new(seconds: i), Time::Span.new(seconds: i + 1), MockLinesWithEscapableCharacter[i]) + end + end + + expect(result).to eq([ + "WEBVTT", + "", + "00:00:00.000 --> 00:00:01.000", + "<Line 1>", + "", + "00:00:01.000 --> 00:00:02.000", + "&Line 2>", + "", + "00:00:02.000 --> 00:00:03.000", + "‎Line‏ 3", + "", + "00:00:03.000 --> 00:00:04.000", + " Line 4", + "", + "", + ].join('\n')) + end +end diff --git a/spec/helpers_spec.cr b/spec/helpers_spec.cr deleted file mode 100644 index ed3a3d48..00000000 --- a/spec/helpers_spec.cr +++ /dev/null @@ -1,141 +0,0 @@ -require "kemal" -require "openssl/hmac" -require "pg" -require "protodec/utils" -require "spec" -require "yaml" -require "../src/invidious/helpers/*" -require "../src/invidious/channels" -require "../src/invidious/comments" -require "../src/invidious/playlists" -require "../src/invidious/search" -require "../src/invidious/trending" -require "../src/invidious/users" - -CONFIG = Config.from_yaml(File.open("config/config.example.yml")) - -describe "Helper" do - describe "#produce_channel_videos_url" do - it "correctly produces url for requesting page `x` of a channel's videos" do - produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw").should eq("/browse_ajax?continuation=4qmFsgI8EhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0V4&gl=US&hl=en") - - produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw", sort_by: "popular").should eq("/browse_ajax?continuation=4qmFsgJAEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0V4R0FFPQ%3D%3D&gl=US&hl=en") - - produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw", page: 20).should eq("/browse_ajax?continuation=4qmFsgJAEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0l5TUE9PQ%3D%3D&gl=US&hl=en") - - produce_channel_videos_url(ucid: "UC-9-kyTW8ZkZNDHQJ6FgpwQ", page: 20, sort_by: "popular").should eq("/browse_ajax?continuation=4qmFsgJAEhhVQy05LWt5VFc4WmtaTkRIUUo2Rmdwd1EaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0l5TUJnQg%3D%3D&gl=US&hl=en") - end - end - - describe "#produce_channel_search_continuation" do - it "correctly produces token for searching a specific channel" do - produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100).should eq("4qmFsgJqEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FUZ0JZQUY2QkVkS2IxaTRBUUE9WgCaAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D") - - produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "По ожиशुपतिरपि子而時ஸ்றீனி", 0).should eq("4qmFsgKoARIYVUNYdXFTQmxIQUU2WHcteWVKQTBUdW53GiBFZ1p6WldGeVkyZ3dBVGdCWUFGNkJFZEJRVDI0QVFBPVo-0J_QviDQvtC20LjgpLbgpYHgpKrgpKTgpL_gpLDgpKrgpL_lrZDogIzmmYLgrrjgr43grrHgr4Dgrqngrr-aAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D") - 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=4qmFsgLNARIYVUNDajk1NklGNjJGYlQ3R291c3phajl3GrABRWdsd2JHRjViR2x6ZEhNd0FqZ0JZQUZxQUxnQkFIcG1VVlZzVUdFeGF6VlNWa1ozWVZZNWJtVlhOSGhZTVVaNVVtNVdZVTFZU214VWFtZDRXREF4VG1KVmEzaFhWekZ6VVcxS2MyUjZhSEZPTUhCSlUxaFNSbEpyWXpGaFJHUjRXVEJ3VlZSdFVUQldlbXcwVGxaR01XRXhPVVJXYkc5M1RXcG9ibFozSUFFWUF3PT0%3D&gl=US&hl=en") - end - end - - describe "#produce_playlist_continuation" do - it "correctly produces ctoken for requesting index `x` of a playlist" do - produce_playlist_continuation("UUCla9fZca4I7KagBtgRGnOw", 100).should eq("4qmFsgJNEhpWTFVVQ2xhOWZaY2E0STdLYWdCdGdSR25PdxoUQ0FGNkJsQlVPa05IVVElM0QlM0SaAhhVVUNsYTlmWmNhNEk3S2FnQnRnUkduT3c%3D") - - produce_playlist_continuation("UCCla9fZca4I7KagBtgRGnOw", 200).should eq("4qmFsgJLEhpWTFVVQ2xhOWZaY2E0STdLYWdCdGdSR25PdxoSQ0FKNkIxQlVPa05OWjBJJTNEmgIYVVVDbGE5ZlpjYTRJN0thZ0J0Z1JHbk93") - - produce_playlist_continuation("PL55713C70BA91BD6E", 100).should eq("4qmFsgJBEhRWTFBMNTU3MTNDNzBCQTkxQkQ2RRoUQ0FGNkJsQlVPa05IVVElM0QlM0SaAhJQTDU1NzEzQzcwQkE5MUJENkU%3D") - end - end - - describe "#produce_search_params" do - it "correctly produces token for searching with specified filters" do - produce_search_params.should eq("CAASAhABSAA%3D") - - produce_search_params(sort: "upload_date", content_type: "video").should eq("CAISAhABSAA%3D") - - produce_search_params(content_type: "playlist").should eq("CAASAhADSAA%3D") - - produce_search_params(sort: "date", content_type: "video", features: ["hd", "cc", "purchased", "hdr"]).should eq("CAISCxABIAEwAUgByAEBSAA%3D") - - produce_search_params(content_type: "channel").should eq("CAASAhACSAA%3D") - end - end - - describe "#produce_comment_continuation" do - it "correctly produces a continuation token for comments" do - produce_comment_continuation("_cE8xSu6swE", "ADSJ_i2qvJeFtL0htmS5_K5Ctj3eGFVBMWL9Wd42o3kmUL6_mAzdLp85-liQZL0mYr_16BhaggUqX652Sv9JqV6VXinShSP-ZT6rL4NolPBaPXVtJsO5_rA_qE3GubAuLFw9uzIIXU2-HnpXbdgPLWTFavfX206hqWmmpHwUOrmxQV_OX6tYkM3ux3rPAKCDrT8eWL7MU3bLiNcnbgkW8o0h8KYLL_8BPa8LcHbTv8pAoNkjerlX1x7K4pqxaXPoyz89qNlnh6rRx6AXgAzzoHH1dmcyQ8CIBeOHg-m4i8ZxdX4dP88XWrIFg-jJGhpGP8JUMDgZgavxVx225hUEYZMyrLGler5em4FgbG62YWC51moLDLeYEA").should eq("EkMSC19jRTh4U3U2c3dFyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyjAMK9gJBRFNKX2kycXZKZUZ0TDBodG1TNV9LNUN0ajNlR0ZWQk1XTDlXZDQybzNrbVVMNl9tQXpkTHA4NS1saVFaTDBtWXJfMTZCaGFnZ1VxWDY1MlN2OUpxVjZWWGluU2hTUC1aVDZyTDROb2xQQmFQWFZ0SnNPNV9yQV9xRTNHdWJBdUxGdzl1eklJWFUyLUhucFhiZGdQTFdURmF2ZlgyMDZocVdtbXBId1VPcm14UVZfT1g2dFlrTTN1eDNyUEFLQ0RyVDhlV0w3TVUzYkxpTmNuYmdrVzhvMGg4S1lMTF84QlBhOExjSGJUdjhwQW9Oa2plcmxYMXg3SzRwcXhhWFBveXo4OXFObG5oNnJSeDZBWGdBenpvSEgxZG1jeVE4Q0lCZU9IZy1tNGk4WnhkWDRkUDg4WFdySUZnLWpKR2hwR1A4SlVNRGdaZ2F2eFZ4MjI1aFVFWVpNeXJMR2xlcjVlbTRGZ2JHNjJZV0M1MW1vTERMZVlFQSIPIgtfY0U4eFN1NnN3RTAAKBQ%3D") - - produce_comment_continuation("_cE8xSu6swE", "ADSJ_i1yz21HI4xrtsYXVC-2_kfZ6kx1yjYQumXAAxqH3CAd7ZxKxfLdZS1__fqhCtOASRbbpSBGH_tH1J96Dxux-Qfjk-lUbupMqv08Q3aHzGu7p70VoUMHhI2-GoJpnbpmcOxkGzeIuenRS_ym2Y8fkDowhqLPFgsS0n4djnZ2UmC17F3Ch3N1S1UYf1ZVOc991qOC1iW9kJDzyvRQTWCPsJUPneSaAKW-Rr97pdesOkR4i8cNvHZRnQKe2HEfsvlJOb2C3lF1dJBfJeNfnQYeh5hv6_fZN7bt3-JL1Xk3Qc9NXNxmmbDpwAC_yFR8dthFfUJdyIO9Nu1D79MLYeR-H5HxqUJokkJiGIz4lTE_CXXbhAI").should eq("EkMSC19jRTh4U3U2c3dFyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyiQMK8wJBRFNKX2kxeXoyMUhJNHhydHNZWFZDLTJfa2ZaNmt4MXlqWVF1bVhBQXhxSDNDQWQ3WnhLeGZMZFpTMV9fZnFoQ3RPQVNSYmJwU0JHSF90SDFKOTZEeHV4LVFmamstbFVidXBNcXYwOFEzYUh6R3U3cDcwVm9VTUhoSTItR29KcG5icG1jT3hrR3plSXVlblJTX3ltMlk4ZmtEb3docUxQRmdzUzBuNGRqbloyVW1DMTdGM0NoM04xUzFVWWYxWlZPYzk5MXFPQzFpVzlrSkR6eXZSUVRXQ1BzSlVQbmVTYUFLVy1Scjk3cGRlc09rUjRpOGNOdkhaUm5RS2UySEVmc3ZsSk9iMkMzbEYxZEpCZkplTmZuUVllaDVodjZfZlpON2J0My1KTDFYazNRYzlOWE54bW1iRHB3QUNfeUZSOGR0aEZmVUpkeUlPOU51MUQ3OU1MWWVSLUg1SHhxVUpva2tKaUdJejRsVEVfQ1hYYmhBSSIPIgtfY0U4eFN1NnN3RTAAKBQ%3D") - - produce_comment_continuation("29-q7YnyUmY", "").should eq("EkMSCzI5LXE3WW55VW1ZyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyFQoAIg8iCzI5LXE3WW55VW1ZMAAoFA%3D%3D") - - produce_comment_continuation("CvFH_6DNRCY", "").should eq("EkMSC0N2RkhfNkROUkNZyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyFQoAIg8iC0N2RkhfNkROUkNZMAAoFA%3D%3D") - end - end - - describe "#produce_comment_reply_continuation" do - it "correctly produces a continuation token for replies to a given comment" do - produce_comment_reply_continuation("cIHQWOoJeag", "UCq6VFHwMzcMXbuKyG7SQYIg", "Ugx1IP_wGVv3WtGWcdV4AaABAg").should eq("EiYSC2NJSFFXT29KZWFnwAEByAEB4AEBogINKP___________wFAABgGMk0aSxIaVWd4MUlQX3dHVnYzV3RHV2NkVjRBYUFCQWciAggAKhhVQ3E2VkZId016Y01YYnVLeUc3U1FZSWcyC2NJSFFXT29KZWFnQAFICg%3D%3D") - - produce_comment_reply_continuation("cIHQWOoJeag", "UCq6VFHwMzcMXbuKyG7SQYIg", "Ugza62y_TlmTu9o2RfF4AaABAg").should eq("EiYSC2NJSFFXT29KZWFnwAEByAEB4AEBogINKP___________wFAABgGMk0aSxIaVWd6YTYyeV9UbG1UdTlvMlJmRjRBYUFCQWciAggAKhhVQ3E2VkZId016Y01YYnVLeUc3U1FZSWcyC2NJSFFXT29KZWFnQAFICg%3D%3D") - - produce_comment_reply_continuation("_cE8xSu6swE", "UC1AZY74-dGVPe6bfxFwwEMg", "UgyBUaRGHB9Jmt1dsUZ4AaABAg").should eq("EiYSC19jRTh4U3U2c3dFwAEByAEB4AEBogINKP___________wFAABgGMk0aSxIaVWd5QlVhUkdIQjlKbXQxZHNVWjRBYUFCQWciAggAKhhVQzFBWlk3NC1kR1ZQZTZiZnhGd3dFTWcyC19jRTh4U3U2c3dFQAFICg%3D%3D") - 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("4qmFsgJmEhhVQ0NqOTU2SUY2MkZiVDdHb3VzemFqOXcaSkVnbGpiMjF0ZFc1cGRIbTRBUUNxQXlRYUlCSWFWV2QzY0U5TlFtVndXRWRqY2xoc1VIZzJXalJCWVVGQ1ExRklaR2dES0FBJTNE") - - produce_channel_community_continuation("UC-lHJZR3Gqxm24_Vd_AJ5Yw", "Egljb21tdW5pdHm4AQCqAyQaIBIaVWd5RTI2NW1rUkk2cE9uS21nbDRBYUFCQ1FIZGgDKAA%3D").should eq("4qmFsgJmEhhVQy1sSEpaUjNHcXhtMjRfVmRfQUo1WXcaSkVnbGpiMjF0ZFc1cGRIbTRBUUNxQXlRYUlCSWFWV2Q1UlRJMk5XMXJVa2syY0U5dVMyMW5iRFJCWVVGQ1ExRklaR2dES0FBJTNE") - produce_channel_community_continuation("UC-lHJZR3Gqxm24_Vd_AJ5Yw", "Egljb21tdW5pdHm4AQCqA-cOCsAOUVVSVFNsOXBNWEYxYlVablFXaGFiWFJNTW5WM1ZHSXdPVU5EWTNoeFJWWlVjRWRGVTBOa1prTktjVUoyWjBZemNEZHRPV2cwV1hWbVJtaFVPWFJwVjJaUU4xTXlNRWRaYlZwSVFUa3dlak5pTUV0dll6QkRVMlpsWHpoVFdUbHFSR0o1YkRkM1kydEhMVTVwWDFCdFdXOUhjR0Z6ZEMxbldVcEhUMjkzUm1saGRXSkViVmR6ZFhwd1QxTnpOME54TW5KUloxQlBkME5QU1VWVWMybHlNbFZvUVV0NlVIZFVhMVV5UzNWUmJHRldkRmszU1dKd1pVUllVMkZFVG1aV1ZsRnVUMGhsZFd0T01sVndTbGd3TkhweVdDMVBTRUphV25GNk5Yb3dYMWRCVTFnMlltODBPRmhIV205WlQwNW1YMjV1UlVKTWNucHNNSGR5Y1hKaFltUkVkblJYZG1Kc1FVaHFUV3BwTkc5R1pUQkVlbGw2ZHpSM2FISlBTSFJoYjJGbVMwNTBiV1pxV2pCSVNWWnZTalpRT0RoclVGVmhia1p5VFhsaWFTMVBjREZZV1dSTFdERkZjSHB0ZUhseWFtRXdNR1JmTkhOWmFEVlZTbVZ1ZUVkRU1XRlFhbU4xVERabk4wdDVSSGxHU2xsT1VEQlJXR1ZLTUhGM1UwWkJTSE5oWkRWQ2NXZHNaMFpqYW1ST1YxZFlhMDVOVUZSSFZWVktRekZSYVhodlUxTm1SV1EwTUdsdWNEWXlPV1YwUjNkcGFVcEVTM040YUZadmRXbHJhblkyZFdFelNHWXpUV3hMYURCa2JIRTFSblJ4Wms4NU1XbGtOM0pHYjBGeU4xZFJNMU5qYkZCd05rZE9jV1JqT1hGRGIyNU5Xak5TUlhkemFsUXRObGt4UWxkUE16ZGFaRTlxVGtaZlIweEhRbXRNWXpCWE9GUjNOMHBsYVhwS2RtSlZkMmxGTVhCbVNIWkdkVTFJY0MxbFdYSkVZM0V0ZFROWWRtVlFlV3hhYlVKMmVreGZUMGxOU2xaSlRFTlBZMVpEUjFwd1RHZFhZMmhIYVVKakxUSmFabXd0U1RNeFJEWkhlSGhYTkhOMU1GZGhOMjFCVlVnNGNFTlJXSGx2WW5ScWNUaHZXWGxKT1d0TVRXc3lRMWc0Um5wU2JEVjBlRGxpTW5vMVRYaEtkelExY201S1JHSmZkamhmTlhOWmRGYzRjak5FVVdkMlpXVnNRWEJyZW5OdFpHcEljVGhWYzFsZkxWa3dRVTkyTVZVMmIyMTNVeTFLVEUxeFIwUldRbmc0VEdsTlpGVktjVmxzTkZGa1UwazFabE0wZUhsRk5WZ3lWR0ZaYzJadlYyaHRPRFpzTjNCT1dHRnBiMHhUVDBkMmRuZFVOMlptVm05dWIwRTFZVkZuYldKNmIwMUNaMng2VGkxSk56bHhXV3BJVGt4RFYwVllUM05pTVcwemRHc3lUVWN6TVVKcVRHdElNVWg1YmtKQmVrbFNVMnczZEVKUlJGOUlNVWRyZERsbFJraHVYekJXZUhGbE1rTTBlVE40YVU1T1pFcGpVMkpFZFMxWVdITjNTMnhWVjJwYVgzVXRXbGcwZG5OSE1qUXpYMlJHTVhSV1kxWkZRMlZwU25OdVlXTkdVek5wVUd4b2FUbDVSRVp4YVhsbFRqbG1aRWxYVFZCMVFWbG9OMEl3TW5KV1JUVjRkREJLZG5obmJGZHhSVlY1ZWpjMFIyeGlZemRIVmkxeFpESmlaMnhFZGxkcVRuSjZNVEZWUkRWamVIQlFkRk5DVmtSU2RITlRaSGhWZG05WE9VUkNhWEYwTm1kSFRtb3RNV1pNYlhSeVJWTnJhRWhIVDB0SU0yVkxUbFZ2V1VGNlJTMDJialJZYkRKdFFUVnJhRVJ4WmpjeFptcERNR001UmpkM2QwNW1VRXd5YUZCZlEwWjFSbEUzY0doRk5ISkZZMWxTTWs5d2RXRnhiRzFrYjBVMmIxWkJaRzkyU2xneFZWOXNiMDVWWkUxRFJ6QjBjWGhpVjBVMldYY3pTUzF4UVcxa1RuZEJRVGRvWVZFNGNsSTBaVUl0UmxacVdETnJXazVLY21aRk9HVndRbWxqUjB0blRFZEZVR3N6YzJOclkwSTNlVlZZVEdkcE1YQkdiMHAyZVU1aGRVZFdVblJQYVhaQlZtdHZSa0UzTFU1Sk1XaFJRMUpMV2kxSWJ6WkxjWEkxZGtSTWJsOVdUa0ZFVmpKZmMwUlFWV3gwUTJ0TFRsbDJaM2gxZFVOSVkzbEVORUpRZVUxMVREQnpOMVowWDI1MWRrVmlUMU54TkRkUk5rVjViMEpRTUZGNmR6RlJSR2RxY1U1eVgwNTBjMDkxWm14R2NUVjBlRkJGT1dGVmFXeFJTMEZYYldwQlVVbHNOVmgwZERZdGFFRlViMWxmUjFWc1EycG1WVkJQV0hkcFVRPT0aIBIaVWd5RTI2NW1rUkk2cE9uS21nbDRBYUFCQ1FIZGgDKGM%3D").should eq("4qmFsgKXFBIYVUMtbEhKWlIzR3F4bTI0X1ZkX0FKNVl3GvoTRWdsamIyMXRkVzVwZEhtNEFRQ3FBLWNPQ3NBT1VWVlNWRk5zT1hCTldFWXhZbFZhYmxGWGFHRmlXRkpOVFc1V00xWkhTWGRQVlU1RVdUTm9lRkpXV2xWalJXUkdWVEJPYTFwclRrdGpWVW95V2pCWmVtTkVaSFJQVjJjd1YxaFdiVkp0YUZWUFdGSndWakphVVU0eFRYbE5SV1JhWWxad1NWRlVhM2RsYWs1cFRVVjBkbGw2UWtSVk1scHNXSHBvVkZkVWJIRlNSMG8xWWtSa00xa3lkRWhNVlRWd1dERkNkRmRYT1VoalIwWjZaRU14YmxkVmNFaFVNamt6VW0xc2FHUlhTa1ZpVm1SNlpGaHdkMVF4VG5wT01FNTRUVzVLVWxveFFsQmtNRTVRVTFWV1ZXTXliSGxOYkZadlVWVjBObFZJWkZWaE1WVjVVek5XVW1KSFJsZGtSbXN6VTFkS2QxcFZVbGxWTWtaRlZHMWFWMVpzUm5WVU1HaHNaRmQwVDAxc1ZuZFRiR2QzVGtod2VWZERNVkJUUlVwaFYyNUdOazVZYjNkWU1XUkNWVEZuTWxsdE9EQlBSbWhJVjIwNVdsUXdOVzFZTWpWMVVsVktUV051Y0hOTlNHUjVZMWhLYUZsdFVrVmtibEpZWkcxS2MxRlZhSEZVVjNCd1RrYzVSMXBVUWtWbGJHdzJaSHBTTTJGSVNsQlRTRkpvWWpKR2JWTXdOVEJpVjFweFYycENTVk5XV25aVGFscFJUMFJvY2xWR1ZtaGlhMXA1VkZoc2FXRlRNVkJqUkVaWlYxZFNURmRFUmtaalNIQjBaVWhzZVdGdFJYZE5SMUptVGtoT1dtRkVWbFpUYlZaMVpVVmtSVTFYUmxGaGJVNHhWRVJhYms0d2REVlNTR3hIVTJ4c1QxVkVRbEpYUjFaTFRVaEdNMVV3V2tKVFNFNW9Xa1JXUTJOWFpITmFNRnBxWVcxU1QxWXhaRmxoTURWT1ZVWlNTRlpXVmt0UmVrWlNZVmhvZGxVeFRtMVNWMUV3VFVkc2RXTkVXWGxQVjFZd1VqTmtjR0ZWY0VWVE0wNDBZVVphZG1SWGJISmhibGt5WkZkRmVsTkhXWHBVVjNoTVlVUkNhMkpJUlRGU2JsSjRXbXM0TlUxWGJHdE9NMHBIWWpCR2VVNHhaRkpOTVU1cVlrWkNkMDVyWkU5alYxSnFUMWhHUkdJeU5VNVhhazVUVWxoa2VtRnNVWFJPYkd0NFVXeGtVRTE2WkdGYVJUbHhWR3RhWmxJd2VFaFJiWFJOV1hwQ1dFOUdVak5PTUhCc1lWaHdTMlJ0U2xaa01teEdUVmhDYlZOSVdrZGtWVEZKWTBNeGJGZFlTa1ZaTTBWMFpGUk9XV1J0VmxGbFYzaGhZbFZLTW1WcmVHWlVNR3hPVTJ4YVNsUkZUbEJaTVZwRVVqRndkMVJIWkZoWk1taElZVlZLYWt4VVNtRmFiWGQwVTFSTmVGSkVXa2hsU0doWVRraE9NVTFHWkdoT01qRkNWbFZuTkdORlRsSlhTR3gyV1c1U2NXTlVhSFpYV0d4S1QxZDBUVlJYYzNsUk1XYzBVbTV3VTJKRVZqQmxSR3hwVFc1dk1WUllhRXRrZWxFeFkyMDFTMUpIU21aa2FtaG1UbGhPV21SR1l6UmphazVGVlZka01scFhWbk5SV0VKeVpXNU9kRnBIY0VsalZHaFdZekZzWmt4V2EzZFJWVGt5VFZaVk1tSXlNVE5WZVRGTFZFVXhlRkl3VWxkUmJtYzBWRWRzVGxwR1ZrdGpWbXh6VGtaR2ExVXdhekZhYkUwd1pVaHNSazVXWjNsV1IwWmFZekphZGxZeWFIUlBSRnB6VGpOQ1QxZEhSbkJpTUhoVVZEQmtNbVJ1WkZWT01scHRWbTA1ZFdJd1JURlpWa1p1WWxkS05tSXdNVU5hTW5nMlZHa3hTazU2YkhoWFYzQkpWR3Q0UkZZd1ZsbFVNMDVwVFZjd2VtUkhjM2xVVldONlRWVktjVlJIZEVsTlZXZzFZbXRLUW1WcmJGTlZNbmN6WkVWS1VsSkdPVWxOVldSeVpFUnNiRkpyYUhWWWVrSlhaVWhHYkUxclRUQmxWRTQwWVZVMVQxcEZjR3BWTWtwRlpGTXhXVmRJVGpOVE1uaFdWakp3WVZnelZYUlhiR2N3Wkc1T1NFMXFVWHBZTWxKSFRWaFNWMWt4V2taUk1sWndVMjVPZFZsWFRrZFZlazV3VlVkNGIyRlViRFZTUlZwNFlWaHNiRlJxYkcxYVJXeFlWRlpDTVZGV2JHOU9NRWwzVFc1S1YxSlVWalJrUkVKTFpHNW9ibUpHWkhoU1ZsWTFaV3BqTUZJeWVHbFplbVJJVm1reGVGcEVTbWxhTW5oRlpHeGtjVlJ1U2paTlZFWldVa1JXYW1WSVFsRmtSazVEVm10U1UyUklUbFJhU0doV1pHMDVXRTlWVWtOaFdFWXdUbTFrU0ZSdGIzUk5WMXBOWWxoU2VWSldUbkpoUldoSVZEQjBTVTB5Vmt4VWJGWjJWMVZHTmxKVE1ESmlhbEpaWWtSS2RGRlVWbkpoUlZKNFdtcGplRnB0Y0VSTlIwMDFVbXBrTTJRd05XMVZSWGQ1WVVaQ1psRXdXakZTYkVVelkwZG9SazVJU2taWk1XeFRUV3M1ZDJSWFJuaGlSekZyWWpCVk1tSXhXa0phUnpreVUyeG5lRlpXT1hOaU1EVldXa1V4UkZKNlFqQmpXR2hwVmpCVk1sZFlZM3BUVXpGNFVWY3hhMVJ1WkVKUlZHUnZXVlpGTkdOc1NUQmFWVWwwVW14YWNWZEVUbkpYYXpWTFkyMWFSazlIVm5kUmJXeHFVakIwYmxSRlpFWlZSM042WXpKT2Nsa3dTVE5sVmxaWlZFZGtjRTFZUWtkaU1IQXlaVlUxYUdSVlpGZFZibEpRWVZoYVFsWnRkSFpTYTBVelRGVTFTazFYYUZKUk1VcE1WMmt4U1dKNldreGpXRWt4Wkd0U1RXSnNPVmRVYTBaRlZtcEtabU13VWxGV1YzZ3dVVEowVEZSc2JESmFNMmd4WkZWT1NWa3piRVZPUlVwUlpWVXhNVlJFUW5wT01Wb3dXREkxTVdSclZtbFVNVTU0VGtSa1VrNXJWalZpTUVwUlRVWkdObVI2UmxKU1IyUnhZMVUxZVZnd05UQmpNRGt4V20xNFIyTlVWakJsUmtKR1QxZEdWbUZYZUZKVE1FWllZbGR3UWxWVmJITk9WbWd3WkVSWmRHRkZSbFZpTVd4bVVqRldjMUV5Y0cxV1ZrSlFWMGhrY0ZWUlBUMGFJQklhVldkNVJUSTJOVzFyVWtrMmNFOXVTMjFuYkRSQllVRkNRMUZJWkdnREtHTSUzRA%3D%3D") - 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("Egljb21tdW5pdHk=") - extract_channel_community_cursor("4qmFsgJoEhhVQ0NqOTU2SUY2MkZiVDdHb3VzemFqOXcaTEVnbGpiMjF0ZFc1cGRIbTRBUUNxQXlRYUlCSWFWV2QzY0U5TlFtVndXRWRqY2xoc1VIZzJXalJCWVVGQ1ExRklaR2dES0FBJTI1M0Q%3D").should eq("Egljb21tdW5pdHm4AQCqAyQaIEhkaAMSGlVnd3BPTUJlcFhHY3JYbFB4Nlo0QWFBQkNRKAA=") - - extract_channel_community_cursor("4qmFsgJoEhhVQy1sSEpaUjNHcXhtMjRfVmRfQUo1WXcaTEVnbGpiMjF0ZFc1cGRIbTRBUUNxQXlRYUlCSWFWV2Q1UlRJMk5XMXJVa2syY0U5dVMyMW5iRFJCWVVGQ1ExRklaR2dES0FBJTI1M0Q%3D").should eq("Egljb21tdW5pdHm4AQCqAyQaIEhkaAMSGlVneUUyNjVta1JJNnBPbkttZ2w0QWFBQkNRKAA=") - extract_channel_community_cursor("4qmFsgKZFBIYVUMtbEhKWlIzR3F4bTI0X1ZkX0FKNVl3GvwTRWdsamIyMXRkVzVwZEhtNEFRQ3FBLWNPQ3NBT1VWVlNWRk5zT1hCTldFWXhZbFZhYmxGWGFHRmlXRkpOVFc1V00xWkhTWGRQVlU1RVdUTm9lRkpXV2xWalJXUkdWVEJPYTFwclRrdGpWVW95V2pCWmVtTkVaSFJQVjJjd1YxaFdiVkp0YUZWUFdGSndWakphVVU0eFRYbE5SV1JhWWxad1NWRlVhM2RsYWs1cFRVVjBkbGw2UWtSVk1scHNXSHBvVkZkVWJIRlNSMG8xWWtSa00xa3lkRWhNVlRWd1dERkNkRmRYT1VoalIwWjZaRU14YmxkVmNFaFVNamt6VW0xc2FHUlhTa1ZpVm1SNlpGaHdkMVF4VG5wT01FNTRUVzVLVWxveFFsQmtNRTVRVTFWV1ZXTXliSGxOYkZadlVWVjBObFZJWkZWaE1WVjVVek5XVW1KSFJsZGtSbXN6VTFkS2QxcFZVbGxWTWtaRlZHMWFWMVpzUm5WVU1HaHNaRmQwVDAxc1ZuZFRiR2QzVGtod2VWZERNVkJUUlVwaFYyNUdOazVZYjNkWU1XUkNWVEZuTWxsdE9EQlBSbWhJVjIwNVdsUXdOVzFZTWpWMVVsVktUV051Y0hOTlNHUjVZMWhLYUZsdFVrVmtibEpZWkcxS2MxRlZhSEZVVjNCd1RrYzVSMXBVUWtWbGJHdzJaSHBTTTJGSVNsQlRTRkpvWWpKR2JWTXdOVEJpVjFweFYycENTVk5XV25aVGFscFJUMFJvY2xWR1ZtaGlhMXA1VkZoc2FXRlRNVkJqUkVaWlYxZFNURmRFUmtaalNIQjBaVWhzZVdGdFJYZE5SMUptVGtoT1dtRkVWbFpUYlZaMVpVVmtSVTFYUmxGaGJVNHhWRVJhYms0d2REVlNTR3hIVTJ4c1QxVkVRbEpYUjFaTFRVaEdNMVV3V2tKVFNFNW9Xa1JXUTJOWFpITmFNRnBxWVcxU1QxWXhaRmxoTURWT1ZVWlNTRlpXVmt0UmVrWlNZVmhvZGxVeFRtMVNWMUV3VFVkc2RXTkVXWGxQVjFZd1VqTmtjR0ZWY0VWVE0wNDBZVVphZG1SWGJISmhibGt5WkZkRmVsTkhXWHBVVjNoTVlVUkNhMkpJUlRGU2JsSjRXbXM0TlUxWGJHdE9NMHBIWWpCR2VVNHhaRkpOTVU1cVlrWkNkMDVyWkU5alYxSnFUMWhHUkdJeU5VNVhhazVUVWxoa2VtRnNVWFJPYkd0NFVXeGtVRTE2WkdGYVJUbHhWR3RhWmxJd2VFaFJiWFJOV1hwQ1dFOUdVak5PTUhCc1lWaHdTMlJ0U2xaa01teEdUVmhDYlZOSVdrZGtWVEZKWTBNeGJGZFlTa1ZaTTBWMFpGUk9XV1J0VmxGbFYzaGhZbFZLTW1WcmVHWlVNR3hPVTJ4YVNsUkZUbEJaTVZwRVVqRndkMVJIWkZoWk1taElZVlZLYWt4VVNtRmFiWGQwVTFSTmVGSkVXa2hsU0doWVRraE9NVTFHWkdoT01qRkNWbFZuTkdORlRsSlhTR3gyV1c1U2NXTlVhSFpYV0d4S1QxZDBUVlJYYzNsUk1XYzBVbTV3VTJKRVZqQmxSR3hwVFc1dk1WUllhRXRrZWxFeFkyMDFTMUpIU21aa2FtaG1UbGhPV21SR1l6UmphazVGVlZka01scFhWbk5SV0VKeVpXNU9kRnBIY0VsalZHaFdZekZzWmt4V2EzZFJWVGt5VFZaVk1tSXlNVE5WZVRGTFZFVXhlRkl3VWxkUmJtYzBWRWRzVGxwR1ZrdGpWbXh6VGtaR2ExVXdhekZhYkUwd1pVaHNSazVXWjNsV1IwWmFZekphZGxZeWFIUlBSRnB6VGpOQ1QxZEhSbkJpTUhoVVZEQmtNbVJ1WkZWT01scHRWbTA1ZFdJd1JURlpWa1p1WWxkS05tSXdNVU5hTW5nMlZHa3hTazU2YkhoWFYzQkpWR3Q0UkZZd1ZsbFVNMDVwVFZjd2VtUkhjM2xVVldONlRWVktjVlJIZEVsTlZXZzFZbXRLUW1WcmJGTlZNbmN6WkVWS1VsSkdPVWxOVldSeVpFUnNiRkpyYUhWWWVrSlhaVWhHYkUxclRUQmxWRTQwWVZVMVQxcEZjR3BWTWtwRlpGTXhXVmRJVGpOVE1uaFdWakp3WVZnelZYUlhiR2N3Wkc1T1NFMXFVWHBZTWxKSFRWaFNWMWt4V2taUk1sWndVMjVPZFZsWFRrZFZlazV3VlVkNGIyRlViRFZTUlZwNFlWaHNiRlJxYkcxYVJXeFlWRlpDTVZGV2JHOU9NRWwzVFc1S1YxSlVWalJrUkVKTFpHNW9ibUpHWkhoU1ZsWTFaV3BqTUZJeWVHbFplbVJJVm1reGVGcEVTbWxhTW5oRlpHeGtjVlJ1U2paTlZFWldVa1JXYW1WSVFsRmtSazVEVm10U1UyUklUbFJhU0doV1pHMDVXRTlWVWtOaFdFWXdUbTFrU0ZSdGIzUk5WMXBOWWxoU2VWSldUbkpoUldoSVZEQjBTVTB5Vmt4VWJGWjJWMVZHTmxKVE1ESmlhbEpaWWtSS2RGRlVWbkpoUlZKNFdtcGplRnB0Y0VSTlIwMDFVbXBrTTJRd05XMVZSWGQ1WVVaQ1psRXdXakZTYkVVelkwZG9SazVJU2taWk1XeFRUV3M1ZDJSWFJuaGlSekZyWWpCVk1tSXhXa0phUnpreVUyeG5lRlpXT1hOaU1EVldXa1V4UkZKNlFqQmpXR2hwVmpCVk1sZFlZM3BUVXpGNFVWY3hhMVJ1WkVKUlZHUnZXVlpGTkdOc1NUQmFWVWwwVW14YWNWZEVUbkpYYXpWTFkyMWFSazlIVm5kUmJXeHFVakIwYmxSRlpFWlZSM042WXpKT2Nsa3dTVE5sVmxaWlZFZGtjRTFZUWtkaU1IQXlaVlUxYUdSVlpGZFZibEpRWVZoYVFsWnRkSFpTYTBVelRGVTFTazFYYUZKUk1VcE1WMmt4U1dKNldreGpXRWt4Wkd0U1RXSnNPVmRVYTBaRlZtcEtabU13VWxGV1YzZ3dVVEowVEZSc2JESmFNMmd4WkZWT1NWa3piRVZPUlVwUlpWVXhNVlJFUW5wT01Wb3dXREkxTVdSclZtbFVNVTU0VGtSa1VrNXJWalZpTUVwUlRVWkdObVI2UmxKU1IyUnhZMVUxZVZnd05UQmpNRGt4V20xNFIyTlVWakJsUmtKR1QxZEdWbUZYZUZKVE1FWllZbGR3UWxWVmJITk9WbWd3WkVSWmRHRkZSbFZpTVd4bVVqRldjMUV5Y0cxV1ZrSlFWMGhrY0ZWUlBUMGFJQklhVldkNVJUSTJOVzFyVWtrMmNFOXVTMjFuYkRSQllVRkNRMUZJWkdnREtHTSUyNTNE").should eq("Egljb21tdW5pdHm4AQCqA-kOCsAOUVVSVFNsOXBNWEYxYlVablFXaGFiWFJNTW5WM1ZHSXdPVU5EWTNoeFJWWlVjRWRGVTBOa1prTktjVUoyWjBZemNEZHRPV2cwV1hWbVJtaFVPWFJwVjJaUU4xTXlNRWRaYlZwSVFUa3dlak5pTUV0dll6QkRVMlpsWHpoVFdUbHFSR0o1YkRkM1kydEhMVTVwWDFCdFdXOUhjR0Z6ZEMxbldVcEhUMjkzUm1saGRXSkViVmR6ZFhwd1QxTnpOME54TW5KUloxQlBkME5QU1VWVWMybHlNbFZvUVV0NlVIZFVhMVV5UzNWUmJHRldkRmszU1dKd1pVUllVMkZFVG1aV1ZsRnVUMGhsZFd0T01sVndTbGd3TkhweVdDMVBTRUphV25GNk5Yb3dYMWRCVTFnMlltODBPRmhIV205WlQwNW1YMjV1UlVKTWNucHNNSGR5Y1hKaFltUkVkblJYZG1Kc1FVaHFUV3BwTkc5R1pUQkVlbGw2ZHpSM2FISlBTSFJoYjJGbVMwNTBiV1pxV2pCSVNWWnZTalpRT0RoclVGVmhia1p5VFhsaWFTMVBjREZZV1dSTFdERkZjSHB0ZUhseWFtRXdNR1JmTkhOWmFEVlZTbVZ1ZUVkRU1XRlFhbU4xVERabk4wdDVSSGxHU2xsT1VEQlJXR1ZLTUhGM1UwWkJTSE5oWkRWQ2NXZHNaMFpqYW1ST1YxZFlhMDVOVUZSSFZWVktRekZSYVhodlUxTm1SV1EwTUdsdWNEWXlPV1YwUjNkcGFVcEVTM040YUZadmRXbHJhblkyZFdFelNHWXpUV3hMYURCa2JIRTFSblJ4Wms4NU1XbGtOM0pHYjBGeU4xZFJNMU5qYkZCd05rZE9jV1JqT1hGRGIyNU5Xak5TUlhkemFsUXRObGt4UWxkUE16ZGFaRTlxVGtaZlIweEhRbXRNWXpCWE9GUjNOMHBsYVhwS2RtSlZkMmxGTVhCbVNIWkdkVTFJY0MxbFdYSkVZM0V0ZFROWWRtVlFlV3hhYlVKMmVreGZUMGxOU2xaSlRFTlBZMVpEUjFwd1RHZFhZMmhIYVVKakxUSmFabXd0U1RNeFJEWkhlSGhYTkhOMU1GZGhOMjFCVlVnNGNFTlJXSGx2WW5ScWNUaHZXWGxKT1d0TVRXc3lRMWc0Um5wU2JEVjBlRGxpTW5vMVRYaEtkelExY201S1JHSmZkamhmTlhOWmRGYzRjak5FVVdkMlpXVnNRWEJyZW5OdFpHcEljVGhWYzFsZkxWa3dRVTkyTVZVMmIyMTNVeTFLVEUxeFIwUldRbmc0VEdsTlpGVktjVmxzTkZGa1UwazFabE0wZUhsRk5WZ3lWR0ZaYzJadlYyaHRPRFpzTjNCT1dHRnBiMHhUVDBkMmRuZFVOMlptVm05dWIwRTFZVkZuYldKNmIwMUNaMng2VGkxSk56bHhXV3BJVGt4RFYwVllUM05pTVcwemRHc3lUVWN6TVVKcVRHdElNVWg1YmtKQmVrbFNVMnczZEVKUlJGOUlNVWRyZERsbFJraHVYekJXZUhGbE1rTTBlVE40YVU1T1pFcGpVMkpFZFMxWVdITjNTMnhWVjJwYVgzVXRXbGcwZG5OSE1qUXpYMlJHTVhSV1kxWkZRMlZwU25OdVlXTkdVek5wVUd4b2FUbDVSRVp4YVhsbFRqbG1aRWxYVFZCMVFWbG9OMEl3TW5KV1JUVjRkREJLZG5obmJGZHhSVlY1ZWpjMFIyeGlZemRIVmkxeFpESmlaMnhFZGxkcVRuSjZNVEZWUkRWamVIQlFkRk5DVmtSU2RITlRaSGhWZG05WE9VUkNhWEYwTm1kSFRtb3RNV1pNYlhSeVJWTnJhRWhIVDB0SU0yVkxUbFZ2V1VGNlJTMDJialJZYkRKdFFUVnJhRVJ4WmpjeFptcERNR001UmpkM2QwNW1VRXd5YUZCZlEwWjFSbEUzY0doRk5ISkZZMWxTTWs5d2RXRnhiRzFrYjBVMmIxWkJaRzkyU2xneFZWOXNiMDVWWkUxRFJ6QjBjWGhpVjBVMldYY3pTUzF4UVcxa1RuZEJRVGRvWVZFNGNsSTBaVUl0UmxacVdETnJXazVLY21aRk9HVndRbWxqUjB0blRFZEZVR3N6YzJOclkwSTNlVlZZVEdkcE1YQkdiMHAyZVU1aGRVZFdVblJQYVhaQlZtdHZSa0UzTFU1Sk1XaFJRMUpMV2kxSWJ6WkxjWEkxZGtSTWJsOVdUa0ZFVmpKZmMwUlFWV3gwUTJ0TFRsbDJaM2gxZFVOSVkzbEVORUpRZVUxMVREQnpOMVowWDI1MWRrVmlUMU54TkRkUk5rVjViMEpRTUZGNmR6RlJSR2RxY1U1eVgwNTBjMDkxWm14R2NUVjBlRkJGT1dGVmFXeFJTMEZYYldwQlVVbHNOVmgwZERZdGFFRlViMWxmUjFWc1EycG1WVkJQV0hkcFVRPT0aIhIcVWd5RTI2NW1rUkk2cE9uS21nbDRBYUFCQ1E9PUhkaAMoYw==") - end - end - - describe "#extract_plid" do - it "correctly extracts playlist ID from trending URL" do - extract_plid("/feed/trending?bp=4gIuCggvbS8wNHJsZhIiUExGZ3F1TG5MNTlhbVBud2pLbmNhZUp3MDYzZlU1M3Q0cA%3D%3D").should eq("PLFgquLnL59amPnwjKncaeJw063fU53t4p") - extract_plid("/feed/trending?bp=4gIvCgkvbS8wYnp2bTISIlBMaUN2Vkp6QnVwS2tDaFNnUDdGWFhDclo2aEp4NmtlTm0%3D").should eq("PLiCvVJzBupKkChSgP7FXXCrZ6hJx6keNm") - extract_plid("/feed/trending?bp=4gIuCggvbS8wNWpoZxIiUEwzWlE1Q3BOdWxRbUtPUDNJekdsYWN0V1c4dklYX0hFUA%3D%3D").should eq("PL3ZQ5CpNulQmKOP3IzGlactWW8vIX_HEP") - extract_plid("/feed/trending?bp=4gIuCggvbS8wMnZ4bhIiUEx6akZiYUZ6c21NUnFhdEJnVTdPeGNGTkZhQ2hqTkVERA%3D%3D").should eq("PLzjFbaFzsmMRqatBgU7OxcFNFaChjNEDD") - end - end - - describe "#sign_token" do - it "correctly signs a given hash" do - token = { - "session" => "v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - "expires" => 1554680038, - "scopes" => [ - ":notifications", - ":subscriptions/*", - "GET:tokens*", - ], - "signature" => "f__2hS20th8pALF305PJFK-D2aVtvefNnQheILHD2vU=", - } - sign_token("SECRET_KEY", token).should eq(token["signature"]) - - token = { - "session" => "v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - "scopes" => [":notifications", "POST:subscriptions/*"], - "signature" => "fNvXoT0MRAL9eE6lTE33CEg8HitYJDOL9a22rSN2Ihg=", - } - sign_token("SECRET_KEY", token).should eq(token["signature"]) - end - end -end diff --git a/spec/i18next_plurals_spec.cr b/spec/i18next_plurals_spec.cr new file mode 100644 index 00000000..dcd0f5ec --- /dev/null +++ b/spec/i18next_plurals_spec.cr @@ -0,0 +1,231 @@ +require "spectator" +require "../src/invidious/helpers/i18next.cr" + +Spectator.configure do |config| + config.fail_blank + config.randomize +end + +def resolver + I18next::Plurals::RESOLVER +end + +FORM_TESTS = { + "ach" => I18next::Plurals::PluralForms::Single_gt_one, + "ar" => I18next::Plurals::PluralForms::Special_Arabic, + "be" => I18next::Plurals::PluralForms::Dual_Slavic, + "cy" => I18next::Plurals::PluralForms::Special_Welsh, + "fr" => I18next::Plurals::PluralForms::Special_French_Portuguese, + "en" => I18next::Plurals::PluralForms::Single_not_one, + "es" => I18next::Plurals::PluralForms::Special_Spanish_Italian, + "ga" => I18next::Plurals::PluralForms::Special_Irish, + "gd" => I18next::Plurals::PluralForms::Special_Scottish_Gaelic, + "he" => I18next::Plurals::PluralForms::Special_Hebrew, + "hr" => I18next::Plurals::PluralForms::Special_Hungarian_Serbian, + "is" => I18next::Plurals::PluralForms::Special_Icelandic, + "it" => I18next::Plurals::PluralForms::Special_Spanish_Italian, + "jv" => I18next::Plurals::PluralForms::Special_Javanese, + "kw" => I18next::Plurals::PluralForms::Special_Cornish, + "lt" => I18next::Plurals::PluralForms::Special_Lithuanian, + "lv" => I18next::Plurals::PluralForms::Special_Latvian, + "mk" => I18next::Plurals::PluralForms::Special_Macedonian, + "mnk" => I18next::Plurals::PluralForms::Special_Mandinka, + "mt" => I18next::Plurals::PluralForms::Special_Maltese, + "or" => I18next::Plurals::PluralForms::Special_Odia, + "pl" => I18next::Plurals::PluralForms::Special_Polish_Kashubian, + "pt" => I18next::Plurals::PluralForms::Special_French_Portuguese, + "pt-PT" => I18next::Plurals::PluralForms::Single_gt_one, + "pt-BR" => I18next::Plurals::PluralForms::Special_French_Portuguese, + "ro" => I18next::Plurals::PluralForms::Special_Romanian, + "sk" => I18next::Plurals::PluralForms::Special_Czech_Slovak, + "sl" => I18next::Plurals::PluralForms::Special_Slovenian, + "su" => I18next::Plurals::PluralForms::None, + "sr" => I18next::Plurals::PluralForms::Special_Hungarian_Serbian, +} + +SUFFIX_TESTS = { + "ach" => [ + {num: 0, suffix: ""}, + {num: 1, suffix: ""}, + {num: 10, suffix: "_plural"}, + ], + "ar" => [ + {num: 0, suffix: "_0"}, + {num: 1, suffix: "_1"}, + {num: 2, suffix: "_2"}, + {num: 3, suffix: "_3"}, + {num: 4, suffix: "_3"}, + {num: 104, suffix: "_3"}, + {num: 11, suffix: "_4"}, + {num: 99, suffix: "_4"}, + {num: 199, suffix: "_4"}, + {num: 100, suffix: "_5"}, + ], + "be" => [ + {num: 0, suffix: "_2"}, + {num: 1, suffix: "_0"}, + {num: 5, suffix: "_2"}, + ], + "cy" => [ + {num: 0, suffix: "_2"}, + {num: 1, suffix: "_0"}, + {num: 3, suffix: "_2"}, + {num: 8, suffix: "_3"}, + ], + "en" => [ + {num: 0, suffix: "_plural"}, + {num: 1, suffix: ""}, + {num: 10, suffix: "_plural"}, + ], + "es" => [ + {num: 0, suffix: "_2"}, + {num: 1, suffix: "_0"}, + {num: 10, suffix: "_2"}, + {num: 6_000_000, suffix: "_1"}, + ], + "fr" => [ + {num: 0, suffix: "_0"}, + {num: 1, suffix: "_0"}, + {num: 10, suffix: "_2"}, + {num: 4_000_000, suffix: "_1"}, + {num: 6_260_000, suffix: "_2"}, + ], + "ga" => [ + {num: 1, suffix: "_0"}, + {num: 2, suffix: "_1"}, + {num: 3, suffix: "_2"}, + {num: 7, suffix: "_3"}, + {num: 11, suffix: "_4"}, + ], + "gd" => [ + {num: 1, suffix: "_0"}, + {num: 2, suffix: "_1"}, + {num: 3, suffix: "_2"}, + {num: 20, suffix: "_3"}, + ], + "he" => [ + {num: 0, suffix: "_3"}, + {num: 1, suffix: "_0"}, + {num: 2, suffix: "_1"}, + {num: 3, suffix: "_3"}, + {num: 20, suffix: "_2"}, + {num: 21, suffix: "_3"}, + {num: 30, suffix: "_2"}, + {num: 100, suffix: "_2"}, + {num: 101, suffix: "_3"}, + ], + "is" => [ + {num: 1, suffix: ""}, + {num: 2, suffix: "_plural"}, + ], + "jv" => [ + {num: 0, suffix: "_0"}, + {num: 1, suffix: "_1"}, + ], + "kw" => [ + {num: 1, suffix: "_0"}, + {num: 2, suffix: "_1"}, + {num: 3, suffix: "_2"}, + {num: 4, suffix: "_3"}, + ], + "lt" => [ + {num: 1, suffix: "_0"}, + {num: 2, suffix: "_1"}, + {num: 10, suffix: "_2"}, + ], + "lv" => [ + {num: 1, suffix: "_0"}, + {num: 2, suffix: "_1"}, + {num: 0, suffix: "_2"}, + ], + "mk" => [ + {num: 1, suffix: ""}, + {num: 2, suffix: "_plural"}, + {num: 0, suffix: "_plural"}, + {num: 11, suffix: "_plural"}, + {num: 21, suffix: ""}, + {num: 31, suffix: ""}, + {num: 311, suffix: "_plural"}, + ], + "mnk" => [ + {num: 0, suffix: "_0"}, + {num: 1, suffix: "_1"}, + {num: 2, suffix: "_2"}, + ], + "mt" => [ + {num: 1, suffix: "_0"}, + {num: 2, suffix: "_1"}, + {num: 11, suffix: "_2"}, + {num: 20, suffix: "_3"}, + ], + "or" => [ + {num: 2, suffix: "_1"}, + {num: 1, suffix: "_0"}, + ], + "pl" => [ + {num: 0, suffix: "_2"}, + {num: 1, suffix: "_0"}, + {num: 5, suffix: "_2"}, + ], + "pt-BR" => [ + {num: 0, suffix: "_0"}, + {num: 1, suffix: "_0"}, + {num: 10, suffix: "_2"}, + {num: 42, suffix: "_2"}, + {num: 9_000_000, suffix: "_1"}, + ], + "pt-PT" => [ + {num: 0, suffix: ""}, + {num: 1, suffix: ""}, + {num: 10, suffix: "_plural"}, + {num: 9_000_000, suffix: "_plural"}, + ], + "ro" => [ + {num: 0, suffix: "_1"}, + {num: 1, suffix: "_0"}, + {num: 20, suffix: "_2"}, + ], + "sk" => [ + {num: 0, suffix: "_2"}, + {num: 1, suffix: "_0"}, + {num: 5, suffix: "_2"}, + ], + "sl" => [ + {num: 5, suffix: "_0"}, + {num: 1, suffix: "_1"}, + {num: 2, suffix: "_2"}, + {num: 3, suffix: "_3"}, + ], + "su" => [ + {num: 0, suffix: "_0"}, + {num: 1, suffix: "_0"}, + {num: 10, suffix: "_0"}, + ], + "sr" => [ + {num: 1, suffix: "_0"}, + {num: 51, suffix: "_0"}, + {num: 32, suffix: "_1"}, + {num: 100, suffix: "_2"}, + {num: 100_000, suffix: "_2"}, + ], +} + +Spectator.describe "i18next_Plural_Resolver" do + describe "get_plural_form" do + sample FORM_TESTS do |locale, form| + it "returns the right plural form for locale '#{locale}'" do + expect(resolver.get_plural_form(locale)).to eq(form) + end + end + end + + describe "get_suffix" do + sample SUFFIX_TESTS do |locale, tests| + it "returns the right suffix for locale '#{locale}'" do + tests.each do |d| + expect(resolver.get_suffix(locale, d[:num])).to eq(d[:suffix]) + end + end + end + end +end diff --git a/spec/invidious/hashtag_spec.cr b/spec/invidious/hashtag_spec.cr new file mode 100644 index 00000000..abc81225 --- /dev/null +++ b/spec/invidious/hashtag_spec.cr @@ -0,0 +1,109 @@ +require "../parsers_helper.cr" + +Spectator.describe Invidious::Hashtag do + it "parses richItemRenderer containers (test 1)" do + # Enable mock + test_content = load_mock("hashtag/martingarrix_page1") + videos, _ = extract_items(test_content) + + expect(typeof(videos)).to eq(Array(SearchItem)) + expect(videos.size).to eq(60) + + # + # Random video check 1 + # + expect(typeof(videos[11])).to eq(SearchItem) + + video_11 = videos[11].as(SearchVideo) + + expect(video_11.id).to eq("06eSsOWcKYA") + expect(video_11.title).to eq("Martin Garrix - Live @ Tomorrowland 2018") + + expect(video_11.ucid).to eq("UC5H_KXkPbEsGs0tFt8R35mA") + expect(video_11.author).to eq("Martin Garrix") + expect(video_11.author_verified).to be_true + + expect(video_11.published).to be_close(Time.utc - 3.years, 1.second) + expect(video_11.length_seconds).to eq((56.minutes + 41.seconds).total_seconds.to_i32) + expect(video_11.views).to eq(40_504_893) + + expect(video_11.badges.live_now?).to be_false + expect(video_11.badges.premium?).to be_false + expect(video_11.premiere_timestamp).to be_nil + + # + # Random video check 2 + # + expect(typeof(videos[35])).to eq(SearchItem) + + video_35 = videos[35].as(SearchVideo) + + expect(video_35.id).to eq("b9HpOAYjY9I") + expect(video_35.title).to eq("Martin Garrix feat. Mike Yung - Dreamer (Official Video)") + + expect(video_35.ucid).to eq("UC5H_KXkPbEsGs0tFt8R35mA") + expect(video_35.author).to eq("Martin Garrix") + expect(video_35.author_verified).to be_true + + expect(video_35.published).to be_close(Time.utc - 3.years, 1.second) + expect(video_35.length_seconds).to eq((3.minutes + 14.seconds).total_seconds.to_i32) + expect(video_35.views).to eq(30_790_049) + + expect(video_35.badges.live_now?).to be_false + expect(video_35.badges.premium?).to be_false + expect(video_35.premiere_timestamp).to be_nil + end + + it "parses richItemRenderer containers (test 2)" do + # Enable mock + test_content = load_mock("hashtag/martingarrix_page2") + videos, _ = extract_items(test_content) + + expect(typeof(videos)).to eq(Array(SearchItem)) + expect(videos.size).to eq(60) + + # + # Random video check 1 + # + expect(typeof(videos[41])).to eq(SearchItem) + + video_41 = videos[41].as(SearchVideo) + + expect(video_41.id).to eq("qhstH17zAjs") + expect(video_41.title).to eq("Martin Garrix Radio - Episode 391") + + expect(video_41.ucid).to eq("UC5H_KXkPbEsGs0tFt8R35mA") + expect(video_41.author).to eq("Martin Garrix") + expect(video_41.author_verified).to be_true + + expect(video_41.published).to be_close(Time.utc - 2.months, 1.second) + expect(video_41.length_seconds).to eq((1.hour).total_seconds.to_i32) + expect(video_41.views).to eq(63_240) + + expect(video_41.badges.live_now?).to be_false + expect(video_41.badges.premium?).to be_false + expect(video_41.premiere_timestamp).to be_nil + + # + # Random video check 2 + # + expect(typeof(videos[48])).to eq(SearchItem) + + video_48 = videos[48].as(SearchVideo) + + expect(video_48.id).to eq("lqGvW0NIfdc") + expect(video_48.title).to eq("Martin Garrix SENTIO Full Album Mix by Sakul") + + expect(video_48.ucid).to eq("UC3833PXeLTS6yRpwGMQpp4Q") + expect(video_48.author).to eq("SAKUL") + expect(video_48.author_verified).to be_false + + expect(video_48.published).to be_close(Time.utc - 3.weeks, 1.second) + expect(video_48.length_seconds).to eq((35.minutes + 46.seconds).total_seconds.to_i32) + expect(video_48.views).to eq(68_704) + + expect(video_48.badges.live_now?).to be_false + expect(video_48.badges.premium?).to be_false + expect(video_48.premiere_timestamp).to be_nil + end +end diff --git a/spec/invidious/helpers_spec.cr b/spec/invidious/helpers_spec.cr new file mode 100644 index 00000000..9fbb6d6f --- /dev/null +++ b/spec/invidious/helpers_spec.cr @@ -0,0 +1,56 @@ +require "../spec_helper" + +CONFIG = Config.from_yaml(File.open("config/config.example.yml")) + +Spectator.describe "Helper" do + describe "#produce_channel_search_continuation" do + it "correctly produces token for searching a specific channel" do + expect(produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100)).to eq("4qmFsgJqEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FUZ0JZQUY2QkVkS2IxaTRBUUE9WgCaAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D") + + expect(produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "По ожиशुपतिरपि子而時ஸ்றீனி", 0)).to eq("4qmFsgKoARIYVUNYdXFTQmxIQUU2WHcteWVKQTBUdW53GiBFZ1p6WldGeVkyZ3dBVGdCWUFGNkJFZEJRVDI0QVFBPVo-0J_QviDQvtC20LjgpLbgpYHgpKrgpKTgpL_gpLDgpKrgpL_lrZDogIzmmYLgrrjgr43grrHgr4Dgrqngrr-aAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D") + end + end + + describe "#produce_channel_community_continuation" do + it "correctly produces a continuation token for a channel community" do + expect(produce_channel_community_continuation("UCCj956IF62FbT7Gouszaj9w", "Egljb21tdW5pdHm4")).to eq("4qmFsgIsEhhVQ0NqOTU2SUY2MkZiVDdHb3VzemFqOXcaEEVnbGpiMjF0ZFc1cGRIbTQ%3D") + expect(produce_channel_community_continuation("UCCj956IF62FbT7Gouszaj9w", "Egljb21tdW5pdHm4AQCqAyQaIBIaVWd3cE9NQmVwWEdjclhsUHg2WjRBYUFCQ1FIZGgDKAA%3D")).to eq("4qmFsgJmEhhVQ0NqOTU2SUY2MkZiVDdHb3VzemFqOXcaSkVnbGpiMjF0ZFc1cGRIbTRBUUNxQXlRYUlCSWFWV2QzY0U5TlFtVndXRWRqY2xoc1VIZzJXalJCWVVGQ1ExRklaR2dES0FBJTNE") + + expect(produce_channel_community_continuation("UC-lHJZR3Gqxm24_Vd_AJ5Yw", "Egljb21tdW5pdHm4AQCqAyQaIBIaVWd5RTI2NW1rUkk2cE9uS21nbDRBYUFCQ1FIZGgDKAA%3D")).to eq("4qmFsgJmEhhVQy1sSEpaUjNHcXhtMjRfVmRfQUo1WXcaSkVnbGpiMjF0ZFc1cGRIbTRBUUNxQXlRYUlCSWFWV2Q1UlRJMk5XMXJVa2syY0U5dVMyMW5iRFJCWVVGQ1ExRklaR2dES0FBJTNE") + expect(produce_channel_community_continuation("UC-lHJZR3Gqxm24_Vd_AJ5Yw", "Egljb21tdW5pdHm4AQCqA-cOCsAOUVVSVFNsOXBNWEYxYlVablFXaGFiWFJNTW5WM1ZHSXdPVU5EWTNoeFJWWlVjRWRGVTBOa1prTktjVUoyWjBZemNEZHRPV2cwV1hWbVJtaFVPWFJwVjJaUU4xTXlNRWRaYlZwSVFUa3dlak5pTUV0dll6QkRVMlpsWHpoVFdUbHFSR0o1YkRkM1kydEhMVTVwWDFCdFdXOUhjR0Z6ZEMxbldVcEhUMjkzUm1saGRXSkViVmR6ZFhwd1QxTnpOME54TW5KUloxQlBkME5QU1VWVWMybHlNbFZvUVV0NlVIZFVhMVV5UzNWUmJHRldkRmszU1dKd1pVUllVMkZFVG1aV1ZsRnVUMGhsZFd0T01sVndTbGd3TkhweVdDMVBTRUphV25GNk5Yb3dYMWRCVTFnMlltODBPRmhIV205WlQwNW1YMjV1UlVKTWNucHNNSGR5Y1hKaFltUkVkblJYZG1Kc1FVaHFUV3BwTkc5R1pUQkVlbGw2ZHpSM2FISlBTSFJoYjJGbVMwNTBiV1pxV2pCSVNWWnZTalpRT0RoclVGVmhia1p5VFhsaWFTMVBjREZZV1dSTFdERkZjSHB0ZUhseWFtRXdNR1JmTkhOWmFEVlZTbVZ1ZUVkRU1XRlFhbU4xVERabk4wdDVSSGxHU2xsT1VEQlJXR1ZLTUhGM1UwWkJTSE5oWkRWQ2NXZHNaMFpqYW1ST1YxZFlhMDVOVUZSSFZWVktRekZSYVhodlUxTm1SV1EwTUdsdWNEWXlPV1YwUjNkcGFVcEVTM040YUZadmRXbHJhblkyZFdFelNHWXpUV3hMYURCa2JIRTFSblJ4Wms4NU1XbGtOM0pHYjBGeU4xZFJNMU5qYkZCd05rZE9jV1JqT1hGRGIyNU5Xak5TUlhkemFsUXRObGt4UWxkUE16ZGFaRTlxVGtaZlIweEhRbXRNWXpCWE9GUjNOMHBsYVhwS2RtSlZkMmxGTVhCbVNIWkdkVTFJY0MxbFdYSkVZM0V0ZFROWWRtVlFlV3hhYlVKMmVreGZUMGxOU2xaSlRFTlBZMVpEUjFwd1RHZFhZMmhIYVVKakxUSmFabXd0U1RNeFJEWkhlSGhYTkhOMU1GZGhOMjFCVlVnNGNFTlJXSGx2WW5ScWNUaHZXWGxKT1d0TVRXc3lRMWc0Um5wU2JEVjBlRGxpTW5vMVRYaEtkelExY201S1JHSmZkamhmTlhOWmRGYzRjak5FVVdkMlpXVnNRWEJyZW5OdFpHcEljVGhWYzFsZkxWa3dRVTkyTVZVMmIyMTNVeTFLVEUxeFIwUldRbmc0VEdsTlpGVktjVmxzTkZGa1UwazFabE0wZUhsRk5WZ3lWR0ZaYzJadlYyaHRPRFpzTjNCT1dHRnBiMHhUVDBkMmRuZFVOMlptVm05dWIwRTFZVkZuYldKNmIwMUNaMng2VGkxSk56bHhXV3BJVGt4RFYwVllUM05pTVcwemRHc3lUVWN6TVVKcVRHdElNVWg1YmtKQmVrbFNVMnczZEVKUlJGOUlNVWRyZERsbFJraHVYekJXZUhGbE1rTTBlVE40YVU1T1pFcGpVMkpFZFMxWVdITjNTMnhWVjJwYVgzVXRXbGcwZG5OSE1qUXpYMlJHTVhSV1kxWkZRMlZwU25OdVlXTkdVek5wVUd4b2FUbDVSRVp4YVhsbFRqbG1aRWxYVFZCMVFWbG9OMEl3TW5KV1JUVjRkREJLZG5obmJGZHhSVlY1ZWpjMFIyeGlZemRIVmkxeFpESmlaMnhFZGxkcVRuSjZNVEZWUkRWamVIQlFkRk5DVmtSU2RITlRaSGhWZG05WE9VUkNhWEYwTm1kSFRtb3RNV1pNYlhSeVJWTnJhRWhIVDB0SU0yVkxUbFZ2V1VGNlJTMDJialJZYkRKdFFUVnJhRVJ4WmpjeFptcERNR001UmpkM2QwNW1VRXd5YUZCZlEwWjFSbEUzY0doRk5ISkZZMWxTTWs5d2RXRnhiRzFrYjBVMmIxWkJaRzkyU2xneFZWOXNiMDVWWkUxRFJ6QjBjWGhpVjBVMldYY3pTUzF4UVcxa1RuZEJRVGRvWVZFNGNsSTBaVUl0UmxacVdETnJXazVLY21aRk9HVndRbWxqUjB0blRFZEZVR3N6YzJOclkwSTNlVlZZVEdkcE1YQkdiMHAyZVU1aGRVZFdVblJQYVhaQlZtdHZSa0UzTFU1Sk1XaFJRMUpMV2kxSWJ6WkxjWEkxZGtSTWJsOVdUa0ZFVmpKZmMwUlFWV3gwUTJ0TFRsbDJaM2gxZFVOSVkzbEVORUpRZVUxMVREQnpOMVowWDI1MWRrVmlUMU54TkRkUk5rVjViMEpRTUZGNmR6RlJSR2RxY1U1eVgwNTBjMDkxWm14R2NUVjBlRkJGT1dGVmFXeFJTMEZYYldwQlVVbHNOVmgwZERZdGFFRlViMWxmUjFWc1EycG1WVkJQV0hkcFVRPT0aIBIaVWd5RTI2NW1rUkk2cE9uS21nbDRBYUFCQ1FIZGgDKGM%3D")).to eq("4qmFsgKXFBIYVUMtbEhKWlIzR3F4bTI0X1ZkX0FKNVl3GvoTRWdsamIyMXRkVzVwZEhtNEFRQ3FBLWNPQ3NBT1VWVlNWRk5zT1hCTldFWXhZbFZhYmxGWGFHRmlXRkpOVFc1V00xWkhTWGRQVlU1RVdUTm9lRkpXV2xWalJXUkdWVEJPYTFwclRrdGpWVW95V2pCWmVtTkVaSFJQVjJjd1YxaFdiVkp0YUZWUFdGSndWakphVVU0eFRYbE5SV1JhWWxad1NWRlVhM2RsYWs1cFRVVjBkbGw2UWtSVk1scHNXSHBvVkZkVWJIRlNSMG8xWWtSa00xa3lkRWhNVlRWd1dERkNkRmRYT1VoalIwWjZaRU14YmxkVmNFaFVNamt6VW0xc2FHUlhTa1ZpVm1SNlpGaHdkMVF4VG5wT01FNTRUVzVLVWxveFFsQmtNRTVRVTFWV1ZXTXliSGxOYkZadlVWVjBObFZJWkZWaE1WVjVVek5XVW1KSFJsZGtSbXN6VTFkS2QxcFZVbGxWTWtaRlZHMWFWMVpzUm5WVU1HaHNaRmQwVDAxc1ZuZFRiR2QzVGtod2VWZERNVkJUUlVwaFYyNUdOazVZYjNkWU1XUkNWVEZuTWxsdE9EQlBSbWhJVjIwNVdsUXdOVzFZTWpWMVVsVktUV051Y0hOTlNHUjVZMWhLYUZsdFVrVmtibEpZWkcxS2MxRlZhSEZVVjNCd1RrYzVSMXBVUWtWbGJHdzJaSHBTTTJGSVNsQlRTRkpvWWpKR2JWTXdOVEJpVjFweFYycENTVk5XV25aVGFscFJUMFJvY2xWR1ZtaGlhMXA1VkZoc2FXRlRNVkJqUkVaWlYxZFNURmRFUmtaalNIQjBaVWhzZVdGdFJYZE5SMUptVGtoT1dtRkVWbFpUYlZaMVpVVmtSVTFYUmxGaGJVNHhWRVJhYms0d2REVlNTR3hIVTJ4c1QxVkVRbEpYUjFaTFRVaEdNMVV3V2tKVFNFNW9Xa1JXUTJOWFpITmFNRnBxWVcxU1QxWXhaRmxoTURWT1ZVWlNTRlpXVmt0UmVrWlNZVmhvZGxVeFRtMVNWMUV3VFVkc2RXTkVXWGxQVjFZd1VqTmtjR0ZWY0VWVE0wNDBZVVphZG1SWGJISmhibGt5WkZkRmVsTkhXWHBVVjNoTVlVUkNhMkpJUlRGU2JsSjRXbXM0TlUxWGJHdE9NMHBIWWpCR2VVNHhaRkpOTVU1cVlrWkNkMDVyWkU5alYxSnFUMWhHUkdJeU5VNVhhazVUVWxoa2VtRnNVWFJPYkd0NFVXeGtVRTE2WkdGYVJUbHhWR3RhWmxJd2VFaFJiWFJOV1hwQ1dFOUdVak5PTUhCc1lWaHdTMlJ0U2xaa01teEdUVmhDYlZOSVdrZGtWVEZKWTBNeGJGZFlTa1ZaTTBWMFpGUk9XV1J0VmxGbFYzaGhZbFZLTW1WcmVHWlVNR3hPVTJ4YVNsUkZUbEJaTVZwRVVqRndkMVJIWkZoWk1taElZVlZLYWt4VVNtRmFiWGQwVTFSTmVGSkVXa2hsU0doWVRraE9NVTFHWkdoT01qRkNWbFZuTkdORlRsSlhTR3gyV1c1U2NXTlVhSFpYV0d4S1QxZDBUVlJYYzNsUk1XYzBVbTV3VTJKRVZqQmxSR3hwVFc1dk1WUllhRXRrZWxFeFkyMDFTMUpIU21aa2FtaG1UbGhPV21SR1l6UmphazVGVlZka01scFhWbk5SV0VKeVpXNU9kRnBIY0VsalZHaFdZekZzWmt4V2EzZFJWVGt5VFZaVk1tSXlNVE5WZVRGTFZFVXhlRkl3VWxkUmJtYzBWRWRzVGxwR1ZrdGpWbXh6VGtaR2ExVXdhekZhYkUwd1pVaHNSazVXWjNsV1IwWmFZekphZGxZeWFIUlBSRnB6VGpOQ1QxZEhSbkJpTUhoVVZEQmtNbVJ1WkZWT01scHRWbTA1ZFdJd1JURlpWa1p1WWxkS05tSXdNVU5hTW5nMlZHa3hTazU2YkhoWFYzQkpWR3Q0UkZZd1ZsbFVNMDVwVFZjd2VtUkhjM2xVVldONlRWVktjVlJIZEVsTlZXZzFZbXRLUW1WcmJGTlZNbmN6WkVWS1VsSkdPVWxOVldSeVpFUnNiRkpyYUhWWWVrSlhaVWhHYkUxclRUQmxWRTQwWVZVMVQxcEZjR3BWTWtwRlpGTXhXVmRJVGpOVE1uaFdWakp3WVZnelZYUlhiR2N3Wkc1T1NFMXFVWHBZTWxKSFRWaFNWMWt4V2taUk1sWndVMjVPZFZsWFRrZFZlazV3VlVkNGIyRlViRFZTUlZwNFlWaHNiRlJxYkcxYVJXeFlWRlpDTVZGV2JHOU9NRWwzVFc1S1YxSlVWalJrUkVKTFpHNW9ibUpHWkhoU1ZsWTFaV3BqTUZJeWVHbFplbVJJVm1reGVGcEVTbWxhTW5oRlpHeGtjVlJ1U2paTlZFWldVa1JXYW1WSVFsRmtSazVEVm10U1UyUklUbFJhU0doV1pHMDVXRTlWVWtOaFdFWXdUbTFrU0ZSdGIzUk5WMXBOWWxoU2VWSldUbkpoUldoSVZEQjBTVTB5Vmt4VWJGWjJWMVZHTmxKVE1ESmlhbEpaWWtSS2RGRlVWbkpoUlZKNFdtcGplRnB0Y0VSTlIwMDFVbXBrTTJRd05XMVZSWGQ1WVVaQ1psRXdXakZTYkVVelkwZG9SazVJU2taWk1XeFRUV3M1ZDJSWFJuaGlSekZyWWpCVk1tSXhXa0phUnpreVUyeG5lRlpXT1hOaU1EVldXa1V4UkZKNlFqQmpXR2hwVmpCVk1sZFlZM3BUVXpGNFVWY3hhMVJ1WkVKUlZHUnZXVlpGTkdOc1NUQmFWVWwwVW14YWNWZEVUbkpYYXpWTFkyMWFSazlIVm5kUmJXeHFVakIwYmxSRlpFWlZSM042WXpKT2Nsa3dTVE5sVmxaWlZFZGtjRTFZUWtkaU1IQXlaVlUxYUdSVlpGZFZibEpRWVZoYVFsWnRkSFpTYTBVelRGVTFTazFYYUZKUk1VcE1WMmt4U1dKNldreGpXRWt4Wkd0U1RXSnNPVmRVYTBaRlZtcEtabU13VWxGV1YzZ3dVVEowVEZSc2JESmFNMmd4WkZWT1NWa3piRVZPUlVwUlpWVXhNVlJFUW5wT01Wb3dXREkxTVdSclZtbFVNVTU0VGtSa1VrNXJWalZpTUVwUlRVWkdObVI2UmxKU1IyUnhZMVUxZVZnd05UQmpNRGt4V20xNFIyTlVWakJsUmtKR1QxZEdWbUZYZUZKVE1FWllZbGR3UWxWVmJITk9WbWd3WkVSWmRHRkZSbFZpTVd4bVVqRldjMUV5Y0cxV1ZrSlFWMGhrY0ZWUlBUMGFJQklhVldkNVJUSTJOVzFyVWtrMmNFOXVTMjFuYkRSQllVRkNRMUZJWkdnREtHTSUzRA%3D%3D") + end + end + + describe "#extract_channel_community_cursor" do + it "correctly extracts a community cursor from a given continuation" do + expect(extract_channel_community_cursor("4qmFsgIsEhhVQ0NqOTU2SUY2MkZiVDdHb3VzemFqOXcaEEVnbGpiMjF0ZFc1cGRIbTQ%3D")).to eq("Egljb21tdW5pdHk=") + expect(extract_channel_community_cursor("4qmFsgJoEhhVQ0NqOTU2SUY2MkZiVDdHb3VzemFqOXcaTEVnbGpiMjF0ZFc1cGRIbTRBUUNxQXlRYUlCSWFWV2QzY0U5TlFtVndXRWRqY2xoc1VIZzJXalJCWVVGQ1ExRklaR2dES0FBJTI1M0Q%3D")).to eq("Egljb21tdW5pdHm4AQCqAyQaIEhkaAMSGlVnd3BPTUJlcFhHY3JYbFB4Nlo0QWFBQkNRKAA=") + + expect(extract_channel_community_cursor("4qmFsgJoEhhVQy1sSEpaUjNHcXhtMjRfVmRfQUo1WXcaTEVnbGpiMjF0ZFc1cGRIbTRBUUNxQXlRYUlCSWFWV2Q1UlRJMk5XMXJVa2syY0U5dVMyMW5iRFJCWVVGQ1ExRklaR2dES0FBJTI1M0Q%3D")).to eq("Egljb21tdW5pdHm4AQCqAyQaIEhkaAMSGlVneUUyNjVta1JJNnBPbkttZ2w0QWFBQkNRKAA=") + expect(extract_channel_community_cursor("4qmFsgKZFBIYVUMtbEhKWlIzR3F4bTI0X1ZkX0FKNVl3GvwTRWdsamIyMXRkVzVwZEhtNEFRQ3FBLWNPQ3NBT1VWVlNWRk5zT1hCTldFWXhZbFZhYmxGWGFHRmlXRkpOVFc1V00xWkhTWGRQVlU1RVdUTm9lRkpXV2xWalJXUkdWVEJPYTFwclRrdGpWVW95V2pCWmVtTkVaSFJQVjJjd1YxaFdiVkp0YUZWUFdGSndWakphVVU0eFRYbE5SV1JhWWxad1NWRlVhM2RsYWs1cFRVVjBkbGw2UWtSVk1scHNXSHBvVkZkVWJIRlNSMG8xWWtSa00xa3lkRWhNVlRWd1dERkNkRmRYT1VoalIwWjZaRU14YmxkVmNFaFVNamt6VW0xc2FHUlhTa1ZpVm1SNlpGaHdkMVF4VG5wT01FNTRUVzVLVWxveFFsQmtNRTVRVTFWV1ZXTXliSGxOYkZadlVWVjBObFZJWkZWaE1WVjVVek5XVW1KSFJsZGtSbXN6VTFkS2QxcFZVbGxWTWtaRlZHMWFWMVpzUm5WVU1HaHNaRmQwVDAxc1ZuZFRiR2QzVGtod2VWZERNVkJUUlVwaFYyNUdOazVZYjNkWU1XUkNWVEZuTWxsdE9EQlBSbWhJVjIwNVdsUXdOVzFZTWpWMVVsVktUV051Y0hOTlNHUjVZMWhLYUZsdFVrVmtibEpZWkcxS2MxRlZhSEZVVjNCd1RrYzVSMXBVUWtWbGJHdzJaSHBTTTJGSVNsQlRTRkpvWWpKR2JWTXdOVEJpVjFweFYycENTVk5XV25aVGFscFJUMFJvY2xWR1ZtaGlhMXA1VkZoc2FXRlRNVkJqUkVaWlYxZFNURmRFUmtaalNIQjBaVWhzZVdGdFJYZE5SMUptVGtoT1dtRkVWbFpUYlZaMVpVVmtSVTFYUmxGaGJVNHhWRVJhYms0d2REVlNTR3hIVTJ4c1QxVkVRbEpYUjFaTFRVaEdNMVV3V2tKVFNFNW9Xa1JXUTJOWFpITmFNRnBxWVcxU1QxWXhaRmxoTURWT1ZVWlNTRlpXVmt0UmVrWlNZVmhvZGxVeFRtMVNWMUV3VFVkc2RXTkVXWGxQVjFZd1VqTmtjR0ZWY0VWVE0wNDBZVVphZG1SWGJISmhibGt5WkZkRmVsTkhXWHBVVjNoTVlVUkNhMkpJUlRGU2JsSjRXbXM0TlUxWGJHdE9NMHBIWWpCR2VVNHhaRkpOTVU1cVlrWkNkMDVyWkU5alYxSnFUMWhHUkdJeU5VNVhhazVUVWxoa2VtRnNVWFJPYkd0NFVXeGtVRTE2WkdGYVJUbHhWR3RhWmxJd2VFaFJiWFJOV1hwQ1dFOUdVak5PTUhCc1lWaHdTMlJ0U2xaa01teEdUVmhDYlZOSVdrZGtWVEZKWTBNeGJGZFlTa1ZaTTBWMFpGUk9XV1J0VmxGbFYzaGhZbFZLTW1WcmVHWlVNR3hPVTJ4YVNsUkZUbEJaTVZwRVVqRndkMVJIWkZoWk1taElZVlZLYWt4VVNtRmFiWGQwVTFSTmVGSkVXa2hsU0doWVRraE9NVTFHWkdoT01qRkNWbFZuTkdORlRsSlhTR3gyV1c1U2NXTlVhSFpYV0d4S1QxZDBUVlJYYzNsUk1XYzBVbTV3VTJKRVZqQmxSR3hwVFc1dk1WUllhRXRrZWxFeFkyMDFTMUpIU21aa2FtaG1UbGhPV21SR1l6UmphazVGVlZka01scFhWbk5SV0VKeVpXNU9kRnBIY0VsalZHaFdZekZzWmt4V2EzZFJWVGt5VFZaVk1tSXlNVE5WZVRGTFZFVXhlRkl3VWxkUmJtYzBWRWRzVGxwR1ZrdGpWbXh6VGtaR2ExVXdhekZhYkUwd1pVaHNSazVXWjNsV1IwWmFZekphZGxZeWFIUlBSRnB6VGpOQ1QxZEhSbkJpTUhoVVZEQmtNbVJ1WkZWT01scHRWbTA1ZFdJd1JURlpWa1p1WWxkS05tSXdNVU5hTW5nMlZHa3hTazU2YkhoWFYzQkpWR3Q0UkZZd1ZsbFVNMDVwVFZjd2VtUkhjM2xVVldONlRWVktjVlJIZEVsTlZXZzFZbXRLUW1WcmJGTlZNbmN6WkVWS1VsSkdPVWxOVldSeVpFUnNiRkpyYUhWWWVrSlhaVWhHYkUxclRUQmxWRTQwWVZVMVQxcEZjR3BWTWtwRlpGTXhXVmRJVGpOVE1uaFdWakp3WVZnelZYUlhiR2N3Wkc1T1NFMXFVWHBZTWxKSFRWaFNWMWt4V2taUk1sWndVMjVPZFZsWFRrZFZlazV3VlVkNGIyRlViRFZTUlZwNFlWaHNiRlJxYkcxYVJXeFlWRlpDTVZGV2JHOU9NRWwzVFc1S1YxSlVWalJrUkVKTFpHNW9ibUpHWkhoU1ZsWTFaV3BqTUZJeWVHbFplbVJJVm1reGVGcEVTbWxhTW5oRlpHeGtjVlJ1U2paTlZFWldVa1JXYW1WSVFsRmtSazVEVm10U1UyUklUbFJhU0doV1pHMDVXRTlWVWtOaFdFWXdUbTFrU0ZSdGIzUk5WMXBOWWxoU2VWSldUbkpoUldoSVZEQjBTVTB5Vmt4VWJGWjJWMVZHTmxKVE1ESmlhbEpaWWtSS2RGRlVWbkpoUlZKNFdtcGplRnB0Y0VSTlIwMDFVbXBrTTJRd05XMVZSWGQ1WVVaQ1psRXdXakZTYkVVelkwZG9SazVJU2taWk1XeFRUV3M1ZDJSWFJuaGlSekZyWWpCVk1tSXhXa0phUnpreVUyeG5lRlpXT1hOaU1EVldXa1V4UkZKNlFqQmpXR2hwVmpCVk1sZFlZM3BUVXpGNFVWY3hhMVJ1WkVKUlZHUnZXVlpGTkdOc1NUQmFWVWwwVW14YWNWZEVUbkpYYXpWTFkyMWFSazlIVm5kUmJXeHFVakIwYmxSRlpFWlZSM042WXpKT2Nsa3dTVE5sVmxaWlZFZGtjRTFZUWtkaU1IQXlaVlUxYUdSVlpGZFZibEpRWVZoYVFsWnRkSFpTYTBVelRGVTFTazFYYUZKUk1VcE1WMmt4U1dKNldreGpXRWt4Wkd0U1RXSnNPVmRVYTBaRlZtcEtabU13VWxGV1YzZ3dVVEowVEZSc2JESmFNMmd4WkZWT1NWa3piRVZPUlVwUlpWVXhNVlJFUW5wT01Wb3dXREkxTVdSclZtbFVNVTU0VGtSa1VrNXJWalZpTUVwUlRVWkdObVI2UmxKU1IyUnhZMVUxZVZnd05UQmpNRGt4V20xNFIyTlVWakJsUmtKR1QxZEdWbUZYZUZKVE1FWllZbGR3UWxWVmJITk9WbWd3WkVSWmRHRkZSbFZpTVd4bVVqRldjMUV5Y0cxV1ZrSlFWMGhrY0ZWUlBUMGFJQklhVldkNVJUSTJOVzFyVWtrMmNFOXVTMjFuYkRSQllVRkNRMUZJWkdnREtHTSUyNTNE")).to eq("Egljb21tdW5pdHm4AQCqA-kOCsAOUVVSVFNsOXBNWEYxYlVablFXaGFiWFJNTW5WM1ZHSXdPVU5EWTNoeFJWWlVjRWRGVTBOa1prTktjVUoyWjBZemNEZHRPV2cwV1hWbVJtaFVPWFJwVjJaUU4xTXlNRWRaYlZwSVFUa3dlak5pTUV0dll6QkRVMlpsWHpoVFdUbHFSR0o1YkRkM1kydEhMVTVwWDFCdFdXOUhjR0Z6ZEMxbldVcEhUMjkzUm1saGRXSkViVmR6ZFhwd1QxTnpOME54TW5KUloxQlBkME5QU1VWVWMybHlNbFZvUVV0NlVIZFVhMVV5UzNWUmJHRldkRmszU1dKd1pVUllVMkZFVG1aV1ZsRnVUMGhsZFd0T01sVndTbGd3TkhweVdDMVBTRUphV25GNk5Yb3dYMWRCVTFnMlltODBPRmhIV205WlQwNW1YMjV1UlVKTWNucHNNSGR5Y1hKaFltUkVkblJYZG1Kc1FVaHFUV3BwTkc5R1pUQkVlbGw2ZHpSM2FISlBTSFJoYjJGbVMwNTBiV1pxV2pCSVNWWnZTalpRT0RoclVGVmhia1p5VFhsaWFTMVBjREZZV1dSTFdERkZjSHB0ZUhseWFtRXdNR1JmTkhOWmFEVlZTbVZ1ZUVkRU1XRlFhbU4xVERabk4wdDVSSGxHU2xsT1VEQlJXR1ZLTUhGM1UwWkJTSE5oWkRWQ2NXZHNaMFpqYW1ST1YxZFlhMDVOVUZSSFZWVktRekZSYVhodlUxTm1SV1EwTUdsdWNEWXlPV1YwUjNkcGFVcEVTM040YUZadmRXbHJhblkyZFdFelNHWXpUV3hMYURCa2JIRTFSblJ4Wms4NU1XbGtOM0pHYjBGeU4xZFJNMU5qYkZCd05rZE9jV1JqT1hGRGIyNU5Xak5TUlhkemFsUXRObGt4UWxkUE16ZGFaRTlxVGtaZlIweEhRbXRNWXpCWE9GUjNOMHBsYVhwS2RtSlZkMmxGTVhCbVNIWkdkVTFJY0MxbFdYSkVZM0V0ZFROWWRtVlFlV3hhYlVKMmVreGZUMGxOU2xaSlRFTlBZMVpEUjFwd1RHZFhZMmhIYVVKakxUSmFabXd0U1RNeFJEWkhlSGhYTkhOMU1GZGhOMjFCVlVnNGNFTlJXSGx2WW5ScWNUaHZXWGxKT1d0TVRXc3lRMWc0Um5wU2JEVjBlRGxpTW5vMVRYaEtkelExY201S1JHSmZkamhmTlhOWmRGYzRjak5FVVdkMlpXVnNRWEJyZW5OdFpHcEljVGhWYzFsZkxWa3dRVTkyTVZVMmIyMTNVeTFLVEUxeFIwUldRbmc0VEdsTlpGVktjVmxzTkZGa1UwazFabE0wZUhsRk5WZ3lWR0ZaYzJadlYyaHRPRFpzTjNCT1dHRnBiMHhUVDBkMmRuZFVOMlptVm05dWIwRTFZVkZuYldKNmIwMUNaMng2VGkxSk56bHhXV3BJVGt4RFYwVllUM05pTVcwemRHc3lUVWN6TVVKcVRHdElNVWg1YmtKQmVrbFNVMnczZEVKUlJGOUlNVWRyZERsbFJraHVYekJXZUhGbE1rTTBlVE40YVU1T1pFcGpVMkpFZFMxWVdITjNTMnhWVjJwYVgzVXRXbGcwZG5OSE1qUXpYMlJHTVhSV1kxWkZRMlZwU25OdVlXTkdVek5wVUd4b2FUbDVSRVp4YVhsbFRqbG1aRWxYVFZCMVFWbG9OMEl3TW5KV1JUVjRkREJLZG5obmJGZHhSVlY1ZWpjMFIyeGlZemRIVmkxeFpESmlaMnhFZGxkcVRuSjZNVEZWUkRWamVIQlFkRk5DVmtSU2RITlRaSGhWZG05WE9VUkNhWEYwTm1kSFRtb3RNV1pNYlhSeVJWTnJhRWhIVDB0SU0yVkxUbFZ2V1VGNlJTMDJialJZYkRKdFFUVnJhRVJ4WmpjeFptcERNR001UmpkM2QwNW1VRXd5YUZCZlEwWjFSbEUzY0doRk5ISkZZMWxTTWs5d2RXRnhiRzFrYjBVMmIxWkJaRzkyU2xneFZWOXNiMDVWWkUxRFJ6QjBjWGhpVjBVMldYY3pTUzF4UVcxa1RuZEJRVGRvWVZFNGNsSTBaVUl0UmxacVdETnJXazVLY21aRk9HVndRbWxqUjB0blRFZEZVR3N6YzJOclkwSTNlVlZZVEdkcE1YQkdiMHAyZVU1aGRVZFdVblJQYVhaQlZtdHZSa0UzTFU1Sk1XaFJRMUpMV2kxSWJ6WkxjWEkxZGtSTWJsOVdUa0ZFVmpKZmMwUlFWV3gwUTJ0TFRsbDJaM2gxZFVOSVkzbEVORUpRZVUxMVREQnpOMVowWDI1MWRrVmlUMU54TkRkUk5rVjViMEpRTUZGNmR6RlJSR2RxY1U1eVgwNTBjMDkxWm14R2NUVjBlRkJGT1dGVmFXeFJTMEZYYldwQlVVbHNOVmgwZERZdGFFRlViMWxmUjFWc1EycG1WVkJQV0hkcFVRPT0aIhIcVWd5RTI2NW1rUkk2cE9uS21nbDRBYUFCQ1E9PUhkaAMoYw==") + end + end + + describe "#sign_token" do + it "correctly signs a given hash" do + token = { + "session" => "v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "expires" => 1554680038, + "scopes" => [ + ":notifications", + ":subscriptions/*", + "GET:tokens*", + ], + "signature" => "f__2hS20th8pALF305PJFK-D2aVtvefNnQheILHD2vU=", + } + expect(sign_token("SECRET_KEY", token)).to eq(token["signature"]) + + token = { + "session" => "v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "scopes" => [":notifications", "POST:subscriptions/*"], + "signature" => "fNvXoT0MRAL9eE6lTE33CEg8HitYJDOL9a22rSN2Ihg=", + } + expect(sign_token("SECRET_KEY", token)).to eq(token["signature"]) + end + end +end diff --git a/spec/invidious/search/iv_filters_spec.cr b/spec/invidious/search/iv_filters_spec.cr new file mode 100644 index 00000000..3cefafa1 --- /dev/null +++ b/spec/invidious/search/iv_filters_spec.cr @@ -0,0 +1,370 @@ +require "../../../src/invidious/search/filters" + +require "http/params" +require "spectator" + +Spectator.configure do |config| + config.fail_blank + config.randomize +end + +FEATURES_TEXT = { + Invidious::Search::Filters::Features::Live => "live", + Invidious::Search::Filters::Features::FourK => "4k", + Invidious::Search::Filters::Features::HD => "hd", + Invidious::Search::Filters::Features::Subtitles => "subtitles", + Invidious::Search::Filters::Features::CCommons => "commons", + Invidious::Search::Filters::Features::ThreeSixty => "360", + Invidious::Search::Filters::Features::VR180 => "vr180", + Invidious::Search::Filters::Features::ThreeD => "3d", + Invidious::Search::Filters::Features::HDR => "hdr", + Invidious::Search::Filters::Features::Location => "location", + Invidious::Search::Filters::Features::Purchased => "purchased", +} + +Spectator.describe Invidious::Search::Filters do + # ------------------- + # Decode (legacy) + # ------------------- + + describe "#from_legacy_filters" do + it "Decodes channel: filter" do + query = "test channel:UC123456 request" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new) + expect(chan).to eq("UC123456") + expect(qury).to eq("test request") + expect(subs).to be_false + end + + it "Decodes user: filter" do + query = "user:LinusTechTips broke something (again)" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new) + expect(chan).to eq("LinusTechTips") + expect(qury).to eq("broke something (again)") + expect(subs).to be_false + end + + it "Decodes type: filter" do + Invidious::Search::Filters::Type.each do |value| + query = "Eiffel 65 - Blue [1 Hour] type:#{value}" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new(type: value)) + expect(chan).to eq("") + expect(qury).to eq("Eiffel 65 - Blue [1 Hour]") + expect(subs).to be_false + end + end + + it "Decodes content_type: filter" do + Invidious::Search::Filters::Type.each do |value| + query = "I like to watch content_type:#{value}" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new(type: value)) + expect(chan).to eq("") + expect(qury).to eq("I like to watch") + expect(subs).to be_false + end + end + + it "Decodes date: filter" do + Invidious::Search::Filters::Date.each do |value| + query = "This date:#{value} is old!" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new(date: value)) + expect(chan).to eq("") + expect(qury).to eq("This is old!") + expect(subs).to be_false + end + end + + it "Decodes duration: filter" do + Invidious::Search::Filters::Duration.each do |value| + query = "This duration:#{value} is old!" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new(duration: value)) + expect(chan).to eq("") + expect(qury).to eq("This is old!") + expect(subs).to be_false + end + end + + it "Decodes feature: filter" do + Invidious::Search::Filters::Features.each do |value| + string = FEATURES_TEXT[value] + query = "I like my precious feature:#{string} ^^" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new(features: value)) + expect(chan).to eq("") + expect(qury).to eq("I like my precious ^^") + expect(subs).to be_false + end + end + + it "Decodes features: filter" do + query = "This search has many features:vr180,cc,hdr :o" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + features = Invidious::Search::Filters::Features.flags(HDR, VR180, CCommons) + + expect(fltr).to eq(described_class.new(features: features)) + expect(chan).to eq("") + expect(qury).to eq("This search has many :o") + expect(subs).to be_false + end + + it "Decodes sort: filter" do + Invidious::Search::Filters::Sort.each do |value| + query = "Computer? sort:#{value} my files!" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new(sort: value)) + expect(chan).to eq("") + expect(qury).to eq("Computer? my files!") + expect(subs).to be_false + end + end + + it "Decodes subscriptions: filter" do + query = "enable subscriptions:true" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new) + expect(chan).to eq("") + expect(qury).to eq("enable") + expect(subs).to be_true + end + + it "Ignores junk data" do + query = "duration:I sort:like type:cleaning features:stuff date:up!" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new) + expect(chan).to eq("") + expect(qury).to eq("") + expect(subs).to be_false + end + + it "Keeps unknown keys" do + query = "to:be or:not to:be" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new) + expect(chan).to eq("") + expect(qury).to eq("to:be or:not to:be") + expect(subs).to be_false + end + end + + # ------------------- + # Decode (URL) + # ------------------- + + describe "#from_iv_params" do + it "Decodes type= filter" do + Invidious::Search::Filters::Type.each do |value| + params = HTTP::Params.parse("type=#{value}") + + expect(described_class.from_iv_params(params)) + .to eq(described_class.new(type: value)) + end + end + + it "Decodes date= filter" do + Invidious::Search::Filters::Date.each do |value| + params = HTTP::Params.parse("date=#{value}") + + expect(described_class.from_iv_params(params)) + .to eq(described_class.new(date: value)) + end + end + + it "Decodes duration= filter" do + Invidious::Search::Filters::Duration.each do |value| + params = HTTP::Params.parse("duration=#{value}") + + expect(described_class.from_iv_params(params)) + .to eq(described_class.new(duration: value)) + end + end + + it "Decodes features= filter (single)" do + Invidious::Search::Filters::Features.each do |value| + string = described_class.format_features(value) + params = HTTP::Params.parse("features=#{string}") + + expect(described_class.from_iv_params(params)) + .to eq(described_class.new(features: value)) + end + end + + it "Decodes features= filter (multiple - comma separated)" do + features = Invidious::Search::Filters::Features.flags(HDR, VR180, CCommons) + params = HTTP::Params.parse("features=vr180%2Ccc%2Chdr") # %2C is a comma + + expect(described_class.from_iv_params(params)) + .to eq(described_class.new(features: features)) + end + + it "Decodes features= filter (multiple - URL parameters)" do + features = Invidious::Search::Filters::Features.flags(ThreeSixty, HD, FourK) + params = HTTP::Params.parse("features=4k&features=360&features=hd") + + expect(described_class.from_iv_params(params)) + .to eq(described_class.new(features: features)) + end + + it "Decodes sort= filter" do + Invidious::Search::Filters::Sort.each do |value| + params = HTTP::Params.parse("sort=#{value}") + + expect(described_class.from_iv_params(params)) + .to eq(described_class.new(sort: value)) + end + end + + it "Ignores junk data" do + params = HTTP::Params.parse("foo=bar&sort=views&answer=42&type=channel") + + expect(described_class.from_iv_params(params)).to eq( + described_class.new( + sort: Invidious::Search::Filters::Sort::Views, + type: Invidious::Search::Filters::Type::Channel + ) + ) + end + end + + # ------------------- + # Encode (URL) + # ------------------- + + describe "#to_iv_params" do + it "Encodes date filter" do + Invidious::Search::Filters::Date.each do |value| + filters = described_class.new(date: value) + params = filters.to_iv_params + + if value.none? + expect("#{params}").to eq("") + else + expect("#{params}").to eq("date=#{value.to_s.underscore}") + end + end + end + + it "Encodes type filter" do + Invidious::Search::Filters::Type.each do |value| + filters = described_class.new(type: value) + params = filters.to_iv_params + + if value.all? + expect("#{params}").to eq("") + else + expect("#{params}").to eq("type=#{value.to_s.underscore}") + end + end + end + + it "Encodes duration filter" do + Invidious::Search::Filters::Duration.each do |value| + filters = described_class.new(duration: value) + params = filters.to_iv_params + + if value.none? + expect("#{params}").to eq("") + else + expect("#{params}").to eq("duration=#{value.to_s.underscore}") + end + end + end + + it "Encodes features filter (single)" do + Invidious::Search::Filters::Features.each do |value| + filters = described_class.new(features: value) + + expect("#{filters.to_iv_params}") + .to eq("features=" + FEATURES_TEXT[value]) + end + end + + it "Encodes features filter (multiple)" do + features = Invidious::Search::Filters::Features.flags(Subtitles, Live, ThreeSixty) + filters = described_class.new(features: features) + + expect("#{filters.to_iv_params}") + .to eq("features=live%2Csubtitles%2C360") # %2C is a comma + end + + it "Encodes sort filter" do + Invidious::Search::Filters::Sort.each do |value| + filters = described_class.new(sort: value) + params = filters.to_iv_params + + if value.relevance? + expect("#{params}").to eq("") + else + expect("#{params}").to eq("sort=#{value.to_s.underscore}") + end + end + end + + it "Encodes multiple filters" do + filters = described_class.new( + date: Invidious::Search::Filters::Date::Today, + duration: Invidious::Search::Filters::Duration::Medium, + features: Invidious::Search::Filters::Features.flags(Location, Purchased), + sort: Invidious::Search::Filters::Sort::Relevance + ) + + params = filters.to_iv_params + + # Check the `date` param + expect(params).to have_key("date") + expect(params.fetch_all("date")).to contain_exactly("today") + + # Check the `type` param + expect(params).to_not have_key("type") + expect(params["type"]?).to be_nil + + # Check the `duration` param + expect(params).to have_key("duration") + expect(params.fetch_all("duration")).to contain_exactly("medium") + + # Check the `features` param + expect(params).to have_key("features") + expect(params.fetch_all("features")).to contain_exactly("location,purchased") + + # Check the `sort` param + expect(params).to_not have_key("sort") + expect(params["sort"]?).to be_nil + + # Check if there aren't other parameters + params.delete("date") + params.delete("duration") + params.delete("features") + + expect(params).to be_empty + end + end +end diff --git a/spec/invidious/search/query_spec.cr b/spec/invidious/search/query_spec.cr new file mode 100644 index 00000000..063b69f1 --- /dev/null +++ b/spec/invidious/search/query_spec.cr @@ -0,0 +1,242 @@ +require "../../../src/invidious/search/filters" +require "../../../src/invidious/search/query" + +require "http/params" +require "spectator" + +Spectator.configure do |config| + config.fail_blank + config.randomize +end + +Spectator.describe Invidious::Search::Query do + describe Type::Regular do + # ------------------- + # Query parsing + # ------------------- + + it "parses query with URL prameters (q)" do + query = described_class.new( + HTTP::Params.parse("q=What+is+Love+10+hour&type=video&duration=long"), + Invidious::Search::Query::Type::Regular, nil + ) + + expect(query.type).to eq(Invidious::Search::Query::Type::Regular) + expect(query.channel).to be_empty + expect(query.text).to eq("What is Love 10 hour") + + expect(query.filters).to eq( + Invidious::Search::Filters.new( + type: Invidious::Search::Filters::Type::Video, + duration: Invidious::Search::Filters::Duration::Long + ) + ) + end + + it "parses query with URL prameters (search_query)" do + query = described_class.new( + HTTP::Params.parse("search_query=What+is+Love+10+hour&type=video&duration=long"), + Invidious::Search::Query::Type::Regular, nil + ) + + expect(query.type).to eq(Invidious::Search::Query::Type::Regular) + expect(query.channel).to be_empty + expect(query.text).to eq("What is Love 10 hour") + + expect(query.filters).to eq( + Invidious::Search::Filters.new( + type: Invidious::Search::Filters::Type::Video, + duration: Invidious::Search::Filters::Duration::Long + ) + ) + end + + it "parses query with legacy filters (q)" do + query = described_class.new( + HTTP::Params.parse("q=Nyan+cat+duration:long"), + Invidious::Search::Query::Type::Regular, nil + ) + + expect(query.type).to eq(Invidious::Search::Query::Type::Regular) + expect(query.channel).to be_empty + expect(query.text).to eq("Nyan cat") + + expect(query.filters).to eq( + Invidious::Search::Filters.new( + duration: Invidious::Search::Filters::Duration::Long + ) + ) + end + + it "parses query with legacy filters (search_query)" do + query = described_class.new( + HTTP::Params.parse("search_query=Nyan+cat+duration:long"), + Invidious::Search::Query::Type::Regular, nil + ) + + expect(query.type).to eq(Invidious::Search::Query::Type::Regular) + expect(query.channel).to be_empty + expect(query.text).to eq("Nyan cat") + + expect(query.filters).to eq( + Invidious::Search::Filters.new( + duration: Invidious::Search::Filters::Duration::Long + ) + ) + end + + it "parses query with both URL params and legacy filters" do + query = described_class.new( + HTTP::Params.parse("q=Vamos+a+la+playa+duration:long&type=Video&date=year"), + Invidious::Search::Query::Type::Regular, nil + ) + + expect(query.type).to eq(Invidious::Search::Query::Type::Regular) + expect(query.channel).to be_empty + expect(query.text).to eq("Vamos a la playa duration:long") + + expect(query.filters).to eq( + Invidious::Search::Filters.new( + type: Invidious::Search::Filters::Type::Video, + date: Invidious::Search::Filters::Date::Year + ) + ) + end + + # ------------------- + # Type switching + # ------------------- + + it "switches to channel search (URL param)" do + query = described_class.new( + HTTP::Params.parse("q=thunderbolt+4&channel=UC0vBXGSyV14uvJ4hECDOl0Q"), + Invidious::Search::Query::Type::Regular, nil + ) + + expect(query.type).to eq(Invidious::Search::Query::Type::Channel) + expect(query.channel).to eq("UC0vBXGSyV14uvJ4hECDOl0Q") + expect(query.text).to eq("thunderbolt 4") + expect(query.filters.default?).to be_true + end + + it "switches to channel search (legacy)" do + query = described_class.new( + HTTP::Params.parse("q=channel%3AUCRPdsCVuH53rcbTcEkuY4uQ+rdna3"), + Invidious::Search::Query::Type::Regular, nil + ) + + expect(query.type).to eq(Invidious::Search::Query::Type::Channel) + expect(query.channel).to eq("UCRPdsCVuH53rcbTcEkuY4uQ") + expect(query.text).to eq("rdna3") + expect(query.filters.default?).to be_true + end + + it "switches to subscriptions search" do + query = described_class.new( + HTTP::Params.parse("q=subscriptions:true+tunak+tunak+tun"), + Invidious::Search::Query::Type::Regular, nil + ) + + expect(query.type).to eq(Invidious::Search::Query::Type::Subscriptions) + expect(query.channel).to be_empty + expect(query.text).to eq("tunak tunak tun") + expect(query.filters.default?).to be_true + end + end + + describe Type::Channel do + it "ignores extra parameters" do + query = described_class.new( + HTTP::Params.parse("q=Take+on+me+channel%3AUC12345679&type=video&date=year"), + Invidious::Search::Query::Type::Channel, nil + ) + + expect(query.type).to eq(Invidious::Search::Query::Type::Channel) + expect(query.channel).to be_empty + expect(query.text).to eq("Take on me") + expect(query.filters.default?).to be_true + end + end + + describe Type::Subscriptions do + it "works" do + query = described_class.new( + HTTP::Params.parse("q=Harlem+shake&type=video&date=year"), + Invidious::Search::Query::Type::Subscriptions, nil + ) + + expect(query.type).to eq(Invidious::Search::Query::Type::Subscriptions) + expect(query.channel).to be_empty + expect(query.text).to eq("Harlem shake") + + expect(query.filters).to eq( + Invidious::Search::Filters.new( + type: Invidious::Search::Filters::Type::Video, + date: Invidious::Search::Filters::Date::Year + ) + ) + end + end + + describe Type::Playlist do + it "ignores extra parameters" do + query = described_class.new( + HTTP::Params.parse("q=Harlem+shake+type:video+date:year&channel=UC12345679"), + Invidious::Search::Query::Type::Playlist, nil + ) + + expect(query.type).to eq(Invidious::Search::Query::Type::Playlist) + expect(query.channel).to be_empty + expect(query.text).to eq("Harlem shake") + + expect(query.filters).to eq( + Invidious::Search::Filters.new( + type: Invidious::Search::Filters::Type::Video, + date: Invidious::Search::Filters::Date::Year + ) + ) + end + end + + describe "#to_http_params" do + it "formats regular search" do + query = described_class.new( + HTTP::Params.parse("q=The+Simpsons+hiding+in+bush&duration=short"), + Invidious::Search::Query::Type::Regular, nil + ) + + params = query.to_http_params + + expect(params).to have_key("duration") + expect(params["duration"]?).to eq("short") + + expect(params).to have_key("q") + expect(params["q"]?).to eq("The Simpsons hiding in bush") + + # Check if there aren't other parameters + params.delete("duration") + params.delete("q") + expect(params).to be_empty + end + + it "formats channel search" do + query = described_class.new( + HTTP::Params.parse("q=channel:UC2DjFE7Xf11URZqWBigcVOQ%20multimeter"), + Invidious::Search::Query::Type::Regular, nil + ) + + params = query.to_http_params + + expect(params).to have_key("channel") + expect(params["channel"]?).to eq("UC2DjFE7Xf11URZqWBigcVOQ") + + expect(params).to have_key("q") + expect(params["q"]?).to eq("multimeter") + + # Check if there aren't other parameters + params.delete("channel") + params.delete("q") + expect(params).to be_empty + end + end +end diff --git a/spec/invidious/search/yt_filters_spec.cr b/spec/invidious/search/yt_filters_spec.cr new file mode 100644 index 00000000..8abed5ce --- /dev/null +++ b/spec/invidious/search/yt_filters_spec.cr @@ -0,0 +1,143 @@ +require "../../../src/invidious/search/filters" + +require "http/params" +require "spectator" + +Spectator.configure do |config| + config.fail_blank + config.randomize +end + +# Encoded filter values are extracted from the search +# page of Youtube with any browser devtools HTML inspector. + +DATE_FILTERS = { + Invidious::Search::Filters::Date::Hour => "EgIIAfABAQ%3D%3D", + Invidious::Search::Filters::Date::Today => "EgIIAvABAQ%3D%3D", + Invidious::Search::Filters::Date::Week => "EgIIA_ABAQ%3D%3D", + Invidious::Search::Filters::Date::Month => "EgIIBPABAQ%3D%3D", + Invidious::Search::Filters::Date::Year => "EgIIBfABAQ%3D%3D", +} + +TYPE_FILTERS = { + Invidious::Search::Filters::Type::Video => "EgIQAfABAQ%3D%3D", + Invidious::Search::Filters::Type::Channel => "EgIQAvABAQ%3D%3D", + Invidious::Search::Filters::Type::Playlist => "EgIQA_ABAQ%3D%3D", + Invidious::Search::Filters::Type::Movie => "EgIQBPABAQ%3D%3D", +} + +DURATION_FILTERS = { + Invidious::Search::Filters::Duration::Short => "EgIYAfABAQ%3D%3D", + Invidious::Search::Filters::Duration::Medium => "EgIYA_ABAQ%3D%3D", + Invidious::Search::Filters::Duration::Long => "EgIYAvABAQ%3D%3D", +} + +FEATURE_FILTERS = { + Invidious::Search::Filters::Features::Live => "EgJAAfABAQ%3D%3D", + Invidious::Search::Filters::Features::FourK => "EgJwAfABAQ%3D%3D", + Invidious::Search::Filters::Features::HD => "EgIgAfABAQ%3D%3D", + Invidious::Search::Filters::Features::Subtitles => "EgIoAfABAQ%3D%3D", + Invidious::Search::Filters::Features::CCommons => "EgIwAfABAQ%3D%3D", + Invidious::Search::Filters::Features::ThreeSixty => "EgJ4AfABAQ%3D%3D", + Invidious::Search::Filters::Features::VR180 => "EgPQAQHwAQE%3D", + Invidious::Search::Filters::Features::ThreeD => "EgI4AfABAQ%3D%3D", + Invidious::Search::Filters::Features::HDR => "EgPIAQHwAQE%3D", + Invidious::Search::Filters::Features::Location => "EgO4AQHwAQE%3D", + Invidious::Search::Filters::Features::Purchased => "EgJIAfABAQ%3D%3D", +} + +SORT_FILTERS = { + Invidious::Search::Filters::Sort::Relevance => "8AEB", + Invidious::Search::Filters::Sort::Date => "CALwAQE%3D", + Invidious::Search::Filters::Sort::Views => "CAPwAQE%3D", + Invidious::Search::Filters::Sort::Rating => "CAHwAQE%3D", +} + +Spectator.describe Invidious::Search::Filters do + # ------------------- + # Encode YT params + # ------------------- + + describe "#to_yt_params" do + sample DATE_FILTERS do |value, result| + it "Encodes upload date filter '#{value}'" do + expect(described_class.new(date: value).to_yt_params).to eq(result) + end + end + + sample TYPE_FILTERS do |value, result| + it "Encodes content type filter '#{value}'" do + expect(described_class.new(type: value).to_yt_params).to eq(result) + end + end + + sample DURATION_FILTERS do |value, result| + it "Encodes duration filter '#{value}'" do + expect(described_class.new(duration: value).to_yt_params).to eq(result) + end + end + + sample FEATURE_FILTERS do |value, result| + it "Encodes feature filter '#{value}'" do + expect(described_class.new(features: value).to_yt_params).to eq(result) + end + end + + sample SORT_FILTERS do |value, result| + it "Encodes sort filter '#{value}'" do + expect(described_class.new(sort: value).to_yt_params).to eq(result) + end + end + end + + # ------------------- + # Decode YT params + # ------------------- + + describe "#from_yt_params" do + sample DATE_FILTERS do |value, encoded| + it "Decodes upload date filter '#{value}'" do + params = HTTP::Params.parse("sp=#{encoded}") + + expect(described_class.from_yt_params(params)) + .to eq(described_class.new(date: value)) + end + end + + sample TYPE_FILTERS do |value, encoded| + it "Decodes content type filter '#{value}'" do + params = HTTP::Params.parse("sp=#{encoded}") + + expect(described_class.from_yt_params(params)) + .to eq(described_class.new(type: value)) + end + end + + sample DURATION_FILTERS do |value, encoded| + it "Decodes duration filter '#{value}'" do + params = HTTP::Params.parse("sp=#{encoded}") + + expect(described_class.from_yt_params(params)) + .to eq(described_class.new(duration: value)) + end + end + + sample FEATURE_FILTERS do |value, encoded| + it "Decodes feature filter '#{value}'" do + params = HTTP::Params.parse("sp=#{encoded}") + + expect(described_class.from_yt_params(params)) + .to eq(described_class.new(features: value)) + end + end + + sample SORT_FILTERS do |value, encoded| + it "Decodes sort filter '#{value}'" do + params = HTTP::Params.parse("sp=#{encoded}") + + expect(described_class.from_yt_params(params)) + .to eq(described_class.new(sort: value)) + end + end + end +end diff --git a/spec/invidious/user/imports_spec.cr b/spec/invidious/user/imports_spec.cr new file mode 100644 index 00000000..762ce0d8 --- /dev/null +++ b/spec/invidious/user/imports_spec.cr @@ -0,0 +1,51 @@ +require "spectator" +require "../../../src/invidious/user/imports" + +Spectator.configure do |config| + config.fail_blank + config.randomize +end + +def csv_sample + return <<-CSV + Kanal-ID,Kanal-URL,Kanaltitel + UC0hHW5Y08ggq-9kbrGgWj0A,http://www.youtube.com/channel/UC0hHW5Y08ggq-9kbrGgWj0A,Matias Marolla + UC0vBXGSyV14uvJ4hECDOl0Q,http://www.youtube.com/channel/UC0vBXGSyV14uvJ4hECDOl0Q,Techquickie + UC1sELGmy5jp5fQUugmuYlXQ,http://www.youtube.com/channel/UC1sELGmy5jp5fQUugmuYlXQ,Minecraft + UC9kFnwdCRrX7oTjqKd6-tiQ,http://www.youtube.com/channel/UC9kFnwdCRrX7oTjqKd6-tiQ,LUMOX - Topic + UCBa659QWEk1AI4Tg--mrJ2A,http://www.youtube.com/channel/UCBa659QWEk1AI4Tg--mrJ2A,Tom Scott + UCGu6_XQ64rXPR6nuitMQE_A,http://www.youtube.com/channel/UCGu6_XQ64rXPR6nuitMQE_A,Callcenter Fun + UCGwu0nbY2wSkW8N-cghnLpA,http://www.youtube.com/channel/UCGwu0nbY2wSkW8N-cghnLpA,Jaiden Animations + UCQ0OvZ54pCFZwsKxbltg_tg,http://www.youtube.com/channel/UCQ0OvZ54pCFZwsKxbltg_tg,Methos + UCRE6itj4Jte4manQEu3Y7OA,http://www.youtube.com/channel/UCRE6itj4Jte4manQEu3Y7OA,Chipflake + UCRLc6zsv_d0OEBO8OOkz-DA,http://www.youtube.com/channel/UCRLc6zsv_d0OEBO8OOkz-DA,Kegy + UCSl5Uxu2LyaoAoMMGp6oTJA,http://www.youtube.com/channel/UCSl5Uxu2LyaoAoMMGp6oTJA,Atomic Shrimp + UCXuqSBlHAE6Xw-yeJA0Tunw,http://www.youtube.com/channel/UCXuqSBlHAE6Xw-yeJA0Tunw,Linus Tech Tips + UCZ5XnGb-3t7jCkXdawN2tkA,http://www.youtube.com/channel/UCZ5XnGb-3t7jCkXdawN2tkA,Discord + CSV +end + +Spectator.describe Invidious::User::Import do + it "imports CSV" do + subscriptions = Invidious::User::Import.parse_subscription_export_csv(csv_sample) + + expect(subscriptions).to be_an(Array(String)) + expect(subscriptions.size).to eq(13) + + expect(subscriptions).to contain_exactly( + "UC0hHW5Y08ggq-9kbrGgWj0A", + "UC0vBXGSyV14uvJ4hECDOl0Q", + "UC1sELGmy5jp5fQUugmuYlXQ", + "UC9kFnwdCRrX7oTjqKd6-tiQ", + "UCBa659QWEk1AI4Tg--mrJ2A", + "UCGu6_XQ64rXPR6nuitMQE_A", + "UCGwu0nbY2wSkW8N-cghnLpA", + "UCQ0OvZ54pCFZwsKxbltg_tg", + "UCRE6itj4Jte4manQEu3Y7OA", + "UCRLc6zsv_d0OEBO8OOkz-DA", + "UCSl5Uxu2LyaoAoMMGp6oTJA", + "UCXuqSBlHAE6Xw-yeJA0Tunw", + "UCZ5XnGb-3t7jCkXdawN2tkA", + ).in_order + end +end diff --git a/spec/invidious/utils_spec.cr b/spec/invidious/utils_spec.cr new file mode 100644 index 00000000..7c2c2711 --- /dev/null +++ b/spec/invidious/utils_spec.cr @@ -0,0 +1,46 @@ +require "../spec_helper" + +Spectator.describe "Utils" do + describe "decode_date" do + it "parses short dates (en-US)" do + expect(decode_date("1s ago")).to be_close(Time.utc - 1.second, 500.milliseconds) + expect(decode_date("2min ago")).to be_close(Time.utc - 2.minutes, 500.milliseconds) + expect(decode_date("3h ago")).to be_close(Time.utc - 3.hours, 500.milliseconds) + expect(decode_date("4d ago")).to be_close(Time.utc - 4.days, 500.milliseconds) + expect(decode_date("5w ago")).to be_close(Time.utc - 5.weeks, 500.milliseconds) + expect(decode_date("6mo ago")).to be_close(Time.utc - 6.months, 500.milliseconds) + expect(decode_date("7y ago")).to be_close(Time.utc - 7.years, 500.milliseconds) + end + + it "parses short dates (en-GB)" do + expect(decode_date("55s ago")).to be_close(Time.utc - 55.seconds, 500.milliseconds) + expect(decode_date("44min ago")).to be_close(Time.utc - 44.minutes, 500.milliseconds) + expect(decode_date("22hr ago")).to be_close(Time.utc - 22.hours, 500.milliseconds) + expect(decode_date("1day ago")).to be_close(Time.utc - 1.day, 500.milliseconds) + expect(decode_date("2days ago")).to be_close(Time.utc - 2.days, 500.milliseconds) + expect(decode_date("3wk ago")).to be_close(Time.utc - 3.weeks, 500.milliseconds) + expect(decode_date("11mo ago")).to be_close(Time.utc - 11.months, 500.milliseconds) + expect(decode_date("11yr ago")).to be_close(Time.utc - 11.years, 500.milliseconds) + end + + it "parses long forms (singular)" do + expect(decode_date("1 second ago")).to be_close(Time.utc - 1.second, 500.milliseconds) + expect(decode_date("1 minute ago")).to be_close(Time.utc - 1.minute, 500.milliseconds) + expect(decode_date("1 hour ago")).to be_close(Time.utc - 1.hour, 500.milliseconds) + expect(decode_date("1 day ago")).to be_close(Time.utc - 1.day, 500.milliseconds) + expect(decode_date("1 week ago")).to be_close(Time.utc - 1.week, 500.milliseconds) + expect(decode_date("1 month ago")).to be_close(Time.utc - 1.month, 500.milliseconds) + expect(decode_date("1 year ago")).to be_close(Time.utc - 1.year, 500.milliseconds) + end + + it "parses long forms (plural)" do + expect(decode_date("5 seconds ago")).to be_close(Time.utc - 5.seconds, 500.milliseconds) + expect(decode_date("17 minutes ago")).to be_close(Time.utc - 17.minutes, 500.milliseconds) + expect(decode_date("23 hours ago")).to be_close(Time.utc - 23.hours, 500.milliseconds) + expect(decode_date("3 days ago")).to be_close(Time.utc - 3.days, 500.milliseconds) + expect(decode_date("2 weeks ago")).to be_close(Time.utc - 2.weeks, 500.milliseconds) + expect(decode_date("9 months ago")).to be_close(Time.utc - 9.months, 500.milliseconds) + expect(decode_date("8 years ago")).to be_close(Time.utc - 8.years, 500.milliseconds) + end + end +end diff --git a/spec/invidious/videos/regular_videos_extract_spec.cr b/spec/invidious/videos/regular_videos_extract_spec.cr new file mode 100644 index 00000000..f96703f6 --- /dev/null +++ b/spec/invidious/videos/regular_videos_extract_spec.cr @@ -0,0 +1,168 @@ +require "../../parsers_helper.cr" + +Spectator.describe "parse_video_info" do + it "parses a regular video" do + # Enable mock + _player = load_mock("video/regular_mrbeast.player") + _next = load_mock("video/regular_mrbeast.next") + + raw_data = _player.merge!(_next) + info = parse_video_info("2isYuQZMbdU", raw_data) + + # Some basic verifications + expect(typeof(info)).to eq(Hash(String, JSON::Any)) + + expect(info["videoType"].as_s).to eq("Video") + + # Basic video infos + + expect(info["title"].as_s).to eq("I Gave My 100,000,000th Subscriber An Island") + expect(info["views"].as_i).to eq(220_226_287) + expect(info["likes"].as_i).to eq(6_870_691) + + # For some reason the video length from VideoDetails and the + # one from microformat differs by 1s... + expect(info["lengthSeconds"].as_i).to be_between(930_i64, 931_i64) + + expect(info["published"].as_s).to eq("2022-08-04T00:00:00Z") + + # Extra video infos + + expect(info["allowedRegions"].as_a).to_not be_empty + expect(info["allowedRegions"].as_a.size).to eq(249) + + expect(info["allowedRegions"].as_a).to contain( + "AD", "BA", "BB", "BW", "BY", "EG", "GG", "HN", "NP", "NR", "TR", + "TT", "TV", "TW", "TZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", + "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW" + ) + + expect(info["keywords"].as_a).to be_empty + + expect(info["allowRatings"].as_bool).to be_true + expect(info["isFamilyFriendly"].as_bool).to be_true + expect(info["isListed"].as_bool).to be_true + expect(info["isUpcoming"].as_bool).to be_false + + # Related videos + + expect(info["relatedVideos"].as_a.size).to eq(20) + + expect(info["relatedVideos"][0]["id"]).to eq("krsBRQbOPQ4") + expect(info["relatedVideos"][0]["title"]).to eq("$1 vs $250,000,000 Private Island!") + expect(info["relatedVideos"][0]["author"]).to eq("MrBeast") + expect(info["relatedVideos"][0]["ucid"]).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA") + expect(info["relatedVideos"][0]["view_count"]).to eq("230617484") + expect(info["relatedVideos"][0]["short_view_count"]).to eq("230M") + expect(info["relatedVideos"][0]["author_verified"]).to eq("true") + + # Description + + description = "🚀Launch a store on Shopify, I’ll buy from 100 random stores that do ▸ " + + expect(info["description"].as_s).to start_with(description) + expect(info["shortDescription"].as_s).to start_with(description) + expect(info["descriptionHtml"].as_s).to start_with(description) + + # Video metadata + + expect(info["genre"].as_s).to eq("Entertainment") + expect(info["genreUcid"].as_s?).to be_nil + expect(info["license"].as_s).to be_empty + + # Author infos + + expect(info["author"].as_s).to eq("MrBeast") + expect(info["ucid"].as_s).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA") + + expect(info["authorThumbnail"].as_s).to eq( + "https://yt3.ggpht.com/fxGKYucJAVme-Yz4fsdCroCFCrANWqw0ql4GYuvx8Uq4l_euNJHgE-w9MTkLQA805vWCi-kE0g=s48-c-k-c0x00ffffff-no-rj" + ) + + expect(info["authorVerified"].as_bool).to be_true + expect(info["subCountText"].as_s).to eq("320M") + end + + it "parses a regular video with no descrition/comments" do + # Enable mock + _player = load_mock("video/regular_no-description.player") + _next = load_mock("video/regular_no-description.next") + + raw_data = _player.merge!(_next) + info = parse_video_info("iuevw6218F0", raw_data) + + # Some basic verifications + expect(typeof(info)).to eq(Hash(String, JSON::Any)) + + expect(info["videoType"].as_s).to eq("Video") + + # Basic video infos + + expect(info["title"].as_s).to eq("Chris Rea - Auberge") + expect(info["views"].as_i).to eq(14_324_584) + expect(info["likes"].as_i).to eq(35_870) + expect(info["lengthSeconds"].as_i).to eq(283_i64) + expect(info["published"].as_s).to eq("2012-05-21T00:00:00Z") + + # Extra video infos + + expect(info["allowedRegions"].as_a).to_not be_empty + expect(info["allowedRegions"].as_a.size).to eq(249) + + expect(info["allowedRegions"].as_a).to contain( + "AD", "BA", "BB", "BW", "BY", "EG", "GG", "HN", "NP", "NR", "TR", + "TT", "TV", "TW", "TZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", + "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW" + ) + + expect(info["keywords"].as_a).to_not be_empty + expect(info["keywords"].as_a.size).to eq(4) + + expect(info["keywords"].as_a).to contain_exactly( + "Chris", + "Rea", + "Auberge", + "1991" + ).in_any_order + + expect(info["allowRatings"].as_bool).to be_true + expect(info["isFamilyFriendly"].as_bool).to be_true + expect(info["isListed"].as_bool).to be_true + expect(info["isUpcoming"].as_bool).to be_false + + # Related videos + + expect(info["relatedVideos"].as_a.size).to eq(20) + + expect(info["relatedVideos"][0]["id"]).to eq("gUUdQfnshJ4") + expect(info["relatedVideos"][0]["title"]).to eq("Chris Rea - The Road To Hell 1989 Full Version") + expect(info["relatedVideos"][0]["author"]).to eq("NEA ZIXNH") + expect(info["relatedVideos"][0]["ucid"]).to eq("UCYMEOGcvav3gCgImK2J07CQ") + expect(info["relatedVideos"][0]["view_count"]).to eq("53298661") + expect(info["relatedVideos"][0]["short_view_count"]).to eq("53M") + expect(info["relatedVideos"][0]["author_verified"]).to eq("false") + + # Description + + expect(info["description"].as_s).to eq(" ") + expect(info["shortDescription"].as_s).to be_empty + expect(info["descriptionHtml"].as_s).to eq("") + + # Video metadata + + expect(info["genre"].as_s).to eq("Music") + expect(info["genreUcid"].as_s?).to be_nil + expect(info["license"].as_s).to be_empty + + # Author infos + + expect(info["author"].as_s).to eq("ChrisReaVideos") + expect(info["ucid"].as_s).to eq("UC_5q6nWPbD30-y6oiWF_oNA") + + expect(info["authorThumbnail"].as_s).to eq( + "https://yt3.ggpht.com/ytc/AIdro_n71nsegpKfjeRKwn1JJmK5IVMh_7j5m_h3_1KnUUg=s48-c-k-c0x00ffffff-no-rj" + ) + expect(info["authorVerified"].as_bool).to be_false + expect(info["subCountText"].as_s).to eq("3.11K") + end +end diff --git a/spec/invidious/videos/scheduled_live_extract_spec.cr b/spec/invidious/videos/scheduled_live_extract_spec.cr new file mode 100644 index 00000000..c3a9b228 --- /dev/null +++ b/spec/invidious/videos/scheduled_live_extract_spec.cr @@ -0,0 +1,111 @@ +require "../../parsers_helper.cr" + +Spectator.describe "parse_video_info" do + it "parses scheduled livestreams data" do + # Enable mock + _player = load_mock("video/scheduled_live_PBD-Podcast.player") + _next = load_mock("video/scheduled_live_PBD-Podcast.next") + + raw_data = _player.merge!(_next) + info = parse_video_info("N-yVic7BbY0", raw_data) + + # Some basic verifications + expect(typeof(info)).to eq(Hash(String, JSON::Any)) + + expect(info["videoType"].as_s).to eq("Scheduled") + + # Basic video infos + + expect(info["title"].as_s).to eq("Home Team | PBD Podcast | Ep. 241") + expect(info["views"].as_i).to eq(6) + expect(info["likes"].as_i).to eq(7) + expect(info["lengthSeconds"].as_i).to eq(0_i64) + expect(info["published"].as_s).to eq("2023-02-28T14:00:00Z") # Unix 1677592800 + + # Extra video infos + + expect(info["allowedRegions"].as_a).to_not be_empty + expect(info["allowedRegions"].as_a.size).to eq(249) + + expect(info["allowedRegions"].as_a).to contain( + "AD", "AR", "BA", "BT", "CZ", "FO", "GL", "IO", "KE", "KH", "LS", + "LT", "MP", "NO", "PR", "RO", "SE", "SK", "SS", "SX", "SZ", "ZW" + ) + + expect(info["keywords"].as_a).to_not be_empty + expect(info["keywords"].as_a.size).to eq(25) + + expect(info["keywords"].as_a).to contain_exactly( + "Patrick Bet-David", + "Valeutainment", + "The BetDavid Podcast", + "The BetDavid Show", + "Betdavid", + "PBD", + "BetDavid show", + "Betdavid podcast", + "podcast betdavid", + "podcast patrick", + "patrick bet david podcast", + "Valuetainment podcast", + "Entrepreneurs", + "Entrepreneurship", + "Entrepreneur Motivation", + "Entrepreneur Advice", + "Startup Entrepreneurs", + "valuetainment", + "patrick bet david", + "PBD podcast", + "Betdavid show", + "Betdavid Podcast", + "Podcast Betdavid", + "Show Betdavid", + "PBDPodcast" + ).in_any_order + + expect(info["allowRatings"].as_bool).to be_true + expect(info["isFamilyFriendly"].as_bool).to be_true + expect(info["isListed"].as_bool).to be_true + expect(info["isUpcoming"].as_bool).to be_true + + # Related videos + + expect(info["relatedVideos"].as_a.size).to eq(20) + + expect(info["relatedVideos"][0]["id"]).to eq("j7jPzzjbVuk") + expect(info["relatedVideos"][0]["author"]).to eq("Democracy Now!") + expect(info["relatedVideos"][0]["ucid"]).to eq("UCzuqE7-t13O4NIDYJfakrhw") + expect(info["relatedVideos"][0]["view_count"]).to eq("7576") + expect(info["relatedVideos"][0]["short_view_count"]).to eq("7.5K") + expect(info["relatedVideos"][0]["author_verified"]).to eq("true") + + # Description + + description_start_text = "PBD Podcast Episode 241. The home team is ready and at it again with the latest news, interesting topics and trending conversations on topics that matter. Try our sponsor Aura for 14 days free - https://aura.com/pbd" + + expect(info["description"].as_s).to start_with(description_start_text) + expect(info["shortDescription"].as_s).to start_with(description_start_text) + + # TODO: Update mocks right before the start of PDB podcast, either on friday or saturday (time unknown) + # expect(info["descriptionHtml"].as_s).to start_with( + # "PBD Podcast Episode 241. The home team is ready and at it again with the latest news, interesting topics and trending conversations on topics that matter. Try our sponsor Aura for 14 days free - <a href=\"https://aura.com/pbd\">aura.com/pbd</a>" + # ) + + # Video metadata + + expect(info["genre"].as_s).to eq("Entertainment") + expect(info["genreUcid"].as_s?).to be_nil + expect(info["license"].as_s).to be_empty + + # Author infos + + expect(info["author"].as_s).to eq("PBD Podcast") + expect(info["ucid"].as_s).to eq("UCGX7nGXpz-CmO_Arg-cgJ7A") + + expect(info["authorThumbnail"].as_s).to eq( + "https://yt3.ggpht.com/61ArDiQshJrvSXcGLhpFfIO3hlMabe2fksitcf6oGob0Mdr5gztdkXxRljICUodL4iuTSrtxW4A=s48-c-k-c0x00ffffff-no-rj" + ) + expect(info["authorVerified"].as_bool).to be_false + expect(info["subCountText"].as_s).to eq("594K") + end +end diff --git a/spec/locales_spec.cr b/spec/locales_spec.cr deleted file mode 100644 index 6a083ee7..00000000 --- a/spec/locales_spec.cr +++ /dev/null @@ -1,29 +0,0 @@ -require "spec" -require "json" -require "../src/invidious/helpers/i18n.cr" - -describe "Locales" do - describe "#consistency" do - locales_list = LOCALES.keys.select! { |key| key != "en-US" } - - locales_list.each do |locale| - puts "\nChecking locale #{locale}" - failed = false - - # Use "en-US" as the reference - LOCALES["en-US"].each_key do |ref_key| - # Catch exception in order to give a hint on what caused - # the failure, and test one locale completely before failing - begin - LOCALES[locale].has_key?(ref_key).should be_true - rescue - failed = true - puts " Missing key in locale #{locale}: '#{ref_key}'" - end - end - - # Throw failed assertion exception in here - failed.should be_false - end - end -end diff --git a/spec/parsers_helper.cr b/spec/parsers_helper.cr new file mode 100644 index 00000000..6589acad --- /dev/null +++ b/spec/parsers_helper.cr @@ -0,0 +1,35 @@ +require "db" +require "json" +require "kemal" + +require "protodec/utils" + +require "spectator" + +require "../src/invidious/exceptions" +require "../src/invidious/helpers/macros" +require "../src/invidious/helpers/logger" +require "../src/invidious/helpers/utils" + +require "../src/invidious/videos" +require "../src/invidious/videos/*" +require "../src/invidious/comments/content" + +require "../src/invidious/helpers/serialized_yt_data" +require "../src/invidious/yt_backend/extractors" +require "../src/invidious/yt_backend/extractors_utils" + +OUTPUT = File.open(File::NULL, "w") +LOGGER = Invidious::LogHandler.new(OUTPUT, LogLevel::Off) + +def load_mock(file) : Hash(String, JSON::Any) + file = File.join(__DIR__, "..", "mocks", file + ".json") + content = File.read(file) + + return JSON.parse(content).as_h +end + +Spectator.configure do |config| + config.fail_blank + config.randomize +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr new file mode 100644 index 00000000..b3060acf --- /dev/null +++ b/spec/spec_helper.cr @@ -0,0 +1,18 @@ +require "kemal" +require "openssl/hmac" +require "pg" +require "protodec/utils" +require "yaml" +require "../src/invidious/helpers/*" +require "../src/invidious/channels/*" +require "../src/invidious/videos/caption" +require "../src/invidious/videos" +require "../src/invidious/playlists" +require "../src/invidious/search/ctoken" +require "../src/invidious/trending" +require "spectator" + +Spectator.configure do |config| + config.fail_blank + config.randomize +end |
