yt 0.5.4 → 0.5.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/HISTORY.md +3 -0
- data/README.md +2 -4
- data/bin/yt +0 -7
- data/lib/yt/actions/delete.rb +3 -3
- data/lib/yt/actions/insert.rb +3 -3
- data/lib/yt/actions/list.rb +4 -6
- data/lib/yt/actions/update.rb +7 -5
- data/lib/yt/associations/authentications.rb +114 -0
- data/lib/yt/associations.rb +1 -0
- data/lib/yt/collections/annotations.rb +2 -2
- data/lib/yt/collections/authentications.rb +42 -0
- data/lib/yt/collections/base.rb +1 -1
- data/lib/yt/collections/playlist_items.rb +1 -1
- data/lib/yt/collections/snippets.rb +1 -2
- data/lib/yt/collections/subscriptions.rb +1 -1
- data/lib/yt/collections/user_infos.rb +2 -2
- data/lib/yt/config.rb +0 -2
- data/lib/yt/errors/missing_auth.rb +50 -0
- data/lib/yt/errors/no_items.rb +5 -9
- data/lib/yt/errors/request_error.rb +52 -0
- data/lib/yt/models/account.rb +17 -44
- data/lib/yt/models/annotation.rb +117 -115
- data/lib/yt/models/authentication.rb +27 -0
- data/lib/yt/models/base.rb +9 -5
- data/lib/yt/models/channel.rb +6 -4
- data/lib/yt/models/configuration.rb +27 -35
- data/lib/yt/models/description.rb +61 -59
- data/lib/yt/models/details_set.rb +26 -24
- data/lib/yt/models/id.rb +3 -1
- data/lib/yt/models/playlist.rb +38 -36
- data/lib/yt/models/playlist_item.rb +29 -27
- data/lib/yt/models/rating.rb +18 -16
- data/lib/yt/models/request.rb +95 -73
- data/lib/yt/models/resource.rb +19 -17
- data/lib/yt/models/snippet.rb +39 -37
- data/lib/yt/models/status.rb +20 -18
- data/lib/yt/models/subscription.rb +24 -22
- data/lib/yt/models/url.rb +68 -68
- data/lib/yt/models/user_info.rb +51 -49
- data/lib/yt/models/video.rb +6 -4
- data/lib/yt/version.rb +1 -1
- data/spec/associations/device_auth/authentications_spec.rb +78 -0
- data/spec/associations/device_auth/channels_spec.rb +2 -4
- data/spec/associations/device_auth/details_sets_spec.rb +4 -5
- data/spec/associations/device_auth/ids_spec.rb +2 -3
- data/spec/associations/device_auth/playlist_items_spec.rb +3 -4
- data/spec/associations/device_auth/playlists_spec.rb +14 -15
- data/spec/associations/device_auth/ratings_spec.rb +2 -4
- data/spec/associations/device_auth/snippets_spec.rb +5 -7
- data/spec/associations/device_auth/subscriptions_spec.rb +2 -4
- data/spec/associations/device_auth/user_infos_spec.rb +2 -5
- data/spec/associations/device_auth/videos_spec.rb +3 -5
- data/spec/associations/server_auth/details_sets_spec.rb +1 -1
- data/spec/associations/server_auth/ids_spec.rb +1 -1
- data/spec/associations/server_auth/playlist_items_spec.rb +1 -1
- data/spec/associations/server_auth/playlists_spec.rb +1 -1
- data/spec/associations/server_auth/snippets_spec.rb +1 -1
- data/spec/associations/server_auth/videos_spec.rb +1 -1
- data/spec/collections/playlist_items_spec.rb +3 -4
- data/spec/collections/subscriptions_spec.rb +2 -3
- data/spec/errors/missing_auth_spec.rb +10 -0
- data/spec/errors/no_items_spec.rb +2 -1
- data/spec/errors/request_error_spec.rb +18 -0
- data/spec/models/configuration_spec.rb +0 -17
- data/spec/models/description_spec.rb +5 -5
- data/spec/models/request_spec.rb +1 -7
- data/spec/models/subscription_spec.rb +2 -3
- data/spec/models/url_spec.rb +6 -6
- data/spec/support/fail_matcher.rb +1 -1
- data/spec/support/global_hooks.rb +33 -0
- metadata +15 -14
- data/lib/yt/errors/base.rb +0 -43
- data/lib/yt/errors/error.rb +0 -8
- data/lib/yt/errors/failed.rb +0 -17
- data/lib/yt/errors/unauthenticated.rb +0 -34
- data/spec/errors/failed_spec.rb +0 -9
- data/spec/errors/unauthenticated_spec.rb +0 -9
- data/spec/support/device_app.rb +0 -15
- data/spec/support/server_app.rb +0 -10
data/lib/yt/models/annotation.rb
CHANGED
@@ -1,142 +1,144 @@
|
|
1
1
|
module Yt
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
2
|
+
module Models
|
3
|
+
# Provides methods to access and analyze a single YouTube annotation.
|
4
|
+
class Annotation
|
5
|
+
# Instantiate an Annotation object from its YouTube XML representation.
|
6
|
+
#
|
7
|
+
# @note There is no documented way to access annotations through API.
|
8
|
+
# There is an endpoint that returns an XML in an undocumented format,
|
9
|
+
# which is here parsed into a comprehensible set of attributes.
|
10
|
+
#
|
11
|
+
# @param [String] xml_data The YouTube XML representation of an annotation
|
12
|
+
def initialize(options = {})
|
13
|
+
@data = options[:data]
|
14
|
+
end
|
14
15
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
16
|
+
# Checks whether the entire annotation box remains above y
|
17
|
+
#
|
18
|
+
# @param [Integer] y Vertical position in the Youtube video (0 to 100)
|
19
|
+
#
|
20
|
+
# @return [Boolean] Whether the box remains above y
|
21
|
+
def above?(y)
|
22
|
+
top && top < y
|
23
|
+
end
|
23
24
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
25
|
+
# Checks whether the entire annotation box remains below y
|
26
|
+
#
|
27
|
+
# @param [Integer] y Vertical position in the Youtube video (0 to 100)
|
28
|
+
#
|
29
|
+
# @return [Boolean] Whether the box remains below y
|
30
|
+
def below?(y)
|
31
|
+
bottom && bottom > y
|
32
|
+
end
|
32
33
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
34
|
+
# Checks whether there is a link to subscribe.
|
35
|
+
# Should a branding watermark also counts, because it links to the channel?
|
36
|
+
#
|
37
|
+
# @return [Boolean] Whether there is a link to subscribe in the annotation
|
38
|
+
def has_link_to_subscribe?(options = {}) # TODO: options for which videos
|
39
|
+
link_class == '5'
|
40
|
+
end
|
40
41
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
42
|
+
# Checks whether there is a link to a video.
|
43
|
+
# An Invideo featured video also counts
|
44
|
+
#
|
45
|
+
# @return [Boolean] Whether there is a link to a video in the annotation
|
46
|
+
def has_link_to_video?(options = {}) # TODO: options for which videos
|
47
|
+
link_class == '1' || type == 'promotion'
|
48
|
+
end
|
48
49
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
50
|
+
# Checks whether there is a link to a playlist.
|
51
|
+
# A link to a video with the playlist in the URL also counts
|
52
|
+
#
|
53
|
+
# @return [Boolean] Whether there is a link to a playlist in the annotation
|
54
|
+
def has_link_to_playlist?
|
55
|
+
link_class == '2' || text.include?('&list=')
|
56
|
+
end
|
56
57
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
58
|
+
# Checks whether the link opens in the same window.
|
59
|
+
#
|
60
|
+
# @return [Boolean] Whether the link opens in the same window
|
61
|
+
def has_link_to_same_window?
|
62
|
+
link_target == 'current'
|
63
|
+
end
|
63
64
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
65
|
+
# Checks whether the annotation comes from InVideo Programming
|
66
|
+
#
|
67
|
+
# @return [Boolean] Whether the annotation comes from InVideo Programming
|
68
|
+
def has_invideo_programming?
|
69
|
+
type == 'promotion' || type == 'branding'
|
70
|
+
end
|
70
71
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
72
|
+
# @return [Boolean] Whether the annotation starts after the number of seconds
|
73
|
+
# @note This is broken for invideo programming, because they do not
|
74
|
+
# have the timestamp in the region, but in the "data" field
|
75
|
+
def starts_after?(seconds)
|
76
|
+
timestamps.first > seconds if timestamps.any?
|
77
|
+
end
|
77
78
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
79
|
+
# @return [Boolean] Whether the annotation starts before the number of seconds
|
80
|
+
# @note This is broken for invideo programming, because they do not
|
81
|
+
# have the timestamp in the region, but in the "data" field
|
82
|
+
def starts_before?(seconds)
|
83
|
+
timestamps.first < seconds if timestamps.any?
|
84
|
+
end
|
84
85
|
|
85
|
-
|
86
|
+
private
|
86
87
|
|
87
|
-
|
88
|
-
|
89
|
-
|
88
|
+
def text
|
89
|
+
@text ||= @data.fetch 'TEXT', ''
|
90
|
+
end
|
90
91
|
|
91
|
-
|
92
|
-
|
93
|
-
|
92
|
+
def type
|
93
|
+
@type ||= @data.fetch 'type', ''
|
94
|
+
end
|
94
95
|
|
95
|
-
|
96
|
-
|
97
|
-
|
96
|
+
def link_class
|
97
|
+
@link_class ||= url['link_class']
|
98
|
+
end
|
98
99
|
|
99
|
-
|
100
|
-
|
101
|
-
|
100
|
+
def link_target
|
101
|
+
@link_target ||= url['target']
|
102
|
+
end
|
102
103
|
|
103
|
-
|
104
|
-
|
105
|
-
|
104
|
+
def url
|
105
|
+
@url ||= action.fetch 'url', {}
|
106
|
+
end
|
106
107
|
|
107
|
-
|
108
|
-
|
109
|
-
|
108
|
+
def action
|
109
|
+
@action ||= @data.fetch 'action', {}
|
110
|
+
end
|
110
111
|
|
111
|
-
|
112
|
-
|
113
|
-
|
112
|
+
def top
|
113
|
+
@top ||= positions.map{|pos| pos['y'].to_f}.max
|
114
|
+
end
|
114
115
|
|
115
|
-
|
116
|
-
|
117
|
-
|
116
|
+
def bottom
|
117
|
+
@bottom ||= positions.map{|pos| pos['y'].to_f + pos['h'].to_f}.max
|
118
|
+
end
|
118
119
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
120
|
+
def timestamps
|
121
|
+
@timestamps ||= positions.map do |pos|
|
122
|
+
regex = %r{(?:|(?<hours>\d*):)(?:|(?<min>\d*):)(?<sec>\d*)\.(?<ms>\d*)}
|
123
|
+
match = pos['t'].match regex
|
124
|
+
hours = (match[:hours] || '0').to_i
|
125
|
+
minutes = (match[:min] || '0').to_i
|
126
|
+
seconds = (match[:sec]).to_i
|
127
|
+
(hours * 60 + minutes) * 60 + seconds
|
128
|
+
end
|
127
129
|
end
|
128
|
-
end
|
129
130
|
|
130
|
-
|
131
|
-
|
132
|
-
|
131
|
+
def positions
|
132
|
+
@positions ||= region['rectRegion'] || region['anchoredRegion'] || []
|
133
|
+
end
|
133
134
|
|
134
|
-
|
135
|
-
|
136
|
-
|
135
|
+
def region
|
136
|
+
@region ||= segment.fetch 'movingRegion', {}
|
137
|
+
end
|
137
138
|
|
138
|
-
|
139
|
-
|
139
|
+
def segment
|
140
|
+
@segment ||= (@data['segment'] || {})
|
141
|
+
end
|
140
142
|
end
|
141
143
|
end
|
142
144
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Yt
|
2
|
+
module Models
|
3
|
+
class Authentication
|
4
|
+
attr_reader :access_token, :refresh_token, :expires_at
|
5
|
+
|
6
|
+
def initialize(data = {})
|
7
|
+
@access_token = data['access_token']
|
8
|
+
@refresh_token = data['refresh_token']
|
9
|
+
@expires_at = expiration_date data.slice('expires_at', 'expires_in')
|
10
|
+
end
|
11
|
+
|
12
|
+
def expired?
|
13
|
+
@expires_at && @expires_at.past?
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def expiration_date(options = {})
|
19
|
+
if options['expires_in']
|
20
|
+
Time.now + options['expires_in'].seconds
|
21
|
+
else
|
22
|
+
Time.parse options['expires_at'] rescue nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/yt/models/base.rb
CHANGED
@@ -1,12 +1,16 @@
|
|
1
1
|
require 'yt/associations'
|
2
2
|
require 'yt/actions/delete'
|
3
3
|
require 'yt/actions/update'
|
4
|
-
require 'yt/errors/
|
4
|
+
require 'yt/errors/request_error'
|
5
5
|
|
6
6
|
module Yt
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
module Models
|
8
|
+
class Base
|
9
|
+
extend Associations
|
10
|
+
include Actions::Delete
|
11
|
+
include Actions::Update
|
12
|
+
end
|
11
13
|
end
|
14
|
+
|
15
|
+
include Models
|
12
16
|
end
|
data/lib/yt/models/channel.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
require 'yt/models/resource'
|
2
2
|
|
3
3
|
module Yt
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
module Models
|
5
|
+
class Channel < Resource
|
6
|
+
has_many :subscriptions
|
7
|
+
has_many :videos
|
8
|
+
has_many :playlists
|
9
|
+
end
|
8
10
|
end
|
9
11
|
end
|
@@ -1,40 +1,32 @@
|
|
1
1
|
module Yt
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
2
|
+
module Models
|
3
|
+
# Stores runtime configuration information.
|
4
|
+
#
|
5
|
+
# Configuration options are loaded from `~/.yt`, `.yt`, command line
|
6
|
+
# switches, and the `YT_OPTS` environment variable (listed in lowest to
|
7
|
+
# highest precedence).
|
8
|
+
#
|
9
|
+
# @example A server-to-server YouTube client app
|
10
|
+
#
|
11
|
+
# Yt.configure do |config|
|
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 :api_key, :client_id, :client_secret
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
end
|
31
|
-
|
32
|
-
private
|
33
|
-
|
34
|
-
def set_scenario(value)
|
35
|
-
valid_scenarios = [:web_app, :device_app, :server_app]
|
36
|
-
scenario = valid_scenarios.find{|scenario| scenario.to_s == value}
|
37
|
-
scenario || :web_app
|
25
|
+
def initialize
|
26
|
+
@client_id = ENV['YT_CLIENT_ID']
|
27
|
+
@client_secret = ENV['YT_CLIENT_SECRET']
|
28
|
+
@api_key = ENV['YT_API_KEY']
|
29
|
+
end
|
38
30
|
end
|
39
31
|
end
|
40
32
|
end
|
@@ -1,74 +1,76 @@
|
|
1
1
|
require 'yt/models/url'
|
2
2
|
|
3
3
|
module Yt
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
# @example
|
8
|
-
#
|
9
|
-
# 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.'
|
10
|
-
# description.to_s.slice(0,19) # => 'Fullscreen provides'
|
11
|
-
# description.length # => 127
|
12
|
-
#
|
13
|
-
class Description < String
|
14
|
-
# Returns whether the description includes a YouTube video URL
|
4
|
+
module Models
|
5
|
+
# Provides read-only access to the description of a YouTube resource.
|
6
|
+
# Resources with descriptions are: videos and channels.
|
15
7
|
#
|
16
8
|
# @example
|
17
9
|
#
|
18
|
-
# description = Yt::Description.new '
|
19
|
-
# description.
|
10
|
+
# 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.'
|
11
|
+
# description.to_s.slice(0,19) # => 'Fullscreen provides'
|
12
|
+
# description.length # => 127
|
20
13
|
#
|
21
|
-
|
22
|
-
|
23
|
-
#
|
24
|
-
#
|
25
|
-
|
26
|
-
|
14
|
+
class Description < String
|
15
|
+
# Returns whether the description includes a YouTube video URL
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
#
|
19
|
+
# description = Yt::Description.new 'Link to video: youtube.com/watch?v=MESycYJytkU'
|
20
|
+
# description.has_link_to_video? #=> true
|
21
|
+
#
|
22
|
+
# @return [Boolean] Whether the description includes a link to a video
|
23
|
+
def has_link_to_video?
|
24
|
+
# TODO: might take as an option WHICH video to link to
|
25
|
+
# in order to check if it's my own video
|
26
|
+
links.any?{|link| link.kind == :video}
|
27
|
+
end
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
29
|
+
# Returns whether the description includes a YouTube channel URL
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
#
|
33
|
+
# description = Yt::Description.new 'Link to channel: youtube.com/fullscreen'
|
34
|
+
# description.has_link_to_channel? #=> true
|
35
|
+
#
|
36
|
+
# @return [Boolean] Whether the description includes a link to a channel
|
37
|
+
def has_link_to_channel?(options = {}) # TODO: which channel
|
38
|
+
# TODO: might take as an option WHICH channel to link to
|
39
|
+
# in order to check if it's my own channel
|
40
|
+
links.any?{|link| link.kind == :channel}
|
41
|
+
end
|
41
42
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
43
|
+
# Returns whether the description includes a YouTube subscription URL
|
44
|
+
#
|
45
|
+
# @example
|
46
|
+
#
|
47
|
+
# description = Yt::Description.new 'Link to subscribe: youtube.com/subscription_center?add_user=fullscreen'
|
48
|
+
# description.has_link_to_subscribe? #=> true
|
49
|
+
#
|
50
|
+
# @return [Boolean] Whether the description includes a link to subscribe
|
51
|
+
def has_link_to_subscribe?(options = {}) # TODO: which channel
|
52
|
+
# TODO: might take as an option WHICH channel to subscribe to
|
53
|
+
# in order to check if it's my own channel
|
54
|
+
links.any?{|link| link.kind == :subscription}
|
55
|
+
end
|
55
56
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
57
|
+
# Returns whether the description includes a YouTube playlist URL
|
58
|
+
#
|
59
|
+
# @example
|
60
|
+
#
|
61
|
+
# description = Yt::Description.new 'Link to playlist: youtube.com/playlist?list=LLxO1tY8h1AhOz0T4ENwmpow'
|
62
|
+
# description.has_link_to_playlist? #=> true
|
63
|
+
#
|
64
|
+
# @return [Boolean] Whether the description includes a link to a playlist
|
65
|
+
def has_link_to_playlist?
|
66
|
+
links.any?{|link| link.kind == :playlist}
|
67
|
+
end
|
67
68
|
|
68
|
-
|
69
|
+
private
|
69
70
|
|
70
|
-
|
71
|
-
|
71
|
+
def links
|
72
|
+
@links ||= self.split(' ').map{|word| URL.new word}
|
73
|
+
end
|
72
74
|
end
|
73
75
|
end
|
74
76
|
end
|
@@ -1,34 +1,36 @@
|
|
1
1
|
require 'yt/models/base'
|
2
2
|
|
3
3
|
module Yt
|
4
|
-
|
4
|
+
module Models
|
5
|
+
class DetailsSet < Base
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
7
|
+
def initialize(options = {})
|
8
|
+
@data = options[:data]
|
9
|
+
end
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
# Return the duration of the YouTube video.
|
12
|
+
#
|
13
|
+
# @return [Integer] Duration in seconds of the YouTube video
|
14
|
+
def duration
|
15
|
+
@duration = to_seconds @data.fetch('duration', 0)
|
16
|
+
end
|
17
|
+
# also available: dimension, definition, caption, licensed_content?
|
17
18
|
|
18
|
-
|
19
|
+
private
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
21
|
+
# The length of the video. The tag value is an ISO 8601 duration in the format PT#M#S,
|
22
|
+
# in which the letters PT indicate that the value specifies a period of time, and
|
23
|
+
# the letters M and S refer to length in minutes and seconds, respectively. The #
|
24
|
+
# characters preceding the M and S letters are both integers that specify the number
|
25
|
+
# of minutes (or seconds) of the video. For example, a value of PT15M51S indicates
|
26
|
+
# that the video is 15 minutes and 51 seconds long.
|
27
|
+
def to_seconds(iso8601_duration)
|
28
|
+
match = iso8601_duration.match %r{^PT(?:|(?<hours>\d*?)H)(?:|(?<min>\d*?)M)(?:|(?<sec>\d*?)S)$}
|
29
|
+
hours = (match[:hours] || '0').to_i
|
30
|
+
minutes = (match[:min] || '0').to_i
|
31
|
+
seconds = (match[:sec]).to_i
|
32
|
+
(hours * 60 + minutes) * 60 + seconds
|
33
|
+
end
|
32
34
|
end
|
33
35
|
end
|
34
36
|
end
|