yt-andrewroth 0.25.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (191) hide show
  1. data/.gitignore +27 -0
  2. data/.rspec +3 -0
  3. data/.travis.yml +9 -0
  4. data/.yardopts +5 -0
  5. data/CHANGELOG.md +732 -0
  6. data/Gemfile +4 -0
  7. data/MIT-LICENSE +20 -0
  8. data/README.md +489 -0
  9. data/Rakefile +11 -0
  10. data/YOUTUBE_IT.md +835 -0
  11. data/bin/yt +30 -0
  12. data/gemfiles/Gemfile.activesupport-3.x +4 -0
  13. data/gemfiles/Gemfile.activesupport-4.x +4 -0
  14. data/lib/yt.rb +21 -0
  15. data/lib/yt/actions/base.rb +32 -0
  16. data/lib/yt/actions/delete.rb +19 -0
  17. data/lib/yt/actions/delete_all.rb +32 -0
  18. data/lib/yt/actions/insert.rb +42 -0
  19. data/lib/yt/actions/list.rb +139 -0
  20. data/lib/yt/actions/modify.rb +37 -0
  21. data/lib/yt/actions/patch.rb +19 -0
  22. data/lib/yt/actions/update.rb +19 -0
  23. data/lib/yt/associations/has_attribute.rb +55 -0
  24. data/lib/yt/associations/has_authentication.rb +214 -0
  25. data/lib/yt/associations/has_many.rb +22 -0
  26. data/lib/yt/associations/has_one.rb +22 -0
  27. data/lib/yt/associations/has_reports.rb +320 -0
  28. data/lib/yt/collections/advertising_options_sets.rb +34 -0
  29. data/lib/yt/collections/annotations.rb +62 -0
  30. data/lib/yt/collections/assets.rb +58 -0
  31. data/lib/yt/collections/authentications.rb +47 -0
  32. data/lib/yt/collections/base.rb +62 -0
  33. data/lib/yt/collections/channels.rb +31 -0
  34. data/lib/yt/collections/claim_histories.rb +34 -0
  35. data/lib/yt/collections/claims.rb +56 -0
  36. data/lib/yt/collections/content_details.rb +30 -0
  37. data/lib/yt/collections/content_owner_details.rb +34 -0
  38. data/lib/yt/collections/content_owners.rb +32 -0
  39. data/lib/yt/collections/device_flows.rb +23 -0
  40. data/lib/yt/collections/file_details.rb +30 -0
  41. data/lib/yt/collections/ids.rb +27 -0
  42. data/lib/yt/collections/live_streaming_details.rb +30 -0
  43. data/lib/yt/collections/ownerships.rb +34 -0
  44. data/lib/yt/collections/partnered_channels.rb +28 -0
  45. data/lib/yt/collections/players.rb +30 -0
  46. data/lib/yt/collections/playlist_items.rb +53 -0
  47. data/lib/yt/collections/playlists.rb +28 -0
  48. data/lib/yt/collections/policies.rb +28 -0
  49. data/lib/yt/collections/ratings.rb +23 -0
  50. data/lib/yt/collections/references.rb +46 -0
  51. data/lib/yt/collections/related_playlists.rb +43 -0
  52. data/lib/yt/collections/reports.rb +161 -0
  53. data/lib/yt/collections/resources.rb +57 -0
  54. data/lib/yt/collections/resumable_sessions.rb +51 -0
  55. data/lib/yt/collections/snippets.rb +27 -0
  56. data/lib/yt/collections/statistics_sets.rb +30 -0
  57. data/lib/yt/collections/statuses.rb +27 -0
  58. data/lib/yt/collections/subscribed_channels.rb +46 -0
  59. data/lib/yt/collections/subscribers.rb +33 -0
  60. data/lib/yt/collections/subscriptions.rb +50 -0
  61. data/lib/yt/collections/user_infos.rb +36 -0
  62. data/lib/yt/collections/video_categories.rb +35 -0
  63. data/lib/yt/collections/videos.rb +137 -0
  64. data/lib/yt/config.rb +54 -0
  65. data/lib/yt/errors/forbidden.rb +13 -0
  66. data/lib/yt/errors/missing_auth.rb +81 -0
  67. data/lib/yt/errors/no_items.rb +13 -0
  68. data/lib/yt/errors/request_error.rb +74 -0
  69. data/lib/yt/errors/server_error.rb +13 -0
  70. data/lib/yt/errors/unauthorized.rb +50 -0
  71. data/lib/yt/models/account.rb +216 -0
  72. data/lib/yt/models/advertising_options_set.rb +38 -0
  73. data/lib/yt/models/annotation.rb +132 -0
  74. data/lib/yt/models/asset.rb +111 -0
  75. data/lib/yt/models/asset_metadata.rb +38 -0
  76. data/lib/yt/models/asset_snippet.rb +46 -0
  77. data/lib/yt/models/authentication.rb +83 -0
  78. data/lib/yt/models/base.rb +32 -0
  79. data/lib/yt/models/channel.rb +302 -0
  80. data/lib/yt/models/claim.rb +156 -0
  81. data/lib/yt/models/claim_event.rb +67 -0
  82. data/lib/yt/models/claim_history.rb +29 -0
  83. data/lib/yt/models/configuration.rb +70 -0
  84. data/lib/yt/models/content_detail.rb +65 -0
  85. data/lib/yt/models/content_owner.rb +48 -0
  86. data/lib/yt/models/content_owner_detail.rb +18 -0
  87. data/lib/yt/models/description.rb +58 -0
  88. data/lib/yt/models/device_flow.rb +16 -0
  89. data/lib/yt/models/file_detail.rb +21 -0
  90. data/lib/yt/models/id.rb +9 -0
  91. data/lib/yt/models/iterator.rb +16 -0
  92. data/lib/yt/models/live_streaming_detail.rb +23 -0
  93. data/lib/yt/models/match_policy.rb +34 -0
  94. data/lib/yt/models/ownership.rb +75 -0
  95. data/lib/yt/models/player.rb +18 -0
  96. data/lib/yt/models/playlist.rb +218 -0
  97. data/lib/yt/models/playlist_item.rb +112 -0
  98. data/lib/yt/models/policy.rb +36 -0
  99. data/lib/yt/models/policy_rule.rb +124 -0
  100. data/lib/yt/models/rating.rb +37 -0
  101. data/lib/yt/models/reference.rb +172 -0
  102. data/lib/yt/models/resource.rb +136 -0
  103. data/lib/yt/models/resumable_session.rb +52 -0
  104. data/lib/yt/models/right_owner.rb +58 -0
  105. data/lib/yt/models/snippet.rb +50 -0
  106. data/lib/yt/models/statistics_set.rb +26 -0
  107. data/lib/yt/models/status.rb +32 -0
  108. data/lib/yt/models/subscription.rb +38 -0
  109. data/lib/yt/models/timestamp.rb +13 -0
  110. data/lib/yt/models/url.rb +90 -0
  111. data/lib/yt/models/user_info.rb +26 -0
  112. data/lib/yt/models/video.rb +630 -0
  113. data/lib/yt/models/video_category.rb +12 -0
  114. data/lib/yt/request.rb +278 -0
  115. data/lib/yt/version.rb +3 -0
  116. data/spec/collections/claims_spec.rb +30 -0
  117. data/spec/collections/playlist_items_spec.rb +44 -0
  118. data/spec/collections/playlists_spec.rb +27 -0
  119. data/spec/collections/policies_spec.rb +30 -0
  120. data/spec/collections/references_spec.rb +30 -0
  121. data/spec/collections/reports_spec.rb +30 -0
  122. data/spec/collections/subscriptions_spec.rb +25 -0
  123. data/spec/collections/videos_spec.rb +43 -0
  124. data/spec/errors/forbidden_spec.rb +10 -0
  125. data/spec/errors/missing_auth_spec.rb +24 -0
  126. data/spec/errors/no_items_spec.rb +10 -0
  127. data/spec/errors/request_error_spec.rb +44 -0
  128. data/spec/errors/server_error_spec.rb +10 -0
  129. data/spec/errors/unauthorized_spec.rb +10 -0
  130. data/spec/models/account_spec.rb +138 -0
  131. data/spec/models/annotation_spec.rb +180 -0
  132. data/spec/models/asset_spec.rb +20 -0
  133. data/spec/models/channel_spec.rb +127 -0
  134. data/spec/models/claim_event_spec.rb +62 -0
  135. data/spec/models/claim_history_spec.rb +27 -0
  136. data/spec/models/claim_spec.rb +211 -0
  137. data/spec/models/configuration_spec.rb +44 -0
  138. data/spec/models/content_detail_spec.rb +45 -0
  139. data/spec/models/content_owner_detail_spec.rb +6 -0
  140. data/spec/models/description_spec.rb +94 -0
  141. data/spec/models/file_detail_spec.rb +13 -0
  142. data/spec/models/live_streaming_detail_spec.rb +6 -0
  143. data/spec/models/ownership_spec.rb +59 -0
  144. data/spec/models/player_spec.rb +13 -0
  145. data/spec/models/playlist_item_spec.rb +120 -0
  146. data/spec/models/playlist_spec.rb +138 -0
  147. data/spec/models/policy_rule_spec.rb +63 -0
  148. data/spec/models/policy_spec.rb +41 -0
  149. data/spec/models/rating_spec.rb +12 -0
  150. data/spec/models/reference_spec.rb +249 -0
  151. data/spec/models/request_spec.rb +163 -0
  152. data/spec/models/resource_spec.rb +57 -0
  153. data/spec/models/right_owner_spec.rb +71 -0
  154. data/spec/models/snippet_spec.rb +13 -0
  155. data/spec/models/statistics_set_spec.rb +13 -0
  156. data/spec/models/status_spec.rb +13 -0
  157. data/spec/models/subscription_spec.rb +30 -0
  158. data/spec/models/url_spec.rb +78 -0
  159. data/spec/models/video_category_spec.rb +21 -0
  160. data/spec/models/video_spec.rb +669 -0
  161. data/spec/requests/as_account/account_spec.rb +125 -0
  162. data/spec/requests/as_account/authentications_spec.rb +139 -0
  163. data/spec/requests/as_account/channel_spec.rb +259 -0
  164. data/spec/requests/as_account/channels_spec.rb +18 -0
  165. data/spec/requests/as_account/playlist_item_spec.rb +56 -0
  166. data/spec/requests/as_account/playlist_spec.rb +244 -0
  167. data/spec/requests/as_account/resource_spec.rb +18 -0
  168. data/spec/requests/as_account/thumbnail.jpg +0 -0
  169. data/spec/requests/as_account/video.mp4 +0 -0
  170. data/spec/requests/as_account/video_spec.rb +408 -0
  171. data/spec/requests/as_content_owner/account_spec.rb +25 -0
  172. data/spec/requests/as_content_owner/advertising_options_set_spec.rb +15 -0
  173. data/spec/requests/as_content_owner/asset_spec.rb +20 -0
  174. data/spec/requests/as_content_owner/channel_spec.rb +1934 -0
  175. data/spec/requests/as_content_owner/claim_history_spec.rb +20 -0
  176. data/spec/requests/as_content_owner/content_owner_spec.rb +241 -0
  177. data/spec/requests/as_content_owner/match_policy_spec.rb +17 -0
  178. data/spec/requests/as_content_owner/ownership_spec.rb +25 -0
  179. data/spec/requests/as_content_owner/playlist_spec.rb +782 -0
  180. data/spec/requests/as_content_owner/video_spec.rb +1239 -0
  181. data/spec/requests/as_server_app/channel_spec.rb +74 -0
  182. data/spec/requests/as_server_app/playlist_item_spec.rb +30 -0
  183. data/spec/requests/as_server_app/playlist_spec.rb +53 -0
  184. data/spec/requests/as_server_app/video_spec.rb +58 -0
  185. data/spec/requests/as_server_app/videos_spec.rb +40 -0
  186. data/spec/requests/unauthenticated/video_spec.rb +22 -0
  187. data/spec/spec_helper.rb +20 -0
  188. data/spec/support/fail_matcher.rb +15 -0
  189. data/spec/support/global_hooks.rb +48 -0
  190. data/yt.gemspec +32 -0
  191. metadata +416 -0
@@ -0,0 +1,13 @@
1
+ require 'yt/errors/request_error'
2
+
3
+ module Yt
4
+ module Errors
5
+ class ServerError < RequestError
6
+ private
7
+
8
+ def explanation
9
+ 'A request to YouTube API caused an unexpected server error'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,50 @@
1
+ require 'yt/errors/request_error'
2
+ require 'yt/config'
3
+
4
+ module Yt
5
+ module Errors
6
+ class Unauthorized < RequestError
7
+ private
8
+
9
+ def explanation
10
+ 'A request to YouTube API was sent without a valid authentication'
11
+ end
12
+
13
+ def more_details
14
+ if [Yt.configuration.client_id, Yt.configuration.api_key].none?
15
+ <<-MSG.gsub(/^ {10}/, '')
16
+ In order to perform this request, you need to register your app with
17
+ Google Developers Console (https://console.developers.google.com).
18
+
19
+ Make sure your app has access to the Google+ and YouTube APIs.
20
+
21
+ If your app requires read-only access to public YouTube data, then
22
+ generate a server API key and set its value with the initializer:
23
+
24
+ Yt.configure do |config|
25
+ config.api_key = '123456789012345678901234567890'
26
+ end
27
+
28
+ or through an environment variable:
29
+
30
+ export YT_API_KEY="123456789012345678901234567890"
31
+
32
+ If your app needs to perform actions on behalf of YouTube accounts,
33
+ then generate a client ID and SECRET and set their values with the
34
+ initializer:
35
+
36
+ Yt.configure do |config|
37
+ config.client_id = '1234567890.apps.googleusercontent.com'
38
+ config.client_secret = '1234567890'
39
+ end
40
+
41
+ or through environment variables:
42
+
43
+ export YT_CLIENT_ID="1234567890.apps.googleusercontent.com"
44
+ export YT_CLIENT_SECRET="1234567890"
45
+ MSG
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,216 @@
1
+ require 'open-uri'
2
+ require 'yt/models/base'
3
+
4
+ module Yt
5
+ module Models
6
+ # Provides methods to interact with YouTube accounts.
7
+ # @see https://developers.google.com/youtube/v3/guides/authentication
8
+ class Account < Base
9
+
10
+ ### USER INFO ###
11
+
12
+ has_one :user_info
13
+
14
+ # @!attribute [r] id
15
+ # @return [String] the (Google+) account’s ID.
16
+ delegate :id, to: :user_info
17
+
18
+ # @!attribute [r] email
19
+ # @return [String] the account’s email address.
20
+ delegate :email, to: :user_info
21
+
22
+ # @return [Boolean] whether the email address is verified.
23
+ def has_verified_email?
24
+ user_info.verified_email
25
+ end
26
+
27
+ # @!attribute [r] name
28
+ # @return [String] the account’s full name.
29
+ delegate :name, to: :user_info
30
+
31
+ # @!attribute [r] given_name
32
+ # @return [String] the user’s given (first) name.
33
+ delegate :given_name, to: :user_info
34
+
35
+ # @!attribute [r] family_name
36
+ # @return [String] the user’s family (last) name.
37
+ delegate :family_name, to: :user_info
38
+
39
+ # @return [String] the URL of the account’s (Google +) profile page.
40
+ def profile_url
41
+ user_info.link
42
+ end
43
+
44
+ # @return [String] the URL of the account’s (Google +) profile picture.
45
+ def avatar_url
46
+ user_info.picture
47
+ end
48
+
49
+ # @return [String] the account’s gender. Possible values include, but
50
+ # are not limited to, +'male'+, +'female'+, +'other'+.
51
+ delegate :gender, to: :user_info
52
+
53
+ # @return [String] the account’s preferred locale.
54
+ delegate :locale, to: :user_info
55
+
56
+ # @return [String] the hosted domain name for the user’s Google Apps
57
+ # account. For instance, example.com.
58
+ delegate :hd, to: :user_info
59
+
60
+ ### ACTIONS ###
61
+
62
+ # Uploads a video to the account’s channel.
63
+ # @param [String] path_or_url the video to upload. Can either be the
64
+ # path of a local file or the URL of a remote file.
65
+ # @param [Hash] params the metadata to add to the uploaded video.
66
+ # @option params [String] :title The video’s title.
67
+ # @option params [String] :description The video’s description.
68
+ # @option params [Array<String>] :title The video’s tags.
69
+ # @option params [String] :privacy_status The video’s privacy status.
70
+ # @return [Yt::Models::Video] the newly uploaded video.
71
+ def upload_video(path_or_url, params = {})
72
+ file = open path_or_url, 'rb'
73
+ session = resumable_sessions.insert file.size, upload_body(params)
74
+
75
+ session.update(body: file) do |data|
76
+ Yt::Video.new id: data['id'], snippet: data['snippet'], status: data['privacyStatus'], auth: self
77
+ end
78
+ end
79
+
80
+ # Creates a playlist in the account’s channel.
81
+ # @return [Yt::Models::Playlist] the newly created playlist.
82
+ # @param [Hash] params the attributes of the playlist.
83
+ # @option params [String] :title The new playlist’s title.
84
+ # Cannot have more than 100 characters. Can include the characters
85
+ # < and >, which are replaced to ‹ › in order to be accepted by YouTube.
86
+ # @option params [String] :description The new playlist’s description.
87
+ # Cannot have more than 5000 bytes. Can include the characters
88
+ # < and >, which are replaced to ‹ › in order to be accepted by YouTube.
89
+ # @option params [Array<String>] :tags The new playlist’s tags.
90
+ # Cannot have more than 500 characters. Can include the characters
91
+ # < and >, which are replaced to ‹ › in order to be accepted by YouTube.
92
+ # @option params [String] :privacy_status The new playlist’s privacy
93
+ # status. Must be one of: private, unscheduled, public.
94
+ # @example Create a playlist titled "My favorites".
95
+ # account.create_playlist title: 'My favorites'
96
+ def create_playlist(params = {})
97
+ playlists.insert params
98
+ end
99
+
100
+ # @!method delete_playlists(attributes = {})
101
+ # Deletes the account’s playlists matching all the given attributes.
102
+ # @return [Array<Boolean>] whether each playlist matching the given
103
+ # attributes was deleted.
104
+ # @param [Hash] attributes the attributes to match the playlists by.
105
+ # @option attributes [<String, Regexp>] :title The playlist’s title.
106
+ # Pass a String for perfect match or a Regexp for advanced match.
107
+ # @option attributes [<String, Regexp>] :description The playlist’s
108
+ # description. Pass a String (perfect match) or a Regexp (advanced).
109
+ # @option attributes [Array<String>] :tags The playlist’s tags.
110
+ # All tags must match exactly.
111
+ # @option attributes [String] :privacy_status The playlist’s privacy
112
+ # status.
113
+ delegate :delete_playlists, to: :channel
114
+
115
+ ### CONTENT OWNERS ###
116
+
117
+ # @!attribute [r] content_owners
118
+ # @return [Yt::Collections::ContentOwners] the content owners that
119
+ # the account can manage.
120
+ has_many :content_owners
121
+
122
+ # The name of the content owner managing the account.
123
+ # @return [String] name of the CMS account, if the account is partnered.
124
+ # @return [nil] if the account is not a partnered content owner.
125
+ attr_reader :owner_name
126
+
127
+ ### ASSOCIATIONS ###
128
+
129
+ # @!attribute [r] channel
130
+ # @return [Yt::Models::Channel] the YouTube channel of the account.
131
+ has_one :channel
132
+
133
+ # @!attribute [r] playlists
134
+ # @return [Yt::Collections::Playlists] the playlists owned by the account.
135
+ delegate :playlists, to: :channel
136
+
137
+ # @!attribute [r] related_playlists
138
+ # @return [Yt::Collections::Playlists] the playlists associated with the
139
+ # account, such as the playlist of uploaded or liked videos.
140
+ # @see https://developers.google.com/youtube/v3/docs/channels#contentDetails.relatedPlaylists
141
+ delegate :related_playlists, to: :channel
142
+
143
+ # @!attribute [r] subscribed_channels
144
+ # @return [Yt::Collections::SubscribedChannels] the channels that the
145
+ # account is subscribed to.
146
+ delegate :subscribed_channels, to: :channel
147
+
148
+ # @!attribute [r] videos
149
+ # @return [Yt::Collections::Videos] the videos owned by the account.
150
+ has_many :videos
151
+
152
+ # @!attribute [r] subscribers
153
+ # @return [Yt::Collections::Subscribers] the channels subscribed to
154
+ # the account’s channel.
155
+ has_many :subscribers
156
+
157
+ # @!attribute [r] resumable_sessions
158
+ # @private
159
+ # @return [Yt::Collections::ResumableSessions] the sessions used to
160
+ # upload videos using the resumable upload protocol.
161
+ has_many :resumable_sessions
162
+
163
+ ### PRIVATE API ###
164
+
165
+ has_authentication
166
+
167
+ # @private
168
+ # Initialize user info if included in the response
169
+ def initialize(options = {})
170
+ super options
171
+ if options[:user_info]
172
+ @user_info = UserInfo.new data: options[:user_info]
173
+ end
174
+ end
175
+
176
+ # @private
177
+ # Tells `has_many :videos` that account.videos should return all the
178
+ # videos *owned by* the account (public, private, unlisted).
179
+ def videos_params
180
+ {for_mine: true}
181
+ end
182
+
183
+ # @private
184
+ # Tells `has_many :resumable_sessions` what path to hit to upload a file.
185
+ def upload_path
186
+ '/upload/youtube/v3/videos'
187
+ end
188
+ # @private
189
+ # Tells `has_many :resumable_sessions` what params are set for the object
190
+ # associated to the uploaded file.
191
+ def upload_params
192
+ {part: 'snippet,status'}
193
+ end
194
+
195
+ # @private
196
+ # Tells `has_many :resumable_sessions` what metadata to set in the object
197
+ # associated to the uploaded file.
198
+ def upload_body(params = {})
199
+ {}.tap do |body|
200
+ snippet = params.slice :title, :description, :tags, :category_id
201
+ snippet[:categoryId] = snippet.delete(:category_id) if snippet[:category_id]
202
+ body[:snippet] = snippet if snippet.any?
203
+
204
+ status = params[:privacy_status]
205
+ body[:status] = {privacyStatus: status} if status
206
+ end
207
+ end
208
+
209
+ # @private
210
+ # Tells `has_many :resumable_sessions` what type of file can be uploaded.
211
+ def upload_content_type
212
+ 'video/*'
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,38 @@
1
+ require 'yt/models/base'
2
+
3
+ module Yt
4
+ module Models
5
+ # Encapsulates advertising options of a video, such as the types of ads
6
+ # that can run during the video as well as the times when ads are allowed
7
+ # to run during the video.
8
+ # @see https://developers.google.com/youtube/partner/docs/v1/videoAdvertisingOptions#resource
9
+ class AdvertisingOptionsSet < Base
10
+ def initialize(options = {})
11
+ @auth = options[:auth]
12
+ @video_id = options[:video_id]
13
+ @data = options[:data]
14
+ end
15
+
16
+ def update(attributes = {})
17
+ underscore_keys! attributes
18
+ do_patch(body: attributes) {|data| @data = data}
19
+ true
20
+ end
21
+
22
+ def ad_formats
23
+ @data['adFormats']
24
+ end
25
+
26
+ private
27
+
28
+ # @see https://developers.google.com/youtube/partner/docs/v1/videoAdvertisingOptions/patch
29
+ def patch_params
30
+ super.tap do |params|
31
+ params[:expected_response] = Net::HTTPOK
32
+ params[:path] = "/youtube/partner/v1/videoAdvertisingOptions/#{@video_id}"
33
+ params[:params] = {on_behalf_of_content_owner: @auth.owner_name}
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,132 @@
1
+ module Yt
2
+ module Models
3
+ # Provides methods to interact with YouTube annotations.
4
+ # @note YouTube API V3 does not provide access to video annotations,
5
+ # therefore a legacy XML endpoint is used to retrieve annotations.
6
+ # @see https://www.youtube.com/yt/playbook/annotations.html
7
+ class Annotation < Base
8
+ # @param [Hash] options the options to initialize an Annotation.
9
+ # @option options [String] :data The XML representation of an annotation
10
+ def initialize(options = {})
11
+ @data = options[:data]
12
+ end
13
+
14
+ # @return [Boolean] whether the text box surrounding the annotation is
15
+ # completely in the top y% of the video frame.
16
+ # @param [Integer] y Vertical position in the Youtube video (0 to 100)
17
+ def above?(y)
18
+ top && top < y
19
+ end
20
+
21
+ # @return [Boolean] whether the text box surrounding the annotation is
22
+ # completely in the bottom y% of the video frame.
23
+ # @param [Integer] y Vertical position in the Youtube video (0 to 100)
24
+ def below?(y)
25
+ bottom && bottom > y
26
+ end
27
+
28
+ # @return [Boolean] whether the annotation includes a link to subscribe.
29
+ def has_link_to_subscribe?(options = {}) # TODO: options for which videos
30
+ link_class == '5'
31
+ end
32
+
33
+ # @return [Boolean] whether the annotation includes a link to a video,
34
+ # either directly in the text, or as an "Invideo featured video".
35
+ def has_link_to_video?(options = {}) # TODO: options for which videos
36
+ link_class == '1' || type == 'promotion'
37
+ end
38
+
39
+ # @return [Boolean] whether the annotation includes a link to a playlist,
40
+ # or to a video embedded in a playlist.
41
+ def has_link_to_playlist?
42
+ link_class == '2' || text.include?('&list=')
43
+ end
44
+
45
+ # @return [Boolean] whether the annotation includes a link that will
46
+ # open in the current browser window.
47
+ def has_link_to_same_window?
48
+ link_target == 'current'
49
+ end
50
+
51
+ # @return [Boolean] whether the annotation is an "InVideo Programming".
52
+ def has_invideo_programming?
53
+ type == 'promotion' || type == 'branding'
54
+ end
55
+
56
+ # @param [Numeric] seconds the number of seconds
57
+ # @return [Boolean] whether the annotation starts after the number of
58
+ # seconds indicated.
59
+ # @note This is broken for invideo programming, because they do not
60
+ # have the timestamp in the region, but in the "data" field
61
+ def starts_after?(seconds)
62
+ timestamps.first > seconds if timestamps.any?
63
+ end
64
+
65
+ # @param [Numeric] seconds the number of seconds
66
+ # @return [Boolean] whether the annotation starts before the number of
67
+ # seconds indicated.
68
+ # @note This is broken for invideo programming, because they do not
69
+ # have the timestamp in the region, but in the "data" field
70
+ def starts_before?(seconds)
71
+ timestamps.first < seconds if timestamps.any?
72
+ end
73
+
74
+ # @return [String] the textual content of the annotation.
75
+ def text
76
+ @text ||= @data['TEXT'] || ''
77
+ end
78
+
79
+ private
80
+
81
+ has_attribute :type, default: ''
82
+
83
+ def link_class
84
+ @link_class ||= url['link_class']
85
+ end
86
+
87
+ def link_target
88
+ @link_target ||= url['target']
89
+ end
90
+
91
+ def url
92
+ @url ||= action.fetch 'url', {}
93
+ end
94
+
95
+ has_attribute :action, default: {}
96
+
97
+ def top
98
+ @top ||= positions.map{|pos| pos['y'].to_f}.max
99
+ end
100
+
101
+ def bottom
102
+ @bottom ||= positions.map{|pos| pos['y'].to_f + pos['h'].to_f}.max
103
+ end
104
+
105
+ def timestamps
106
+ @timestamps ||= positions.reject{|pos| pos['t'] == 'never'}.map do |pos|
107
+ timestamp_of pos
108
+ end
109
+ end
110
+
111
+ def timestamp_of(position)
112
+ regex = %r{(?:|(?<hours>\d*):)(?:|(?<min>\d*):)(?<sec>\d*)\.(?<ms>\d*)}
113
+ position['t'] = '00:00:00.000' if position['t'] == '0'
114
+ match = position['t'].match regex
115
+ hours = (match[:hours] || '0').to_i
116
+ minutes = (match[:min] || '0').to_i
117
+ seconds = (match[:sec]).to_i
118
+ (hours * 60 + minutes) * 60 + seconds
119
+ end
120
+
121
+ def positions
122
+ @positions ||= Array.wrap region['rectRegion'] || region['anchoredRegion']
123
+ end
124
+
125
+ def region
126
+ @region ||= segment.fetch 'movingRegion', {}
127
+ end
128
+
129
+ has_attribute :segment, type: Hash
130
+ end
131
+ end
132
+ end