Skip to content

Commit 05a0415

Browse files
authored
Get channel id from channel url (#447)
* Add 2 more traffic source types * Uncomment url test, to get channel id * Use prompt=consent * Use open-uri for caption download * Add io method for the io object * Use auth.access_token * Remove commented code
1 parent 16944ff commit 05a0415

File tree

10 files changed

+143
-74
lines changed

10 files changed

+143
-74
lines changed

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ gem 'pry', platforms: not_jruby
1212
gem 'simplecov'
1313
gem 'simplecov-cobertura'
1414
gem 'yard'
15+
gem 'irb'

lib/yt/actions/get.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def get
1414
private
1515

1616
def get_request(params = {})
17-
@list_request = Yt::Request.new(params).tap do |request|
17+
@get_request = Yt::Request.new(params).tap do |request|
1818
print "#{request.as_curl}\n" if Yt.configuration.developing?
1919
end
2020
end
@@ -30,4 +30,4 @@ def get_params
3030
end
3131
end
3232
end
33-
end
33+
end

lib/yt/associations/has_authentication.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ def authentication_url_params
189189
params[:redirect_uri] = @redirect_uri
190190
params[:response_type] = :code
191191
params[:access_type] = :offline
192-
params[:approval_prompt] = @force ? :force : :auto
192+
params[:prompt] = :consent if @force
193193
# params[:include_granted_scopes] = true
194194
params[:state] = @state if @state
195195
end

lib/yt/collections/reports.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ class Reports < Base
5353
product_page: 'PRODUCT_PAGE',
5454
shorts: 'SHORTS',
5555
sound_page: 'SOUND_PAGE',
56-
video_remixes: 'VIDEO_REMIXES'
56+
video_remixes: 'VIDEO_REMIXES',
57+
immersive_live: 'IMMERSIVE_LIVE',
58+
shorts_content_links: 'SHORTS_CONTENT_LINKS'
5759
}
5860

5961
# @see https://developers.google.com/youtube/analytics/dimensions#Playback_Location_Dimensions

lib/yt/models/caption.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'yt/models/resource'
2+
require "fileutils"
23

34
module Yt
45
module Models
@@ -14,6 +15,38 @@ class Caption < Resource
1415
delegate :language, to: :snippet
1516
delegate :name, to: :snippet
1617
delegate :status, to: :snippet
18+
19+
# Downloads a caption file.
20+
# @param [String] path A name for the downloaded file with caption content.
21+
# @see https://developers.google.com/youtube/v3/docs/captions#resource
22+
def download(path)
23+
case io
24+
when StringIO then File.open(path, 'w') { |f| f.write(io.read) }
25+
when Tempfile then io.close; FileUtils.mv(io.path, path)
26+
end
27+
end
28+
29+
def io
30+
@io ||= get_request(download_params).open_uri
31+
end
32+
33+
private
34+
35+
# @return [Hash] the parameters to submit to YouTube to download caption.
36+
# @see https://developers.google.com/youtube/v3/docs/captions/download
37+
def download_params
38+
{}.tap do |params|
39+
params[:method] = :get
40+
params[:host] = 'youtube.googleapis.com'
41+
params[:auth] = @auth
42+
params[:exptected_response] = Net::HTTPOK
43+
params[:api_key] = Yt.configuration.api_key if Yt.configuration.api_key
44+
params[:path] = "/youtube/v3/captions/#{@id}"
45+
if @auth.owner_name
46+
params[:params] = {on_behalf_of_content_owner: @auth.owner_name}
47+
end
48+
end
49+
end
1750
end
1851
end
1952
end

lib/yt/models/resource.rb

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,7 @@ class Resource < Base
1212

1313
# @!attribute [r] id
1414
# @return [String] the ID that YouTube uses to identify each resource.
15-
def id
16-
if @id.nil? && @match && @match[:kind] == :channel
17-
@id ||= fetch_channel_id
18-
else
19-
@id
20-
end
21-
end
15+
attr_reader :id
2216

2317
### STATUS ###
2418

@@ -51,7 +45,11 @@ def initialize(options = {})
5145
if options[:url]
5246
@url = options[:url]
5347
@match = find_pattern_match
54-
@id = @match['id']
48+
if kind == "channel" && @match.key?('format')
49+
@id ||= fetch_channel_id
50+
else
51+
@id = @match['id']
52+
end
5553
else
5654
@id = options[:id]
5755
end
@@ -98,16 +96,16 @@ def update(attributes = {})
9896
# @return [Array<Regexp>] patterns matching URLs of YouTube channels.
9997
CHANNEL_PATTERNS = [
10098
%r{^(?:https?://)?(?:www\.)?youtube\.com/channel/(?<id>UC[a-zA-Z0-9_-]{22})},
101-
%r{^(?:https?://)?(?:www\.)?youtube\.com/(?<format>c/|user/)?(?<name>[a-zA-Z0-9_-]+)}
99+
%r{^(?:https?://)?(?:www\.)?youtube\.com/(?<format>c/|user/)?(?<name>[a-zA-Z0-9_-]+)},
100+
%r{^(?:https?://)?(?:www\.)?youtube\.com/(?<format>@)(?<name>[a-zA-Z0-9_-]+)}
102101
]
103102

104103
private
105104

106105
def find_pattern_match
107106
patterns.find do |kind, regex|
108107
if data = @url.match(regex)
109-
# Note: With Ruby 2.4, the following is data.named_captures
110-
break data.names.zip(data.captures).to_h.merge kind: kind
108+
break data.named_captures.merge kind: kind
111109
end
112110
end || {kind: :unknown}
113111
end
@@ -123,19 +121,44 @@ def patterns
123121
end
124122

125123
def fetch_channel_id
126-
response = Net::HTTP.start 'www.youtube.com', 443, use_ssl: true do |http|
127-
http.request Net::HTTP::Get.new("/#{@match['format']}#{@match['name']}")
128-
end
129-
if response.is_a?(Net::HTTPRedirection)
124+
api_key = Yt.configuration.api_key if Yt.configuration.api_key
125+
case @match['format']
126+
when "@"
127+
handle = "@#{@match['name']}"
128+
response = Net::HTTP.start 'youtube.googleapis.com', 443, use_ssl: true do |http|
129+
http.request Net::HTTP::Get.new("/youtube/v3/channels?part=snippet&forHandle=#{handle}&key=#{api_key}")
130+
end
131+
if response.is_a?(Net::HTTPOK) && item = JSON(response.body)['items']&.first
132+
item['id']
133+
else
134+
raise Yt::Errors::NoItems
135+
end
136+
when "user/"
137+
username = @match['name']
138+
response = Net::HTTP.start 'youtube.googleapis.com', 443, use_ssl: true do |http|
139+
http.request Net::HTTP::Get.new("/youtube/v3/channels?part=snippet&forUsername=#{username}&key=#{api_key}")
140+
end
141+
if response.is_a?(Net::HTTPOK) && item = JSON(response.body)['items']&.first
142+
item['id']
143+
else
144+
raise Yt::Errors::NoItems
145+
end
146+
else # "c/", nil
130147
response = Net::HTTP.start 'www.youtube.com', 443, use_ssl: true do |http|
131-
http.request Net::HTTP::Get.new(response['location'])
148+
http.request Net::HTTP::Get.new("/#{@match['format']}#{@match['name']}")
149+
end
150+
if response.is_a?(Net::HTTPRedirection)
151+
response = Net::HTTP.start 'www.youtube.com', 443, use_ssl: true do |http|
152+
http.request Net::HTTP::Get.new(response['location'])
153+
end
154+
end
155+
# puts response.body
156+
regex = %r{(?<id>UC[a-zA-Z0-9_-]{22})}
157+
if data = response.body.match(regex)
158+
data[:id]
159+
else
160+
raise Yt::Errors::NoItems
132161
end
133-
end
134-
regex = %r{<meta itemprop="channelId" content="(?<id>UC[a-zA-Z0-9_-]{22})">}
135-
if data = response.body.match(regex)
136-
data[:id]
137-
else
138-
raise Yt::Errors::NoItems
139162
end
140163
end
141164

lib/yt/request.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require 'net/http' # for Net::HTTP.start
22
require 'uri' # for URI.json
33
require 'json' # for JSON.parse
4+
require "open-uri" # for URI.open
45
require 'active_support' # does not load anything by default, but is required
56
require 'active_support/core_ext' # for Hash.from_xml, Hash.to_param
67

@@ -84,6 +85,10 @@ def run
8485
end
8586
end
8687

88+
def open_uri
89+
URI.open(uri.to_s, 'Authorization' => "Bearer #{@auth.access_token}")
90+
end
91+
8792
# Returns the +cURL+ version of the request, useful to re-run the request
8893
# in a shell terminal.
8994
# @return [String] the +cURL+ version of the request.

spec/requests/as_account/authentications_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@
120120

121121
context 'given a forced approval prompt' do
122122
let(:attrs) { auth_attrs.merge force: true }
123-
it { expect(account.authentication_url).to match 'approval_prompt=force' }
123+
it { expect(account.authentication_url).to match 'prompt=consent' }
124124
end
125125
end
126126
end

spec/requests/as_server_app/url_spec.rb

Lines changed: 46 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -45,51 +45,50 @@
4545
it {expect{url.resource}.to raise_error Yt::Errors::NoItems }
4646
end
4747

48-
# # TODO: need to fix our code, as YouTube behavior changes
49-
# context 'given a YouTube channel URL in the name form' do
50-
# let(:text) { "http://www.youtube.com/#{name}" }
51-
52-
# describe 'works when the name matches the custom URL' do
53-
# let(:name) { 'nbcsports' }
54-
# it {expect(url.id).to eq 'UCqZQlzSHbVJrwrn5XvzrzcA' }
55-
# end
56-
57-
# describe 'works when the name matches the username' do
58-
# let(:name) { '2012NBCOlympics' }
59-
# it {expect(url.id).to eq 'UCqZQlzSHbVJrwrn5XvzrzcA' }
60-
# end
61-
62-
# describe 'fails with unknown channels' do
63-
# let(:name) { 'not-an-actual-channel' }
64-
# it {expect{url.id}.to raise_error Yt::Errors::NoItems }
65-
# end
66-
# end
67-
68-
# context 'given a YouTube channel URL in the custom form' do
69-
# let(:text) { "https://youtube.com/c/#{name}" }
70-
71-
# describe 'works with existing channels' do
72-
# let(:name) { 'ogeeku' }
73-
# it {expect(url.id).to eq 'UC4nG_NxJniKoB-n6TLT2yaw' }
74-
# end
75-
76-
# describe 'fails with unknown channels' do
77-
# let(:name) { 'not-an-actual-channel' }
78-
# it {expect{url.id}.to raise_error Yt::Errors::NoItems }
79-
# end
80-
# end
81-
82-
# context 'given a YouTube channel URL in the username form' do
83-
# let(:text) { "youtube.com/user/#{name}" }
84-
85-
# describe 'works with existing channels' do
86-
# let(:name) { 'ogeeku' }
87-
# it {expect(url.id).to eq 'UC4lU5YG9QDgs0X2jdnt7cdQ' }
88-
# end
89-
90-
# describe 'fails with unknown channels' do
91-
# let(:name) { 'not-an-actual-channel' }
92-
# it {expect{url.id}.to raise_error Yt::Errors::NoItems }
93-
# end
94-
# end
48+
context 'given a YouTube channel URL in the name form' do
49+
let(:text) { "http://www.youtube.com/#{name}" }
50+
51+
describe 'works when the name matches the custom URL' do
52+
let(:name) { 'nbcsports' }
53+
it {expect(url.id).to eq 'UCqZQlzSHbVJrwrn5XvzrzcA' }
54+
end
55+
56+
describe 'works when the name matches the username' do
57+
let(:name) { '2012NBCOlympics' }
58+
it {expect(url.id).to eq 'UCqZQlzSHbVJrwrn5XvzrzcA' }
59+
end
60+
61+
describe 'fails with unknown channels' do
62+
let(:name) { 'not-an-actual-channel' }
63+
it {expect{url.id}.to raise_error Yt::Errors::NoItems }
64+
end
65+
end
66+
67+
context 'given a YouTube channel URL in the custom form' do
68+
let(:text) { "https://youtube.com/c/#{name}" }
69+
70+
describe 'works with existing channels' do
71+
let(:name) { 'ogeeku' }
72+
it {expect(url.id).to eq 'UC4nG_NxJniKoB-n6TLT2yaw' }
73+
end
74+
75+
describe 'fails with unknown channels' do
76+
let(:name) { 'not-an-actual-channel' }
77+
it {expect{url.id}.to raise_error Yt::Errors::NoItems }
78+
end
79+
end
80+
81+
context 'given a YouTube channel URL in the username form' do
82+
let(:text) { "youtube.com/user/#{name}" }
83+
84+
describe 'works with existing channels' do
85+
let(:name) { 'ogeeku' }
86+
it {expect(url.id).to eq 'UC4lU5YG9QDgs0X2jdnt7cdQ' }
87+
end
88+
89+
describe 'fails with unknown channels' do
90+
let(:name) { 'not-an-actual-channel' }
91+
it {expect{url.id}.to raise_error Yt::Errors::NoItems }
92+
end
93+
end
9594
end

spec/spec_helper.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818
ENV['YT_TEST_API_KEY'] ||= 'ZZZ'
1919
ENV['YT_TEST_REFRESH_TOKEN'] ||= 'ABC'
2020

21+
ENV['YT_TEST_CONTENT_OWNER_NAME'] ||= 'abcd'
22+
ENV['YT_TEST_PARTNER_CLIENT_ID'] ||= 'abcd'
23+
ENV['YT_TEST_PARTNER_CLIENT_SECRET'] ||= 'abcd'
24+
ENV['YT_TEST_CONTENT_OWNER_REFRESH_TOKEN'] ||= 'abcd'
25+
ENV['YT_TEST_CONTENT_OWNER_ACCESS_TOKEN'] ||= 'abcd'
26+
2127
Dir['./spec/support/**/*.rb'].each {|f| require f}
2228

2329
RSpec.configure do |config|

0 commit comments

Comments
 (0)