poms 1.2.2 → 2.0.0.a
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +17 -0
- data/.rubocop.yml +24 -0
- data/.ruby-version +1 -0
- data/.todo.reek +50 -0
- data/CHANGELOG.md +1 -0
- data/Gemfile +4 -0
- data/README.md +54 -6
- data/bin/ci-run +57 -0
- data/bin/ci-setup +46 -0
- data/bin/reek +16 -0
- data/bin/rspec +16 -0
- data/bin/rubocop +16 -0
- data/examples/fetch.rb +16 -0
- data/examples/search.rb +17 -0
- data/lib/poms.rb +84 -156
- data/lib/poms/api/auth.rb +42 -22
- data/lib/poms/api/client.rb +58 -0
- data/lib/poms/api/drivers/net_http.rb +69 -0
- data/lib/poms/api/json_client.rb +35 -0
- data/lib/poms/api/pagination_client.rb +67 -0
- data/lib/poms/api/request.rb +29 -31
- data/lib/poms/api/response.rb +31 -0
- data/lib/poms/api/search.rb +38 -0
- data/lib/poms/api/uris.rb +10 -0
- data/lib/poms/api/uris/media.rb +41 -0
- data/lib/poms/api/uris/schedule.rb +28 -0
- data/lib/poms/configuration.rb +59 -0
- data/lib/poms/errors.rb +26 -0
- data/lib/poms/fields.rb +26 -3
- data/lib/poms/timestamp.rb +10 -3
- data/lib/poms/version.rb +1 -1
- data/poms.gemspec +10 -11
- metadata +59 -128
- data/Gemfile.lock +0 -120
- data/lib/poms/base.rb +0 -8
- data/lib/poms/broadcast.rb +0 -55
- data/lib/poms/builder.rb +0 -92
- data/lib/poms/builderless/broadcast.rb +0 -37
- data/lib/poms/builderless/clip.rb +0 -36
- data/lib/poms/connect.rb +0 -11
- data/lib/poms/has_ancestors.rb +0 -54
- data/lib/poms/has_base_attributes.rb +0 -29
- data/lib/poms/merged_series.rb +0 -28
- data/lib/poms/poms_error.rb +0 -5
- data/lib/poms/schedule_event.rb +0 -34
- data/lib/poms/season.rb +0 -13
- data/lib/poms/series.rb +0 -12
- data/lib/poms/views.rb +0 -52
- data/spec/fabricators/poms_fabricator.rb +0 -44
- data/spec/fixtures/poms_broadcast.json +0 -318
- data/spec/fixtures/poms_broadcast_multiple_schedule_events.json +0 -354
- data/spec/fixtures/poms_broadcast_pippi.json +0 -180
- data/spec/fixtures/poms_group.json +0 -1087
- data/spec/fixtures/poms_series.json +0 -49
- data/spec/fixtures/poms_single_broadcast_by_channel.json +0 -136
- data/spec/fixtures/poms_zapp.json +0 -47363
- data/spec/fixtures/vcr_cassettes/poms/builderless/broadcast_starts_at/returns_a_datetime.yml +0 -53
- data/spec/fixtures/vcr_cassettes/poms/builderless/clip/has_a_position.yml +0 -43
- data/spec/fixtures/vcr_cassettes/poms/builderless/clip/has_a_title.yml +0 -43
- data/spec/fixtures/vcr_cassettes/poms/builderless/clip/has_a_video_url.yml +0 -43
- data/spec/fixtures/vcr_cassettes/poms/builderless/clip/has_an_image_id.yml +0 -43
- data/spec/fixtures/vcr_cassettes/poms/fetch_broadcasts_fetch_current_broadcast_fetches_the_current_broadcast.yml +0 -60
- data/spec/fixtures/vcr_cassettes/poms/fetch_broadcasts_fetch_next_broadcast_and_key_fetches_the_current_broadcast.yml +0 -68
- data/spec/fixtures/vcr_cassettes/poms/fetch_broadcasts_fetch_next_broadcast_and_key_returns_the_key.yml +0 -68
- data/spec/fixtures/vcr_cassettes/poms/merged_series/turns_the_json_into_a_hash.yml +0 -46
- data/spec/fixtures/vcr_cassettes/poms_fetch/a_broadcast_has_scheduled_events_with_last_with_starts_at.yml +0 -43
- data/spec/fixtures/vcr_cassettes/poms_fetch/a_clip_has_a_title.yml +0 -43
- data/spec/fixtures/vcr_cassettes/poms_fetch/an_aankeiler_has_images.yml +0 -43
- data/spec/fixtures/vcr_cassettes/poms_fetch_broadcasts_for_serie/returns_nil_when_a_broadcast_does_not_exist.yml +0 -42
- data/spec/fixtures/vcr_cassettes/poms_fetch_current_broadcast/has_a_mid.yml +0 -51
- data/spec/fixtures/vcr_cassettes/poms_fetch_current_broadcast/has_a_title.yml +0 -51
- data/spec/fixtures/vcr_cassettes/poms_fetch_current_broadcast/has_scheduled_events_with_last_with_starts_at.yml +0 -51
- data/spec/fixtures/vcr_cassettes/poms_fetch_current_broadcast_and_key/has_a_broadcast_with_a_mid.yml +0 -51
- data/spec/fixtures/vcr_cassettes/poms_fetch_current_broadcast_and_key/has_a_key.yml +0 -51
- data/spec/fixtures/vcr_cassettes/poms_fetch_descendant_mids/returns_a_list_of_mids.yml +0 -42
- data/spec/fixtures/vcr_cassettes/poms_fetch_descendants_for_serie/builds_a_list_of_broadcasts.yml +0 -102755
- data/spec/fixtures/vcr_cassettes/poms_fetch_group/has_a_child_with_a_media_type.yml +0 -448
- data/spec/fixtures/vcr_cassettes/poms_fetch_group/has_a_child_with_a_title.yml +0 -448
- data/spec/fixtures/vcr_cassettes/poms_fetch_playlist_clips/creates_an_array_of_clips.yml +0 -1051
- data/spec/integration/poms_spec.rb +0 -87
- data/spec/lib/poms/api/auth_spec.rb +0 -34
- data/spec/lib/poms/api/request_spec.rb +0 -30
- data/spec/lib/poms/broadcast_spec.rb +0 -49
- data/spec/lib/poms/builder_spec.rb +0 -53
- data/spec/lib/poms/builderless/broadcast_spec.rb +0 -11
- data/spec/lib/poms/builderless/clip_spec.rb +0 -20
- data/spec/lib/poms/fields_spec.rb +0 -63
- data/spec/lib/poms/merged_series_spec.rb +0 -26
- data/spec/lib/poms/schedule_event_spec.rb +0 -15
- data/spec/lib/poms/timestamp_spec.rb +0 -13
- data/spec/lib/poms/views_spec.rb +0 -38
- data/spec/lib/poms_spec.rb +0 -137
- data/spec/spec_helper.rb +0 -32
data/Gemfile.lock
DELETED
@@ -1,120 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
poms (1.2.2)
|
5
|
-
activesupport
|
6
|
-
|
7
|
-
GEM
|
8
|
-
remote: https://rubygems.org/
|
9
|
-
specs:
|
10
|
-
activesupport (4.2.1)
|
11
|
-
i18n (~> 0.7)
|
12
|
-
json (~> 1.7, >= 1.7.7)
|
13
|
-
minitest (~> 5.1)
|
14
|
-
thread_safe (~> 0.3, >= 0.3.4)
|
15
|
-
tzinfo (~> 1.1)
|
16
|
-
addressable (2.4.0)
|
17
|
-
ast (2.2.0)
|
18
|
-
astrolabe (1.3.1)
|
19
|
-
parser (~> 2.2)
|
20
|
-
celluloid (0.16.0)
|
21
|
-
timers (~> 4.0.0)
|
22
|
-
coderay (1.1.0)
|
23
|
-
crack (0.4.3)
|
24
|
-
safe_yaml (~> 1.0.0)
|
25
|
-
diff-lcs (1.2.5)
|
26
|
-
fabrication (2.12.2)
|
27
|
-
ffi (1.9.8)
|
28
|
-
formatador (0.2.5)
|
29
|
-
guard (2.12.5)
|
30
|
-
formatador (>= 0.2.4)
|
31
|
-
listen (~> 2.7)
|
32
|
-
lumberjack (~> 1.0)
|
33
|
-
nenv (~> 0.1)
|
34
|
-
notiffany (~> 0.0)
|
35
|
-
pry (>= 0.9.12)
|
36
|
-
shellany (~> 0.0)
|
37
|
-
thor (>= 0.18.1)
|
38
|
-
guard-compat (1.2.1)
|
39
|
-
guard-rspec (4.5.0)
|
40
|
-
guard (~> 2.1)
|
41
|
-
guard-compat (~> 1.1)
|
42
|
-
rspec (>= 2.99.0, < 4.0)
|
43
|
-
hashdiff (0.2.3)
|
44
|
-
hitimes (1.2.2)
|
45
|
-
i18n (0.7.0)
|
46
|
-
json (1.8.2)
|
47
|
-
listen (2.10.0)
|
48
|
-
celluloid (~> 0.16.0)
|
49
|
-
rb-fsevent (>= 0.9.3)
|
50
|
-
rb-inotify (>= 0.9)
|
51
|
-
lumberjack (1.0.9)
|
52
|
-
method_source (0.8.2)
|
53
|
-
minitest (5.7.0)
|
54
|
-
nenv (0.2.0)
|
55
|
-
notiffany (0.0.6)
|
56
|
-
nenv (~> 0.1)
|
57
|
-
shellany (~> 0.0)
|
58
|
-
parser (2.3.0.1)
|
59
|
-
ast (~> 2.2)
|
60
|
-
powerpack (0.1.1)
|
61
|
-
pry (0.10.1)
|
62
|
-
coderay (~> 1.1.0)
|
63
|
-
method_source (~> 0.8.1)
|
64
|
-
slop (~> 3.4)
|
65
|
-
rainbow (2.0.0)
|
66
|
-
rake (10.4.2)
|
67
|
-
rb-fsevent (0.9.4)
|
68
|
-
rb-inotify (0.9.5)
|
69
|
-
ffi (>= 0.5.0)
|
70
|
-
rspec (3.2.0)
|
71
|
-
rspec-core (~> 3.2.0)
|
72
|
-
rspec-expectations (~> 3.2.0)
|
73
|
-
rspec-mocks (~> 3.2.0)
|
74
|
-
rspec-core (3.2.2)
|
75
|
-
rspec-support (~> 3.2.0)
|
76
|
-
rspec-expectations (3.2.0)
|
77
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
78
|
-
rspec-support (~> 3.2.0)
|
79
|
-
rspec-mocks (3.2.1)
|
80
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
81
|
-
rspec-support (~> 3.2.0)
|
82
|
-
rspec-support (3.2.2)
|
83
|
-
rubocop (0.34.2)
|
84
|
-
astrolabe (~> 1.3)
|
85
|
-
parser (>= 2.2.2.5, < 3.0)
|
86
|
-
powerpack (~> 0.1)
|
87
|
-
rainbow (>= 1.99.1, < 3.0)
|
88
|
-
ruby-progressbar (~> 1.4)
|
89
|
-
ruby-progressbar (1.7.5)
|
90
|
-
safe_yaml (1.0.4)
|
91
|
-
shellany (0.0.1)
|
92
|
-
slop (3.6.0)
|
93
|
-
thor (0.19.1)
|
94
|
-
thread_safe (0.3.5)
|
95
|
-
timecop (0.7.3)
|
96
|
-
timers (4.0.1)
|
97
|
-
hitimes
|
98
|
-
tzinfo (1.2.2)
|
99
|
-
thread_safe (~> 0.1)
|
100
|
-
vcr (2.9.3)
|
101
|
-
webmock (1.22.6)
|
102
|
-
addressable (>= 2.3.6)
|
103
|
-
crack (>= 0.3.2)
|
104
|
-
hashdiff
|
105
|
-
|
106
|
-
PLATFORMS
|
107
|
-
ruby
|
108
|
-
|
109
|
-
DEPENDENCIES
|
110
|
-
bundler (~> 1.3)
|
111
|
-
fabrication
|
112
|
-
guard
|
113
|
-
guard-rspec
|
114
|
-
poms!
|
115
|
-
rake
|
116
|
-
rspec
|
117
|
-
rubocop (~> 0.34.1)
|
118
|
-
timecop
|
119
|
-
vcr
|
120
|
-
webmock
|
data/lib/poms/base.rb
DELETED
data/lib/poms/broadcast.rb
DELETED
@@ -1,55 +0,0 @@
|
|
1
|
-
require 'poms/has_ancestors'
|
2
|
-
require 'poms/has_base_attributes'
|
3
|
-
|
4
|
-
module Poms
|
5
|
-
# POMS wrapper for an episode of a Serie.
|
6
|
-
class Broadcast < Poms::Builder::NestedOpenStruct
|
7
|
-
include Poms::HasAncestors
|
8
|
-
include Poms::HasBaseAttributes
|
9
|
-
|
10
|
-
def initialize(hash)
|
11
|
-
super
|
12
|
-
process_schedule_events
|
13
|
-
end
|
14
|
-
|
15
|
-
def process_schedule_events
|
16
|
-
if schedule_events
|
17
|
-
schedule_events.select! { |e| e.channel.match Poms::VALID_CHANNELS }
|
18
|
-
end
|
19
|
-
self.schedule_events = schedule_events.map do |e|
|
20
|
-
Poms::ScheduleEvent.new e.marshal_dump
|
21
|
-
end if schedule_events
|
22
|
-
end
|
23
|
-
|
24
|
-
def series_mid
|
25
|
-
serie.try :mid_ref || serie.mid
|
26
|
-
end
|
27
|
-
|
28
|
-
def odi_streams
|
29
|
-
return [] if locations.nil? || locations.empty?
|
30
|
-
odi_streams = locations.select { |l| l.program_url.match(/^odi/) }
|
31
|
-
streams = odi_streams.map do |l|
|
32
|
-
l.program_url.match(%r{^[\w+]+\:\/\/[\w\.]+\/video\/(\w+)\/\w+})[1]
|
33
|
-
end
|
34
|
-
streams.uniq
|
35
|
-
end
|
36
|
-
|
37
|
-
def available_until
|
38
|
-
return if predictions.blank?
|
39
|
-
timestamp = offline_timestamp(predictions)
|
40
|
-
return Time.at(timestamp / 1000).to_datetime if timestamp
|
41
|
-
end
|
42
|
-
|
43
|
-
private
|
44
|
-
|
45
|
-
def offline_timestamp(predictions, platform = 'INTERNETVOD')
|
46
|
-
timestamps = predictions.map do |p|
|
47
|
-
p.publish_stop if p.platform == platform
|
48
|
-
end.compact
|
49
|
-
timestamps.first unless timestamps.empty?
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
class Strand < Broadcast
|
54
|
-
end
|
55
|
-
end
|
data/lib/poms/builder.rb
DELETED
@@ -1,92 +0,0 @@
|
|
1
|
-
require 'ostruct'
|
2
|
-
require 'active_support/all'
|
3
|
-
|
4
|
-
module Poms
|
5
|
-
# Builds the correct object based on the result from POMS.
|
6
|
-
class Builder
|
7
|
-
SUPPORTED_CLASSES = %w(Broadcast Season Series Views Typeless)
|
8
|
-
|
9
|
-
def self.process_hash(hash)
|
10
|
-
return unless hash
|
11
|
-
underscored_hash = hash.each_with_object({}) do |(k, v), res|
|
12
|
-
res[k.underscore] = v
|
13
|
-
end
|
14
|
-
class_name = pomsify_class_name(underscored_hash['type'])
|
15
|
-
klass = poms_class(class_name)
|
16
|
-
klass.send(:new, underscored_hash)
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.pomsify_class_name(class_name)
|
20
|
-
class_name = class_name.blank? ? 'Typeless' : class_name.capitalize
|
21
|
-
class_name =
|
22
|
-
'Poms' + class_name unless SUPPORTED_CLASSES.include? class_name
|
23
|
-
class_name
|
24
|
-
end
|
25
|
-
|
26
|
-
def self.poms_class(class_name)
|
27
|
-
Poms.const_get class_name
|
28
|
-
rescue NameError
|
29
|
-
Poms.const_set class_name, Class.new(Poms::Builder::NestedOpenStruct)
|
30
|
-
end
|
31
|
-
|
32
|
-
# An OpenStruct subclass that allows nesting to simulate a hash.
|
33
|
-
class NestedOpenStruct < OpenStruct
|
34
|
-
include Poms::Base
|
35
|
-
|
36
|
-
def initialize(hash)
|
37
|
-
@hash = hash
|
38
|
-
@hash.each do |k, v|
|
39
|
-
process_key_value(k, v)
|
40
|
-
end
|
41
|
-
super hash
|
42
|
-
end
|
43
|
-
|
44
|
-
# rubocop:disable Metrics/MethodLength
|
45
|
-
def process_key_value(k, v)
|
46
|
-
case v
|
47
|
-
when Array
|
48
|
-
process_array(k, v)
|
49
|
-
when Hash
|
50
|
-
@hash.send('[]=', k, Poms::Builder.process_hash(v))
|
51
|
-
when String, Integer
|
52
|
-
case k
|
53
|
-
when 'start', 'end', 'sort_date'
|
54
|
-
@hash.send('[]=', k, Time.at(v / 1000))
|
55
|
-
end
|
56
|
-
when NilClass, FalseClass, TrueClass, Time, Poms::Typeless
|
57
|
-
# do nothing
|
58
|
-
else
|
59
|
-
fail Poms::Exceptions::UnkownStructure,
|
60
|
-
"Error processing #{v.class}: #{v}, which was expected to be " \
|
61
|
-
'a String or Array'
|
62
|
-
end
|
63
|
-
end
|
64
|
-
# rubocop:enable Metrics/MethodLength
|
65
|
-
|
66
|
-
def process_array(key, value)
|
67
|
-
struct_array = value.map do |element|
|
68
|
-
process_element(element)
|
69
|
-
end
|
70
|
-
@hash.send('[]=', key, struct_array)
|
71
|
-
end
|
72
|
-
|
73
|
-
def process_element(element)
|
74
|
-
case element
|
75
|
-
when String, Integer
|
76
|
-
element
|
77
|
-
when Hash
|
78
|
-
Poms::Builder.process_hash element
|
79
|
-
else
|
80
|
-
fail Poms::Exceptions::UnkownStructure,
|
81
|
-
"Error processing #{element}: which was expected to be a " \
|
82
|
-
'String nor a Hash'
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
module Exceptions
|
89
|
-
class UnkownStructure < StandardError
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
@@ -1,37 +0,0 @@
|
|
1
|
-
require 'poms/fields'
|
2
|
-
|
3
|
-
module Poms
|
4
|
-
module Builderless
|
5
|
-
# A single broadcast (episode) fetched from Poms
|
6
|
-
class Broadcast
|
7
|
-
def initialize(hash)
|
8
|
-
@hash = hash
|
9
|
-
end
|
10
|
-
|
11
|
-
def title
|
12
|
-
Fields.title(@hash)
|
13
|
-
end
|
14
|
-
|
15
|
-
def mid
|
16
|
-
@hash['mid']
|
17
|
-
end
|
18
|
-
|
19
|
-
def schedule_events
|
20
|
-
@hash['scheduleEvents'].map do |event|
|
21
|
-
Event.new(event)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
# A single event of a broadcast.
|
27
|
-
class Event
|
28
|
-
def initialize(hash)
|
29
|
-
@hash = hash
|
30
|
-
end
|
31
|
-
|
32
|
-
def starts_at
|
33
|
-
Timestamp.convert(@hash['start'])
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
@@ -1,36 +0,0 @@
|
|
1
|
-
require 'poms/fields'
|
2
|
-
|
3
|
-
module Poms
|
4
|
-
module Builderless
|
5
|
-
# A single clip fetched from Poms
|
6
|
-
class Clip
|
7
|
-
def initialize(hash)
|
8
|
-
@hash = hash
|
9
|
-
end
|
10
|
-
|
11
|
-
def title
|
12
|
-
Fields.title(@hash)
|
13
|
-
end
|
14
|
-
|
15
|
-
def mid
|
16
|
-
@hash['mid']
|
17
|
-
end
|
18
|
-
|
19
|
-
def video_url
|
20
|
-
return unless @hash['locations']
|
21
|
-
@hash['locations'].first['programUrl']
|
22
|
-
end
|
23
|
-
|
24
|
-
def position
|
25
|
-
return unless @hash['memberOf']
|
26
|
-
@hash['memberOf'].first['index']
|
27
|
-
end
|
28
|
-
|
29
|
-
def image_id
|
30
|
-
images = Fields.images(@hash)
|
31
|
-
return unless images
|
32
|
-
Fields.image_id(images.first)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
data/lib/poms/connect.rb
DELETED
data/lib/poms/has_ancestors.rb
DELETED
@@ -1,54 +0,0 @@
|
|
1
|
-
module Poms
|
2
|
-
# Mixin for a class that has ancestors.
|
3
|
-
module HasAncestors
|
4
|
-
module ClassMethods
|
5
|
-
end
|
6
|
-
|
7
|
-
module InstanceMethods
|
8
|
-
def series
|
9
|
-
return @series if @series
|
10
|
-
return [] if descendant_of.blank?
|
11
|
-
descendant_series = descendant_of.reject do |obj|
|
12
|
-
obj.class != Poms::Series
|
13
|
-
end
|
14
|
-
if descendant_series.blank?
|
15
|
-
descendant_of
|
16
|
-
else
|
17
|
-
descendant_series
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
def serie
|
22
|
-
series.first
|
23
|
-
end
|
24
|
-
|
25
|
-
def serie_mid
|
26
|
-
return nil if serie.nil?
|
27
|
-
serie.mid_ref || serie.mid
|
28
|
-
end
|
29
|
-
|
30
|
-
def ancestor_mids
|
31
|
-
return @ancestor_mids if @ancestor_mids
|
32
|
-
@ancestor_mids = (descendant_of_mids +
|
33
|
-
episode_of_mids).flatten.compact.uniq
|
34
|
-
end
|
35
|
-
|
36
|
-
def descendant_of_mids
|
37
|
-
descendant_of.map(&:mid_ref)
|
38
|
-
rescue
|
39
|
-
[]
|
40
|
-
end
|
41
|
-
|
42
|
-
def episode_of_mids
|
43
|
-
episode_of.map(&:mid_ref)
|
44
|
-
rescue
|
45
|
-
[]
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def self.included(receiver)
|
50
|
-
receiver.extend ClassMethods
|
51
|
-
receiver.send :include, InstanceMethods
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
@@ -1,29 +0,0 @@
|
|
1
|
-
module Poms
|
2
|
-
# Mixin for classes with a title and description.
|
3
|
-
module HasBaseAttributes
|
4
|
-
def title
|
5
|
-
return @title if @title
|
6
|
-
main_title = select_title_by_type 'MAIN'
|
7
|
-
sub_title = select_title_by_type 'SUB'
|
8
|
-
if sub_title && sub_title.match(main_title)
|
9
|
-
@titel = sub_title
|
10
|
-
else
|
11
|
-
@titel = [main_title, sub_title].compact.join(': ')
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
def description
|
16
|
-
@description ||= begin
|
17
|
-
descriptions.first.value
|
18
|
-
rescue
|
19
|
-
nil
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
private
|
24
|
-
|
25
|
-
def select_title_by_type(type)
|
26
|
-
titles.select { |t| t.type == type }.map(&:value).first
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|