yt 0.25.13 → 0.32.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +305 -1
- data/MIT-LICENSE +1 -1
- data/README.md +86 -5
- data/YOUTUBE_IT.md +3 -3
- data/lib/yt.rb +5 -2
- data/lib/yt/actions/list.rb +3 -3
- data/lib/yt/associations/has_authentication.rb +33 -1
- data/lib/yt/associations/has_reports.rb +13 -18
- data/lib/yt/collections/assets.rb +2 -2
- data/lib/yt/collections/authentications.rb +9 -2
- data/lib/yt/collections/base.rb +3 -3
- data/lib/yt/collections/bulk_report_jobs.rb +28 -0
- data/lib/yt/collections/bulk_reports.rb +24 -0
- data/lib/yt/collections/claims.rb +22 -1
- data/lib/yt/collections/comment_threads.rb +41 -0
- data/lib/yt/collections/content_owners.rb +1 -1
- data/lib/yt/collections/group_infos.rb +27 -0
- data/lib/yt/collections/group_items.rb +45 -0
- data/lib/yt/collections/reports.rb +75 -13
- data/lib/yt/collections/revocations.rb +30 -0
- data/lib/yt/collections/video_groups.rb +29 -0
- data/lib/yt/collections/videos.rb +34 -9
- data/lib/yt/constants/geography.rb +326 -0
- data/lib/yt/errors/forbidden.rb +1 -3
- data/lib/yt/errors/no_items.rb +1 -3
- data/lib/yt/errors/request_error.rb +10 -7
- data/lib/yt/errors/server_error.rb +1 -3
- data/lib/yt/errors/unauthorized.rb +3 -3
- data/lib/yt/models/account.rb +12 -0
- data/lib/yt/models/advertising_options_set.rb +4 -4
- data/lib/yt/models/bulk_report.rb +23 -0
- data/lib/yt/models/bulk_report_job.rb +23 -0
- data/lib/yt/models/channel.rb +21 -12
- data/lib/yt/models/claim.rb +13 -2
- data/lib/yt/models/comment.rb +37 -0
- data/lib/yt/models/comment_thread.rb +50 -0
- data/lib/yt/models/content_detail.rb +6 -0
- data/lib/yt/models/content_owner.rb +31 -1
- data/lib/yt/models/group_info.rb +16 -0
- data/lib/yt/models/group_item.rb +15 -0
- data/lib/yt/models/resource.rb +3 -10
- data/lib/yt/models/revocation.rb +12 -0
- data/lib/yt/models/right_owner.rb +0 -2
- data/lib/yt/models/snippet.rb +24 -3
- data/lib/yt/models/video.rb +42 -11
- data/lib/yt/models/video_group.rb +186 -0
- data/lib/yt/request.rb +5 -3
- data/lib/yt/version.rb +2 -2
- data/spec/collections/comment_threads_spec.rb +46 -0
- data/spec/collections/playlist_items_spec.rb +1 -1
- data/spec/collections/reports_spec.rb +2 -2
- data/spec/constants/geography_spec.rb +16 -0
- data/spec/models/annotation_spec.rb +1 -1
- data/spec/models/claim_spec.rb +15 -3
- data/spec/models/comment_spec.rb +40 -0
- data/spec/models/comment_thread_spec.rb +93 -0
- data/spec/models/content_detail_spec.rb +7 -0
- data/spec/models/reference_spec.rb +2 -2
- data/spec/models/request_spec.rb +21 -0
- data/spec/models/resource_spec.rb +0 -15
- data/spec/models/video_spec.rb +1 -1
- data/spec/requests/as_account/account_spec.rb +16 -4
- data/spec/requests/as_account/authentications_spec.rb +1 -13
- data/spec/requests/as_account/channel_spec.rb +15 -45
- data/spec/requests/as_account/playlist_item_spec.rb +3 -3
- data/spec/requests/as_account/playlist_spec.rb +5 -32
- data/spec/requests/as_account/video_spec.rb +2022 -21
- data/spec/requests/as_content_owner/account_spec.rb +4 -0
- data/spec/requests/as_content_owner/bulk_report_job_spec.rb +19 -0
- data/spec/requests/as_content_owner/channel_spec.rb +59 -270
- data/spec/requests/as_content_owner/content_owner_spec.rb +89 -1
- data/spec/requests/as_content_owner/playlist_spec.rb +0 -15
- data/spec/requests/as_content_owner/video_group_spec.rb +112 -0
- data/spec/requests/as_content_owner/video_spec.rb +72 -146
- data/spec/requests/as_server_app/channel_spec.rb +1 -21
- data/spec/requests/as_server_app/comment_spec.rb +22 -0
- data/spec/requests/as_server_app/comment_thread_spec.rb +27 -0
- data/spec/requests/as_server_app/comment_threads_spec.rb +41 -0
- data/spec/requests/as_server_app/playlist_item_spec.rb +2 -2
- data/spec/requests/as_server_app/playlist_spec.rb +1 -22
- data/spec/requests/as_server_app/video_spec.rb +21 -19
- data/spec/requests/as_server_app/videos_spec.rb +5 -5
- data/spec/requests/unauthenticated/video_spec.rb +1 -9
- data/spec/spec_helper.rb +1 -1
- data/yt.gemspec +2 -1
- metadata +51 -17
- data/lib/yt/collections/ids.rb +0 -27
- data/lib/yt/config.rb +0 -54
- data/lib/yt/models/configuration.rb +0 -70
- data/lib/yt/models/description.rb +0 -58
- data/lib/yt/models/url.rb +0 -91
- data/spec/models/configuration_spec.rb +0 -44
- data/spec/models/description_spec.rb +0 -94
- data/spec/models/url_spec.rb +0 -84
- data/spec/requests/as_account/resource_spec.rb +0 -18
@@ -0,0 +1,16 @@
|
|
1
|
+
module Yt
|
2
|
+
module Models
|
3
|
+
class GroupInfo < Base
|
4
|
+
attr_reader :data
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
@data = options[:data]['snippet'].merge options[:data]['contentDetails']
|
8
|
+
@auth = options[:auth]
|
9
|
+
end
|
10
|
+
|
11
|
+
has_attribute :title, default: ''
|
12
|
+
has_attribute :item_count, type: Integer
|
13
|
+
has_attribute :published_at, type: Time
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'yt/models/video'
|
2
|
+
|
3
|
+
module Yt
|
4
|
+
module Models
|
5
|
+
class GroupItem < Base
|
6
|
+
attr_reader :auth, :data, :video
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
@data = options[:data]
|
10
|
+
@auth = options[:auth]
|
11
|
+
@video = options[:video] if options[:video]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/yt/models/resource.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
|
3
3
|
require 'yt/models/base'
|
4
|
-
require 'yt/models/url'
|
5
4
|
|
6
5
|
module Yt
|
7
6
|
module Models
|
@@ -13,7 +12,7 @@ module Yt
|
|
13
12
|
|
14
13
|
# @!attribute [r] id
|
15
14
|
# @return [String] the ID that YouTube uses to identify each resource.
|
16
|
-
|
15
|
+
attr_reader :id
|
17
16
|
|
18
17
|
### STATUS ###
|
19
18
|
|
@@ -43,8 +42,7 @@ module Yt
|
|
43
42
|
|
44
43
|
# @private
|
45
44
|
def initialize(options = {})
|
46
|
-
@
|
47
|
-
@id = options[:id] || (@url.id if @url)
|
45
|
+
@id = options[:id]
|
48
46
|
@auth = options[:auth]
|
49
47
|
@snippet = Snippet.new(data: options[:snippet]) if options[:snippet]
|
50
48
|
@status = Status.new(data: options[:status]) if options[:status]
|
@@ -52,12 +50,7 @@ module Yt
|
|
52
50
|
|
53
51
|
# @private
|
54
52
|
def kind
|
55
|
-
|
56
|
-
end
|
57
|
-
|
58
|
-
# @private
|
59
|
-
def username
|
60
|
-
@url.username if @url
|
53
|
+
self.class.to_s.demodulize.underscore
|
61
54
|
end
|
62
55
|
|
63
56
|
# @private
|
data/lib/yt/models/snippet.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'yt/models/
|
1
|
+
require 'yt/models/comment'
|
2
2
|
|
3
3
|
module Yt
|
4
4
|
module Models
|
@@ -8,6 +8,8 @@ module Yt
|
|
8
8
|
# @see https://developers.google.com/youtube/v3/docs/videos#resource
|
9
9
|
# @see https://developers.google.com/youtube/v3/docs/playlists#resource
|
10
10
|
# @see https://developers.google.com/youtube/v3/docs/playlistItems#resource
|
11
|
+
# @see https://developers.google.com/youtube/v3/docs/commentThreads#resource
|
12
|
+
# @see https://developers.google.com/youtube/v3/docs/comments#resource
|
11
13
|
class Snippet < Base
|
12
14
|
attr_reader :data
|
13
15
|
|
@@ -17,7 +19,7 @@ module Yt
|
|
17
19
|
end
|
18
20
|
|
19
21
|
has_attribute :title, default: ''
|
20
|
-
has_attribute
|
22
|
+
has_attribute :description, default: ''
|
21
23
|
has_attribute :published_at, type: Time
|
22
24
|
has_attribute :channel_id
|
23
25
|
has_attribute :channel_title
|
@@ -28,11 +30,30 @@ module Yt
|
|
28
30
|
has_attribute :position, type: Integer
|
29
31
|
has_attribute :resource_id, default: {}
|
30
32
|
has_attribute :thumbnails, default: {}
|
33
|
+
has_attribute :video_id
|
34
|
+
has_attribute :total_reply_count, type: Integer
|
35
|
+
has_attribute :author_display_name
|
36
|
+
has_attribute :text_display
|
37
|
+
has_attribute :parent_id
|
38
|
+
has_attribute :like_count, type: Integer
|
39
|
+
has_attribute :updated_at, type: Time
|
31
40
|
|
32
41
|
def thumbnail_url(size = :default)
|
33
42
|
thumbnails.fetch(size.to_s, {})['url']
|
34
43
|
end
|
35
44
|
|
45
|
+
def public?
|
46
|
+
@public ||= data.fetch 'isPublic', false
|
47
|
+
end
|
48
|
+
|
49
|
+
def can_reply?
|
50
|
+
@can_reply ||= data.fetch 'canReply', false
|
51
|
+
end
|
52
|
+
|
53
|
+
def top_level_comment
|
54
|
+
@top_level_comment ||= Yt::Comment.new data['topLevelComment'].symbolize_keys
|
55
|
+
end
|
56
|
+
|
36
57
|
# Returns whether YouTube API includes all attributes in this snippet.
|
37
58
|
# For instance, YouTube API only returns tags and categoryId on
|
38
59
|
# Videos#list, not on Videos#search. And returns position on
|
@@ -47,4 +68,4 @@ module Yt
|
|
47
68
|
end
|
48
69
|
end
|
49
70
|
end
|
50
|
-
end
|
71
|
+
end
|
data/lib/yt/models/video.rb
CHANGED
@@ -37,6 +37,11 @@ module Yt
|
|
37
37
|
# @return [String] the title of the channel that the video belongs to.
|
38
38
|
delegate :channel_title, to: :snippet
|
39
39
|
|
40
|
+
# @return [<String>] the URL of the channel that the video belongs to.
|
41
|
+
def channel_url
|
42
|
+
"https://www.youtube.com/channel/#{channel_id}"
|
43
|
+
end
|
44
|
+
|
40
45
|
# @!attribute [r] live_broadcast_content
|
41
46
|
# @return [String] the type of live broadcast that the video contains.
|
42
47
|
# Possible values are: +'live'+, +'none'+, +'upcoming'+.
|
@@ -226,6 +231,10 @@ module Yt
|
|
226
231
|
# @return [Integer] the duration of the video (in seconds).
|
227
232
|
delegate :duration, to: :content_detail
|
228
233
|
|
234
|
+
# @!attribute [r] duration
|
235
|
+
# @return [String] the length of the video as an ISO 8601 time, HH:MM:SS.
|
236
|
+
delegate :length, to: :content_detail
|
237
|
+
|
229
238
|
# @return [Boolean] whether the video is available in 3D.
|
230
239
|
def stereoscopic?
|
231
240
|
content_detail.dimension == '3d'
|
@@ -369,6 +378,9 @@ module Yt
|
|
369
378
|
delegate :concurrent_viewers, to: :live_streaming_detail
|
370
379
|
|
371
380
|
### ASSOCIATIONS ###
|
381
|
+
# @!attribute [r] comments
|
382
|
+
# @return [Yt::Collections::Comments] the video’s comments.
|
383
|
+
has_many :comment_threads
|
372
384
|
|
373
385
|
# @!attribute [r] annotations
|
374
386
|
# @return [Yt::Collections::Annotations] the video’s annotations.
|
@@ -376,6 +388,13 @@ module Yt
|
|
376
388
|
|
377
389
|
has_many :resumable_sessions
|
378
390
|
|
391
|
+
# @!attribute [r] channel
|
392
|
+
# @return [Yt::Models::Claim, nil] the first claim on the video by
|
393
|
+
# the content owner of the video, if eagerly loaded.
|
394
|
+
def claim
|
395
|
+
@claim
|
396
|
+
end
|
397
|
+
|
379
398
|
### ANALYTICS ###
|
380
399
|
|
381
400
|
# @macro reports
|
@@ -383,9 +402,6 @@ module Yt
|
|
383
402
|
# @macro report_by_video_dimensions
|
384
403
|
has_report :views, Integer
|
385
404
|
|
386
|
-
# @macro report_by_day
|
387
|
-
has_report :uniques, Integer
|
388
|
-
|
389
405
|
# @macro report_by_video_dimensions
|
390
406
|
has_report :estimated_minutes_watched, Integer
|
391
407
|
|
@@ -414,12 +430,6 @@ module Yt
|
|
414
430
|
# @macro report_by_day_and_country
|
415
431
|
has_report :subscribers_lost, Integer
|
416
432
|
|
417
|
-
# @macro report_by_day_and_country
|
418
|
-
has_report :favorites_added, Integer
|
419
|
-
|
420
|
-
# @macro report_by_day_and_country
|
421
|
-
has_report :favorites_removed, Integer
|
422
|
-
|
423
433
|
# @macro report_by_day_and_country
|
424
434
|
has_report :videos_added_to_playlists, Integer
|
425
435
|
|
@@ -441,11 +451,29 @@ module Yt
|
|
441
451
|
# @macro report_by_day_and_state
|
442
452
|
has_report :annotation_close_rate, Float
|
443
453
|
|
454
|
+
# @macro report_by_day_and_state
|
455
|
+
has_report :card_impressions, Integer
|
456
|
+
|
457
|
+
# @macro report_by_day_and_state
|
458
|
+
has_report :card_clicks, Integer
|
459
|
+
|
460
|
+
# @macro report_by_day_and_state
|
461
|
+
has_report :card_click_rate, Float
|
462
|
+
|
463
|
+
# @macro report_by_day_and_state
|
464
|
+
has_report :card_teaser_impressions, Integer
|
465
|
+
|
466
|
+
# @macro report_by_day_and_state
|
467
|
+
has_report :card_teaser_clicks, Integer
|
468
|
+
|
469
|
+
# @macro report_by_day_and_state
|
470
|
+
has_report :card_teaser_click_rate, Float
|
471
|
+
|
444
472
|
# @macro report_by_day_and_country
|
445
|
-
has_report :
|
473
|
+
has_report :estimated_revenue, Float
|
446
474
|
|
447
475
|
# @macro report_by_day_and_country
|
448
|
-
has_report :
|
476
|
+
has_report :ad_impressions, Integer
|
449
477
|
|
450
478
|
# @macro report_by_day_and_country
|
451
479
|
has_report :monetized_playbacks, Integer
|
@@ -576,6 +604,9 @@ module Yt
|
|
576
604
|
if options[:player]
|
577
605
|
@player = Player.new data: options[:player]
|
578
606
|
end
|
607
|
+
if options[:claim]
|
608
|
+
@claim = options[:claim]
|
609
|
+
end
|
579
610
|
end
|
580
611
|
|
581
612
|
# @private
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require 'yt/models/base'
|
2
|
+
|
3
|
+
module Yt
|
4
|
+
module Models
|
5
|
+
# Provides methods to interact with YouTube Analytics video-groups.
|
6
|
+
# @see https://developers.google.com/youtube/analytics/v1/reference/groups
|
7
|
+
class VideoGroup < Base
|
8
|
+
# @private
|
9
|
+
attr_reader :id, :auth
|
10
|
+
|
11
|
+
### GROUP INFO ###
|
12
|
+
|
13
|
+
has_one :group_info
|
14
|
+
|
15
|
+
# @!attribute [r] title
|
16
|
+
# @return [String] the title of the group.
|
17
|
+
delegate :title, to: :group_info
|
18
|
+
|
19
|
+
# @!attribute [r] item_count
|
20
|
+
# @return [Integer] the number of resources in the group.
|
21
|
+
delegate :item_count, to: :group_info
|
22
|
+
|
23
|
+
# @!attribute [r] published_at
|
24
|
+
# @return [Time] the date and time when the group was created.
|
25
|
+
delegate :published_at, to: :group_info
|
26
|
+
|
27
|
+
### ASSOCIATIONS ###
|
28
|
+
|
29
|
+
# @!attribute [r] group_items
|
30
|
+
# @return [Yt::Collections::GroupItems] the group’s items.
|
31
|
+
has_many :group_items
|
32
|
+
|
33
|
+
### ANALYTICS ###
|
34
|
+
|
35
|
+
# @macro reports
|
36
|
+
|
37
|
+
# @macro report_by_video_dimensions
|
38
|
+
has_report :views, Integer
|
39
|
+
|
40
|
+
# @macro report_by_video_dimensions
|
41
|
+
has_report :estimated_minutes_watched, Integer
|
42
|
+
|
43
|
+
# @macro report_by_gender_and_age_group
|
44
|
+
has_report :viewer_percentage, Float
|
45
|
+
|
46
|
+
# @macro report_by_day_and_country
|
47
|
+
has_report :comments, Integer
|
48
|
+
|
49
|
+
# @macro report_by_day_and_country
|
50
|
+
has_report :likes, Integer
|
51
|
+
|
52
|
+
# @macro report_by_day_and_country
|
53
|
+
has_report :dislikes, Integer
|
54
|
+
|
55
|
+
# @macro report_by_day_and_country
|
56
|
+
has_report :shares, Integer
|
57
|
+
|
58
|
+
# @note This is not the total number of subscribers gained by the video’s
|
59
|
+
# channel, but the subscribers gained *from* the video’s page.
|
60
|
+
# @macro report_by_day_and_country
|
61
|
+
has_report :subscribers_gained, Integer
|
62
|
+
|
63
|
+
# @note This is not the total number of subscribers lost by the video’s
|
64
|
+
# channel, but the subscribers lost *from* the video’s page.
|
65
|
+
# @macro report_by_day_and_country
|
66
|
+
has_report :subscribers_lost, Integer
|
67
|
+
|
68
|
+
# @macro report_by_day_and_country
|
69
|
+
has_report :videos_added_to_playlists, Integer
|
70
|
+
|
71
|
+
# @macro report_by_day_and_country
|
72
|
+
has_report :videos_removed_from_playlists, Integer
|
73
|
+
|
74
|
+
# @macro report_by_day_and_state
|
75
|
+
has_report :average_view_duration, Integer
|
76
|
+
|
77
|
+
# @macro report_by_day_and_state
|
78
|
+
has_report :average_view_percentage, Float
|
79
|
+
|
80
|
+
# @macro report_by_day_and_state
|
81
|
+
has_report :annotation_clicks, Integer
|
82
|
+
|
83
|
+
# @macro report_by_day_and_state
|
84
|
+
has_report :annotation_click_through_rate, Float
|
85
|
+
|
86
|
+
# @macro report_by_day_and_state
|
87
|
+
has_report :annotation_close_rate, Float
|
88
|
+
|
89
|
+
# @macro report_by_day_and_state
|
90
|
+
has_report :card_impressions, Integer
|
91
|
+
|
92
|
+
# @macro report_by_day_and_state
|
93
|
+
has_report :card_clicks, Integer
|
94
|
+
|
95
|
+
# @macro report_by_day_and_state
|
96
|
+
has_report :card_click_rate, Float
|
97
|
+
|
98
|
+
# @macro report_by_day_and_state
|
99
|
+
has_report :card_teaser_impressions, Integer
|
100
|
+
|
101
|
+
# @macro report_by_day_and_state
|
102
|
+
has_report :card_teaser_clicks, Integer
|
103
|
+
|
104
|
+
# @macro report_by_day_and_state
|
105
|
+
has_report :card_teaser_click_rate, Float
|
106
|
+
|
107
|
+
# @macro report_by_day_and_country
|
108
|
+
has_report :estimated_revenue, Float
|
109
|
+
|
110
|
+
# @macro report_by_day_and_country
|
111
|
+
has_report :ad_impressions, Integer
|
112
|
+
|
113
|
+
# @macro report_by_day_and_country
|
114
|
+
has_report :monetized_playbacks, Integer
|
115
|
+
|
116
|
+
# @macro report_by_day_and_country
|
117
|
+
has_report :playback_based_cpm, Float
|
118
|
+
|
119
|
+
### PRIVATE API ###
|
120
|
+
|
121
|
+
# @private
|
122
|
+
def initialize(options = {})
|
123
|
+
@id = options[:id]
|
124
|
+
@auth = options[:auth]
|
125
|
+
@group_info = options[:group_info] if options[:group_info]
|
126
|
+
end
|
127
|
+
|
128
|
+
# @private
|
129
|
+
# Tells `has_reports` to retrieve group reports from the Analytics API.
|
130
|
+
def reports_params
|
131
|
+
{}.tap do |params|
|
132
|
+
if auth.owner_name
|
133
|
+
params[:ids] = "contentOwner==#{auth.owner_name}"
|
134
|
+
else
|
135
|
+
params[:ids] = "channel==mine"
|
136
|
+
end
|
137
|
+
params[:filters] = "group==#{id}"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def all_video_ids
|
142
|
+
resource_ids = group_items.map{|item| item.data['resource']['id']}.uniq
|
143
|
+
case group_info.data["itemType"]
|
144
|
+
when "youtube#video"
|
145
|
+
resource_ids
|
146
|
+
when "youtube#channel"
|
147
|
+
resource_ids.flat_map do |channel_id|
|
148
|
+
Yt::Channel.new(id: channel_id, auth: @auth).videos.map(&:id)
|
149
|
+
end
|
150
|
+
else
|
151
|
+
[]
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def videos
|
156
|
+
all_video_ids.each_slice(50).flat_map do |video_ids|
|
157
|
+
conditions = {id: video_ids.join(',')}
|
158
|
+
conditions[:part] = 'snippet,status,statistics,contentDetails'
|
159
|
+
Collections::Videos.new(auth: @auth).where(conditions).map(&:itself)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def all_channel_ids
|
164
|
+
resource_ids = group_items.map {|item| item.data['resource']['id']}.uniq
|
165
|
+
case group_info.data['itemType']
|
166
|
+
when "youtube#video"
|
167
|
+
resource_ids.flat_map do |video_id|
|
168
|
+
Yt::Video.new(id: video_id, auth: @auth).channel_id
|
169
|
+
end.uniq
|
170
|
+
when "youtube#channel"
|
171
|
+
resource_ids
|
172
|
+
else
|
173
|
+
[]
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def channels
|
178
|
+
all_channel_ids.each_slice(50).flat_map do |channel_ids|
|
179
|
+
conditions = {id: channel_ids.join(',')}
|
180
|
+
conditions[:part] = 'snippet'
|
181
|
+
Collections::Channels.new(auth: @auth).where(conditions).map(&:itself)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|