diff --git a/Gemfile b/Gemfile index 9bb1eff7..1bc0c19e 100644 --- a/Gemfile +++ b/Gemfile @@ -12,3 +12,4 @@ gem 'pry', platforms: not_jruby gem 'simplecov' gem 'simplecov-cobertura' gem 'yard' +gem 'irb' diff --git a/lib/yt/actions/get.rb b/lib/yt/actions/get.rb index 83fb9e1a..ea620ca0 100644 --- a/lib/yt/actions/get.rb +++ b/lib/yt/actions/get.rb @@ -14,7 +14,7 @@ def get private def get_request(params = {}) - @list_request = Yt::Request.new(params).tap do |request| + @get_request = Yt::Request.new(params).tap do |request| print "#{request.as_curl}\n" if Yt.configuration.developing? end end @@ -30,4 +30,4 @@ def get_params end end end -end \ No newline at end of file +end diff --git a/lib/yt/associations/has_authentication.rb b/lib/yt/associations/has_authentication.rb index d9c90b15..63d2a4ad 100644 --- a/lib/yt/associations/has_authentication.rb +++ b/lib/yt/associations/has_authentication.rb @@ -189,7 +189,7 @@ def authentication_url_params params[:redirect_uri] = @redirect_uri params[:response_type] = :code params[:access_type] = :offline - params[:approval_prompt] = @force ? :force : :auto + params[:prompt] = :consent if @force # params[:include_granted_scopes] = true params[:state] = @state if @state end diff --git a/lib/yt/collections/reports.rb b/lib/yt/collections/reports.rb index 41c67f93..89108e71 100644 --- a/lib/yt/collections/reports.rb +++ b/lib/yt/collections/reports.rb @@ -53,7 +53,9 @@ class Reports < Base product_page: 'PRODUCT_PAGE', shorts: 'SHORTS', sound_page: 'SOUND_PAGE', - video_remixes: 'VIDEO_REMIXES' + video_remixes: 'VIDEO_REMIXES', + immersive_live: 'IMMERSIVE_LIVE', + shorts_content_links: 'SHORTS_CONTENT_LINKS' } # @see https://developers.google.com/youtube/analytics/dimensions#Playback_Location_Dimensions diff --git a/lib/yt/models/caption.rb b/lib/yt/models/caption.rb index 2d2d337e..bcbfe980 100644 --- a/lib/yt/models/caption.rb +++ b/lib/yt/models/caption.rb @@ -1,4 +1,5 @@ require 'yt/models/resource' +require "fileutils" module Yt module Models @@ -14,6 +15,38 @@ class Caption < Resource delegate :language, to: :snippet delegate :name, to: :snippet delegate :status, to: :snippet + + # Downloads a caption file. + # @param [String] path A name for the downloaded file with caption content. + # @see https://developers.google.com/youtube/v3/docs/captions#resource + def download(path) + case io + when StringIO then File.open(path, 'w') { |f| f.write(io.read) } + when Tempfile then io.close; FileUtils.mv(io.path, path) + end + end + + def io + @io ||= get_request(download_params).open_uri + end + + private + + # @return [Hash] the parameters to submit to YouTube to download caption. + # @see https://developers.google.com/youtube/v3/docs/captions/download + def download_params + {}.tap do |params| + params[:method] = :get + params[:host] = 'youtube.googleapis.com' + params[:auth] = @auth + params[:exptected_response] = Net::HTTPOK + params[:api_key] = Yt.configuration.api_key if Yt.configuration.api_key + params[:path] = "/youtube/v3/captions/#{@id}" + if @auth.owner_name + params[:params] = {on_behalf_of_content_owner: @auth.owner_name} + end + end + end end end end diff --git a/lib/yt/models/resource.rb b/lib/yt/models/resource.rb index b075891a..3733f93e 100644 --- a/lib/yt/models/resource.rb +++ b/lib/yt/models/resource.rb @@ -12,13 +12,7 @@ class Resource < Base # @!attribute [r] id # @return [String] the ID that YouTube uses to identify each resource. - def id - if @id.nil? && @match && @match[:kind] == :channel - @id ||= fetch_channel_id - else - @id - end - end + attr_reader :id ### STATUS ### @@ -51,7 +45,11 @@ def initialize(options = {}) if options[:url] @url = options[:url] @match = find_pattern_match - @id = @match['id'] + if kind == "channel" && @match.key?('format') + @id ||= fetch_channel_id + else + @id = @match['id'] + end else @id = options[:id] end @@ -98,7 +96,8 @@ def update(attributes = {}) # @return [Array] patterns matching URLs of YouTube channels. CHANNEL_PATTERNS = [ %r{^(?:https?://)?(?:www\.)?youtube\.com/channel/(?UC[a-zA-Z0-9_-]{22})}, - %r{^(?:https?://)?(?:www\.)?youtube\.com/(?c/|user/)?(?[a-zA-Z0-9_-]+)} + %r{^(?:https?://)?(?:www\.)?youtube\.com/(?c/|user/)?(?[a-zA-Z0-9_-]+)}, + %r{^(?:https?://)?(?:www\.)?youtube\.com/(?@)(?[a-zA-Z0-9_-]+)} ] private @@ -106,8 +105,7 @@ def update(attributes = {}) def find_pattern_match patterns.find do |kind, regex| if data = @url.match(regex) - # Note: With Ruby 2.4, the following is data.named_captures - break data.names.zip(data.captures).to_h.merge kind: kind + break data.named_captures.merge kind: kind end end || {kind: :unknown} end @@ -123,19 +121,44 @@ def patterns end def fetch_channel_id - response = Net::HTTP.start 'www.youtube.com', 443, use_ssl: true do |http| - http.request Net::HTTP::Get.new("/#{@match['format']}#{@match['name']}") - end - if response.is_a?(Net::HTTPRedirection) + api_key = Yt.configuration.api_key if Yt.configuration.api_key + case @match['format'] + when "@" + handle = "@#{@match['name']}" + response = Net::HTTP.start 'youtube.googleapis.com', 443, use_ssl: true do |http| + http.request Net::HTTP::Get.new("/youtube/v3/channels?part=snippet&forHandle=#{handle}&key=#{api_key}") + end + if response.is_a?(Net::HTTPOK) && item = JSON(response.body)['items']&.first + item['id'] + else + raise Yt::Errors::NoItems + end + when "user/" + username = @match['name'] + response = Net::HTTP.start 'youtube.googleapis.com', 443, use_ssl: true do |http| + http.request Net::HTTP::Get.new("/youtube/v3/channels?part=snippet&forUsername=#{username}&key=#{api_key}") + end + if response.is_a?(Net::HTTPOK) && item = JSON(response.body)['items']&.first + item['id'] + else + raise Yt::Errors::NoItems + end + else # "c/", nil response = Net::HTTP.start 'www.youtube.com', 443, use_ssl: true do |http| - http.request Net::HTTP::Get.new(response['location']) + http.request Net::HTTP::Get.new("/#{@match['format']}#{@match['name']}") + end + if response.is_a?(Net::HTTPRedirection) + response = Net::HTTP.start 'www.youtube.com', 443, use_ssl: true do |http| + http.request Net::HTTP::Get.new(response['location']) + end + end + # puts response.body + regex = %r{(?UC[a-zA-Z0-9_-]{22})} + if data = response.body.match(regex) + data[:id] + else + raise Yt::Errors::NoItems end - end - regex = %r{} - if data = response.body.match(regex) - data[:id] - else - raise Yt::Errors::NoItems end end diff --git a/lib/yt/request.rb b/lib/yt/request.rb index 35414515..a75921eb 100644 --- a/lib/yt/request.rb +++ b/lib/yt/request.rb @@ -1,6 +1,7 @@ require 'net/http' # for Net::HTTP.start require 'uri' # for URI.json require 'json' # for JSON.parse +require "open-uri" # for URI.open require 'active_support' # does not load anything by default, but is required require 'active_support/core_ext' # for Hash.from_xml, Hash.to_param @@ -84,6 +85,10 @@ def run end end + def open_uri + URI.open(uri.to_s, 'Authorization' => "Bearer #{@auth.access_token}") + end + # Returns the +cURL+ version of the request, useful to re-run the request # in a shell terminal. # @return [String] the +cURL+ version of the request. diff --git a/spec/requests/as_account/authentications_spec.rb b/spec/requests/as_account/authentications_spec.rb index eee6ad08..9336980c 100644 --- a/spec/requests/as_account/authentications_spec.rb +++ b/spec/requests/as_account/authentications_spec.rb @@ -120,7 +120,7 @@ context 'given a forced approval prompt' do let(:attrs) { auth_attrs.merge force: true } - it { expect(account.authentication_url).to match 'approval_prompt=force' } + it { expect(account.authentication_url).to match 'prompt=consent' } end end end diff --git a/spec/requests/as_server_app/url_spec.rb b/spec/requests/as_server_app/url_spec.rb index 823a5322..44de53e1 100644 --- a/spec/requests/as_server_app/url_spec.rb +++ b/spec/requests/as_server_app/url_spec.rb @@ -45,51 +45,50 @@ it {expect{url.resource}.to raise_error Yt::Errors::NoItems } end - # # TODO: need to fix our code, as YouTube behavior changes - # context 'given a YouTube channel URL in the name form' do - # let(:text) { "http://www.youtube.com/#{name}" } - - # describe 'works when the name matches the custom URL' do - # let(:name) { 'nbcsports' } - # it {expect(url.id).to eq 'UCqZQlzSHbVJrwrn5XvzrzcA' } - # end - - # describe 'works when the name matches the username' do - # let(:name) { '2012NBCOlympics' } - # it {expect(url.id).to eq 'UCqZQlzSHbVJrwrn5XvzrzcA' } - # end - - # describe 'fails with unknown channels' do - # let(:name) { 'not-an-actual-channel' } - # it {expect{url.id}.to raise_error Yt::Errors::NoItems } - # end - # end - - # context 'given a YouTube channel URL in the custom form' do - # let(:text) { "https://youtube.com/c/#{name}" } - - # describe 'works with existing channels' do - # let(:name) { 'ogeeku' } - # it {expect(url.id).to eq 'UC4nG_NxJniKoB-n6TLT2yaw' } - # end - - # describe 'fails with unknown channels' do - # let(:name) { 'not-an-actual-channel' } - # it {expect{url.id}.to raise_error Yt::Errors::NoItems } - # end - # end - - # context 'given a YouTube channel URL in the username form' do - # let(:text) { "youtube.com/user/#{name}" } - - # describe 'works with existing channels' do - # let(:name) { 'ogeeku' } - # it {expect(url.id).to eq 'UC4lU5YG9QDgs0X2jdnt7cdQ' } - # end - - # describe 'fails with unknown channels' do - # let(:name) { 'not-an-actual-channel' } - # it {expect{url.id}.to raise_error Yt::Errors::NoItems } - # end - # end + context 'given a YouTube channel URL in the name form' do + let(:text) { "http://www.youtube.com/#{name}" } + + describe 'works when the name matches the custom URL' do + let(:name) { 'nbcsports' } + it {expect(url.id).to eq 'UCqZQlzSHbVJrwrn5XvzrzcA' } + end + + describe 'works when the name matches the username' do + let(:name) { '2012NBCOlympics' } + it {expect(url.id).to eq 'UCqZQlzSHbVJrwrn5XvzrzcA' } + end + + describe 'fails with unknown channels' do + let(:name) { 'not-an-actual-channel' } + it {expect{url.id}.to raise_error Yt::Errors::NoItems } + end + end + + context 'given a YouTube channel URL in the custom form' do + let(:text) { "https://youtube.com/c/#{name}" } + + describe 'works with existing channels' do + let(:name) { 'ogeeku' } + it {expect(url.id).to eq 'UC4nG_NxJniKoB-n6TLT2yaw' } + end + + describe 'fails with unknown channels' do + let(:name) { 'not-an-actual-channel' } + it {expect{url.id}.to raise_error Yt::Errors::NoItems } + end + end + + context 'given a YouTube channel URL in the username form' do + let(:text) { "youtube.com/user/#{name}" } + + describe 'works with existing channels' do + let(:name) { 'ogeeku' } + it {expect(url.id).to eq 'UC4lU5YG9QDgs0X2jdnt7cdQ' } + end + + describe 'fails with unknown channels' do + let(:name) { 'not-an-actual-channel' } + it {expect{url.id}.to raise_error Yt::Errors::NoItems } + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d7f814ba..cb5c52a8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -18,6 +18,12 @@ ENV['YT_TEST_API_KEY'] ||= 'ZZZ' ENV['YT_TEST_REFRESH_TOKEN'] ||= 'ABC' +ENV['YT_TEST_CONTENT_OWNER_NAME'] ||= 'abcd' +ENV['YT_TEST_PARTNER_CLIENT_ID'] ||= 'abcd' +ENV['YT_TEST_PARTNER_CLIENT_SECRET'] ||= 'abcd' +ENV['YT_TEST_CONTENT_OWNER_REFRESH_TOKEN'] ||= 'abcd' +ENV['YT_TEST_CONTENT_OWNER_ACCESS_TOKEN'] ||= 'abcd' + Dir['./spec/support/**/*.rb'].each {|f| require f} RSpec.configure do |config|