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.
- checksums.yaml +4 -4
- data/.gitignore +24 -0
- data/.rspec +2 -0
- data/.travis.yml +10 -0
- data/.yardopts +1 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +78 -0
- data/HISTORY.md +37 -0
- data/MIT-LICENSE +20 -0
- data/README.md +325 -0
- data/Rakefile +1 -0
- data/TODO.md +11 -0
- data/bin/yt +31 -0
- data/lib/yt.rb +2 -0
- data/lib/yt/actions/delete.rb +27 -0
- data/lib/yt/actions/delete_all.rb +28 -0
- data/lib/yt/actions/insert.rb +29 -0
- data/lib/yt/actions/list.rb +65 -0
- data/lib/yt/actions/update.rb +25 -0
- data/lib/yt/associations.rb +33 -0
- data/lib/yt/associations/annotations.rb +15 -0
- data/lib/yt/associations/channels.rb +20 -0
- data/lib/yt/associations/details_sets.rb +20 -0
- data/lib/yt/associations/playlist_items.rb +26 -0
- data/lib/yt/associations/playlists.rb +22 -0
- data/lib/yt/associations/ratings.rb +39 -0
- data/lib/yt/associations/snippets.rb +20 -0
- data/lib/yt/associations/statuses.rb +14 -0
- data/lib/yt/associations/subscriptions.rb +38 -0
- data/lib/yt/associations/user_infos.rb +21 -0
- data/lib/yt/associations/videos.rb +14 -0
- data/lib/yt/collections/annotations.rb +43 -0
- data/lib/yt/collections/base.rb +13 -0
- data/lib/yt/collections/channels.rb +32 -0
- data/lib/yt/collections/details_sets.rb +32 -0
- data/lib/yt/collections/playlist_items.rb +50 -0
- data/lib/yt/collections/playlists.rb +56 -0
- data/lib/yt/collections/ratings.rb +32 -0
- data/lib/yt/collections/snippets.rb +38 -0
- data/lib/yt/collections/subscriptions.rb +67 -0
- data/lib/yt/collections/user_infos.rb +41 -0
- data/lib/yt/collections/videos.rb +32 -0
- data/lib/yt/config.rb +55 -0
- data/lib/yt/models/account.rb +68 -0
- data/lib/yt/models/annotation.rb +137 -0
- data/lib/yt/models/base.rb +11 -0
- data/lib/yt/models/channel.rb +17 -0
- data/lib/yt/models/configuration.rb +29 -0
- data/lib/yt/models/description.rb +98 -0
- data/lib/yt/models/details_set.rb +31 -0
- data/lib/yt/models/playlist.rb +65 -0
- data/lib/yt/models/playlist_item.rb +42 -0
- data/lib/yt/models/rating.rb +28 -0
- data/lib/yt/models/snippet.rb +48 -0
- data/lib/yt/models/status.rb +26 -0
- data/lib/yt/models/subscription.rb +35 -0
- data/lib/yt/models/user_info.rb +66 -0
- data/lib/yt/models/video.rb +16 -0
- data/lib/yt/utils/request.rb +85 -0
- data/lib/yt/version.rb +3 -0
- data/spec/associations/device_auth/channels_spec.rb +10 -0
- data/spec/associations/device_auth/details_sets_spec.rb +19 -0
- data/spec/associations/device_auth/playlist_items_spec.rb +42 -0
- data/spec/associations/device_auth/playlists_spec.rb +42 -0
- data/spec/associations/device_auth/ratings_spec.rb +30 -0
- data/spec/associations/device_auth/snippets_spec.rb +30 -0
- data/spec/associations/device_auth/subscriptions_spec.rb +27 -0
- data/spec/associations/device_auth/user_infos_spec.rb +10 -0
- data/spec/associations/device_auth/videos_spec.rb +22 -0
- data/spec/associations/no_auth/annotations_spec.rb +15 -0
- data/spec/associations/server_auth/channels_spec.rb +2 -0
- data/spec/associations/server_auth/details_sets_spec.rb +18 -0
- data/spec/associations/server_auth/playlist_items_spec.rb +17 -0
- data/spec/associations/server_auth/playlists_spec.rb +17 -0
- data/spec/associations/server_auth/ratings_spec.rb +2 -0
- data/spec/associations/server_auth/snippets_spec.rb +28 -0
- data/spec/associations/server_auth/subscriptions_spec.rb +2 -0
- data/spec/associations/server_auth/user_infos_spec.rb +2 -0
- data/spec/associations/server_auth/videos_spec.rb +20 -0
- data/spec/collections/annotations_spec.rb +6 -0
- data/spec/collections/channels_spec.rb +6 -0
- data/spec/collections/details_sets_spec.rb +6 -0
- data/spec/collections/playlist_items_spec.rb +23 -0
- data/spec/collections/playlists_spec.rb +26 -0
- data/spec/collections/ratings_spec.rb +6 -0
- data/spec/collections/snippets_spec.rb +6 -0
- data/spec/collections/subscriptions_spec.rb +30 -0
- data/spec/collections/user_infos_spec.rb +6 -0
- data/spec/collections/videos_spec.rb +6 -0
- data/spec/models/annotation_spec.rb +131 -0
- data/spec/models/channel_spec.rb +13 -0
- data/spec/models/description_spec.rb +94 -0
- data/spec/models/details_set_spec.rb +23 -0
- data/spec/models/playlist_item_spec.rb +32 -0
- data/spec/models/playlist_spec.rb +52 -0
- data/spec/models/rating_spec.rb +13 -0
- data/spec/models/snippet_spec.rb +66 -0
- data/spec/models/status_spec.rb +42 -0
- data/spec/models/subscription_spec.rb +37 -0
- data/spec/models/user_info_spec.rb +69 -0
- data/spec/models/video_spec.rb +13 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/device_app.rb +16 -0
- data/spec/support/server_app.rb +10 -0
- data/yt.gemspec +30 -0
- 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
|
data/lib/yt/config.rb
ADDED
|
@@ -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,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
|