Skip to content

Commit 8df8af8

Browse files
committed
Add support for ingesting etag data from API endpoints
Most of the YouTube API endpoints that return resources expose an etag property. It is highly desirable for consumers to be able to have access to this property in order to perform conditional requests (using If-Match / If-None-Match) or to verify that resources have changed. Delegate etag to list Define a singleton method on list that is the list's etag Set etag instance variable when getting the last response Collections specs now test for etag support Revert "Delegate etag to list" This reverts commit 62fe580. Add etag method that fetches the etag if not already set This allows the etag to be fetched for a collection / list response. Example: account.playlists.etag will now return the etag. Before it would return nil. Remove exists from singular models and moves it into the resource model Removes attr_reader and defines an etag method If the object has an @auth object and an @id then we fetch the etag. This allows us to fetch the etag for an object like the following: Yt::Playlist.new(id: youtube_playlist_id, auth: account).etag Remove counting a collection to force the retrieval of an etag Update etag specs for video and playlist_items models Fix missing question mark on nil call Remove collection.count call in collections / subscriptions and change fetch_page to fetch_etag Correct resource name from channel to video
1 parent 16944ff commit 8df8af8

File tree

16 files changed

+229
-22
lines changed

16 files changed

+229
-22
lines changed

lib/yt/actions/list.rb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,22 @@ def first!
1212
first.tap{|item| raise Errors::NoItems, error_message unless item}
1313
end
1414

15+
def etag
16+
@etag ||= fetch_etag
17+
end
18+
1519
private
1620

1721
def list
22+
owner = self
1823
@last_index, @page_token = 0, nil
1924
Enumerator.new(-> {total_results}) do |items|
2025
while next_item = find_next
2126
items << next_item
2227
end
2328
@where_params = {}
29+
end.tap do |enum|
30+
enum.define_singleton_method(:etag) { owner.instance_variable_get(:@etag) }
2431
end
2532
end
2633

@@ -63,7 +70,7 @@ def resource_class
6370
# Can be overwritten by subclasses that initialize instance with
6471
# a different set of parameters.
6572
def new_item(data)
66-
resource_class.new attributes_for_new_item(data)
73+
resource_class.new attributes_for_new_item(data).merge(etag: data['etag'])
6774
end
6875

6976
# @private
@@ -91,11 +98,17 @@ def eager_load_items_from(items)
9198

9299
def fetch_page(params = {})
93100
@last_response = list_request(params).run
101+
@etag = @last_response.body['etag']
94102
token = @last_response.body['nextPageToken']
95103
items = extract_items @last_response.body
96104
{items: items, token: token}
97105
end
98106

107+
def fetch_etag
108+
response = list_request(list_params).run
109+
response.body['etag']
110+
end
111+
99112
def list_request(params = {})
100113
@list_request = Yt::Request.new(params).tap do |request|
101114
print "#{request.as_curl}\n" if Yt.configuration.developing?

lib/yt/collections/assets.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def insert(attributes = {})
1717

1818
def new_item(data)
1919
klass = (data["kind"] == "youtubePartner#assetSnippet") ? Yt::AssetSnippet : Yt::Asset
20-
klass.new attributes_for_new_item(data)
20+
klass.new attributes_for_new_item(data).merge(etag: data['etag'])
2121
end
2222

2323
# @return [Hash] the parameters to submit to YouTube to list assets

lib/yt/collections/resources.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def insert(attributes = {}, options = {}) #
1919
private
2020

2121
def attributes_for_new_item(data)
22-
{id: data['id'], snippet: data['snippet'], status: data['status'], auth: @auth}
22+
{id: data['id'], snippet: data['snippet'], status: data['status'], auth: @auth, etag: data['etag']}
2323
end
2424

2525
def resources_params

lib/yt/models/playlist.rb

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,11 +209,6 @@ def reports_params
209209
end
210210
end
211211

212-
# @private
213-
def exists?
214-
!@id.nil?
215-
end
216-
217212
private
218213

219214
# @see https://developers.google.com/youtube/v3/docs/playlists/update

lib/yt/models/playlist_item.rb

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,6 @@ def video
8383

8484
### PRIVATE API ###
8585

86-
# @private
87-
def exists?
88-
!@id.nil?
89-
end
90-
9186
# @private
9287
# Override Resource's new to set video if the response includes it
9388
def initialize(options = {})

lib/yt/models/resource.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,19 @@ def id
2020
end
2121
end
2222

23+
### ETAG ###
24+
def etag
25+
return nil unless exists?
26+
27+
@etag ||= fetch_etag
28+
end
29+
30+
### EXISTS? ###
31+
32+
def exists?
33+
!@id.nil?
34+
end
35+
2336
### STATUS ###
2437

2538
has_one :status
@@ -56,6 +69,7 @@ def initialize(options = {})
5669
@id = options[:id]
5770
end
5871
@auth = options[:auth]
72+
@etag = options[:etag]
5973
@snippet = Snippet.new(data: options[:snippet]) if options[:snippet]
6074
@status = Status.new(data: options[:status]) if options[:status]
6175
end
@@ -139,6 +153,20 @@ def fetch_channel_id
139153
end
140154
end
141155

156+
def fetch_etag
157+
return nil if @auth.nil? || @id.nil?
158+
159+
collection = resource_collection.new(auth: @auth).where(id: @id)
160+
resource = collection.first
161+
resource.etag if resource
162+
end
163+
164+
def resource_collection
165+
name = self.class.to_s.demodulize.pluralize
166+
require "yt/collections/#{name.underscore}"
167+
"Yt::Collections::#{name}".constantize
168+
end
169+
142170
# Since YouTube API only returns tags on Videos#list, the memoized
143171
# `@snippet` is erased if the video was instantiated through Video#search
144172
# (e.g., by calling account.videos or channel.videos), so that the full

lib/yt/models/video.rb

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -635,11 +635,6 @@ def initialize(options = {})
635635
end
636636
end
637637

638-
# @private
639-
def exists?
640-
!@id.nil?
641-
end
642-
643638
# @private
644639
# Tells `has_reports` to retrieve the reports from YouTube Analytics API
645640
# either as a Channel or as a Content Owner.

spec/collections/channels_spec.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
require 'spec_helper'
2+
require 'yt/collections/channels'
3+
4+
describe Yt::Collections::Channels do
5+
subject(:collection) { Yt::Collections::Channels.new }
6+
7+
describe '#etag' do
8+
let(:etag) { 'etag123' }
9+
10+
before do
11+
expect_any_instance_of(Yt::Request).to receive(:run).once do
12+
double(body: {'etag'=> etag, 'items'=> [], 'pageInfo'=> {'totalResults'=>0}})
13+
end
14+
end
15+
16+
it 'returns the etag from the list response' do
17+
expect(collection.etag).to eq etag
18+
end
19+
end
20+
end

spec/collections/comment_threads_spec.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,19 @@
4343
end
4444
end
4545
end
46+
47+
describe '#etag' do
48+
let(:parent) { Yt::Video.new id: 'any-id' }
49+
let(:etag) { 'etag123' }
50+
51+
before do
52+
expect_any_instance_of(Yt::Request).to receive(:run).once do
53+
double(body: {'etag'=> etag, 'items'=> [], 'pageInfo'=> {'totalResults'=>0}})
54+
end
55+
end
56+
57+
it 'returns the etag from the list response' do
58+
expect(collection.etag).to eq etag
59+
end
60+
end
4661
end

spec/collections/playlist_items_spec.rb

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,19 @@
4141

4242
it { expect(collection.delete_all).to eq [true] }
4343
end
44-
end
44+
45+
describe '#etag' do
46+
let(:etag) { 'etag123' }
47+
let(:behave) { receive(:fetch_etag).and_call_original }
48+
49+
before do
50+
expect_any_instance_of(Yt::Request).to receive(:run).once do
51+
double(body: {'etag'=> etag, 'items'=> [], 'pageInfo'=> {'totalResults'=>0}})
52+
end
53+
end
54+
55+
it 'returns the etag from the list response' do
56+
expect(collection.etag).to eq etag
57+
end
58+
end
59+
end

0 commit comments

Comments
 (0)