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,31 @@
1
+ module Yt
2
+ class DetailsSet < Base
3
+
4
+ def initialize(options = {})
5
+ @data = options[:data]
6
+ end
7
+
8
+ # Return the duration of the YouTube video.
9
+ #
10
+ # @return [Integer] Duration in seconds of the YouTube video
11
+ def duration
12
+ @duration = to_seconds @data.fetch('duration', 0)
13
+ end
14
+ # also available: dimension, definition, caption, licensed_content?
15
+
16
+ private
17
+
18
+ # The length of the video. The tag value is an ISO 8601 duration in the format PT#M#S,
19
+ # in which the letters PT indicate that the value specifies a period of time, and
20
+ # the letters M and S refer to length in minutes and seconds, respectively. The #
21
+ # characters preceding the M and S letters are both integers that specify the number
22
+ # of minutes (or seconds) of the video. For example, a value of PT15M51S indicates
23
+ # that the video is 15 minutes and 51 seconds long.
24
+ def to_seconds(iso8601_duration)
25
+ match = iso8601_duration.match %r{^PT(?:|(?<min>\d*?)M)(?:|(?<sec>\d*?)S)$}
26
+ minutes = (match[:min] || '0').to_i
27
+ seconds = (match[:sec]).to_i
28
+ minutes * 60 + seconds
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,65 @@
1
+ require 'yt/models/base'
2
+
3
+ module Yt
4
+ class Playlist < Base
5
+ attr_reader :id, :auth
6
+ has_one :snippet, delegate: [:title, :description, :tags, :thumbnail_url, :published_at]
7
+ has_one :status, delegate: [:privacy_status, :public?, :private?, :unlisted?]
8
+ has_many :playlist_items
9
+
10
+ def initialize(options = {})
11
+ @id = options[:id]
12
+ @snippet = Snippet.new(data: options[:snippet]) if options[:snippet]
13
+ @status = Status.new(data: options[:status]) if options[:status]
14
+ @auth = options[:auth]
15
+ end
16
+
17
+ def delete
18
+ do_delete {@id = nil}
19
+ !exists?
20
+ end
21
+
22
+ # Valid body (no defaults) are: title (string), description (string), privacy_status (string),
23
+ # tags (array of strings) - since title is required, we set it again if it's not passed
24
+ def update(options = {})
25
+ parts, body = [], {id: @id}
26
+
27
+ options[:title] ||= title
28
+ parts << :snippet
29
+ body[:snippet] = options.slice :title, :description, :tags
30
+
31
+ if status = options[:privacy_status]
32
+ parts << :status
33
+ body[:status] = {privacyStatus: status}
34
+ end
35
+
36
+ params = {params: {part: parts.join(',')}, body: body}
37
+ do_update(params, expect: Net::HTTPOK) do |data|
38
+ @id = data['id']
39
+ @snippet = Snippet.new data: data['snippet'] if data['snippet']
40
+ @status = Status.new data: data['status'] if data['status']
41
+ true
42
+ end
43
+ end
44
+
45
+ def exists?
46
+ !@id.nil?
47
+ end
48
+
49
+ private
50
+
51
+ def delete_params
52
+ super.tap do |params|
53
+ params[:path] = '/youtube/v3/playlists'
54
+ params[:params] = {id: @id}
55
+ end
56
+ end
57
+
58
+ def update_params
59
+ super.tap do |params|
60
+ params[:path] = '/youtube/v3/playlists'
61
+ params[:body_type] = :json
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,42 @@
1
+ require 'yt/models/base'
2
+
3
+ module Yt
4
+ class PlaylistItem < Base
5
+ attr_reader :id, :video, :position
6
+
7
+ def initialize(options = {})
8
+ @id = options[:id]
9
+ @auth = options[:auth]
10
+ if options[:snippet]
11
+ @position = options[:snippet]['position']
12
+ @video = Video.new video_params_for options
13
+ end
14
+ end
15
+
16
+ def delete
17
+ do_delete {@id = nil}
18
+ !exists?
19
+ end
20
+
21
+ def exists?
22
+ !@id.nil?
23
+ end
24
+
25
+ private
26
+
27
+ def delete_params
28
+ super.tap do |params|
29
+ params[:path] = '/youtube/v3/playlistItems'
30
+ params[:params] = {id: @id}
31
+ end
32
+ end
33
+
34
+ def video_params_for(options = {})
35
+ {}.tap do |params|
36
+ params[:id] = options[:snippet].fetch('resourceId', {})['videoId']
37
+ params[:snippet] = options[:snippet].except 'resourceId'
38
+ params[:auth] = options[:auth]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,28 @@
1
+ require 'yt/models/base'
2
+
3
+ module Yt
4
+ class Rating < Base
5
+ attr_reader :rating
6
+
7
+ def initialize(options = {})
8
+ @rating = options[:rating].to_sym if options[:rating]
9
+ @video_id = options[:video_id]
10
+ @auth = options[:auth]
11
+ end
12
+
13
+ def update(new_rating)
14
+ do_update(params: {rating: new_rating}) {@rating = new_rating}
15
+ end
16
+
17
+ private
18
+
19
+ def update_params
20
+ super.tap do |params|
21
+ params[:method] = :post
22
+ params[:path] = '/youtube/v3/videos/rate'
23
+ params[:params] = {id: @video_id}
24
+ params[:scope] = 'https://www.googleapis.com/auth/youtube'
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,48 @@
1
+ require 'yt/models/description'
2
+
3
+ module Yt
4
+ class Snippet
5
+ def initialize(options = {})
6
+ @data = options[:data]
7
+ end
8
+
9
+ # Return the title of the YouTube resource.
10
+ #
11
+ # @return [String] Title of the YouTube resource
12
+ def title
13
+ @title ||= @data.fetch 'title', ''
14
+ end
15
+
16
+ # Return the description of a YouTube resource.
17
+ #
18
+ # @return [Yt::Description] A Yt::Description object for the YouTube resource
19
+ def description
20
+ @description ||= Description.new @data.fetch('description', '')
21
+ end
22
+
23
+ # Return the publication date of a YouTube resource.
24
+ #
25
+ # @return [Time or nil] The publication date for the YouTube resource
26
+ def published_at
27
+ @published_at ||= Time.parse @data['publishedAt']
28
+ end
29
+
30
+ # Return the tags of a YouTube resource.
31
+ #
32
+ # @return [Array] An array of Yt::Tag object, one for each tag of the resource.
33
+ #
34
+ # @note YouTube API only includes tags in a resource’s snippet if the
35
+ # resource is a video belonging to the authenticated account.
36
+ def tags
37
+ @tags ||= @data.fetch 'tags', []
38
+ end
39
+
40
+ # Return the URL of a thumbnail image of the YouTube resource.
41
+ #
42
+ # @return [String] A URL.
43
+ def thumbnail_url(size = :default)
44
+ @thumbnails ||= @data.fetch 'thumbnails', {}
45
+ @thumbnails.fetch(size.to_s, {})['url']
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,26 @@
1
+ module Yt
2
+ class Status
3
+ def initialize(options = {})
4
+ @data = options[:data]
5
+ end
6
+
7
+ # @return [Boolean] Is the resource public?
8
+ def public?
9
+ privacy_status == 'public'
10
+ end
11
+
12
+ # @return [Boolean] Is the resource private?
13
+ def private?
14
+ privacy_status == 'private'
15
+ end
16
+
17
+ # @return [Boolean] Is the resource unlisted?
18
+ def unlisted?
19
+ privacy_status == 'unlisted'
20
+ end
21
+
22
+ def privacy_status
23
+ @privacy_status ||= @data['privacyStatus']
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,35 @@
1
+ require 'yt/models/base'
2
+
3
+ module Yt
4
+ class Subscription < Base
5
+
6
+ attr_reader :id
7
+
8
+ def initialize(options = {})
9
+ @id = options[:id]
10
+ @auth = options[:auth]
11
+ end
12
+
13
+ def delete(options = {})
14
+ begin
15
+ do_delete {@id = nil}
16
+ rescue Yt::RequestError => error
17
+ raise error unless options[:ignore_not_found] && error.reasons.include?('subscriptionNotFound')
18
+ end
19
+ !exists?
20
+ end
21
+
22
+ def exists?
23
+ !@id.nil?
24
+ end
25
+
26
+ private
27
+
28
+ def delete_params
29
+ super.tap do |params|
30
+ params[:path] = '/youtube/v3/subscriptions'
31
+ params[:params] = {id: @id}
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,66 @@
1
+ require 'yt/models/base'
2
+
3
+ module Yt
4
+ class UserInfo < Base
5
+ def initialize(options = {})
6
+ @data = options[:data]
7
+ end
8
+
9
+ # @return [String] User ID
10
+ def id
11
+ @id ||= @data.fetch 'id', ''
12
+ end
13
+
14
+ # Return the email of the YouTube account.
15
+ #
16
+ # @return [String] Email of the YouTube account
17
+ def email
18
+ @email ||= @data.fetch 'email', ''
19
+ end
20
+
21
+ # @return [Boolean] Email is verified?
22
+ def has_verified_email?
23
+ @verified_email ||= @data.fetch 'verified_email', false
24
+ end
25
+
26
+ # @return [String] name
27
+ def name
28
+ @name ||= @data.fetch 'name', ''
29
+ end
30
+
31
+ # @return [String] given_name
32
+ def given_name
33
+ @given_name ||= @data.fetch 'given_name', ''
34
+ end
35
+
36
+ # @return [String] family_name
37
+ def family_name
38
+ @family_name ||= @data.fetch 'family_name', ''
39
+ end
40
+
41
+ # @return [String] family_name
42
+ def profile_url
43
+ @profile_url ||= @data.fetch 'link', ''
44
+ end
45
+
46
+ # @return [String] avatar_url
47
+ def avatar_url
48
+ @avatar_url ||= @data.fetch 'picture', ''
49
+ end
50
+
51
+ # @return [String] gender
52
+ def gender
53
+ @gender ||= @data.fetch 'gender', ''
54
+ end
55
+
56
+ # @return [String] locale
57
+ def locale
58
+ @locale ||= @data.fetch 'locale', ''
59
+ end
60
+
61
+ # @return [String] hd
62
+ def hd
63
+ @hd ||= @data.fetch 'hd', ''
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,16 @@
1
+ module Yt
2
+ class Video < Base
3
+ has_many :annotations
4
+ has_one :details_set, delegate: [:duration]
5
+ has_one :rating
6
+ has_one :snippet, delegate: [:title, :description, :tags, :thumbnail_url, :published_at]
7
+
8
+ attr_reader :id, :auth
9
+
10
+ def initialize(options = {})
11
+ @id = options[:id]
12
+ @auth = options[:auth]
13
+ @snippet = Snippet.new(data: options[:snippet]) if options[:snippet]
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,85 @@
1
+ require 'yt/config'
2
+
3
+ require 'uri' # for URI.json
4
+ require 'net/http' # for Net::HTTP.start
5
+ require 'json' # for JSON.parse
6
+ require 'active_support/core_ext/hash/conversions' # for Hash.from_xml
7
+
8
+ module Yt
9
+ class RequestError < StandardError
10
+ def reasons
11
+ error.fetch('errors', []).map{|e| e['reason']}
12
+ end
13
+
14
+ def error
15
+ eval(message)['error'] rescue {}
16
+ end
17
+ end
18
+
19
+ class Request
20
+ def initialize(options = {})
21
+ options[:query] ||= options[:params].to_param
22
+ @uri = URI::HTTPS.build options.slice(:host, :path, :query)
23
+ @method = options[:method]
24
+ @format = options[:format]
25
+ @scope = options[:scope]
26
+ @body = options[:body]
27
+ @body_type = options[:body_type]
28
+ @auth = options[:auth]
29
+ @headers = {}
30
+ end
31
+
32
+ def run
33
+ add_authorization_to_request!
34
+ fetch_response.tap do |response|
35
+ response.body = parse_format response.body if response.body
36
+ unless response.is_a? Net::HTTPSuccess
37
+ # puts "You can try again running #{to_curl}"
38
+ raise RequestError, response.body
39
+ end
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def add_authorization_to_request!
46
+ if @auth.respond_to? :access_token_for
47
+ @headers['Authorization'] = "Bearer #{@auth.access_token_for @scope}"
48
+ else # TODO: check if api_key was set in the first place!!
49
+ params = URI.decode_www_form @uri.query || ''
50
+ params << [:key, Yt.configuration.api_key]
51
+ @uri.query = URI.encode_www_form params
52
+ end
53
+ end
54
+
55
+ def fetch_response
56
+ Net::HTTP.start(@uri.host, @uri.port, use_ssl: true) do |http|
57
+ klass = "Net::HTTP::#{@method.capitalize}".constantize
58
+ request = if @body_type == :json
59
+ klass.new @uri, initheader = {'Content-Type' =>'application/json'}
60
+ else
61
+ klass.new @uri
62
+ end
63
+ @headers.each{|k,v| request.add_field k, v}
64
+ case @body_type
65
+ when :json then request.body = @body.to_json
66
+ when :form then request.set_form_data @body
67
+ end if @body
68
+
69
+ http.request request
70
+ # NOTE! Here refresh the token if the access is expired
71
+ end
72
+ end
73
+
74
+ def parse_format(body)
75
+ case @format
76
+ when :xml then Hash.from_xml body
77
+ when :json then JSON body
78
+ end
79
+ end
80
+
81
+ # def to_curl
82
+ # %Q{curl -X #{@method.upcase} "#{@uri}"}
83
+ # end
84
+ end
85
+ end