yt-core 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +14 -22
  3. data/.rspec +3 -0
  4. data/.travis.yml +8 -0
  5. data/.yardopts +3 -0
  6. data/CHANGELOG.md +11 -0
  7. data/Gemfile +2 -1
  8. data/LICENSE.txt +17 -18
  9. data/README.md +81 -17
  10. data/Rakefile +8 -1
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/docs/_config.yml +10 -0
  14. data/docs/_includes/doc.html +1 -0
  15. data/docs/_includes/dt.html +4 -0
  16. data/docs/_includes/example.html +1 -0
  17. data/docs/_includes/footer.html +3 -0
  18. data/docs/_includes/head.html +21 -0
  19. data/docs/_includes/header.html +57 -0
  20. data/docs/_layouts/default.html +58 -0
  21. data/docs/apple-touch-icon-precomposed.png +0 -0
  22. data/docs/channels.html +142 -0
  23. data/docs/css/yt.css +40 -0
  24. data/docs/errors.html +28 -0
  25. data/docs/favicon.ico +0 -0
  26. data/docs/fonts/SecondaRound-Bold.eot +0 -0
  27. data/docs/fonts/SecondaRound-Bold.svg +2160 -0
  28. data/docs/fonts/SecondaRound-Bold.ttf +0 -0
  29. data/docs/fonts/SecondaRound-Bold.woff +0 -0
  30. data/docs/fonts/SecondaRound-Bold.woff2 +0 -0
  31. data/docs/fonts/SecondaRound-Regular.eot +0 -0
  32. data/docs/fonts/SecondaRound-Regular.svg +1873 -0
  33. data/docs/fonts/SecondaRound-Regular.ttf +0 -0
  34. data/docs/fonts/SecondaRound-Regular.woff +0 -0
  35. data/docs/fonts/SecondaRound-Regular.woff2 +0 -0
  36. data/docs/images/console-01.png +0 -0
  37. data/docs/images/console-02.png +0 -0
  38. data/docs/images/console-03.png +0 -0
  39. data/docs/images/console-04.png +0 -0
  40. data/docs/images/console-05.png +0 -0
  41. data/docs/images/console-06.png +0 -0
  42. data/docs/images/console-07.png +0 -0
  43. data/docs/images/logo.png +0 -0
  44. data/docs/images/robot.png +0 -0
  45. data/docs/images/robots.png +0 -0
  46. data/docs/index.html +37 -0
  47. data/docs/playlist_items.html +65 -0
  48. data/docs/playlists.html +114 -0
  49. data/docs/urls.html +52 -0
  50. data/docs/videos.html +104 -0
  51. data/lib/yt/channel.rb +127 -0
  52. data/lib/yt/core/version.rb +8 -0
  53. data/lib/yt/core.rb +18 -0
  54. data/lib/yt/no_items_error.rb +8 -0
  55. data/lib/yt/playlist.rb +75 -0
  56. data/lib/yt/playlist_item.rb +59 -0
  57. data/lib/yt/relation.rb +100 -0
  58. data/lib/yt/resource.rb +85 -0
  59. data/lib/yt/response.rb +91 -0
  60. data/lib/yt/video.rb +216 -0
  61. data/yt-core.gemspec +27 -15
  62. metadata +155 -19
  63. data/lib/yt-core/version.rb +0 -3
  64. data/lib/yt-core.rb +0 -5
@@ -0,0 +1,75 @@
1
+ module Yt
2
+ # Provides methods to interact with YouTube playlists.
3
+ # @see https://developers.google.com/youtube/v3/docs/playlists
4
+ class Playlist < Resource
5
+ # @!attribute [r] title
6
+ # @return [String] the playlist’s title.
7
+ has_attribute :title, in: :snippet
8
+
9
+ # @!attribute [r] description
10
+ # @return [String] the playlist’s description.
11
+ has_attribute :description, in: :snippet
12
+
13
+ # @!attribute [r] published_at
14
+ # @return [Time] the date and time that the playlist was created.
15
+ has_attribute :published_at, in: :snippet, type: Time
16
+
17
+ # @!attribute [r] thumbnails
18
+ # @return [Hash<String, Hash>] the thumbnails associated with the playlist.
19
+ has_attribute :thumbnails, in: :snippet
20
+
21
+ # @!attribute [r] channel_id
22
+ # @return [String] the ID of the channel that published the playlist.
23
+ has_attribute :channel_id, in: :snippet
24
+
25
+ # @!attribute [r] channel_title
26
+ # @return [String] the title of the channel that published the playlist.
27
+ has_attribute :channel_title, in: :snippet
28
+
29
+ # has_attribute :default_language, in: :snippet not sure how to set to test
30
+ # has_attribute :localized, in: :snippet not yet implemented
31
+ # has_attribute :tags, in: :snippet not sure how to set to test
32
+
33
+ # @!attribute [r] privacy_status
34
+ # @return [String] the playlist’s privacy status. Valid values are:
35
+ # +"private"+, +"public"+, and +"unlisted"+.
36
+ has_attribute :privacy_status, in: :status
37
+
38
+ # @!attribute [r] item_count
39
+ # @return [<Integer>] the number of videos in the playlist.
40
+ has_attribute :item_count, in: :content_details, type: Integer
41
+
42
+ # Returns the URL of the playlist’s thumbnail.
43
+ # @param [Symbol, String] size The size of the playlist’s thumbnail.
44
+ # @return [String] if +size+ is +:default+, the URL of a 120x90px image.
45
+ # @return [String] if +size+ is +:medium+, the URL of a 320x180px image.
46
+ # @return [String] if +size+ is +:high+, the URL of a 480x360px image.
47
+ # @return [String] if +size+ is +:standard+, the URL of a 640x480px image.
48
+ # @return [String] if +size+ is +:maxres+, the URL of a 1280x720px image.
49
+ # @return [nil] if the +size+ is none of the above.
50
+ def thumbnail_url(size = :default)
51
+ thumbnails.fetch(size.to_s, {})['url']
52
+ end
53
+
54
+ # @return [String] the canonical form of the playlist’s URL.
55
+ def canonical_url
56
+ "https://www.youtube.com/playlist?list=#{id}"
57
+ end
58
+
59
+ # @return [Yt::Relation<Yt::PlaylistItem>] the items of the playlist.
60
+ def items
61
+ @items ||= Relation.new(PlaylistItem, playlist_id: id) do |options|
62
+ fetch '/youtube/v3/playlistItems', playlist_items_params(options)
63
+ end
64
+ end
65
+
66
+ # @return [Yt::Relation<Yt::Video>] the videos of the playlist.
67
+ def videos
68
+ @videos ||= Relation.new(Video, playlist_id: id) do |options|
69
+ params = playlist_items_params(options.merge parts: [:content_details])
70
+ items = fetch '/youtube/v3/playlistItems', params
71
+ videos_for items, 'contentDetails', options
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,59 @@
1
+ module Yt
2
+ # Provides methods to interact with YouTube playlist items.
3
+ # @see https://developers.google.com/youtube/v3/docs/playlistItems
4
+ class PlaylistItem < Resource
5
+ # @!attribute [r] title
6
+ # @return [String] the item’s title.
7
+ has_attribute :title, in: :snippet
8
+
9
+ # @!attribute [r] description
10
+ # @return [String] the item’s description.
11
+ has_attribute :description, in: :snippet
12
+
13
+ # @!attribute [r] published_at
14
+ # @return [Time] the date and time that the item was added to the playlist.
15
+ has_attribute :published_at, in: :snippet, type: Time
16
+
17
+ # @!attribute [r] thumbnails
18
+ # @return [Hash<String, Hash>] the thumbnails associated with the item.
19
+ has_attribute :thumbnails, in: :snippet
20
+
21
+ # @!attribute [r] channel_id
22
+ # @return [String] the ID of the channel that the playlist belongs to.
23
+ has_attribute :channel_id, in: :snippet
24
+
25
+ # @!attribute [r] channel_title
26
+ # @return [String] the title of the channel that the playlist belongs to.
27
+ has_attribute :channel_title, in: :snippet
28
+
29
+ # @!attribute [r] playlist_id
30
+ # @return [String] the ID of the playlist that the item belongs to.
31
+ has_attribute :playlist_id, in: :snippet
32
+
33
+ # @!attribute [r] position
34
+ # @return [Integer] the order in which the item appears in the playlist.
35
+ # The value uses a zero-based index so the first item has a position of 0.
36
+ has_attribute :position, in: :snippet, type: Integer
37
+
38
+ # @!attribute [r] video_id
39
+ # @return [String] the ID of the video that the item refers to.
40
+ has_attribute :video_id, in: %i(snippet resource_id)
41
+
42
+ # @!attribute [r] privacy_status
43
+ # @return [String] the item’s privacy status. Valid values are:
44
+ # +"private"+, +"public"+, and +"unlisted"+.
45
+ has_attribute :privacy_status, in: :status
46
+
47
+ # Returns the URL of the item’s thumbnail.
48
+ # @param [Symbol, String] size The size of the item’s thumbnail.
49
+ # @return [String] if +size+ is +:default+, the URL of a 120x90px image.
50
+ # @return [String] if +size+ is +:medium+, the URL of a 320x180px image.
51
+ # @return [String] if +size+ is +:high+, the URL of a 480x360px image.
52
+ # @return [String] if +size+ is +:standard+, the URL of a 640x480px image.
53
+ # @return [String] if +size+ is +:maxres+, the URL of a 1280x720px image.
54
+ # @return [nil] if the +size+ is none of the above.
55
+ def thumbnail_url(size = :default)
56
+ thumbnails.fetch(size.to_s, {})['url']
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,100 @@
1
+ module Yt
2
+ # Provides methods to iterate through collections of YouTube resources.
3
+ # @private
4
+ class Relation
5
+ include Enumerable
6
+
7
+ # @param [Class] item_class the class of objects to initialize when
8
+ # iterating through a collection of YouTube resources.
9
+ # @yield [Hash] the options to change which items to iterate through.
10
+ def initialize(item_class, options = {}, &item_block)
11
+ @options = {parts: %i(id), limit: Float::INFINITY, item_class: item_class}
12
+ @options.merge! options
13
+ @item_block = item_block
14
+ end
15
+
16
+ # Let's start without memoizing
17
+ def each
18
+ @last_index = 0
19
+ while next_item = find_next
20
+ break if @last_index > @options[:limit]
21
+ yield next_item
22
+ end
23
+ end
24
+
25
+ def find_next
26
+ @items ||= []
27
+ if @items[@last_index].nil? && more_pages?
28
+ response = Response.new(@options, &@item_block).run
29
+ more_items = response.body['items'].map do |item|
30
+ @options[:item_class].new attributes_for_new_item(item)
31
+ end
32
+ @options.merge! offset: response.body['nextPageToken']
33
+ @items.concat more_items
34
+ end
35
+ @items[(@last_index +=1) -1]
36
+ end
37
+
38
+ def more_pages?
39
+ @last_index.zero? || !@options[:offset].nil?
40
+ end
41
+
42
+ def attributes_for_new_item(item)
43
+ {}.tap do |matching_parts|
44
+ item.each_key do |key|
45
+ part = key.gsub(/([A-Z])/) { "_#{$1.downcase}" }.to_sym
46
+ if @options[:parts].include? part
47
+ matching_parts[part] = item[key]
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ # @return [Integer] the estimated number of items in the collection.
54
+ def size
55
+ size_options = @options.merge parts: %i(id), limit: 1
56
+ @response = Response.new(size_options, &@item_block).run
57
+ [@response.body['pageInfo']['totalResults'], @options[:limit]].min
58
+ end
59
+
60
+ # Specifies which parts of the resource to fetch when hitting the data API.
61
+ # @param [Array<Symbol>] parts The parts to fetch.
62
+ # @return [Yt::Relation] itself.
63
+ def select(*parts)
64
+ if @options[:parts] != parts + %i(id)
65
+ @items = nil
66
+ @options.merge! parts: (parts + %i(id))
67
+ end
68
+ self
69
+ end
70
+
71
+ # Specifies which items to fetch when hitting the data API.
72
+ # @param [Hash<Symbol, String>] conditions The conditions for the items.
73
+ # @return [Yt::Relation] itself.
74
+ def where(conditions = {})
75
+ if @options[:conditions] != conditions
76
+ @items = []
77
+ @options.merge! conditions: conditions
78
+ end
79
+ self
80
+ end
81
+
82
+ # Specifies how many items to fetch when hitting the data API.
83
+ # @param [Integer] max_results The maximum number of items to fetch.
84
+ # @return [Yt::Relation] itself.
85
+ def limit(max_results)
86
+ @options.merge! limit: max_results
87
+ self
88
+ end
89
+
90
+ # @return [String] a representation of the Yt::Relation instance.
91
+ def inspect
92
+ entries = take(3).map!(&:inspect)
93
+ if entries.size == 3
94
+ entries[2] = '...'
95
+ end
96
+
97
+ "#<#{self.class.name} [#{entries.join(', ')}]>"
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,85 @@
1
+ module Yt
2
+ # Provides a base class for YouTube channels, videos, playlists and items.
3
+ # This is an abstract class and should not be instantiated directly.
4
+ class Resource
5
+ # @param [Hash<Symbol, String>] data the options to initialize a resource.
6
+ # @option data [String] :id The unique ID of a YouTube resource.
7
+ def initialize(data = {})
8
+ @data = data
9
+ @selected_data_parts = nil
10
+ end
11
+
12
+ # @return [String] the resource’s unique ID.
13
+ def id
14
+ @data[:id]
15
+ end
16
+
17
+ # @return [Hash] the resource’s data.
18
+ attr_reader :data
19
+
20
+ # @return [String] a representation of the resource instance.
21
+ def inspect
22
+ "#<#{self.class} @id=#{id}>"
23
+ end
24
+
25
+ # Specifies which parts of the resource to fetch when hitting the data API.
26
+ # @param [Array<Symbol>] parts The parts to fetch.
27
+ # @return [Yt::Resource] itself.
28
+ def select(*parts)
29
+ @selected_data_parts = parts
30
+ self
31
+ end
32
+
33
+ # @return [Yt::Relation<Yt::Video>] the videos matching the conditions.
34
+ def self.where(conditions = {})
35
+ @where ||= Relation.new(self) do |options|
36
+ slicing_conditions_every(50) do |slice_options|
37
+ fetch resources_path, where_params(slice_options)
38
+ end
39
+ end
40
+ @where.where conditions
41
+ end
42
+
43
+ private
44
+
45
+ def camelize(part)
46
+ part.to_s.gsub(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" }
47
+ end
48
+
49
+ def self.has_attribute(name, options = {}, &block)
50
+ define_method name do
51
+ keys = Array(options[:in]) + [name]
52
+ part = keys.shift
53
+ value = @data[part] || fetch_part(part)
54
+ keys.each{|key| value = value[camelize key]}
55
+ value = type_cast value, options[:type]
56
+ block_given? ? instance_exec(value, &block) : value
57
+ end
58
+ end
59
+
60
+ def fetch_part(required_part)
61
+ resources = Relation.new(self.class, ids: [id]) do |options|
62
+ fetch resources_path, resource_params(options)
63
+ end
64
+
65
+ parts = @selected_data_parts || [required_part]
66
+ if (resource = resources.select(*parts).first)
67
+ parts.each{|part| @data[part] = resource.data[part]}
68
+ @data[required_part]
69
+ else
70
+ raise NoItemsError
71
+ end
72
+ end
73
+
74
+ def type_cast(value, type)
75
+ case [type]
76
+ when [Time]
77
+ Time.parse value
78
+ when [Integer]
79
+ value.to_i
80
+ else
81
+ value
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,91 @@
1
+ module Yt
2
+ # @private
3
+ class Response
4
+ def initialize(options, &block)
5
+ @options = options
6
+ @block = block
7
+ end
8
+
9
+ def run
10
+ instance_exec @options, &@block
11
+ end
12
+
13
+ private
14
+
15
+ def fetch(path, params)
16
+ HTTPRequest.new(path: path, params: params).run
17
+ end
18
+
19
+ def resources_path
20
+ @options[:item_class].name.split('::').last.gsub(/^(\w{1})(.*)/) do
21
+ "/youtube/v3/#{$1.downcase}#{$2}s"
22
+ end
23
+ end
24
+
25
+ def where_params(options)
26
+ ids = options[:conditions].fetch(:id, []).join ','
27
+ default_params(options).merge id: ids
28
+ end
29
+
30
+ def channel_playlists_params(options)
31
+ default_params(options).merge channel_id: options[:channel_id]
32
+ end
33
+
34
+ def channel_videos_params(options)
35
+ params = {channel_id: options[:channel_id], order: :date, type: :video}
36
+ default_params(options.merge parts: %i(id)).merge params
37
+ end
38
+
39
+ def playlist_items_params(options)
40
+ default_params(options).merge playlist_id: options[:playlist_id]
41
+ end
42
+
43
+ def resource_params(options)
44
+ default_params(options).merge id: options[:ids].join(',')
45
+ end
46
+
47
+ def default_params(options)
48
+ {}.tap do |params|
49
+ params[:max_results] = 50
50
+ params[:key] = Yt.configuration.api_key
51
+ params[:part] = options[:parts].join ','
52
+ params[:page_token] = options[:offset]
53
+ end
54
+ end
55
+
56
+ def slicing_conditions_every(size, &block)
57
+ slices = @options[:conditions].fetch(:id, []).each_slice size
58
+ slices.inject(nil) do |response, ids|
59
+ slice_options = @options.merge conditions: {id: ids}
60
+ response = combine_slices response, yield(slice_options)
61
+ end
62
+ end
63
+
64
+ def combine_slices(total, partial)
65
+ if total
66
+ total.body['items'] += partial.body['items']
67
+ total_results = partial.body['pageInfo']['totalResults']
68
+ total.body['pageInfo']['totalResults'] += total_results
69
+ total
70
+ else
71
+ partial
72
+ end
73
+ end
74
+
75
+ # Expands the resultset into a collection of videos by fetching missing
76
+ # parts, eventually with an additional HTTP request.
77
+ def videos_for(items, key, options)
78
+ items.body['items'].map{|item| item['id'] = item[key]['videoId']}
79
+
80
+ if options[:parts] == %i(id)
81
+ items
82
+ else
83
+ options[:ids] = items.body['items'].map{|item| item['id']}
84
+ options[:offset] = nil
85
+ fetch('/youtube/v3/videos', resource_params(options)).tap do |response|
86
+ response.body['nextPageToken'] = items.body['nextPageToken']
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
data/lib/yt/video.rb ADDED
@@ -0,0 +1,216 @@
1
+ module Yt
2
+ # Provides methods to interact with YouTube videos.
3
+ # @see https://developers.google.com/youtube/v3/docs/videos
4
+ class Video < Resource
5
+ # @!attribute [r] title
6
+ # @return [String] the video’s title. Has a maximum length of 100 characters
7
+ # and may contain all valid UTF-8 characters except < and >.
8
+ has_attribute :title, in: :snippet
9
+
10
+ # @!attribute [r] description
11
+ # @return [String] the video’s description. Has a maximum length of 5000
12
+ # bytes and may contain all valid UTF-8 characters except < and >.
13
+ has_attribute :description, in: :snippet
14
+
15
+ # @!attribute [r] published_at
16
+ # @return [Time] the date and time that the video was published. Note that
17
+ # this time might be different than the time that the video was uploaded.
18
+ # For example, if a video is uploaded as a private video and then made
19
+ # public at a later time, this property will specify the time that the
20
+ # video was made public.
21
+ has_attribute :published_at, in: :snippet, type: Time
22
+
23
+ # @!attribute [r] thumbnails
24
+ # @return [Hash<String, Hash>] the thumbnails associated with the video.
25
+ has_attribute :thumbnails, in: :snippet
26
+
27
+ # @!attribute [r] channel_id
28
+ # @return [String] the ID of the channel that the video was uploaded to.
29
+ has_attribute :channel_id, in: :snippet
30
+
31
+ # @!attribute [r] channel_title
32
+ # @return [String] the title of the channel that the video was uploaded to.
33
+ has_attribute :channel_title, in: :snippet
34
+
35
+ # @!attribute [r] tags
36
+ # @return [Array<String>] the list of tags associated with the video.
37
+ has_attribute :tags, in: :snippet
38
+
39
+ # @!attribute [r] category_id
40
+ # @return [Integer] the ID of the associated YouTube video category.
41
+ # @see https://developers.google.com/youtube/v3/docs/videoCategories/list
42
+ has_attribute :category_id, in: :snippet, type: Integer
43
+
44
+ # @!attribute [r] live_broadcast_content
45
+ # @return [String] whether the video is an upcoming/active live broadcast.
46
+ # Valid values are: +"live"+, +"none"+, +"upcoming"+.
47
+ has_attribute :live_broadcast_content, in: :snippet
48
+
49
+ # has_attribute :default_language, in: :snippet not sure how to set to test
50
+ # has_attribute :localized, in: :snippet not yet implemented
51
+ # has_attribute :default_audio_language, in: :snippet not sure how to test
52
+
53
+ # @!attribute [r] upload_status
54
+ # @return [String] the status of the uploaded video. Valid values are:
55
+ # +"deleted"+, +"failed"+, +"processed"+, +"rejected"+, and +"unlisted"+.
56
+ has_attribute :upload_status, in: :status
57
+
58
+ # @!attribute [r] privacy_status
59
+ # @return [String] the video’s privacy status. Valid values are:
60
+ # +"private"+, +"public"+, and +"unlisted"+.
61
+ has_attribute :privacy_status, in: :status
62
+
63
+ # @!attribute [r] license
64
+ # @return [String] the video’s license. Valid values are:
65
+ # +"creative_common"+ and +"youtube"+.
66
+ has_attribute :license, in: :status
67
+
68
+ # @!attribute [r] embeddable
69
+ # @return [Boolean] whether the video can be embedded on another website.
70
+ has_attribute :embeddable, in: :status
71
+
72
+ # @!attribute [r] public_stats_viewable
73
+ # @return [Boolean] whether the extended video statistics on the video’s
74
+ # watch page are publicly viewable.
75
+ has_attribute :public_stats_viewable, in: :status
76
+
77
+ # has_attribute :failure_reason, in: :status not yet implemented
78
+ # has_attribute :rejection_reason, in: :status not yet implemented
79
+ # has_attribute :publish_at, in: :status not yet implemented
80
+
81
+ # @!attribute [r] view_count
82
+ # @return [<Integer>] the number of times the video has been viewed.
83
+ has_attribute :view_count, in: :statistics, type: Integer
84
+
85
+ # @!attribute [r] like_count
86
+ # @return [<Integer>] the number of users who have liked the video.
87
+ has_attribute :like_count, in: :statistics, type: Integer
88
+
89
+ # @!attribute [r] dislike_count
90
+ # @return [<Integer>] the number of users who have disliked the video.
91
+ has_attribute :dislike_count, in: :statistics, type: Integer
92
+
93
+ # @!attribute [r] comment_count
94
+ # @return [<Integer>] the number of comments for the video.
95
+ has_attribute :comment_count, in: :statistics, type: Integer
96
+
97
+ # @!attribute [r] duration
98
+ # @return [<String>] the length of the video as an ISO 8601 duration.
99
+ has_attribute :duration, in: :content_details
100
+
101
+ # @!attribute [r] dimension
102
+ # @return [String] whether the video is available in 3D or in 2D.
103
+ # Valid values are: +"2d"+ and +"3d".
104
+ has_attribute :dimension, in: :content_details
105
+
106
+ # @!attribute [r] definition
107
+ # @return [String] whether the video is available in high definition or only
108
+ # in standard definition. Valid values are: +"sd"+ and +"hd".
109
+ has_attribute :definition, in: :content_details
110
+
111
+ # @!attribute [r] caption
112
+ # @return [Boolean] whether captions are available for the video.
113
+ has_attribute :caption, in: :content_details do |captioned|
114
+ captioned == 'true'
115
+ end
116
+
117
+ # @!attribute [r] licensed_content
118
+ # @return [Boolean] whether the video represents licensed content, which
119
+ # means that the content was uploaded to a channel linked to a YouTube
120
+ # content partner and then claimed by that partner.
121
+ has_attribute :licensed_content, in: :content_details
122
+
123
+ # @!attribute [r] projection
124
+ # @return [String] the projection format of the video. Valid values are:
125
+ # +"360"+ and +"rectangular".
126
+ has_attribute :projection, in: :content_details
127
+
128
+ # has_attribute :has_custom_thumbnail, in: :content_details to do
129
+ # has_attribute :content_rating, in: :content_details to do
130
+
131
+ # Returns the URL of the video’s thumbnail.
132
+ # @param [Symbol, String] size The size of the video’s thumbnail.
133
+ # @return [String] if +size+ is +:default+, the URL of a 120x90px image.
134
+ # @return [String] if +size+ is +:medium+, the URL of a 320x180px image.
135
+ # @return [String] if +size+ is +:high+, the URL of a 480x360px image.
136
+ # @return [String] if +size+ is +:standard+, the URL of a 640x480px image.
137
+ # @return [String] if +size+ is +:maxres+, the URL of a 1280x720px image.
138
+ # @return [nil] if the +size+ is none of the above.
139
+ def thumbnail_url(size = :default)
140
+ thumbnails.fetch(size.to_s, {})['url']
141
+ end
142
+
143
+ # @return [String] the canonical form of the video’s URL.
144
+ def canonical_url
145
+ "https://www.youtube.com/watch?v=#{id}"
146
+ end
147
+
148
+ # @return [Hash<Integer, String>] the list of YouTube video categories.
149
+ CATEGORIES = {
150
+ 1 => 'Film & Animation', 2 => 'Autos & Vehicles', 10 => 'Music',
151
+ 15 => 'Pets & Animals', 17 => 'Sports', 18 => 'Short Movies',
152
+ 19 => 'Travel & Events', 20 => 'Gaming', 21 => 'Videoblogging',
153
+ 22 => 'People & Blogs', 23 => 'Comedy', 24 => 'Entertainment',
154
+ 25 => 'News & Politics', 26 => 'Howto & Style', 27 => 'Education',
155
+ 28 => 'Science & Technology', 29 => 'Nonprofits & Activism',
156
+ 30 => 'Movies', 31 => 'Anime/Animation', 32 => 'Action/Adventure',
157
+ 33 => 'Classics', 34 => 'Comedy', 35 => 'Documentary', 36 => 'Drama',
158
+ 37 => 'Family', 38 => 'Foreign', 39 => 'Horror', 40 => 'Sci-Fi/Fantasy',
159
+ 41 => 'Thriller', 42 => 'Shorts', 43 => 'Shows', 44 => 'Trailers',
160
+ }
161
+
162
+ # @return [String] the title of the associated YouTube video category.
163
+ def category_title
164
+ CATEGORIES[category_id]
165
+ end
166
+
167
+ # @return [<Integer>] the length of the video in seconds.
168
+ def seconds
169
+ to_seconds duration
170
+ end
171
+
172
+ # @return [<String>] the length of the video as an ISO 8601 time, HH:MM:SS.
173
+ def length
174
+ hh, mm, ss = seconds / 3600, seconds / 60 % 60, seconds % 60
175
+ [hh, mm, ss].map{|t| t.to_s.rjust(2,'0')}.join(':')
176
+ end
177
+
178
+ # @return [Yt::Channel] the channel the video belongs to.
179
+ def channel
180
+ @channel ||= Channel.new id: channel_id
181
+ end
182
+
183
+ private
184
+
185
+ # @return [Integer] the duration of the resource as reported by YouTube.
186
+ # @see https://developers.google.com/youtube/v3/docs/videos
187
+ #
188
+ # According to YouTube documentation, the value is an ISO 8601 duration
189
+ # in the format PT#M#S, in which the letters PT indicate that the value
190
+ # specifies a period of time, and the letters M and S refer to length in
191
+ # minutes and seconds, respectively. The # characters preceding the M and
192
+ # S letters are both integers that specify the number of minutes (or
193
+ # seconds) of the video. For example, a value of PT15M51S indicates that
194
+ # the video is 15 minutes and 51 seconds long.
195
+ #
196
+ # The ISO 8601 duration standard, though, is not +always+ respected by
197
+ # the results returned by YouTube API. For instance: video 2XwmldWC_Ls
198
+ # reports a duration of 'P1W2DT6H21M32S', which is to be interpreted as
199
+ # 1 week, 2 days, 6 hours, 21 minutes, 32 seconds. Mixing weeks with
200
+ # other time units is not strictly part of ISO 8601; in this context,
201
+ # weeks will be interpreted as "the duration of 7 days". Similarly, a
202
+ # day will be interpreted as "the duration of 24 hours".
203
+ # Video tPEE9ZwTmy0 reports a duration of 'PT2S'. Skipping time units
204
+ # such as minutes is not part of the standard either; in this context,
205
+ # it will be interpreted as "0 minutes and 2 seconds".
206
+ def to_seconds(iso8601_duration)
207
+ match = iso8601_duration.match %r{^P(?:|(?<weeks>\d*?)W)(?:|(?<days>\d*?)D)(?:|T(?:|(?<hours>\d*?)H)(?:|(?<min>\d*?)M)(?:|(?<sec>\d*?)S))$}
208
+ weeks = (match[:weeks] || '0').to_i
209
+ days = (match[:days] || '0').to_i
210
+ hours = (match[:hours] || '0').to_i
211
+ minutes = (match[:min] || '0').to_i
212
+ seconds = (match[:sec]).to_i
213
+ (((((weeks * 7) + days) * 24 + hours) * 60) + minutes) * 60 + seconds
214
+ end
215
+ end
216
+ end
data/yt-core.gemspec CHANGED
@@ -1,23 +1,35 @@
1
1
  # coding: utf-8
2
2
  lib = File.expand_path('../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'yt-core/version'
4
+ require 'yt/core/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = "yt-core"
8
- spec.version = YtCore::VERSION
9
- spec.authors = ["claudiob"]
10
- spec.email = ["claudiob@gmail.com"]
11
- spec.summary = %q{Write a short summary. Required.}
12
- spec.description = %q{Write a longer description. Optional.}
13
- spec.homepage = ""
14
- spec.license = "MIT"
7
+ spec.name = 'yt-core'
8
+ spec.version = Yt::Core::VERSION
9
+ spec.authors = ['Claudio Baccigalupo']
10
+ spec.email = ['claudio@fullscreen.net']
11
+ spec.description = %q{Youtube V3 API client.}
12
+ spec.summary = %q{Yt makes it easy to interact with Youtube V3 API by
13
+ providing a modular, intuitive and tested Ruby-style API.}
14
+ spec.homepage = 'http://github.com/fullscreen/yt-core'
15
+ spec.license = 'MIT'
15
16
 
16
- spec.files = `git ls-files -z`.split("\x0")
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
17
+ spec.required_ruby_version = '>= 2.2.2'
20
18
 
21
- spec.add_development_dependency "bundler", "~> 1.6"
22
- spec.add_development_dependency "rake"
19
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
20
+ f.match(%r{^(test|spec|features)/})
21
+ end
22
+ spec.bindir = 'exe'
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ['lib']
25
+
26
+ spec.add_dependency 'yt-support', '>= 0.1.2'
27
+
28
+ spec.add_development_dependency 'bundler', '~> 1.14'
29
+ spec.add_development_dependency 'rspec', '~> 3.5'
30
+ spec.add_development_dependency 'rake', '~> 12.0'
31
+ spec.add_development_dependency 'coveralls', '~> 0.8.20'
32
+ spec.add_development_dependency 'pry-nav', '~> 0.2.4'
33
+ spec.add_development_dependency 'jekyll', '~> 3.4'
34
+ spec.add_development_dependency 'yard', '~> 0.9.8'
23
35
  end