yt 0.0.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +24 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +10 -0
  5. data/.yardopts +1 -0
  6. data/Gemfile +9 -0
  7. data/Gemfile.lock +78 -0
  8. data/HISTORY.md +37 -0
  9. data/MIT-LICENSE +20 -0
  10. data/README.md +325 -0
  11. data/Rakefile +1 -0
  12. data/TODO.md +11 -0
  13. data/bin/yt +31 -0
  14. data/lib/yt.rb +2 -0
  15. data/lib/yt/actions/delete.rb +27 -0
  16. data/lib/yt/actions/delete_all.rb +28 -0
  17. data/lib/yt/actions/insert.rb +29 -0
  18. data/lib/yt/actions/list.rb +65 -0
  19. data/lib/yt/actions/update.rb +25 -0
  20. data/lib/yt/associations.rb +33 -0
  21. data/lib/yt/associations/annotations.rb +15 -0
  22. data/lib/yt/associations/channels.rb +20 -0
  23. data/lib/yt/associations/details_sets.rb +20 -0
  24. data/lib/yt/associations/playlist_items.rb +26 -0
  25. data/lib/yt/associations/playlists.rb +22 -0
  26. data/lib/yt/associations/ratings.rb +39 -0
  27. data/lib/yt/associations/snippets.rb +20 -0
  28. data/lib/yt/associations/statuses.rb +14 -0
  29. data/lib/yt/associations/subscriptions.rb +38 -0
  30. data/lib/yt/associations/user_infos.rb +21 -0
  31. data/lib/yt/associations/videos.rb +14 -0
  32. data/lib/yt/collections/annotations.rb +43 -0
  33. data/lib/yt/collections/base.rb +13 -0
  34. data/lib/yt/collections/channels.rb +32 -0
  35. data/lib/yt/collections/details_sets.rb +32 -0
  36. data/lib/yt/collections/playlist_items.rb +50 -0
  37. data/lib/yt/collections/playlists.rb +56 -0
  38. data/lib/yt/collections/ratings.rb +32 -0
  39. data/lib/yt/collections/snippets.rb +38 -0
  40. data/lib/yt/collections/subscriptions.rb +67 -0
  41. data/lib/yt/collections/user_infos.rb +41 -0
  42. data/lib/yt/collections/videos.rb +32 -0
  43. data/lib/yt/config.rb +55 -0
  44. data/lib/yt/models/account.rb +68 -0
  45. data/lib/yt/models/annotation.rb +137 -0
  46. data/lib/yt/models/base.rb +11 -0
  47. data/lib/yt/models/channel.rb +17 -0
  48. data/lib/yt/models/configuration.rb +29 -0
  49. data/lib/yt/models/description.rb +98 -0
  50. data/lib/yt/models/details_set.rb +31 -0
  51. data/lib/yt/models/playlist.rb +65 -0
  52. data/lib/yt/models/playlist_item.rb +42 -0
  53. data/lib/yt/models/rating.rb +28 -0
  54. data/lib/yt/models/snippet.rb +48 -0
  55. data/lib/yt/models/status.rb +26 -0
  56. data/lib/yt/models/subscription.rb +35 -0
  57. data/lib/yt/models/user_info.rb +66 -0
  58. data/lib/yt/models/video.rb +16 -0
  59. data/lib/yt/utils/request.rb +85 -0
  60. data/lib/yt/version.rb +3 -0
  61. data/spec/associations/device_auth/channels_spec.rb +10 -0
  62. data/spec/associations/device_auth/details_sets_spec.rb +19 -0
  63. data/spec/associations/device_auth/playlist_items_spec.rb +42 -0
  64. data/spec/associations/device_auth/playlists_spec.rb +42 -0
  65. data/spec/associations/device_auth/ratings_spec.rb +30 -0
  66. data/spec/associations/device_auth/snippets_spec.rb +30 -0
  67. data/spec/associations/device_auth/subscriptions_spec.rb +27 -0
  68. data/spec/associations/device_auth/user_infos_spec.rb +10 -0
  69. data/spec/associations/device_auth/videos_spec.rb +22 -0
  70. data/spec/associations/no_auth/annotations_spec.rb +15 -0
  71. data/spec/associations/server_auth/channels_spec.rb +2 -0
  72. data/spec/associations/server_auth/details_sets_spec.rb +18 -0
  73. data/spec/associations/server_auth/playlist_items_spec.rb +17 -0
  74. data/spec/associations/server_auth/playlists_spec.rb +17 -0
  75. data/spec/associations/server_auth/ratings_spec.rb +2 -0
  76. data/spec/associations/server_auth/snippets_spec.rb +28 -0
  77. data/spec/associations/server_auth/subscriptions_spec.rb +2 -0
  78. data/spec/associations/server_auth/user_infos_spec.rb +2 -0
  79. data/spec/associations/server_auth/videos_spec.rb +20 -0
  80. data/spec/collections/annotations_spec.rb +6 -0
  81. data/spec/collections/channels_spec.rb +6 -0
  82. data/spec/collections/details_sets_spec.rb +6 -0
  83. data/spec/collections/playlist_items_spec.rb +23 -0
  84. data/spec/collections/playlists_spec.rb +26 -0
  85. data/spec/collections/ratings_spec.rb +6 -0
  86. data/spec/collections/snippets_spec.rb +6 -0
  87. data/spec/collections/subscriptions_spec.rb +30 -0
  88. data/spec/collections/user_infos_spec.rb +6 -0
  89. data/spec/collections/videos_spec.rb +6 -0
  90. data/spec/models/annotation_spec.rb +131 -0
  91. data/spec/models/channel_spec.rb +13 -0
  92. data/spec/models/description_spec.rb +94 -0
  93. data/spec/models/details_set_spec.rb +23 -0
  94. data/spec/models/playlist_item_spec.rb +32 -0
  95. data/spec/models/playlist_spec.rb +52 -0
  96. data/spec/models/rating_spec.rb +13 -0
  97. data/spec/models/snippet_spec.rb +66 -0
  98. data/spec/models/status_spec.rb +42 -0
  99. data/spec/models/subscription_spec.rb +37 -0
  100. data/spec/models/user_info_spec.rb +69 -0
  101. data/spec/models/video_spec.rb +13 -0
  102. data/spec/spec_helper.rb +15 -0
  103. data/spec/support/device_app.rb +16 -0
  104. data/spec/support/server_app.rb +10 -0
  105. data/yt.gemspec +30 -0
  106. metadata +209 -17
@@ -0,0 +1,41 @@
1
+ require 'yt/collections/base'
2
+ require 'yt/models/user_info'
3
+
4
+ module Yt
5
+ module Collections
6
+ class UserInfos < Base
7
+
8
+ def initialize(options = {})
9
+ @account = options[:account]
10
+ @auth = options[:auth]
11
+ end
12
+
13
+ def self.by_account(account)
14
+ new account: account, auth: account.auth
15
+ end
16
+
17
+ private
18
+
19
+ def new_item(data)
20
+ Yt::UserInfo.new data: data
21
+ end
22
+
23
+ def list_params
24
+ super.tap do |params|
25
+ params[:path] = '/oauth2/v2/userinfo'
26
+ # TODO: Remove youtube from here, implement incremental scopes
27
+ params[:scope] = 'https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile'
28
+ end
29
+ end
30
+
31
+ def next_page
32
+ request = Request.new list_params
33
+ response = request.run
34
+ raise unless response.is_a? Net::HTTPOK
35
+ @page_token = nil
36
+
37
+ Array.wrap response.body
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,32 @@
1
+ require 'yt/collections/base'
2
+ require 'yt/models/video'
3
+
4
+ module Yt
5
+ module Collections
6
+ class Videos < Base
7
+
8
+ def initialize(options = {})
9
+ @channel = options[:channel]
10
+ @auth = options[:auth]
11
+ end
12
+
13
+ def self.by_channel(channel)
14
+ new channel: channel, auth: channel.auth
15
+ end
16
+
17
+ private
18
+
19
+ def new_item(data)
20
+ Yt::Video.new id: data['id']['videoId'], snippet: data['snippet'], auth: @auth
21
+ end
22
+
23
+ def list_params
24
+ super.tap do |params|
25
+ params[:params] = {channelId: @channel.id, type: :video, maxResults: 50, part: 'snippet'}
26
+ params[:scope] = 'https://www.googleapis.com/auth/youtube'
27
+ params[:path] = '/youtube/v3/search'
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,55 @@
1
+ require 'yt/models/configuration'
2
+
3
+ module Yt
4
+ # Provides methods to read and write runtime configuration information.
5
+ #
6
+ # Configuration options are loaded from `~/.yt`, `.yt`, command line
7
+ # switches, and the `YT_OPTS` environment variable (listed in lowest to
8
+ # highest precedence).
9
+ #
10
+ # @note Config is the only module auto-loaded in the Yt module,
11
+ # in order to have a syntax as easy as Yt.configure
12
+ #
13
+ # @example A server-to-server YouTube client app
14
+ #
15
+ # Yt.configure do |config|
16
+ # config.scenario = :server_app
17
+ # config.api_key = 'ABCDEFGHIJ1234567890'
18
+ # end
19
+ #
20
+ # @example A web YouTube client app
21
+ #
22
+ # Yt.configure do |config|
23
+ # config.client_id = 'ABCDEFGHIJ1234567890'
24
+ # config.client_secret = 'ABCDEFGHIJ1234567890'
25
+ # end
26
+ #
27
+ module Config
28
+ # Yields the global configuration to a block.
29
+ # @yield [Yt::Configuration] global configuration
30
+ #
31
+ # @example
32
+ # Yt.configure do |config|
33
+ # config.scenario = :server_app
34
+ # config.api_key = 'ABCDEFGHIJ1234567890'
35
+ # end
36
+ # @see Yt::Configuration
37
+ def configure
38
+ yield configuration if block_given?
39
+ end
40
+
41
+ # Returns the global [Configuration](Yt/Configuration) object. While you
42
+ # _can_ use this method to access the configuration, the more common
43
+ # convention is to use [Yt.configure](Yt#configure-class_method).
44
+ #
45
+ # @example
46
+ # Yt.configuration.api_key = 'ABCDEFGHIJ1234567890'
47
+ # @see Yt.configure
48
+ # @see Yt::Configuration
49
+ def configuration
50
+ @configuration ||= Yt::Configuration.new
51
+ end
52
+ end
53
+
54
+ extend Config
55
+ end
@@ -0,0 +1,68 @@
1
+ require 'yt/models/base'
2
+ require 'yt/config'
3
+
4
+ module Yt
5
+ # Provides methods to access a YouTube account.
6
+ class Account < Base
7
+
8
+ has_one :channel, delegate: [:videos, :playlists, :create_playlist, :delete_playlists, :update_playlists]
9
+ has_one :user_info, delegate: [:id, :email, :has_verified_email?, :gender,
10
+ :name, :given_name, :family_name, :profile_url, :avatar_url, :locale, :hd]
11
+
12
+ def initialize(options = {})
13
+ # By default is someone passes a refresh_token but not a scope, we can assume it's a youtube one
14
+ @scope = options.fetch :scope, 'https://www.googleapis.com/auth/youtube'
15
+ @access_token = options[:access_token]
16
+ @refresh_token = options[:refresh_token]
17
+ @redirect_url = options[:redirect_url]
18
+ end
19
+
20
+ def access_token_for(scope)
21
+ # TODO incremental scope
22
+
23
+ # HERE manage the fact that we must change some scope on device,
24
+ # like 'https://www.googleapis.com/auth/youtube.readonly' is not accepted
25
+ if Yt.configuration.scenario == :device_app && scope == 'https://www.googleapis.com/auth/youtube.readonly'
26
+ scope = 'https://www.googleapis.com/auth/youtube'
27
+ end
28
+
29
+ # TODO !! include? is not enough, because (for instance) 'youtube' also includes 'youtube.readonly'
30
+
31
+ # unless (@scope == scope) || (scope == 'https://www.googleapis.com/auth/youtube.readonly' && @scope =='https://www.googleapis.com/auth/youtube')
32
+ # @scope = scope
33
+ # @access_token = @refresh_token = nil
34
+ # end
35
+ @access_token ||= refresh_access_token || get_access_token
36
+ end
37
+
38
+ def auth
39
+ self
40
+ end
41
+
42
+ private
43
+
44
+ # Obtain a new access token using the refresh token
45
+ def refresh_access_token
46
+ if @refresh_token
47
+ body = {grant_type: 'refresh_token', refresh_token: @refresh_token}
48
+ request = Request.new auth_params.deep_merge(body: body)
49
+ response = request.run
50
+ response.body['access_token']
51
+ end
52
+ end
53
+
54
+ def auth_params
55
+ {
56
+ host: 'accounts.google.com',
57
+ path: '/o/oauth2/token',
58
+ format: :json,
59
+ body_type: :form,
60
+ method: :post,
61
+ body: {
62
+ client_id: Yt.configuration.client_id,
63
+ client_secret: Yt.configuration.client_secret
64
+ }
65
+ }
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,137 @@
1
+ module Yt
2
+ # Provides methods to access and analyze a single YouTube annotation.
3
+ class Annotation
4
+ # Instantiate an Annotation object from its YouTube XML representation.
5
+ #
6
+ # @note There is no documented way to access annotations through API.
7
+ # There is an endpoint that returns an XML in an undocumented format,
8
+ # which is here parsed into a comprehensible set of attributes.
9
+ #
10
+ # @param [String] xml_data The YouTube XML representation of an annotation
11
+ def initialize(options = {})
12
+ @data = options[:data]
13
+ end
14
+
15
+ # Checks whether the entire annotation box remains above y
16
+ #
17
+ # @param [Integer] y Vertical position in the Youtube video (0 to 100)
18
+ #
19
+ # @return [Boolean] Whether the box remains above y
20
+ def above?(y)
21
+ top && top < y
22
+ end
23
+
24
+ # Checks whether the entire annotation box remains below y
25
+ #
26
+ # @param [Integer] y Vertical position in the Youtube video (0 to 100)
27
+ #
28
+ # @return [Boolean] Whether the box remains below y
29
+ def below?(y)
30
+ bottom && bottom > y
31
+ end
32
+
33
+ # Checks whether there is a link to subscribe.
34
+ # Should a branding watermark also counts, because it links to the channel?
35
+ #
36
+ # @return [Boolean] Whether there is a link to subscribe in the annotation
37
+ def has_link_to_subscribe?(options = {}) # TODO: options for which videos
38
+ link_class == '5'
39
+ end
40
+
41
+ # Checks whether there is a link to a video.
42
+ # An Invideo featured video also counts
43
+ #
44
+ # @return [Boolean] Whether there is a link to a video in the annotation
45
+ def has_link_to_video?(options = {}) # TODO: options for which videos
46
+ link_class == '1' || type == 'promotion'
47
+ end
48
+
49
+ # Checks whether there is a link to a playlist.
50
+ # A link to a video with the playlist in the URL also counts
51
+ #
52
+ # @return [Boolean] Whether there is a link to a playlist in the annotation
53
+ def has_link_to_playlist?
54
+ link_class == '2' || text.include?('&list=')
55
+ end
56
+
57
+ # Checks whether the link opens in the same window.
58
+ #
59
+ # @return [Boolean] Whether the link opens in the same window
60
+ def has_link_to_same_window?
61
+ link_target == 'current'
62
+ end
63
+
64
+ # Checks whether the annotation comes from InVideo Programming
65
+ #
66
+ # @return [Boolean] Whether the annotation comes from InVideo Programming
67
+ def has_invideo_programming?
68
+ type == 'promotion' || type == 'branding'
69
+ end
70
+
71
+ # @return [Boolean] Whether the annotation starts after the number of seconds
72
+ # @note This is broken for invideo programming, because they do not
73
+ # have the timestamp in the region, but in the "data" field
74
+ def starts_after?(seconds)
75
+ timestamps.first > seconds if timestamps.any?
76
+ end
77
+
78
+ # @return [Boolean] Whether the annotation starts before the number of seconds
79
+ # @note This is broken for invideo programming, because they do not
80
+ # have the timestamp in the region, but in the "data" field
81
+ def starts_before?(seconds)
82
+ timestamps.first < seconds if timestamps.any?
83
+ end
84
+
85
+ private
86
+
87
+ def text
88
+ @text ||= @data.fetch 'TEXT', ''
89
+ end
90
+
91
+ def type
92
+ @type ||= @data.fetch 'type', ''
93
+ end
94
+
95
+ def link_class
96
+ @link_class ||= url['link_class']
97
+ end
98
+
99
+ def link_target
100
+ @link_target ||= url['target']
101
+ end
102
+
103
+ def url
104
+ @url ||= action.fetch 'url', {}
105
+ end
106
+
107
+ def action
108
+ @action ||= @data.fetch 'action', {}
109
+ end
110
+
111
+ def top
112
+ @top ||= positions.map{|pos| pos['y'].to_f}.max
113
+ end
114
+
115
+ def bottom
116
+ @bottom ||= positions.map{|pos| pos['y'].to_f + pos['h'].to_f}.max
117
+ end
118
+
119
+ def timestamps
120
+ @timestamps ||= positions.map do |pos|
121
+ Time.parse(pos['t']) - Time.parse('0:00')
122
+ end
123
+ end
124
+
125
+ def positions
126
+ @positions ||= region['rectRegion'] || region['anchoredRegion'] || []
127
+ end
128
+
129
+ def region
130
+ @region ||= segment.fetch 'movingRegion', {}
131
+ end
132
+
133
+ def segment
134
+ @segment ||= (@data['segment'] || {})
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,11 @@
1
+ require 'yt/associations'
2
+ require 'yt/actions/delete'
3
+ require 'yt/actions/update'
4
+
5
+ module Yt
6
+ class Base
7
+ extend Associations
8
+ include Actions::Delete
9
+ include Actions::Update
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ require 'yt/models/base'
2
+
3
+ module Yt
4
+ class Channel < Base
5
+ attr_reader :id, :auth
6
+ has_one :snippet, delegate: [:title, :description, :thumbnail_url, :published_at]
7
+ has_many :subscriptions
8
+ has_many :videos
9
+ has_many :playlists
10
+
11
+ def initialize(options = {})
12
+ @id = options[:id]
13
+ @auth = options[:auth]
14
+ @snippet = Snippet.new(data: options[:snippet]) if options[:snippet]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ module Yt
2
+ # Stores runtime configuration information.
3
+ #
4
+ # Configuration options are loaded from `~/.yt`, `.yt`, command line
5
+ # switches, and the `YT_OPTS` environment variable (listed in lowest to
6
+ # highest precedence).
7
+ #
8
+ # @example A server-to-server YouTube client app
9
+ #
10
+ # Yt.configure do |config|
11
+ # config.scenario = :server_app
12
+ # config.api_key = 'ABCDEFGHIJ1234567890'
13
+ # end
14
+ #
15
+ # @example A web YouTube client app
16
+ #
17
+ # Yt.configure do |config|
18
+ # config.client_id = 'ABCDEFGHIJ1234567890'
19
+ # config.client_secret = 'ABCDEFGHIJ1234567890'
20
+ # end
21
+ #
22
+ class Configuration
23
+ attr_accessor :scenario, :api_key, :client_id, :client_secret, :account
24
+
25
+ def initialize
26
+ @scenario = :web_app
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,98 @@
1
+ module Yt
2
+ # Provides read-only access to the description of a YouTube resource.
3
+ # Resources with descriptions are: videos and channels.
4
+ #
5
+ # @example
6
+ #
7
+ # description = Yt::Description.new 'Fullscreen provides a suite of end-to-end YouTube tools and services to many of the world’s leading brands and media companies.'
8
+ # description.to_s.slice(0,19) # => 'Fullscreen provides'
9
+ # description.length # => 127
10
+ #
11
+ class Description < String
12
+ # Returns whether the description includes a YouTube video URL
13
+ #
14
+ # @example
15
+ #
16
+ # description = Yt::Description.new 'Link to video: youtube.com/watch?v=MESycYJytkU'
17
+ # description.has_link_to_video? #=> true
18
+ #
19
+ # @return [Boolean] Whether the description includes a link to a video
20
+ def has_link_to_video?
21
+ # TODO: might take as an option WHICH video to link to
22
+ # in order to check if it's my own video
23
+ regex? :video_long_url, :video_short_url
24
+ end
25
+
26
+ # Returns whether the description includes a YouTube channel URL
27
+ #
28
+ # @example
29
+ #
30
+ # description = Yt::Description.new Link to channel: youtube.com/fullscreen
31
+ # description.has_link_to_channel? #=> true
32
+ #
33
+ # @return [Boolean] Whether the description includes a link to a channel
34
+ def has_link_to_channel?(options = {}) # TODO: which channel
35
+ # TODO: might take as an option WHICH channel to link to
36
+ # in order to check if it's my own channel
37
+ regex? :channel_long_url, :channel_short_url, :channel_user_url
38
+ end
39
+
40
+ # Returns whether the description includes a YouTube subscription URL
41
+ #
42
+ # @example
43
+ #
44
+ # description = Yt::Description.new Link to subscribe: youtube.com/subscription_center?add_user=fullscreen
45
+ # description.has_link_to_subscribe? #=> true
46
+ #
47
+ # @return [Boolean] Whether the description includes a link to subscribe
48
+ def has_link_to_subscribe?(options = {}) # TODO: which channel
49
+ # TODO: might take as an option WHICH channel to subscribe to
50
+ # in order to check if it's my own channel
51
+ regex? :subscribe_center_url, :subscribe_widget_url, :subscribe_confirm_url
52
+ end
53
+
54
+ # Returns whether the description includes a YouTube playlist URL
55
+ #
56
+ # @example
57
+ #
58
+ # description = Yt::Description.new Link to playlist: youtube.com/playlist?list=LLxO1tY8h1AhOz0T4ENwmpow
59
+ # description.has_link_to_playlist? #=> true
60
+ #
61
+ # @return [Boolean] Whether the description includes a link to a playlist
62
+ def has_link_to_playlist?
63
+ regex? :playlist_long_url, :playlist_embed_url
64
+ end
65
+
66
+ private
67
+
68
+ def regex?(*keys)
69
+ keys.find{|key| self =~ regex_for(key)}
70
+ end
71
+
72
+ def regex_for(key)
73
+ host, name = '(?:https?://)?(?:www\.)?', '([a-zA-Z0-9_-]+)'
74
+ case key
75
+ when :video_long_url
76
+ %r{#{host}youtube\.com/watch\?v=#{name}}
77
+ when :video_short_url
78
+ %r{#{host}youtu\.be/#{name}}
79
+ when :channel_long_url
80
+ %r{#{host}youtube\.com/channel/#{name}}
81
+ when :channel_short_url
82
+ %r{#{host}youtube\.com/#{name}}
83
+ when :channel_user_url
84
+ %r{#{host}youtube\.com/user/#{name}}
85
+ when :subscribe_center_url
86
+ %r{#{host}youtube\.com/subscription_center\?add_user=#{name}}
87
+ when :subscribe_widget_url
88
+ %r{#{host}youtube\.com/subscribe_widget\?p=#{name}}
89
+ when :subscribe_confirm_url
90
+ %r{#{host}youtube\.com/channel/(?:[a-zA-Z0-9&_=-]*)\?sub_confirmation=1}
91
+ when :playlist_long_url
92
+ %r{#{host}youtube\.com/playlist\?list=#{name}}
93
+ when :playlist_embed_url
94
+ %r{#{host}youtube\.com/watch\?v=(?:[a-zA-Z0-9&_=-]*)&list=#{name}}
95
+ end
96
+ end
97
+ end
98
+ end