diff --git a/spec/helpers_spec.cr b/spec/helpers_spec.cr index 6f33babd1..307e52d20 100644 --- a/spec/helpers_spec.cr +++ b/spec/helpers_spec.cr @@ -4,8 +4,10 @@ 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/users" describe "Helpers" do describe "#produce_channel_videos_url" do @@ -16,9 +18,7 @@ describe "Helpers" do produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw", page: 20).should eq("/browse_ajax?continuation=4qmFsgJEEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaKEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0l5TUElM0QlM0Q%3D&gl=US&hl=en") - produce_channel_videos_url(ucid: "UC-9-kyTW8ZkZNDHQJ6FgpwQ", auto_generated: true).should eq("/browse_ajax?continuation=4qmFsgJIEhhVQy05LWt5VFc4WmtaTkRIUUo2Rmdwd1EaLEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQTJlZ294TlRVeU1ESXlPVFE1&gl=US&hl=en") - - produce_channel_videos_url(ucid: "UC-9-kyTW8ZkZNDHQJ6FgpwQ", page: 20, auto_generated: true, sort_by: "popular").should eq("/browse_ajax?continuation=4qmFsgJOEhhVQy05LWt5VFc4WmtaTkRIUUo2Rmdwd1EaMkVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQTJlZ294TlRBeU1UY3dNVFE1R0FFJTNE&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 @@ -59,4 +59,26 @@ describe "Helpers" do produce_search_params(content_type: "channel").should eq("CAASAhAC") 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("EiYSC19jRTh4U3U2c3dFwAEByAEB4AEBogINKP___________wFAABgGMowDCvYCQURTSl9pMnF2SmVGdEwwaHRtUzVfSzVDdGozZUdGVkJNV0w5V2Q0Mm8za21VTDZfbUF6ZExwODUtbGlRWkwwbVlyXzE2QmhhZ2dVcVg2NTJTdjlKcVY2VlhpblNoU1AtWlQ2ckw0Tm9sUEJhUFhWdEpzTzVfckFfcUUzR3ViQXVMRnc5dXpJSVhVMi1IbnBYYmRnUExXVEZhdmZYMjA2aHFXbW1wSHdVT3JteFFWX09YNnRZa00zdXgzclBBS0NEclQ4ZVdMN01VM2JMaU5jbmJna1c4bzBoOEtZTExfOEJQYThMY0hiVHY4cEFvTmtqZXJsWDF4N0s0cHF4YVhQb3l6ODlxTmxuaDZyUng2QVhnQXp6b0hIMWRtY3lROENJQmVPSGctbTRpOFp4ZFg0ZFA4OFhXcklGZy1qSkdocEdQOEpVTURnWmdhdnhWeDIyNWhVRVlaTXlyTEdsZXI1ZW00RmdiRzYyWVdDNTFtb0xETGVZRUEiDyILX2NFOHhTdTZzd0UwACgU") + + produce_comment_continuation("_cE8xSu6swE", "ADSJ_i1yz21HI4xrtsYXVC-2_kfZ6kx1yjYQumXAAxqH3CAd7ZxKxfLdZS1__fqhCtOASRbbpSBGH_tH1J96Dxux-Qfjk-lUbupMqv08Q3aHzGu7p70VoUMHhI2-GoJpnbpmcOxkGzeIuenRS_ym2Y8fkDowhqLPFgsS0n4djnZ2UmC17F3Ch3N1S1UYf1ZVOc991qOC1iW9kJDzyvRQTWCPsJUPneSaAKW-Rr97pdesOkR4i8cNvHZRnQKe2HEfsvlJOb2C3lF1dJBfJeNfnQYeh5hv6_fZN7bt3-JL1Xk3Qc9NXNxmmbDpwAC_yFR8dthFfUJdyIO9Nu1D79MLYeR-H5HxqUJokkJiGIz4lTE_CXXbhAI").should eq("EiYSC19jRTh4U3U2c3dFwAEByAEB4AEBogINKP___________wFAABgGMokDCvMCQURTSl9pMXl6MjFISTR4cnRzWVhWQy0yX2tmWjZreDF5allRdW1YQUF4cUgzQ0FkN1p4S3hmTGRaUzFfX2ZxaEN0T0FTUmJicFNCR0hfdEgxSjk2RHh1eC1RZmprLWxVYnVwTXF2MDhRM2FIekd1N3A3MFZvVU1IaEkyLUdvSnBuYnBtY094a0d6ZUl1ZW5SU195bTJZOGZrRG93aHFMUEZnc1MwbjRkam5aMlVtQzE3RjNDaDNOMVMxVVlmMVpWT2M5OTFxT0MxaVc5a0pEenl2UlFUV0NQc0pVUG5lU2FBS1ctUnI5N3BkZXNPa1I0aThjTnZIWlJuUUtlMkhFZnN2bEpPYjJDM2xGMWRKQmZKZU5mblFZZWg1aHY2X2ZaTjdidDMtSkwxWGszUWM5TlhOeG1tYkRwd0FDX3lGUjhkdGhGZlVKZHlJTzlOdTFENzlNTFllUi1INUh4cVVKb2trSmlHSXo0bFRFX0NYWGJoQUkiDyILX2NFOHhTdTZzd0UwACgU") + + produce_comment_continuation("29-q7YnyUmY", "").should eq("EiYSCzI5LXE3WW55VW1ZwAEByAEB4AEBogINKP___________wFAABgGMhMiDyILMjktcTdZbnlVbVkwAHgC") + + produce_comment_continuation("CvFH_6DNRCY", "").should eq("EiYSC0N2RkhfNkROUkNZwAEByAEB4AEBogINKP___________wFAABgGMhMiDyILQ3ZGSF82RE5SQ1kwAHgC") + 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 end diff --git a/src/invidious.cr b/src/invidious.cr index e97198d83..8494f9d90 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -2691,11 +2691,14 @@ get "/api/v1/comments/:id" do |env| format = env.params.query["format"]? format ||= "json" + sort_by = env.params.query["sort_by"]?.try &.downcase + sort_by ||= "top" + continuation = env.params.query["continuation"]? if source == "youtube" begin - comments = fetch_youtube_comments(id, PG_DB, continuation, proxies, format, locale, thin_mode, region) + comments = fetch_youtube_comments(id, PG_DB, continuation, proxies, format, locale, thin_mode, region, sort_by: sort_by) rescue ex error_message = {"error" => ex.message}.to_json env.response.status_code = 500 diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index b5133cf2b..aa3233d46 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -56,15 +56,14 @@ class RedditListing }) end -def fetch_youtube_comments(id, db, continuation, proxies, format, locale, thin_mode, region) +def fetch_youtube_comments(id, db, continuation, proxies, format, locale, thin_mode, region, sort_by = "top") video = get_video(id, db, proxies, region: region) - session_token = video.info["session_token"]? - itct = video.info["itct"]? - ctoken = video.info["ctoken"]? + + ctoken = produce_comment_continuation(id, cursor: "", sort_by: sort_by) continuation ||= ctoken - if !continuation || !itct || !session_token + if !continuation || !session_token if format == "json" return {"comments" => [] of String}.to_json else @@ -73,7 +72,7 @@ def fetch_youtube_comments(id, db, continuation, proxies, format, locale, thin_m end post_req = { - "session_token" => session_token.not_nil!, + "session_token" => session_token, } post_req = HTTP::Params.encode(post_req) @@ -90,7 +89,7 @@ def fetch_youtube_comments(id, db, continuation, proxies, format, locale, thin_m headers["x-youtube-client-name"] = "1" headers["x-youtube-client-version"] = "2.20180719" - response = client.post("/comment_service_ajax?action_get_comments=1&pbj=1&ctoken=#{continuation}&continuation=#{continuation}&itct=#{itct}&hl=en&gl=US", headers, post_req) + response = client.post("/comment_service_ajax?action_get_comments=1&ctoken=#{continuation}&continuation=#{continuation}&hl=en&gl=US", headers, post_req) response = JSON.parse(response.body) if !response["response"]["continuationContents"]? @@ -516,3 +515,111 @@ def content_to_comment_html(content) return comment_html end + +def produce_comment_continuation(video_id, cursor = "", sort_by = "top") + continuation = IO::Memory.new + + continuation.write(Bytes[0x12, 0x26]) + + continuation.write(Bytes[0x12, video_id.size]) + continuation.print(video_id) + + continuation.write(Bytes[0xc0, 0x01, 0x01]) + continuation.write(Bytes[0xc8, 0x01, 0x01]) + continuation.write(Bytes[0xe0, 0x01, 0x01]) + + continuation.write(Bytes[0xa2, 0x02, 0x0d]) + continuation.write(Bytes[0x28, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]) + + continuation.write(Bytes[0x40, 0x00]) + continuation.write(Bytes[0x18, 0x06]) + + if cursor.empty? + continuation.write(Bytes[0x32]) + continuation.write(write_var_int(video_id.size + 8)) + + continuation.write(Bytes[0x22, video_id.size + 4]) + continuation.write(Bytes[0x22, video_id.size]) + continuation.print(video_id) + + case sort_by + when "top" + continuation.write(Bytes[0x30, 0x00]) + when "new", "newest" + continuation.write(Bytes[0x30, 0x01]) + end + + continuation.write(Bytes[0x78, 0x02]) + else + continuation.write(Bytes[0x32]) + continuation.write(write_var_int(cursor.size + video_id.size + 11)) + + continuation.write(Bytes[0x0a]) + continuation.write(write_var_int(cursor.size)) + continuation.print(cursor) + + continuation.write(Bytes[0x22, video_id.size + 4]) + continuation.write(Bytes[0x22, video_id.size]) + continuation.print(video_id) + + case sort_by + when "top" + continuation.write(Bytes[0x30, 0x00]) + when "new", "newest" + continuation.write(Bytes[0x30, 0x01]) + end + + continuation.write(Bytes[0x28, 0x14]) + end + + continuation.rewind + continuation = continuation.gets_to_end + + continuation = Base64.urlsafe_encode(continuation.to_slice) + continuation = URI.escape(continuation) + + return continuation +end + +def produce_comment_reply_continuation(video_id, ucid, comment_id) + continuation = IO::Memory.new + + continuation.write(Bytes[0x12, 0x26]) + + continuation.write(Bytes[0x12, video_id.size]) + continuation.print(video_id) + + continuation.write(Bytes[0xc0, 0x01, 0x01]) + continuation.write(Bytes[0xc8, 0x01, 0x01]) + continuation.write(Bytes[0xe0, 0x01, 0x01]) + + continuation.write(Bytes[0xa2, 0x02, 0x0d]) + continuation.write(Bytes[0x28, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]) + + continuation.write(Bytes[0x40, 0x00]) + continuation.write(Bytes[0x18, 0x06]) + + continuation.write(Bytes[0x32, ucid.size + video_id.size + comment_id.size + 16]) + continuation.write(Bytes[0x1a, ucid.size + video_id.size + comment_id.size + 14]) + + continuation.write(Bytes[0x12, comment_id.size]) + continuation.print(comment_id) + + continuation.write(Bytes[0x22, 0x02, 0x08, 0x00]) # ?? + + continuation.write(Bytes[ucid.size + video_id.size + 7]) + continuation.write(Bytes[ucid.size]) + continuation.print(ucid) + continuation.write(Bytes[0x32, video_id.size]) + continuation.print(video_id) + continuation.write(Bytes[0x40, 0x01]) + continuation.write(Bytes[0x48, 0x0a]) + + continuation.rewind + continuation = continuation.gets_to_end + + continuation = Base64.urlsafe_encode(continuation.to_slice) + continuation = URI.escape(continuation) + + return continuation +end diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 856b64478..3887277a5 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -608,14 +608,6 @@ def extract_player_config(body, html) params["session_token"] = md["session_token"] end - if md = body.match(/itct=(?[^"]+)"/) - params["itct"] = md["itct"] - end - - if md = body.match(/'COMMENTS_TOKEN': "(?[^"]+)"/) - params["ctoken"] = md["ctoken"] - end - if md = body.match(/'RELATED_PLAYER_ARGS': (?{"rvs":"[^"]+"})/) params["rvs"] = JSON.parse(md["rvs"])["rvs"].as_s end