yt 0.11.6 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/README.md +13 -2
  4. data/lib/yt/actions/list.rb +11 -1
  5. data/lib/yt/associations/has_attribute.rb +51 -0
  6. data/lib/yt/associations/has_many.rb +0 -15
  7. data/lib/yt/collections/content_owners.rb +0 -4
  8. data/lib/yt/collections/playlist_items.rb +1 -1
  9. data/lib/yt/collections/resources.rb +1 -1
  10. data/lib/yt/collections/subscribed_channels.rb +46 -0
  11. data/lib/yt/collections/subscribers.rb +33 -0
  12. data/lib/yt/collections/subscriptions.rb +0 -23
  13. data/lib/yt/models/account.rb +6 -1
  14. data/lib/yt/models/annotation.rb +4 -10
  15. data/lib/yt/models/asset.rb +3 -10
  16. data/lib/yt/models/base.rb +2 -0
  17. data/lib/yt/models/channel.rb +53 -25
  18. data/lib/yt/models/claim.rb +13 -25
  19. data/lib/yt/models/content_detail.rb +9 -11
  20. data/lib/yt/models/content_owner_detail.rb +2 -8
  21. data/lib/yt/models/device_flow.rb +3 -11
  22. data/lib/yt/models/live_streaming_detail.rb +5 -25
  23. data/lib/yt/models/ownership.rb +8 -8
  24. data/lib/yt/models/playlist.rb +14 -10
  25. data/lib/yt/models/playlist_item.rb +13 -0
  26. data/lib/yt/models/policy.rb +10 -14
  27. data/lib/yt/models/policy_rule.rb +15 -21
  28. data/lib/yt/models/reference.rb +13 -37
  29. data/lib/yt/models/right_owner.rb +6 -17
  30. data/lib/yt/models/snippet.rb +19 -33
  31. data/lib/yt/models/statistics_set.rb +9 -23
  32. data/lib/yt/models/status.rb +20 -25
  33. data/lib/yt/models/subscription.rb +2 -8
  34. data/lib/yt/models/user_info.rb +11 -33
  35. data/lib/yt/version.rb +1 -1
  36. data/spec/collections/subscriptions_spec.rb +0 -7
  37. data/spec/errors/forbidden_spec.rb +10 -0
  38. data/spec/errors/server_error_spec.rb +10 -0
  39. data/spec/models/subscription_spec.rb +0 -9
  40. data/spec/requests/as_account/account_spec.rb +8 -0
  41. data/spec/requests/as_account/channel_spec.rb +40 -6
  42. data/spec/requests/as_account/playlist_item_spec.rb +26 -0
  43. data/spec/requests/as_account/playlist_spec.rb +1 -0
  44. data/spec/requests/as_server_app/channel_spec.rb +9 -0
  45. metadata +9 -2
@@ -2,6 +2,7 @@ require 'yt/actions/delete'
2
2
  require 'yt/actions/update'
3
3
  require 'yt/actions/patch'
4
4
 
5
+ require 'yt/associations/has_attribute'
5
6
  require 'yt/associations/has_authentication'
6
7
  require 'yt/associations/has_many'
7
8
  require 'yt/associations/has_one'
@@ -17,6 +18,7 @@ module Yt
17
18
  include Actions::Update
18
19
  include Actions::Patch
19
20
 
21
+ include Associations::HasAttribute
20
22
  extend Associations::HasReports
21
23
  extend Associations::HasViewerPercentages
22
24
  extend Associations::HasOne
@@ -5,10 +5,6 @@ module Yt
5
5
  # A channel resource contains information about a YouTube channel.
6
6
  # @see https://developers.google.com/youtube/v3/docs/channels
7
7
  class Channel < Resource
8
- # @!attribute [r] subscriptions
9
- # @return [Yt::Collections::Subscriptions] the channel’s subscriptions.
10
- has_many :subscriptions
11
-
12
8
  # @!attribute [r] videos
13
9
  # @return [Yt::Collections::Videos] the channel’s videos.
14
10
  has_many :videos
@@ -53,6 +49,20 @@ module Yt
53
49
  has_one :content_owner_detail
54
50
  delegate :content_owner, :linked_at, to: :content_owner_detail
55
51
 
52
+ # @!attribute [r] subscribed_channels
53
+ # @return [Yt::Collections::SubscribedChannels] the channels that the channel is subscribed to.
54
+ # @raise [Yt::Errors::Forbidden] if the owner of the channel has
55
+ # explicitly select the option to keep all subscriptions private.
56
+ has_many :subscribed_channels
57
+
58
+ # @!attribute [r] subscription
59
+ # @return [Yt::Models::Subscription] the channel’s subscription by auth.
60
+ # @raise [Yt::Errors::NoItems] if {Resource#auth auth} is not
61
+ # subscribed to the channel.
62
+ # @raise [Yt::Errors::Unauthorized] if {Resource#auth auth} does not
63
+ # return an authenticated account.
64
+ has_one :subscription
65
+
56
66
  # Returns whether the authenticated account is subscribed to the channel.
57
67
  #
58
68
  # This method requires {Resource#auth auth} to return an
@@ -61,55 +71,64 @@ module Yt
61
71
  # @raise [Yt::Errors::Unauthorized] if {Resource#auth auth} does not
62
72
  # return an authenticated account.
63
73
  def subscribed?
64
- subscriptions.any?{|s| s.exists?}
74
+ sleep [(@subscriptions_updated_at || Time.now) - Time.now, 0].max
75
+ subscription.exists?
76
+ rescue Errors::NoItems
77
+ false
65
78
  end
66
79
 
67
- # Subscribes the authenticated account to the channel.
68
- # Does not raise an error if the account was already subscribed.
80
+ # Unsubscribes the authenticated account from the channel.
81
+ # Raises an error if the account was not subscribed.
69
82
  #
70
83
  # This method requires {Resource#auth auth} to return an
71
84
  # authenticated instance of {Yt::Account}.
85
+ # @raise [Yt::Errors::RequestError] if {Resource#auth auth} was not
86
+ # subscribed to the channel.
72
87
  # @raise [Yt::Errors::Unauthorized] if {Resource#auth auth} does not
73
88
  # return an authenticated account.
74
- def subscribe
75
- subscriptions.insert ignore_errors: true
89
+ def unsubscribe!
90
+ subscription.delete.tap{ throttle_subscriptions }
76
91
  end
77
92
 
78
- # Subscribes the authenticated account to the channel.
79
- # Raises an error if the account was already subscribed.
93
+ # Unsubscribes the authenticated account from the channel.
94
+ # Does not raise an error if the account was not subscribed.
80
95
  #
81
96
  # This method requires {Resource#auth auth} to return an
82
97
  # authenticated instance of {Yt::Account}.
83
- # @raise [Yt::Errors::RequestError] if {Resource#auth auth} was already
84
- # subscribed to the channel.
85
98
  # @raise [Yt::Errors::Unauthorized] if {Resource#auth auth} does not
86
99
  # return an authenticated account.
87
- def subscribe!
88
- subscriptions.insert
100
+ def unsubscribe
101
+ unsubscribe! if subscribed?
89
102
  end
90
103
 
91
- # Unsubscribes the authenticated account from the channel.
92
- # Does not raise an error if the account was not subscribed.
104
+ # Subscribes the authenticated account to the channel.
105
+ # Raises an error if the account was already subscribed.
93
106
  #
94
107
  # This method requires {Resource#auth auth} to return an
95
108
  # authenticated instance of {Yt::Account}.
109
+ # @raise [Yt::Errors::RequestError] if {Resource#auth auth} was already
110
+ # subscribed to the channel.
96
111
  # @raise [Yt::Errors::Unauthorized] if {Resource#auth auth} does not
97
112
  # return an authenticated account.
98
- def unsubscribe
99
- subscriptions.delete_all({}, ignore_errors: true)
113
+ def subscribe!
114
+ subscriptions.insert.tap do |subscription|
115
+ throttle_subscriptions
116
+ @subscription = subscription
117
+ end
100
118
  end
101
119
 
102
- # Unsubscribes the authenticated account from the channel.
103
- # Raises an error if the account was not subscribed.
120
+ # Subscribes the authenticated account to the channel.
121
+ # Does not raise an error if the account was already subscribed.
104
122
  #
105
123
  # This method requires {Resource#auth auth} to return an
106
124
  # authenticated instance of {Yt::Account}.
107
- # @raise [Yt::Errors::RequestError] if {Resource#auth auth} was not
108
- # subscribed to the channel.
109
125
  # @raise [Yt::Errors::Unauthorized] if {Resource#auth auth} does not
110
126
  # return an authenticated account.
111
- def unsubscribe!
112
- subscriptions.delete_all
127
+ def subscribe
128
+ subscriptions.insert(ignore_errors: true).tap do |subscription|
129
+ throttle_subscriptions
130
+ @subscription = subscription
131
+ end
113
132
  end
114
133
 
115
134
  def create_playlist(params = {})
@@ -150,6 +169,15 @@ module Yt
150
169
  def content_owner_details_params
151
170
  {on_behalf_of_content_owner: auth.owner_name || auth.id}
152
171
  end
172
+
173
+ # @private
174
+ # @note Google API must have some caching layer by which if we try to
175
+ # delete a subscription that we just created, we encounter an error.
176
+ # To overcome this, if we have just updated the subscription, we must
177
+ # wait some time before requesting it again.
178
+ def throttle_subscriptions(seconds = 10)
179
+ @subscriptions_updated_at = Time.now + seconds
180
+ end
153
181
  end
154
182
  end
155
183
  end
@@ -24,21 +24,15 @@ module Yt
24
24
 
25
25
  # @return [String] the ID that YouTube assigns and uses to uniquely
26
26
  # identify the claim.
27
- def id
28
- @id ||= @data['id']
29
- end
27
+ has_attribute :id
30
28
 
31
29
  # @return [String] the unique YouTube asset ID that identifies the asset
32
30
  # associated with the claim.
33
- def asset_id
34
- @asset_id ||= @data["assetId"]
35
- end
31
+ has_attribute :asset_id
36
32
 
37
33
  # @return [String] the unique YouTube video ID that identifies the video
38
34
  # associated with the claim.
39
- def video_id
40
- @video_id ||= @data["videoId"]
41
- end
35
+ has_attribute :video_id
42
36
 
43
37
  # Status
44
38
 
@@ -50,9 +44,7 @@ module Yt
50
44
  # @note When updating a claim, you can update its status from active to
51
45
  # inactive to effectively release the claim, but the API does not
52
46
  # support other updates to a claim’s status.
53
- def status
54
- @status ||= @data["status"]
55
- end
47
+ has_attribute :status
56
48
 
57
49
  # @return [Boolean] whether the claim is active.
58
50
  def active?
@@ -101,9 +93,7 @@ module Yt
101
93
  # @return [String] whether the claim covers the audio, video, or
102
94
  # audiovisual portion of the claimed content. Valid values are: audio,
103
95
  # audiovisual, video.
104
- def content_type
105
- @content_type ||= @data["contentType"]
106
- end
96
+ has_attribute :content_type
107
97
 
108
98
  # @return [Boolean] whether the covers the audio of the content.
109
99
  def audio?
@@ -121,13 +111,11 @@ module Yt
121
111
  end
122
112
 
123
113
  # @return [Time] the date and time that the claim was created.
124
- def created_at
125
- @created_at ||= Time.parse @data["timeCreated"]
126
- end
114
+ has_attribute :created_at, type: Time, from: :time_created
127
115
 
128
116
  # @return [Boolean] whether a third party created the claim.
129
- def third_party?
130
- @third_party_claim ||= @data['thirdPartyClaim'] == true
117
+ has_attribute :third_party?, from: :third_party_claim do |value|
118
+ value == true
131
119
  end
132
120
 
133
121
  # Return whether the video should be blocked where not explicitly owned.
@@ -141,16 +129,16 @@ module Yt
141
129
  # though it will not be monetized in those countries. However, if you
142
130
  # set this property to true, then the video will be monetized in the
143
131
  # United States and Canada and blocked in all other countries.
144
- def block_outside_ownership?
145
- @block_outside_ownership ||= @data["blockOutsideOwnership"]
146
- end
132
+ has_attribute :block_outside_ownership?, from: :block_outside_ownership
147
133
 
148
134
  # @return [String] The unique ID that YouTube uses to identify the
149
135
  # reference that generated the match.
150
- def match_reference_id
151
- @match_reference_id ||= @data.fetch('matchInfo', {})['referenceId']
136
+ has_attribute :match_reference_id, from: :match_info do |match_info|
137
+ (match_info || {})['referenceId']
152
138
  end
153
139
 
140
+ private
141
+
154
142
  # @see https://developers.google.com/youtube/partner/docs/v1/claims/update
155
143
  def patch_params
156
144
  super.tap do |params|
@@ -12,30 +12,28 @@ module Yt
12
12
  end
13
13
 
14
14
  # @return [Integer] the duration of the video (in seconds).
15
- def duration
16
- @duration ||= to_seconds @data.fetch('duration', 0)
15
+ has_attribute :duration, default: 0 do |value|
16
+ to_seconds value
17
17
  end
18
18
 
19
19
  # @return [Boolean] whether the video is available in 3D.
20
- def stereoscopic?
21
- @stereoscopic ||= @data['dimension'] == '3d'
20
+ has_attribute :stereoscopic?, from: :dimension do |dimension|
21
+ dimension == '3d'
22
22
  end
23
23
 
24
24
  # @return [Boolean] whether the video is available in high definition.
25
- def hd?
26
- @hd ||= @data['definition'] == 'hd'
25
+ has_attribute :hd?, from: :definition do |definition|
26
+ definition == 'hd'
27
27
  end
28
28
 
29
29
  # @return [Boolean] whether captions are available for the video.
30
- def captioned?
31
- @hd ||= @data['caption'] == 'true'
30
+ has_attribute :captioned?, from: :caption do |caption|
31
+ caption == 'true'
32
32
  end
33
33
 
34
34
  # @return [Boolean] whether the video represents licensed content, which
35
35
  # means that the content has been claimed by a YouTube content partner.
36
- def licensed?
37
- @licensed ||= @data.fetch 'licensedContent', false
38
- end
36
+ has_attribute :licensed?, default: false, from: :licensed_content
39
37
 
40
38
  private
41
39
 
@@ -21,9 +21,7 @@ module Yt
21
21
  # permissions to administer the channel.
22
22
  # @raise [Yt::Errors::Forbidden] if {Resource#auth auth} does not
23
23
  # return an authenticated content owner.
24
- def content_owner
25
- @content_owner ||= @data['contentOwner']
26
- end
24
+ has_attribute :content_owner
27
25
 
28
26
  # Returns the date and time of when the channel was linked to the content
29
27
  # owner.
@@ -37,11 +35,7 @@ module Yt
37
35
  # permissions to administer the channel.
38
36
  # @raise [Yt::Errors::Forbidden] if {Resource#auth auth} does not
39
37
  # return an authenticated content owner.
40
- def linked_at
41
- @linked_at ||= if @data['timeLinked']
42
- Time.parse @data['timeLinked']
43
- end
44
- end
38
+ has_attribute :linked_at, type: Time, from: :time_linked
45
39
  end
46
40
  end
47
41
  end
@@ -7,17 +7,9 @@ module Yt
7
7
  @data = options[:data]
8
8
  end
9
9
 
10
- def device_code
11
- @device_code ||= @data['device_code']
12
- end
13
-
14
- def user_code
15
- @user_code ||= @data['user_code']
16
- end
17
-
18
- def verification_url
19
- @verification_url ||= @data['verification_url']
20
- end
10
+ has_attribute :device_code, camelize: false
11
+ has_attribute :user_code, camelize: false
12
+ has_attribute :verification_url, camelize: false
21
13
  end
22
14
  end
23
15
  end
@@ -15,47 +15,27 @@ module Yt
15
15
  # @return [Time] if the broadcast has begun, the time that the broadcast
16
16
  # actually started.
17
17
  # @return [nil] if the broadcast has not begun.
18
- def actual_start_time
19
- @actual_start_time ||= if @data['actualStartTime']
20
- Time.parse @data['actualStartTime']
21
- end
22
- end
18
+ has_attribute :actual_start_time, type: Time
23
19
 
24
20
  # @return [Time] if the broadcast is over, the time that the broadcast
25
21
  # actually ended.
26
22
  # @return [nil] if the broadcast is not over.
27
- def actual_end_time
28
- @actual_end_time ||= if @data['actualEndTime']
29
- Time.parse @data['actualEndTime']
30
- end
31
- end
23
+ has_attribute :actual_end_time, type: Time
32
24
 
33
25
  # @return [Time] the time that the broadcast is scheduled to begin.
34
- def scheduled_start_time
35
- @scheduled_start_time ||= if @data['scheduledStartTime']
36
- Time.parse @data['scheduledStartTime']
37
- end
38
- end
26
+ has_attribute :scheduled_start_time, type: Time
39
27
 
40
28
  # @return [Time] if the broadcast is scheduled to end, the time that the
41
29
  # broadcast is scheduled to end.
42
30
  # @return [nil] if the broadcast is scheduled to continue indefinitely.
43
- def scheduled_end_time
44
- @scheduled_end_time ||= if @data['scheduledEndTime']
45
- Time.parse @data['scheduledEndTime']
46
- end
47
- end
31
+ has_attribute :scheduled_end_time, type: Time
48
32
 
49
33
  # @return [Integer] if the broadcast has current viewers and the
50
34
  # broadcast owner has not hidden the viewcount for the video, the
51
35
  # number of viewers currently watching the broadcast.
52
36
  # @return [nil] if the broadcast has ended or the broadcast owner has
53
37
  # hidden the viewcount for the video.
54
- def concurrent_viewers
55
- @concurrent_viewers ||= if @data['concurrentViewers']
56
- @data['concurrentViewers'].to_i
57
- end
58
- end
38
+ has_attribute :concurrent_viewers, type: Integer
59
39
  end
60
40
  end
61
41
  end
@@ -29,26 +29,26 @@ module Yt
29
29
  # General asset ownership is used for all types of assets and is the
30
30
  # only type of ownership data that can be provided for assets that are
31
31
  # not compositions.
32
- def general_owners
33
- @general_owners ||= as_owners @data['general']
32
+ has_attribute :general_owners, from: :general do |data|
33
+ as_owners data
34
34
  end
35
35
 
36
36
  # @return [Array<RightOwner>] a list that identifies owners of the
37
37
  # performance rights for a composition asset.
38
- def performance_owners
39
- @performance_owners ||= as_owners @data['performance']
38
+ has_attribute :performance_owners, from: :performance do |data|
39
+ as_owners data
40
40
  end
41
41
 
42
42
  # @return [Array<RightOwner>] a list that identifies owners of the
43
43
  # synchronization rights for a composition asset.
44
- def synchronization_owners
45
- @synchronization_owners ||= as_owners @data['synchronization']
44
+ has_attribute :synchronization_owners, from: :synchronization do |data|
45
+ as_owners data
46
46
  end
47
47
 
48
48
  # @return [Array<RightOwner>] a list that identifies owners of the
49
49
  # mechanical rights for a composition asset.
50
- def mechanical_owners
51
- @mechanical_owners ||= as_owners @data['mechanical']
50
+ has_attribute :mechanical_owners, from: :mechanical do |data|
51
+ as_owners data
52
52
  end
53
53
 
54
54
  private
@@ -35,20 +35,22 @@ module Yt
35
35
  # @raise [Yt::Errors::Unauthorized] if {Resource#auth auth} does not
36
36
  # return an account with permissions to update the playlist.
37
37
  # @return [Yt::PlaylistItem] the item added to the playlist.
38
- def add_video(video_id)
39
- playlist_items.insert video_params(video_id), ignore_errors: true
38
+ def add_video(video_id, attributes = {})
39
+ playlist_item_params = playlist_item_params(video_id, attributes)
40
+ playlist_items.insert playlist_item_params, ignore_errors: true
40
41
  end
41
42
 
42
- def add_video!(video_id)
43
- playlist_items.insert video_params(video_id)
43
+ def add_video!(video_id, attributes = {})
44
+ playlist_item_params = playlist_item_params(video_id, attributes)
45
+ playlist_items.insert playlist_item_params
44
46
  end
45
47
 
46
- def add_videos(video_ids = [])
47
- video_ids.map{|video_id| add_video video_id}
48
+ def add_videos(video_ids = [], attributes = {})
49
+ video_ids.map{|video_id| add_video video_id, attributes}
48
50
  end
49
51
 
50
- def add_videos!(video_ids = [])
51
- video_ids.map{|video_id| add_video! video_id}
52
+ def add_videos!(video_ids = [], attributes = {})
53
+ video_ids.map{|video_id| add_video! video_id, attributes}
52
54
  end
53
55
 
54
56
  def delete_playlist_items(attrs = {})
@@ -67,8 +69,10 @@ module Yt
67
69
 
68
70
  # @todo: extend camelize to also camelize the nested hashes, so we
69
71
  # don’t have to write videoId
70
- def video_params(video_id)
71
- {resource_id: {kind: 'youtube#video', videoId: video_id}}
72
+ def playlist_item_params(video_id, params = {})
73
+ params.dup.tap do |params|
74
+ params[:resource_id] ||= {kind: 'youtube#video', videoId: video_id}
75
+ end
72
76
  end
73
77
  end
74
78
  end