yt 0.0.1 → 0.4.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 (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