video_info 3.0.2 → 4.0.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/.github/workflows/ci.yml +44 -0
- data/.gitignore +1 -1
- data/.rspec +1 -0
- data/CONTRIBUTING.md +6 -6
- data/Gemfile +1 -9
- data/README.md +49 -32
- data/Rakefile +3 -2
- data/lib/video_info/provider.rb +28 -22
- data/lib/video_info/providers/dailymotion.rb +25 -18
- data/lib/video_info/providers/vimeo.rb +12 -12
- data/lib/video_info/providers/vimeo_api.rb +19 -15
- data/lib/video_info/providers/vimeo_scraper.rb +41 -40
- data/lib/video_info/providers/wistia.rb +7 -7
- data/lib/video_info/providers/youtube.rb +8 -9
- data/lib/video_info/providers/youtube_api.rb +26 -26
- data/lib/video_info/providers/youtube_scraper.rb +34 -29
- data/lib/video_info/providers/youtubeplaylist.rb +6 -7
- data/lib/video_info/providers/youtubeplaylist_api.rb +12 -12
- data/lib/video_info/providers/youtubeplaylist_scraper.rb +10 -12
- data/lib/video_info/version.rb +1 -1
- data/lib/video_info.rb +27 -23
- data/video_info.gemspec +23 -25
- metadata +31 -20
- data/.github/workflows/unit_test.yml +0 -42
- data/.hound.yml +0 -3
- data/.rubocop.yml +0 -243
- data/Guardfile +0 -8
- data/tmp/rspec_guard_result +0 -25
@@ -1,7 +1,7 @@
|
|
1
1
|
class VideoInfo
|
2
2
|
module Providers
|
3
3
|
module VimeoAPI
|
4
|
-
THUMBNAIL_LINK_REGEX = /.*\/(\d
|
4
|
+
THUMBNAIL_LINK_REGEX = /.*\/(\d+-[^_]+)/
|
5
5
|
|
6
6
|
def api_key
|
7
7
|
VideoInfo.provider_api_keys[:vimeo]
|
@@ -16,24 +16,24 @@ class VideoInfo
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def title
|
19
|
-
_video[
|
19
|
+
_video["name"]
|
20
20
|
end
|
21
21
|
|
22
22
|
def author
|
23
|
-
_video[
|
23
|
+
_video["user"]["name"]
|
24
24
|
end
|
25
25
|
|
26
26
|
def author_thumbnail_id
|
27
|
-
author_uri = _video[
|
27
|
+
author_uri = _video["user"]["pictures"]["uri"]
|
28
28
|
@author_thumbnail_id ||= parse_picture_id_from_user(author_uri)
|
29
29
|
end
|
30
30
|
|
31
31
|
def author_url
|
32
|
-
_video[
|
32
|
+
_video["user"]["link"]
|
33
33
|
end
|
34
34
|
|
35
35
|
def author_thumbnail(width = 75)
|
36
|
-
|
36
|
+
"https://i.vimeocdn.com/portrait/" \
|
37
37
|
"#{author_thumbnail_id}_#{width}x#{width}.jpg"
|
38
38
|
end
|
39
39
|
|
@@ -58,15 +58,19 @@ class VideoInfo
|
|
58
58
|
end
|
59
59
|
|
60
60
|
def keywords_array
|
61
|
-
_video[
|
61
|
+
_video["tags"].map { |t| t["tag"] }
|
62
62
|
end
|
63
63
|
|
64
64
|
def date
|
65
|
-
Time.parse(_video[
|
65
|
+
Time.parse(_video["created_time"], Time.now.utc).utc
|
66
66
|
end
|
67
67
|
|
68
68
|
def view_count
|
69
|
-
|
69
|
+
stats["plays"].to_i
|
70
|
+
end
|
71
|
+
|
72
|
+
def stats
|
73
|
+
_video["stats"]
|
70
74
|
end
|
71
75
|
|
72
76
|
private
|
@@ -86,15 +90,15 @@ class VideoInfo
|
|
86
90
|
end
|
87
91
|
|
88
92
|
def _api_version
|
89
|
-
|
93
|
+
"3.2"
|
90
94
|
end
|
91
95
|
|
92
96
|
def _authorization_headers
|
93
|
-
{
|
97
|
+
{"Authorization" => "bearer #{api_key}"}
|
94
98
|
end
|
95
99
|
|
96
100
|
def _api_version_headers
|
97
|
-
{
|
101
|
+
{"Accept" => "application/vnd.vimeo.*+json;version=#{_api_version}"}
|
98
102
|
end
|
99
103
|
|
100
104
|
def _video
|
@@ -102,7 +106,7 @@ class VideoInfo
|
|
102
106
|
end
|
103
107
|
|
104
108
|
def _api_base
|
105
|
-
|
109
|
+
"api.vimeo.com"
|
106
110
|
end
|
107
111
|
|
108
112
|
def _api_path
|
@@ -113,8 +117,8 @@ class VideoInfo
|
|
113
117
|
"https://#{_api_base}#{_api_path}"
|
114
118
|
end
|
115
119
|
|
116
|
-
def parse_picture_id_from_user
|
117
|
-
%r{
|
120
|
+
def parse_picture_id_from_user(uri)
|
121
|
+
%r{/pictures/(\d+)}.match(uri)[1]
|
118
122
|
end
|
119
123
|
|
120
124
|
def parse_picture_id(uri)
|
@@ -1,15 +1,12 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require 'json'
|
4
|
-
require 'openssl'
|
5
|
-
require 'cgi'
|
1
|
+
require "oga"
|
2
|
+
require "cgi"
|
6
3
|
|
7
4
|
class VideoInfo
|
8
5
|
module Providers
|
9
6
|
module VimeoScraper
|
10
7
|
def author
|
11
8
|
if available?
|
12
|
-
json_info[
|
9
|
+
json_info["author"]["name"]
|
13
10
|
end
|
14
11
|
end
|
15
12
|
|
@@ -18,8 +15,8 @@ class VideoInfo
|
|
18
15
|
return nil
|
19
16
|
end
|
20
17
|
|
21
|
-
split_point =
|
22
|
-
script_tags = data.css(
|
18
|
+
split_point = "window.vimeo.clip_page_config ="
|
19
|
+
script_tags = data.css("script")
|
23
20
|
|
24
21
|
script_index = script_tags.find_index do |x|
|
25
22
|
x.text.include?(split_point)
|
@@ -31,12 +28,12 @@ class VideoInfo
|
|
31
28
|
|
32
29
|
parsed_data = JSON.parse(split_script_text.split(";\n")[0])
|
33
30
|
|
34
|
-
parsed_data[
|
31
|
+
parsed_data["owner"]["portrait"]["src"]
|
35
32
|
end
|
36
33
|
|
37
34
|
def author_url
|
38
35
|
if available?
|
39
|
-
json_info[
|
36
|
+
json_info["author"]["url"]
|
40
37
|
end
|
41
38
|
end
|
42
39
|
|
@@ -46,7 +43,7 @@ class VideoInfo
|
|
46
43
|
if data.nil?
|
47
44
|
is_available = false
|
48
45
|
elsif is_available
|
49
|
-
password_elements = data.css(
|
46
|
+
password_elements = data.css(".exception_title--password")
|
50
47
|
|
51
48
|
unless password_elements.empty?
|
52
49
|
is_available = false
|
@@ -57,23 +54,23 @@ class VideoInfo
|
|
57
54
|
end
|
58
55
|
|
59
56
|
def title
|
60
|
-
meta_node_value(
|
57
|
+
meta_node_value("og:title")
|
61
58
|
end
|
62
59
|
|
63
60
|
def description
|
64
|
-
meta_node_value(
|
61
|
+
meta_node_value("og:description")
|
65
62
|
end
|
66
63
|
|
67
64
|
def date
|
68
65
|
if available?
|
69
|
-
upload_date = json_info[
|
66
|
+
upload_date = json_info["uploadDate"]
|
70
67
|
ISO8601::DateTime.new(upload_date).to_time
|
71
68
|
end
|
72
69
|
end
|
73
70
|
|
74
71
|
def duration
|
75
72
|
if available?
|
76
|
-
duration = json_info[
|
73
|
+
duration = json_info["duration"]
|
77
74
|
ISO8601::Duration.new(duration).to_seconds.to_i
|
78
75
|
end
|
79
76
|
end
|
@@ -83,54 +80,59 @@ class VideoInfo
|
|
83
80
|
return nil
|
84
81
|
end
|
85
82
|
|
86
|
-
|
87
|
-
json_info['keywords']
|
88
|
-
else
|
89
|
-
[]
|
90
|
-
end
|
83
|
+
json_info["keywords"] || []
|
91
84
|
end
|
92
85
|
|
93
86
|
def height
|
94
87
|
if available?
|
95
|
-
json_info[
|
88
|
+
json_info["height"]
|
96
89
|
end
|
97
90
|
end
|
98
91
|
|
99
92
|
def width
|
100
93
|
if available?
|
101
|
-
json_info[
|
94
|
+
json_info["width"]
|
102
95
|
end
|
103
96
|
end
|
104
97
|
|
105
98
|
def thumbnail_small
|
106
99
|
if available?
|
107
|
-
thumbnail_url.split(
|
100
|
+
thumbnail_url.split("_")[0] + "_100x75.jpg"
|
108
101
|
end
|
109
102
|
end
|
110
103
|
|
111
104
|
def thumbnail_medium
|
112
105
|
if available?
|
113
|
-
thumbnail_url.split(
|
106
|
+
thumbnail_url.split("_")[0] + "_200x150.jpg"
|
114
107
|
end
|
115
108
|
end
|
116
109
|
|
117
110
|
def thumbnail_large
|
118
111
|
if available?
|
119
|
-
thumbnail_url.split(
|
112
|
+
thumbnail_url.split("_")[0] + "_640.jpg"
|
120
113
|
end
|
121
114
|
end
|
122
115
|
|
123
116
|
def view_count
|
124
117
|
if available?
|
125
|
-
user_interaction_count(interaction_type: "
|
118
|
+
user_interaction_count(interaction_type: "WatchAction")
|
126
119
|
end
|
127
120
|
end
|
128
121
|
|
122
|
+
def stats
|
123
|
+
return {} unless available?
|
124
|
+
{
|
125
|
+
"plays" => view_count,
|
126
|
+
"likes" => user_interaction_count(interaction_type: "LikeAction"),
|
127
|
+
"comments" => user_interaction_count(interaction_type: "CommentAction")
|
128
|
+
}
|
129
|
+
end
|
130
|
+
|
129
131
|
private
|
130
132
|
|
131
133
|
def user_interaction_count(interaction_type:)
|
132
134
|
interaction_statistic.find do |stat|
|
133
|
-
stat["interactionType"] == interaction_type
|
135
|
+
stat["interactionType"] == "http://schema.org/#{interaction_type}"
|
134
136
|
end["userInteractionCount"]
|
135
137
|
end
|
136
138
|
|
@@ -139,39 +141,39 @@ class VideoInfo
|
|
139
141
|
end
|
140
142
|
|
141
143
|
def json_info
|
142
|
-
@json_info ||= JSON.parse(data.css(
|
143
|
-
type = n.attr(
|
144
|
+
@json_info ||= JSON.parse(data.css("script").detect do |n|
|
145
|
+
type = n.attr("type")
|
144
146
|
|
145
147
|
if type.nil?
|
146
148
|
false
|
147
149
|
else
|
148
|
-
type.value ==
|
150
|
+
type.value == "application/ld+json"
|
149
151
|
end
|
150
152
|
end.text)[0]
|
151
153
|
end
|
152
154
|
|
153
155
|
def thumbnail_url
|
154
|
-
@thumbnail_url ||= remove_overlay(meta_node_value(
|
156
|
+
@thumbnail_url ||= remove_overlay(meta_node_value("og:image"))
|
155
157
|
end
|
156
158
|
|
157
159
|
def remove_overlay(url)
|
158
160
|
uri = URI.parse(url)
|
159
|
-
|
160
|
-
if uri.path ==
|
161
|
-
CGI
|
161
|
+
|
162
|
+
if uri.path == "/filter/overlay"
|
163
|
+
CGI.parse(uri.query)["src0"][0]
|
162
164
|
else
|
163
165
|
url
|
164
166
|
end
|
165
167
|
end
|
166
168
|
|
167
169
|
def meta_nodes
|
168
|
-
@meta_nodes ||= data.css(
|
170
|
+
@meta_nodes ||= data.css("meta")
|
169
171
|
end
|
170
172
|
|
171
173
|
def meta_node_value(name)
|
172
174
|
if available?
|
173
175
|
node = meta_nodes.detect do |n|
|
174
|
-
property = n.attr(
|
176
|
+
property = n.attr("property")
|
175
177
|
|
176
178
|
if property.nil?
|
177
179
|
false
|
@@ -180,20 +182,19 @@ class VideoInfo
|
|
180
182
|
end
|
181
183
|
end
|
182
184
|
|
183
|
-
node.attr(
|
185
|
+
node.attr("content").value
|
184
186
|
end
|
185
187
|
end
|
186
188
|
|
187
189
|
def _set_data_from_api_impl(api_url)
|
188
|
-
Oga.parse_html(URI.
|
189
|
-
|
190
|
+
Oga.parse_html(URI.parse(api_url.to_s).read)
|
190
191
|
rescue OpenURI::HTTPError
|
191
192
|
nil
|
192
193
|
end
|
193
194
|
|
194
195
|
def _api_url
|
195
196
|
uri = URI.parse(@url)
|
196
|
-
uri.scheme =
|
197
|
+
uri.scheme = "https"
|
197
198
|
uri.to_s
|
198
199
|
end
|
199
200
|
|
@@ -2,11 +2,11 @@ class VideoInfo
|
|
2
2
|
module Providers
|
3
3
|
class Wistia < Provider
|
4
4
|
def self.usable?(url)
|
5
|
-
url
|
5
|
+
url.match?(/(.*)(wistia.com|wistia.net|wi.st)/)
|
6
6
|
end
|
7
7
|
|
8
8
|
def provider
|
9
|
-
|
9
|
+
"Wistia"
|
10
10
|
end
|
11
11
|
|
12
12
|
%w[title duration width height].each do |method|
|
@@ -22,26 +22,26 @@ class VideoInfo
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def thumbnail_small
|
25
|
-
data[
|
25
|
+
data["thumbnail_url"]
|
26
26
|
end
|
27
27
|
|
28
28
|
def thumbnail_medium
|
29
|
-
data[
|
29
|
+
data["thumbnail_url"]
|
30
30
|
end
|
31
31
|
|
32
32
|
def thumbnail_large
|
33
|
-
data[
|
33
|
+
data["thumbnail_url"]
|
34
34
|
end
|
35
35
|
|
36
36
|
private
|
37
37
|
|
38
38
|
def _url_regex
|
39
39
|
%r{(?:.*)(?:wistia.com|wi.st|wistia.net)
|
40
|
-
|
40
|
+
/(?:embed/)?(?:medias/)?(?:iframe/)?(.+)}x
|
41
41
|
end
|
42
42
|
|
43
43
|
def _api_base
|
44
|
-
|
44
|
+
"fast.wistia.com"
|
45
45
|
end
|
46
46
|
|
47
47
|
def _api_path
|
@@ -1,6 +1,6 @@
|
|
1
|
-
require
|
2
|
-
require_relative
|
3
|
-
require_relative
|
1
|
+
require "iso8601"
|
2
|
+
require_relative "youtube_api"
|
3
|
+
require_relative "youtube_scraper"
|
4
4
|
|
5
5
|
class VideoInfo
|
6
6
|
module Providers
|
@@ -16,12 +16,11 @@ class VideoInfo
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def self.usable?(url)
|
19
|
-
url
|
20
|
-
(youtu\.be)}x
|
19
|
+
url.match?(/(youtube\.com\/(?!playlist|embed\/videoseries).*)|(youtu\.be)/)
|
21
20
|
end
|
22
21
|
|
23
22
|
def provider
|
24
|
-
|
23
|
+
"YouTube"
|
25
24
|
end
|
26
25
|
|
27
26
|
%w[width height].each do |method|
|
@@ -55,12 +54,12 @@ class VideoInfo
|
|
55
54
|
private
|
56
55
|
|
57
56
|
def _url_regex
|
58
|
-
%r{(?:youtube(?:-nocookie)?\.com
|
59
|
-
.*[?&]v=)|youtu\.be
|
57
|
+
%r{(?:youtube(?:-nocookie)?\.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|
|
58
|
+
.*[?&]v=)|youtu\.be/)([^"&?/ ]{11})}x
|
60
59
|
end
|
61
60
|
|
62
61
|
def _default_iframe_attributes
|
63
|
-
{
|
62
|
+
{allowfullscreen: "allowfullscreen"}
|
64
63
|
end
|
65
64
|
|
66
65
|
def _default_url_attributes
|
@@ -2,9 +2,9 @@ class VideoInfo
|
|
2
2
|
module Providers
|
3
3
|
module YoutubeAPI
|
4
4
|
def available?
|
5
|
-
if !data[
|
6
|
-
upload_status = data[
|
7
|
-
|
5
|
+
if !data["items"].empty?
|
6
|
+
upload_status = data["items"][0]["status"]["uploadStatus"]
|
7
|
+
upload_status != "rejected"
|
8
8
|
else
|
9
9
|
false
|
10
10
|
end
|
@@ -17,53 +17,58 @@ class VideoInfo
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def author
|
20
|
-
_video_snippet[
|
20
|
+
_video_snippet["channelTitle"]
|
21
21
|
end
|
22
22
|
|
23
23
|
def author_thumbnail
|
24
|
-
_channel_snippet[
|
24
|
+
_channel_snippet["thumbnails"]["default"]["url"]
|
25
25
|
end
|
26
26
|
|
27
27
|
def author_url
|
28
|
-
channel_id = _channel_info[
|
29
|
-
|
28
|
+
channel_id = _channel_info["items"][0]["id"]
|
29
|
+
"https://www.youtube.com/channel/" + channel_id
|
30
30
|
end
|
31
31
|
|
32
32
|
def title
|
33
|
-
_video_snippet[
|
33
|
+
_video_snippet["title"]
|
34
34
|
end
|
35
35
|
|
36
36
|
def description
|
37
|
-
_video_snippet[
|
37
|
+
_video_snippet["description"]
|
38
38
|
end
|
39
39
|
|
40
40
|
def keywords
|
41
|
-
_video_snippet[
|
41
|
+
_video_snippet["tags"]
|
42
42
|
end
|
43
43
|
|
44
44
|
def duration
|
45
|
-
video_duration = _video_content_details[
|
45
|
+
video_duration = _video_content_details["duration"] || 0
|
46
46
|
ISO8601::Duration.new(video_duration).to_seconds.to_i
|
47
47
|
end
|
48
48
|
|
49
49
|
def date
|
50
|
-
return unless published_at = _video_snippet[
|
50
|
+
return unless (published_at = _video_snippet["publishedAt"])
|
51
51
|
Time.parse(published_at, Time.now.utc)
|
52
52
|
end
|
53
53
|
|
54
54
|
def view_count
|
55
|
-
|
55
|
+
stats["viewCount"].to_i
|
56
|
+
end
|
57
|
+
|
58
|
+
def stats
|
59
|
+
return {} unless available?
|
60
|
+
data["items"][0]["statistics"]
|
56
61
|
end
|
57
62
|
|
58
63
|
private
|
59
64
|
|
60
65
|
def _api_base
|
61
|
-
|
66
|
+
"www.googleapis.com"
|
62
67
|
end
|
63
68
|
|
64
69
|
def _api_path
|
65
70
|
"/youtube/v3/videos?id=#{video_id}" \
|
66
|
-
|
71
|
+
"&part=snippet,statistics,status,contentDetails&fields=" \
|
67
72
|
"items(id,snippet,statistics,status,contentDetails)&key=#{api_key}"
|
68
73
|
end
|
69
74
|
|
@@ -73,7 +78,7 @@ class VideoInfo
|
|
73
78
|
|
74
79
|
def _video_snippet
|
75
80
|
return {} unless available?
|
76
|
-
data[
|
81
|
+
data["items"][0]["snippet"]
|
77
82
|
end
|
78
83
|
|
79
84
|
def _channel_api_url(channel_id)
|
@@ -82,26 +87,21 @@ class VideoInfo
|
|
82
87
|
end
|
83
88
|
|
84
89
|
def _channel_info
|
85
|
-
channel_url = _channel_api_url(_video_snippet[
|
86
|
-
@_channel_info ||=
|
90
|
+
channel_url = _channel_api_url(_video_snippet["channelId"])
|
91
|
+
@_channel_info ||= JSON.parse(URI.parse(channel_url).read)
|
87
92
|
end
|
88
93
|
|
89
94
|
def _channel_snippet
|
90
|
-
_channel_info[
|
95
|
+
_channel_info["items"][0]["snippet"]
|
91
96
|
end
|
92
97
|
|
93
98
|
def _video_content_details
|
94
99
|
return {} unless available?
|
95
|
-
data[
|
96
|
-
end
|
97
|
-
|
98
|
-
def _video_statistics
|
99
|
-
return {} unless available?
|
100
|
-
data['items'][0]['statistics']
|
100
|
+
data["items"][0]["contentDetails"]
|
101
101
|
end
|
102
102
|
|
103
103
|
def _video_thumbnail(id)
|
104
|
-
_video_entry[
|
104
|
+
_video_entry["media$group"]["media$thumbnail"][id]["url"]
|
105
105
|
end
|
106
106
|
end
|
107
107
|
end
|