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.
- checksums.yaml +4 -4
- data/.gitignore +14 -22
- data/.rspec +3 -0
- data/.travis.yml +8 -0
- data/.yardopts +3 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +2 -1
- data/LICENSE.txt +17 -18
- data/README.md +81 -17
- data/Rakefile +8 -1
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docs/_config.yml +10 -0
- data/docs/_includes/doc.html +1 -0
- data/docs/_includes/dt.html +4 -0
- data/docs/_includes/example.html +1 -0
- data/docs/_includes/footer.html +3 -0
- data/docs/_includes/head.html +21 -0
- data/docs/_includes/header.html +57 -0
- data/docs/_layouts/default.html +58 -0
- data/docs/apple-touch-icon-precomposed.png +0 -0
- data/docs/channels.html +142 -0
- data/docs/css/yt.css +40 -0
- data/docs/errors.html +28 -0
- data/docs/favicon.ico +0 -0
- data/docs/fonts/SecondaRound-Bold.eot +0 -0
- data/docs/fonts/SecondaRound-Bold.svg +2160 -0
- data/docs/fonts/SecondaRound-Bold.ttf +0 -0
- data/docs/fonts/SecondaRound-Bold.woff +0 -0
- data/docs/fonts/SecondaRound-Bold.woff2 +0 -0
- data/docs/fonts/SecondaRound-Regular.eot +0 -0
- data/docs/fonts/SecondaRound-Regular.svg +1873 -0
- data/docs/fonts/SecondaRound-Regular.ttf +0 -0
- data/docs/fonts/SecondaRound-Regular.woff +0 -0
- data/docs/fonts/SecondaRound-Regular.woff2 +0 -0
- data/docs/images/console-01.png +0 -0
- data/docs/images/console-02.png +0 -0
- data/docs/images/console-03.png +0 -0
- data/docs/images/console-04.png +0 -0
- data/docs/images/console-05.png +0 -0
- data/docs/images/console-06.png +0 -0
- data/docs/images/console-07.png +0 -0
- data/docs/images/logo.png +0 -0
- data/docs/images/robot.png +0 -0
- data/docs/images/robots.png +0 -0
- data/docs/index.html +37 -0
- data/docs/playlist_items.html +65 -0
- data/docs/playlists.html +114 -0
- data/docs/urls.html +52 -0
- data/docs/videos.html +104 -0
- data/lib/yt/channel.rb +127 -0
- data/lib/yt/core/version.rb +8 -0
- data/lib/yt/core.rb +18 -0
- data/lib/yt/no_items_error.rb +8 -0
- data/lib/yt/playlist.rb +75 -0
- data/lib/yt/playlist_item.rb +59 -0
- data/lib/yt/relation.rb +100 -0
- data/lib/yt/resource.rb +85 -0
- data/lib/yt/response.rb +91 -0
- data/lib/yt/video.rb +216 -0
- data/yt-core.gemspec +27 -15
- metadata +155 -19
- data/lib/yt-core/version.rb +0 -3
- data/lib/yt-core.rb +0 -5
data/lib/yt/playlist.rb
ADDED
@@ -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
|
data/lib/yt/relation.rb
ADDED
@@ -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
|
data/lib/yt/resource.rb
ADDED
@@ -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
|
data/lib/yt/response.rb
ADDED
@@ -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
|
4
|
+
require 'yt/core/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
8
|
-
spec.version =
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
11
|
-
spec.
|
12
|
-
spec.
|
13
|
-
|
14
|
-
spec.
|
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.
|
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.
|
22
|
-
|
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
|