YPBT 0.2.5 → 0.2.11
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 +5 -5
- data/.gitignore +4 -4
- data/.rubocop.yml +3 -3
- data/.travis.yml +7 -7
- data/Gemfile +4 -4
- data/LICENSE +22 -22
- data/README.md +47 -47
- data/Rakefile +58 -58
- data/YPBT.gemspec +36 -34
- data/bin/YPBT +7 -7
- data/lib/YPBT.rb +4 -4
- data/lib/YPBT/author.rb +15 -15
- data/lib/YPBT/comment.rb +33 -33
- data/lib/YPBT/runner.rb +38 -37
- data/lib/YPBT/time_tag.rb +55 -55
- data/lib/YPBT/version.rb +5 -5
- data/lib/YPBT/video.rb +73 -63
- data/lib/YPBT/youtube_api.rb +127 -108
- data/spec/Ytapi_spec.rb +73 -68
- data/spec/comment_spec.rb +30 -30
- data/spec/fixtures/cassettes/youtube_api.yml +8465 -7707
- data/spec/fixtures/yt_api_results.yml +81 -81
- data/spec/spec_helper.rb +29 -29
- data/spec/time_tag_spec.rb +31 -31
- data/spec/video_spec.rb +61 -56
- metadata +5 -6
data/lib/YPBT/runner.rb
CHANGED
@@ -1,37 +1,38 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module YoutubeVideo
|
4
|
-
# Executable code for file(s) in bin/ folder
|
5
|
-
class Runner
|
6
|
-
def self.run!(args)
|
7
|
-
video_id = args[0] || ENV['YT_VIDEO_ID']
|
8
|
-
unless video_id
|
9
|
-
puts 'USAGE: YPBT [video_id]'
|
10
|
-
exit(1)
|
11
|
-
end
|
12
|
-
|
13
|
-
video = YoutubeVideo::Video.find(video_id: video_id)
|
14
|
-
|
15
|
-
output_info(video)
|
16
|
-
end
|
17
|
-
|
18
|
-
def self.output_info(video)
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
"
|
32
|
-
"
|
33
|
-
"
|
34
|
-
"
|
35
|
-
|
36
|
-
|
37
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module YoutubeVideo
|
4
|
+
# Executable code for file(s) in bin/ folder
|
5
|
+
class Runner
|
6
|
+
def self.run!(args)
|
7
|
+
video_id = args[0] || ENV['YT_VIDEO_ID']
|
8
|
+
unless video_id
|
9
|
+
puts 'USAGE: YPBT [video_id]'
|
10
|
+
exit(1)
|
11
|
+
end
|
12
|
+
|
13
|
+
video = YoutubeVideo::Video.find(video_id: video_id)
|
14
|
+
|
15
|
+
output_info(video)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.output_info(video)
|
19
|
+
return 'Nothing found. (Invalid video id or api-key)' if video.nil?
|
20
|
+
title = video.title
|
21
|
+
separator = Array.new(video.title.length) { '-' }.join
|
22
|
+
video_info =
|
23
|
+
video.comments.map.with_index do |comment, index|
|
24
|
+
comment_info(comment, index)
|
25
|
+
end.join
|
26
|
+
|
27
|
+
[title, separator, video_info].join("\n")
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.comment_info(comment, index)
|
31
|
+
"#{index + 1}:\n"\
|
32
|
+
" Author: #{comment.author.author_name}\n"\
|
33
|
+
" Comment: #{comment.text_display}\n"\
|
34
|
+
" LIKE: #{comment.author.like_count}\n"\
|
35
|
+
" AuthorChannelUrl: #{comment.author.author_channel_url}\n"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/YPBT/time_tag.rb
CHANGED
@@ -1,55 +1,55 @@
|
|
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
|
-
:like_count
|
9
|
-
def initialize(start_time:, comment:, end_time: nil, like_count: nil,
|
10
|
-
tag_type: nil)
|
11
|
-
@start_time = string_to_time start_time
|
12
|
-
@end_time = end_time
|
13
|
-
@like_count = like_count ? like_count : comment.like_count
|
14
|
-
@tag_type = tag_type
|
15
|
-
end
|
16
|
-
|
17
|
-
def start_time
|
18
|
-
@start_time&.iso8601
|
19
|
-
end
|
20
|
-
|
21
|
-
def end_time=(end_time)
|
22
|
-
@end_time = string_to_time end_time if end_time
|
23
|
-
end
|
24
|
-
|
25
|
-
def end_time
|
26
|
-
@end_time&.iso8601 if @end_time
|
27
|
-
end
|
28
|
-
|
29
|
-
def duration
|
30
|
-
@duration = @end_time - @start_time if @end_time && @start_time
|
31
|
-
@duration&.iso8601 if @duration
|
32
|
-
end
|
33
|
-
|
34
|
-
def tag_type=(tag_type)
|
35
|
-
@tag_type = TAG_TYPES[tag_type.to_sym] if tag_type
|
36
|
-
end
|
37
|
-
|
38
|
-
def self.find(comment:)
|
39
|
-
time_tag_pattern = /http.+?youtube.+?\?.+?t=.+?\>([0-9:]+)<\/a>/
|
40
|
-
start_times_string = comment.text_display.scan time_tag_pattern
|
41
|
-
tags = start_times_string.map do |match_parts|
|
42
|
-
Timetag.new(start_time: match_parts[0], comment: comment)
|
43
|
-
end
|
44
|
-
tags
|
45
|
-
end
|
46
|
-
|
47
|
-
private
|
48
|
-
|
49
|
-
def string_to_time(time_string)
|
50
|
-
time_unit = [:seconds, :minutes, :hours, :day, :weeks]
|
51
|
-
time_array = time_string.scan(/[0-9]+/).map(&:to_i).reverse
|
52
|
-
Duration.new(Hash[time_unit.zip(time_array)])
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
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
|
+
:like_count
|
9
|
+
def initialize(start_time:, comment:, end_time: nil, like_count: nil,
|
10
|
+
tag_type: nil)
|
11
|
+
@start_time = string_to_time start_time
|
12
|
+
@end_time = end_time
|
13
|
+
@like_count = like_count ? like_count : comment.like_count
|
14
|
+
@tag_type = tag_type
|
15
|
+
end
|
16
|
+
|
17
|
+
def start_time
|
18
|
+
@start_time&.iso8601
|
19
|
+
end
|
20
|
+
|
21
|
+
def end_time=(end_time)
|
22
|
+
@end_time = string_to_time end_time if end_time
|
23
|
+
end
|
24
|
+
|
25
|
+
def end_time
|
26
|
+
@end_time&.iso8601 if @end_time
|
27
|
+
end
|
28
|
+
|
29
|
+
def duration
|
30
|
+
@duration = @end_time - @start_time if @end_time && @start_time
|
31
|
+
@duration&.iso8601 if @duration
|
32
|
+
end
|
33
|
+
|
34
|
+
def tag_type=(tag_type)
|
35
|
+
@tag_type = TAG_TYPES[tag_type.to_sym] if tag_type
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.find(comment:)
|
39
|
+
time_tag_pattern = /http.+?youtube.+?\?.+?t=.+?\>([0-9:]+)<\/a>/
|
40
|
+
start_times_string = comment.text_display.scan time_tag_pattern
|
41
|
+
tags = start_times_string.map do |match_parts|
|
42
|
+
Timetag.new(start_time: match_parts[0], comment: comment)
|
43
|
+
end
|
44
|
+
tags
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def string_to_time(time_string)
|
50
|
+
time_unit = [:seconds, :minutes, :hours, :day, :weeks]
|
51
|
+
time_array = time_string.scan(/[0-9]+/).map(&:to_i).reverse
|
52
|
+
Duration.new(Hash[time_unit.zip(time_array)])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/YPBT/version.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module YoutubeVideo
|
4
|
-
VERSION = '0.2.
|
5
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module YoutubeVideo
|
4
|
+
VERSION = '0.2.11'
|
5
|
+
end
|
data/lib/YPBT/video.rb
CHANGED
@@ -1,63 +1,73 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
require_relative 'comment'
|
3
|
-
require_relative 'youtube_api'
|
4
|
-
|
5
|
-
module YoutubeVideo
|
6
|
-
# Main class to setup a Video
|
7
|
-
class Video
|
8
|
-
attr_reader :title, :description, :dislike_count, :like_count,
|
9
|
-
:comment_count, :view_count, :duration, :id, :channel_id
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
@
|
14
|
-
@
|
15
|
-
@
|
16
|
-
@
|
17
|
-
@
|
18
|
-
@
|
19
|
-
@
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
end
|
62
|
-
|
63
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'comment'
|
3
|
+
require_relative 'youtube_api'
|
4
|
+
|
5
|
+
module YoutubeVideo
|
6
|
+
# Main class to setup a Video
|
7
|
+
class Video
|
8
|
+
attr_reader :title, :description, :dislike_count, :like_count,
|
9
|
+
:comment_count, :view_count, :duration, :id, :channel_id,
|
10
|
+
:thumbnail_url, :category_id
|
11
|
+
|
12
|
+
def initialize(data:)
|
13
|
+
@id = data['id']
|
14
|
+
@title = data['snippet']['title']
|
15
|
+
@channel_id = data['snippet']['channelId']
|
16
|
+
@description = data['snippet']['description']
|
17
|
+
@category_id = data['snippet']['categoryId']
|
18
|
+
@thumbnail_url = data['snippet']['thumbnails']['medium']['url']
|
19
|
+
@dislike_count = data['statistics']['dislikeCount'].to_i
|
20
|
+
@like_count = data['statistics']['likeCount'].to_i
|
21
|
+
@view_count = data['statistics']['viewCount'].to_i
|
22
|
+
@duration = data['contentDetails']['duration']
|
23
|
+
@is_channel = false
|
24
|
+
end
|
25
|
+
|
26
|
+
def comments
|
27
|
+
# contain only the comments which have time tag.
|
28
|
+
return @comments if @comments
|
29
|
+
raw_comments = YtApi.time_tags_info(@id)
|
30
|
+
@comments = raw_comments.map { |comment| Comment.new(data: comment) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def channel_title
|
34
|
+
load_channel_info unless @is_channel
|
35
|
+
@channel_title
|
36
|
+
end
|
37
|
+
|
38
|
+
def channel_image_url
|
39
|
+
load_channel_info unless @is_channel
|
40
|
+
@channel_image_url
|
41
|
+
end
|
42
|
+
|
43
|
+
def channel_description
|
44
|
+
load_channel_info unless @is_channel
|
45
|
+
@channel_description
|
46
|
+
end
|
47
|
+
|
48
|
+
def embed_url
|
49
|
+
return @embed_url if @embed_url
|
50
|
+
@embed_url = "https://www.youtube.com/embed/#{@id}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.find(video_id:)
|
54
|
+
video_data = YtApi.video_info(video_id)
|
55
|
+
new(data: video_data) if video_data
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.find_popular(max_results: 25)
|
59
|
+
videos_data = YtApi.popular_videos_info(max_results)
|
60
|
+
videos_data.map { |data| new(data: data) } unless videos_data.empty?
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def load_channel_info
|
66
|
+
channel_data = YtApi.channel_info @channel_id
|
67
|
+
@channel_title = channel_data['title'] if channel_data
|
68
|
+
@channel_image_url = channel_data['image_url'] if channel_data
|
69
|
+
@channel_description = channel_data['description'] if channel_data
|
70
|
+
@is_channel = true
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/YPBT/youtube_api.rb
CHANGED
@@ -1,108 +1,127 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
require 'http'
|
3
|
-
require 'json'
|
4
|
-
|
5
|
-
module YoutubeVideo
|
6
|
-
# Service for all Youtube API calls
|
7
|
-
class YtApi
|
8
|
-
YT_URL = 'https://
|
9
|
-
YT_COMPANY = 'youtube'
|
10
|
-
YT_COMPANY_URL = URI.join(YT_URL, "#{YT_COMPANY}/")
|
11
|
-
API_VER = 'v3'
|
12
|
-
YT_API_URL = URI.join(YT_COMPANY_URL, "#{API_VER}/")
|
13
|
-
TIME_TAG_PATTERN = /http.+?youtube.+?\?.+?t=.+?\>([0-9:]+)<\/a>/
|
14
|
-
def self.api_key
|
15
|
-
return @api_key if @api_key
|
16
|
-
@api_key = ENV['YOUTUBE_API_KEY']
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.config=(credentials)
|
20
|
-
@config ? @config.update(credentials) : @config = credentials
|
21
|
-
end
|
22
|
-
|
23
|
-
def self.video_info(video_id)
|
24
|
-
field = 'items(id,
|
25
|
-
'
|
26
|
-
'
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'http'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module YoutubeVideo
|
6
|
+
# Service for all Youtube API calls
|
7
|
+
class YtApi
|
8
|
+
YT_URL = 'https://youtube.googleapis.com'
|
9
|
+
YT_COMPANY = 'youtube'
|
10
|
+
YT_COMPANY_URL = URI.join(YT_URL, "#{YT_COMPANY}/")
|
11
|
+
API_VER = 'v3'
|
12
|
+
YT_API_URL = URI.join(YT_COMPANY_URL, "#{API_VER}/")
|
13
|
+
TIME_TAG_PATTERN = /http.+?youtube.+?\?.+?t=.+?\>([0-9:]+)<\/a>/
|
14
|
+
def self.api_key
|
15
|
+
return @api_key if @api_key
|
16
|
+
@api_key = ENV['YOUTUBE_API_KEY']
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.config=(credentials)
|
20
|
+
@config ? @config.update(credentials) : @config = credentials
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.video_info(video_id)
|
24
|
+
field = 'items(id,'\
|
25
|
+
'snippet(thumbnails(medium),channelId,description,'\
|
26
|
+
'publishedAt,title,categoryId),'\
|
27
|
+
'statistics(likeCount,dislikeCount,viewCount),'\
|
28
|
+
'contentDetails(duration))'
|
29
|
+
video_response = HTTP.get(yt_resource_url('videos'),
|
30
|
+
params: { id: video_id,
|
31
|
+
key: api_key,
|
32
|
+
part: 'snippet,statistics,'\
|
33
|
+
'contentDetails',
|
34
|
+
fields: field })
|
35
|
+
JSON.parse(video_response.to_s)['items']&.first
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.popular_videos_info(max_results = 25)
|
39
|
+
field = 'items(id,'\
|
40
|
+
'snippet(thumbnails(medium),channelId,description,'\
|
41
|
+
'publishedAt,title,categoryId),'\
|
42
|
+
'statistics(likeCount,dislikeCount,viewCount),'\
|
43
|
+
'contentDetails(duration))'
|
44
|
+
video_response = HTTP.get(yt_resource_url('videos'),
|
45
|
+
params: { chart: 'mostpopular',
|
46
|
+
key: api_key,
|
47
|
+
maxResults: max_results,
|
48
|
+
part: 'snippet,statistics,'\
|
49
|
+
'contentDetails',
|
50
|
+
fields: field })
|
51
|
+
JSON.parse(video_response.to_s)['items']
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.comment_info(comment_id)
|
55
|
+
comment_response = HTTP.get(yt_resource_url('comments'),
|
56
|
+
params: { id: comment_id,
|
57
|
+
key: api_key,
|
58
|
+
part: 'snippet' })
|
59
|
+
item = JSON.parse(comment_response.to_s)['items'].first
|
60
|
+
comment = item['snippet']
|
61
|
+
comment['id'] = comment_id
|
62
|
+
comment
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.video_comments_info(video_id, page_token = '', max_results = 100)
|
66
|
+
comment_threads_response = HTTP.get(yt_resource_url('commentThreads'),
|
67
|
+
params: { videoId: video_id,
|
68
|
+
key: api_key,
|
69
|
+
order: 'relevance',
|
70
|
+
part: 'snippet',
|
71
|
+
maxResults: max_results,
|
72
|
+
pageToken: page_token })
|
73
|
+
comment_threads = JSON.parse(comment_threads_response.to_s)
|
74
|
+
comments = extract_comment(comment_threads)
|
75
|
+
next_page_token = comment_threads['nextPageToken']
|
76
|
+
[next_page_token, comments]
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.channel_info(channel_id)
|
80
|
+
fields = 'items(id,snippet(title,description,thumbnails(default(url))))'
|
81
|
+
channel_response = HTTP.get(yt_resource_url('channels'),
|
82
|
+
params: { id: channel_id,
|
83
|
+
key: api_key,
|
84
|
+
part: 'snippet',
|
85
|
+
fields: fields })
|
86
|
+
channel_data = JSON.parse(channel_response.to_s)['items'].first
|
87
|
+
if channel_data
|
88
|
+
{
|
89
|
+
'title' => channel_data['snippet']['title'],
|
90
|
+
'description' => channel_data['snippet']['description'],
|
91
|
+
'image_url' => channel_data['snippet']['thumbnails']['default']['url']
|
92
|
+
}
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.extract_comment(comment_threads)
|
97
|
+
comments = comment_threads['items'].map do |item|
|
98
|
+
comment = item['snippet']['topLevelComment']['snippet']
|
99
|
+
comment['id'] = item['id']
|
100
|
+
comment
|
101
|
+
end
|
102
|
+
comments
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.time_tags_info(video_id, max_search_time = 5)
|
106
|
+
next_page = ''
|
107
|
+
comments_with_tags = []
|
108
|
+
max_search_time.times do
|
109
|
+
next_page, tmp_comments = video_comments_info(video_id, next_page)
|
110
|
+
tmp_comments.each do |comment|
|
111
|
+
comments_with_tags.push(comment) if time_tag? comment
|
112
|
+
end
|
113
|
+
break unless next_page
|
114
|
+
end
|
115
|
+
comments_with_tags
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.time_tag?(comment)
|
119
|
+
!(comment['textDisplay'] =~ TIME_TAG_PATTERN).nil?
|
120
|
+
end
|
121
|
+
|
122
|
+
private_class_method
|
123
|
+
def self.yt_resource_url(resouce_name)
|
124
|
+
URI.join(YT_API_URL, resouce_name.to_s)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|