YPBT 0.1.5 → 0.2.2
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/.travis.yml +0 -2
- data/Rakefile +3 -2
- data/YPBT.gemspec +1 -2
- data/lib/YPBT/author.rb +4 -4
- data/lib/YPBT/comment.rb +9 -11
- data/lib/YPBT/runner.rb +2 -2
- data/lib/YPBT/time_tag.rb +54 -0
- data/lib/YPBT/version.rb +1 -1
- data/lib/YPBT/video.rb +7 -10
- data/lib/YPBT/youtube_api.rb +47 -21
- data/spec/Ytapi_spec.rb +60 -0
- data/spec/comment_spec.rb +30 -0
- data/spec/fixtures/cassettes/youtube_api.yml +6615 -0
- data/spec/spec_helper.rb +2 -2
- data/spec/time_tag_spec.rb +31 -0
- data/spec/video_spec.rb +13 -43
- metadata +23 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2af780eba918c5d863f386c47a56487d155bb9bd
|
4
|
+
data.tar.gz: 20fd1fafe9e5e595edb071ffda8b7f9beec74d7f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dbd7823b446f6bba748db5988292e754ceaa8933722d6947b959c422c8edacf38d24c5719ba1610a832907defb13bbfe5e181d2efb06779f1afb5deb56ed35c4
|
7
|
+
data.tar.gz: d424ff4da46cdfb44b8a23e66cd2fad2c5fa4e6c88b022459a378ff309b86bdb791e530c2e840d2c10e22ea577026a7e1850e23227431d544931addbca52ec6d
|
data/.travis.yml
CHANGED
data/Rakefile
CHANGED
data/YPBT.gemspec
CHANGED
@@ -19,7 +19,7 @@ Gem::Specification.new do |s|
|
|
19
19
|
s.executables << 'YPBT'
|
20
20
|
|
21
21
|
s.add_runtime_dependency 'http', '~> 2.0'
|
22
|
-
|
22
|
+
s.add_runtime_dependency 'ruby-duration', '~>3.2.3'
|
23
23
|
s.add_development_dependency 'minitest', '~> 5.9'
|
24
24
|
s.add_development_dependency 'minitest-rg', '~> 5.2'
|
25
25
|
s.add_development_dependency 'rake', '~> 11.3'
|
@@ -29,7 +29,6 @@ Gem::Specification.new do |s|
|
|
29
29
|
s.add_development_dependency 'flog', '~> 4.4'
|
30
30
|
s.add_development_dependency 'flay', '~> 2.8'
|
31
31
|
s.add_development_dependency 'rubocop', '~> 0.42'
|
32
|
-
|
33
32
|
s.homepage = 'https://github.com/RubyStarts3/YPBT'
|
34
33
|
s.license = 'MIT'
|
35
34
|
end
|
data/lib/YPBT/author.rb
CHANGED
@@ -6,10 +6,10 @@ module YoutubeVideo
|
|
6
6
|
:like_count
|
7
7
|
def initialize(data)
|
8
8
|
return unless data
|
9
|
-
@author_name = data[
|
10
|
-
@author_image_url = data[
|
11
|
-
@author_channel_url = data[
|
12
|
-
@like_count = data[
|
9
|
+
@author_name = data['authorDisplayName']
|
10
|
+
@author_image_url = data['authorProfileImageUrl']
|
11
|
+
@author_channel_url = data['authorChannelUrl']
|
12
|
+
@like_count = data['likeCount'].to_i
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|
data/lib/YPBT/comment.rb
CHANGED
@@ -6,18 +6,13 @@ require_relative 'author'
|
|
6
6
|
module YoutubeVideo
|
7
7
|
# signle comment on video's comment threads
|
8
8
|
class Comment
|
9
|
-
attr_reader :comment_id, :updated_at, :text_display, :published_at
|
9
|
+
attr_reader :comment_id, :updated_at, :text_display, :published_at, :author,
|
10
|
+
:time_tags, :like_count
|
10
11
|
|
11
12
|
def initialize(data: nil)
|
12
13
|
load_data(data)
|
13
14
|
end
|
14
15
|
|
15
|
-
def author
|
16
|
-
return @author if @author
|
17
|
-
author_data = YtApi.authors_info(@comment_id)
|
18
|
-
@author = YoutubeVideo::Author.new(author_data)
|
19
|
-
end
|
20
|
-
|
21
16
|
def self.find(comment_id:)
|
22
17
|
comment_data = YoutubeVideo::YtApi.comment_info(comment_id)
|
23
18
|
new(data: comment_data)
|
@@ -26,10 +21,13 @@ module YoutubeVideo
|
|
26
21
|
private
|
27
22
|
|
28
23
|
def load_data(comment_data)
|
29
|
-
@comment_id =
|
30
|
-
@
|
31
|
-
@
|
32
|
-
@
|
24
|
+
@comment_id = comment_data['id']
|
25
|
+
@like_count = comment_data['likeCount'].to_i
|
26
|
+
@updated_at = comment_data['updateAt']
|
27
|
+
@text_display = comment_data['textDisplay']
|
28
|
+
@published_at = comment_data['publishedAt']
|
29
|
+
@author = YoutubeVideo::Author.new(comment_data)
|
30
|
+
@time_tags = YoutubeVideo::Timetag.find(comment: self)
|
33
31
|
end
|
34
32
|
end
|
35
33
|
end
|
data/lib/YPBT/runner.rb
CHANGED
@@ -19,7 +19,7 @@ module YoutubeVideo
|
|
19
19
|
title = video.title
|
20
20
|
separator = Array.new(video.title.length) { '-' }.join
|
21
21
|
video_info =
|
22
|
-
video.
|
22
|
+
video.comments.map.with_index do |comment, index|
|
23
23
|
comment_info(comment, index)
|
24
24
|
end.join
|
25
25
|
|
@@ -28,7 +28,7 @@ module YoutubeVideo
|
|
28
28
|
|
29
29
|
def self.comment_info(comment, index)
|
30
30
|
"#{index + 1}:\n"\
|
31
|
-
"
|
31
|
+
" Author: #{comment.author.author_name}\n"\
|
32
32
|
" Comment: #{comment.text_display}\n"\
|
33
33
|
" LIKE: #{comment.author.like_count}\n"\
|
34
34
|
" AuthorChannelUrl: #{comment.author.author_channel_url}\n"
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'ruby-duration'
|
2
|
+
|
3
|
+
module YoutubeVideo
|
4
|
+
TAG_TYPES = { MUSIC: 'music', VIDEO: 'video' }.freeze
|
5
|
+
# comment's time tag infomation
|
6
|
+
class Timetag
|
7
|
+
attr_reader :start_time, :end_time, :tag_type, :duration, :comment
|
8
|
+
def initialize(start_time:, comment:, end_time: nil, like_count: nil,
|
9
|
+
tag_type: nil)
|
10
|
+
@start_time = string_to_time start_time
|
11
|
+
@end_time = end_time
|
12
|
+
@like_count = like_count ? like_count : comment.like_count
|
13
|
+
@tag_type = tag_type
|
14
|
+
end
|
15
|
+
|
16
|
+
def start_time
|
17
|
+
@start_time&.iso8601
|
18
|
+
end
|
19
|
+
|
20
|
+
def end_time=(end_time)
|
21
|
+
@end_time = string_to_time end_time if end_time
|
22
|
+
end
|
23
|
+
|
24
|
+
def end_time
|
25
|
+
@end_time&.iso8601 if @end_time
|
26
|
+
end
|
27
|
+
|
28
|
+
def duration
|
29
|
+
@duration = @end_time - @start_time if @end_time && @start_time
|
30
|
+
@duration&.iso8601 if @duration
|
31
|
+
end
|
32
|
+
|
33
|
+
def tag_type=(tag_type)
|
34
|
+
@tag_type = TAG_TYPES[tag_type.to_sym] if tag_type
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.find(comment:)
|
38
|
+
time_tag_pattern = /http.+?youtube.+?\?.+?t=.+?\>([0-9:]+)<\/a>/
|
39
|
+
start_times_string = comment.text_display.scan time_tag_pattern
|
40
|
+
tags = start_times_string.map do |match_parts|
|
41
|
+
Timetag.new(start_time: match_parts[0], comment: comment)
|
42
|
+
end
|
43
|
+
tags
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def string_to_time(time_string)
|
49
|
+
time_unit = [:seconds, :minutes, :hours, :day, :weeks]
|
50
|
+
time_array = time_string.scan(/[0-9]+/).map(&:to_i).reverse
|
51
|
+
Duration.new(Hash[time_unit.zip(time_array)])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/YPBT/version.rb
CHANGED
data/lib/YPBT/video.rb
CHANGED
@@ -14,18 +14,15 @@ module YoutubeVideo
|
|
14
14
|
@description = data['snippet']['description']
|
15
15
|
@dislike_count = data['statistics']['dislikeCount'].to_i
|
16
16
|
@like_count = data['statistics']['likeCount'].to_i
|
17
|
-
@comment_count = data['statistics']['commentCount'].to_i
|
18
17
|
@view_count = data['statistics']['viewCount'].to_i
|
18
|
+
@duration = data['contentDetails']['duration']
|
19
19
|
end
|
20
20
|
|
21
|
-
def
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
data: comment['snippet']['topLevelComment']
|
27
|
-
)
|
28
|
-
end
|
21
|
+
def comments
|
22
|
+
# contain only the comments which have time tag.
|
23
|
+
return @comments if @comments
|
24
|
+
raw_comments = YtApi.time_tags_info(@id)
|
25
|
+
@comments = raw_comments.map { |comment| Comment.new(data: comment) }
|
29
26
|
end
|
30
27
|
|
31
28
|
def embed_url
|
@@ -34,7 +31,7 @@ module YoutubeVideo
|
|
34
31
|
end
|
35
32
|
|
36
33
|
def self.find(video_id:)
|
37
|
-
video_data =
|
34
|
+
video_data = YtApi.video_info(video_id)
|
38
35
|
new(data: video_data)
|
39
36
|
end
|
40
37
|
end
|
data/lib/YPBT/youtube_api.rb
CHANGED
@@ -10,7 +10,7 @@ module YoutubeVideo
|
|
10
10
|
YT_COMPANY_URL = URI.join(YT_URL, "#{YT_COMPANY}/")
|
11
11
|
API_VER = 'v3'
|
12
12
|
YT_API_URL = URI.join(YT_COMPANY_URL, "#{API_VER}/")
|
13
|
-
|
13
|
+
TIME_TAG_PATTERN = /http.+?youtube.+?\?.+?t=.+?\>([0-9:]+)<\/a>/
|
14
14
|
def self.api_key
|
15
15
|
return @api_key if @api_key
|
16
16
|
@api_key = ENV['YOUTUBE_API_KEY']
|
@@ -22,42 +22,68 @@ module YoutubeVideo
|
|
22
22
|
|
23
23
|
def self.video_info(video_id)
|
24
24
|
field = 'items(id,snippet(channelId,description,publishedAt,title),'\
|
25
|
-
'statistics)'
|
25
|
+
'statistics(likeCount,dislikeCount,viewCount),'\
|
26
|
+
'contentDetails(duration))'
|
26
27
|
video_response = HTTP.get(yt_resource_url('videos'),
|
27
28
|
params: { id: video_id,
|
28
29
|
key: api_key,
|
29
|
-
part: 'snippet,statistics
|
30
|
+
part: 'snippet,statistics,
|
31
|
+
contentDetails',
|
30
32
|
fields: field })
|
31
33
|
JSON.parse(video_response.to_s)['items'].first
|
32
34
|
end
|
33
35
|
|
34
|
-
def self.
|
36
|
+
def self.comment_info(comment_id)
|
37
|
+
comment_response = HTTP.get(yt_resource_url('comments'),
|
38
|
+
params: { id: comment_id,
|
39
|
+
key: api_key,
|
40
|
+
part: 'snippet' })
|
41
|
+
item = JSON.parse(comment_response.to_s)['items'].first
|
42
|
+
comment = item['snippet']
|
43
|
+
comment['id'] = comment_id
|
44
|
+
comment
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.video_comments_info(video_id, page_token = '', max_results = 100)
|
35
48
|
comment_threads_response = HTTP.get(yt_resource_url('commentThreads'),
|
36
49
|
params: { videoId: video_id,
|
37
50
|
key: api_key,
|
38
51
|
order: 'relevance',
|
39
|
-
part: 'snippet'
|
40
|
-
|
52
|
+
part: 'snippet',
|
53
|
+
maxResults: max_results,
|
54
|
+
pageToken: page_token })
|
55
|
+
comment_threads = JSON.parse(comment_threads_response.to_s)
|
56
|
+
comments = extract_comment(comment_threads)
|
57
|
+
next_page_token = comment_threads['nextPageToken']
|
58
|
+
[next_page_token, comments]
|
41
59
|
end
|
42
60
|
|
43
|
-
def self.
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
61
|
+
def self.extract_comment(comment_threads)
|
62
|
+
comments = comment_threads['items'].map do |item|
|
63
|
+
comment = item['snippet']['topLevelComment']['snippet']
|
64
|
+
comment['id'] = item['id']
|
65
|
+
comment
|
66
|
+
end
|
67
|
+
comments
|
49
68
|
end
|
50
69
|
|
51
|
-
def self.
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
70
|
+
def self.time_tags_info(video_id, max_search_time = 5)
|
71
|
+
next_page = ''
|
72
|
+
comments_with_tags = []
|
73
|
+
max_search_time.times do
|
74
|
+
next_page, tmp_comments = video_comments_info(video_id, next_page)
|
75
|
+
tmp_comments.each do |comment|
|
76
|
+
comments_with_tags.push(comment) if time_tag? comment
|
77
|
+
end
|
78
|
+
break unless next_page
|
79
|
+
end
|
80
|
+
comments_with_tags
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.time_tag?(comment)
|
84
|
+
!(comment['textDisplay'] =~ TIME_TAG_PATTERN).nil?
|
60
85
|
end
|
86
|
+
|
61
87
|
private_class_method
|
62
88
|
def self.yt_resource_url(resouce_name)
|
63
89
|
URI.join(YT_API_URL, resouce_name.to_s)
|
data/spec/Ytapi_spec.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'spec_helper.rb'
|
3
|
+
|
4
|
+
describe 'YtApi specifications' do
|
5
|
+
VCR.configure do |c|
|
6
|
+
c.cassette_library_dir = CASSETTES_FOLDER
|
7
|
+
c.hook_into :webmock
|
8
|
+
|
9
|
+
c.filter_sensitive_data('<API_KEY>') { ENV['YOUTUBE_API_KEY'] }
|
10
|
+
c.filter_sensitive_data('<API_KEY_ESCAPED>') do
|
11
|
+
URI.escape(ENV['YOUTUBE_API_KEY'])
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
before do
|
16
|
+
VCR.insert_cassette CASSETTE_FILE, record: :new_episodes
|
17
|
+
end
|
18
|
+
|
19
|
+
after do
|
20
|
+
VCR.eject_cassette
|
21
|
+
end
|
22
|
+
|
23
|
+
describe 'YtApi Credentials' do
|
24
|
+
it 'should be able to get a new api key with ENV credentials' do
|
25
|
+
YoutubeVideo::YtApi.api_key.length.must_be :>, 0
|
26
|
+
end
|
27
|
+
it 'should be able to get a new access token with file credentials' do
|
28
|
+
YoutubeVideo::YtApi.config = { api_key: ENV['YOUTUBE_API_KEY'] }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'YtApi functions' do
|
33
|
+
it 'should be able to find video by video id' do
|
34
|
+
YoutubeVideo::YtApi.video_info(TEST_VIDEO_ID).must_be_instance_of Hash
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should be able find comment by comment id' do
|
38
|
+
result = YoutubeVideo::YtApi.comment_info(TEST_COMMENT_ID)
|
39
|
+
result.must_be_instance_of Hash
|
40
|
+
result['id'].must_equal TEST_COMMENT_ID
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should be able find comments by video id' do
|
44
|
+
next_page_token, comments = YoutubeVideo::YtApi
|
45
|
+
.video_comments_info(TEST_VIDEO_ID)
|
46
|
+
next_page_token.must_be_instance_of String
|
47
|
+
comments.must_be_instance_of Array
|
48
|
+
comments.length.must_be :>, 1
|
49
|
+
comments[0].must_be_instance_of Hash
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should be able find comments that contain time tags by video_id' do
|
53
|
+
comments = YoutubeVideo::YtApi.time_tags_info(TEST_VIDEO_ID)
|
54
|
+
comments.must_be_instance_of Array
|
55
|
+
comments.length.must_be :>, 1
|
56
|
+
comments[0].must_be_instance_of Hash
|
57
|
+
comments[0]['textDisplay'].must_match(/[0-9]+:\d+/)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'spec_helper.rb'
|
3
|
+
|
4
|
+
describe 'comment specifications' do
|
5
|
+
VCR.configure do |c|
|
6
|
+
c.cassette_library_dir = CASSETTES_FOLDER
|
7
|
+
c.hook_into :webmock
|
8
|
+
|
9
|
+
c.filter_sensitive_data('<API_KEY>') { ENV['YOUTUBE_API_KEY'] }
|
10
|
+
c.filter_sensitive_data('<API_KEY_ESCAPED>') do
|
11
|
+
URI.escape(ENV['YOUTUBE_API_KEY'])
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
before do
|
16
|
+
VCR.insert_cassette CASSETTE_FILE, record: :new_episodes
|
17
|
+
end
|
18
|
+
|
19
|
+
after do
|
20
|
+
VCR.eject_cassette
|
21
|
+
end
|
22
|
+
|
23
|
+
describe 'comment functions' do
|
24
|
+
it 'should has the abilit to find by comment id' do
|
25
|
+
comment = YoutubeVideo::Comment.find(comment_id: TEST_COMMENT_ID)
|
26
|
+
comment.must_be_instance_of YoutubeVideo::Comment
|
27
|
+
comment.comment_id.must_equal TEST_COMMENT_ID
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|