sportradar-api 0.9.59 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.env.sample +5 -1
- data/Gemfile.lock +4 -4
- data/README.md +1 -1
- data/lib/sportradar/api.rb +1 -0
- data/lib/sportradar/api/basketball.rb +15 -6
- data/lib/sportradar/api/basketball/game.rb +285 -0
- data/lib/sportradar/api/basketball/injury.rb +6 -6
- data/lib/sportradar/api/basketball/nba.rb +22 -25
- data/lib/sportradar/api/basketball/nba/conference.rb +1 -1
- data/lib/sportradar/api/basketball/nba/division.rb +2 -1
- data/lib/sportradar/api/basketball/nba/game.rb +12 -249
- data/lib/sportradar/api/basketball/nba/hierarchy.rb +5 -4
- data/lib/sportradar/api/basketball/nba/player.rb +8 -8
- data/lib/sportradar/api/basketball/nba/quarter.rb +1 -1
- data/lib/sportradar/api/basketball/nba/schedule.rb +17 -6
- data/lib/sportradar/api/basketball/nba/scoring.rb +15 -95
- data/lib/sportradar/api/basketball/nba/season.rb +6 -5
- data/lib/sportradar/api/basketball/nba/team.rb +5 -122
- data/lib/sportradar/api/basketball/ncaamb.rb +133 -0
- data/lib/sportradar/api/basketball/ncaamb/conference.rb +41 -0
- data/lib/sportradar/api/basketball/ncaamb/division.rb +42 -0
- data/lib/sportradar/api/basketball/ncaamb/game.rb +60 -0
- data/lib/sportradar/api/basketball/ncaamb/half.rb +49 -0
- data/lib/sportradar/api/basketball/ncaamb/hierarchy.rb +65 -0
- data/lib/sportradar/api/basketball/ncaamb/player.rb +162 -0
- data/lib/sportradar/api/basketball/ncaamb/schedule.rb +41 -0
- data/lib/sportradar/api/basketball/ncaamb/season.rb +42 -0
- data/lib/sportradar/api/basketball/ncaamb/team.rb +40 -0
- data/lib/sportradar/api/basketball/play.rb +15 -5
- data/lib/sportradar/api/basketball/plays.rb +1 -0
- data/lib/sportradar/api/basketball/plays/base.rb +30 -6
- data/lib/sportradar/api/basketball/plays/deadball.rb +11 -0
- data/lib/sportradar/api/basketball/plays/end_period.rb +2 -2
- data/lib/sportradar/api/basketball/plays/free_throw_miss.rb +3 -1
- data/lib/sportradar/api/basketball/plays/rebound.rb +5 -1
- data/lib/sportradar/api/basketball/plays/shot_made.rb +4 -3
- data/lib/sportradar/api/basketball/plays/shot_miss.rb +4 -3
- data/lib/sportradar/api/basketball/plays/timeout.rb +0 -3
- data/lib/sportradar/api/basketball/plays/turnover.rb +1 -1
- data/lib/sportradar/api/basketball/record.rb +32 -0
- data/lib/sportradar/api/basketball/scoring.rb +103 -0
- data/lib/sportradar/api/basketball/team.rb +148 -0
- data/lib/sportradar/api/data.rb +2 -0
- data/lib/sportradar/api/poll.rb +40 -0
- data/lib/sportradar/api/request.rb +15 -6
- data/lib/sportradar/api/version.rb +1 -1
- metadata +18 -7
- data/lib/sportradar/api/basketball/bio.rb +0 -33
- data/lib/sportradar/api/basketball/nba/overview.rb +0 -64
- data/lib/sportradar/api/basketball/nba/record.rb +0 -34
- data/lib/sportradar/api/basketball/overview.rb +0 -74
- data/lib/sportradar/api/basketball/profile.rb +0 -48
@@ -11,6 +11,7 @@ require_relative 'plays/shot_miss'
|
|
11
11
|
require_relative 'plays/free_throw_made'
|
12
12
|
require_relative 'plays/free_throw_miss'
|
13
13
|
require_relative 'plays/jumpball'
|
14
|
+
require_relative 'plays/deadball'
|
14
15
|
require_relative 'plays/kickball'
|
15
16
|
require_relative 'plays/lineup_change'
|
16
17
|
require_relative 'plays/offensive_foul'
|
@@ -3,17 +3,23 @@ module Sportradar
|
|
3
3
|
module Basketball
|
4
4
|
class Play
|
5
5
|
class Base < Data
|
6
|
-
attr_accessor :response, :id, :clock, :event_type, :description, :statistics, :score, :team_id, :player_id, :quarter, :updated, :location, :possession, :on_court
|
6
|
+
attr_accessor :response, :id, :clock, :event_type, :description, :statistics, :score, :team_id, :player_id, :quarter, :half, :updated, :location, :possession, :on_court, :game_seconds, :identifier
|
7
7
|
alias :type :event_type
|
8
8
|
|
9
9
|
def initialize(data, **opts)
|
10
10
|
@response = data
|
11
11
|
@api = opts[:api]
|
12
12
|
|
13
|
-
@quarter = opts[:quarter].sequence.to_i rescue opts[:quarter].to_i
|
14
13
|
@id = data['id']
|
15
14
|
|
16
|
-
|
15
|
+
@quarter = opts[:quarter].sequence.to_i rescue opts[:quarter].to_i
|
16
|
+
@half = opts[:half].sequence.to_i rescue opts[:half].to_i
|
17
|
+
|
18
|
+
update(data, **opts)
|
19
|
+
end
|
20
|
+
|
21
|
+
def period
|
22
|
+
@quarter.nonzero? || @half.nonzero?
|
17
23
|
end
|
18
24
|
|
19
25
|
# def game
|
@@ -66,7 +72,10 @@ module Sportradar
|
|
66
72
|
def nba_game_seconds
|
67
73
|
([quarter, 4].min * 720) + ([quarter - 4, 0].max * 300) - clock_seconds # seconds elapsed in game, only works for NBA
|
68
74
|
end
|
69
|
-
|
75
|
+
|
76
|
+
private def ncaa_game_seconds
|
77
|
+
([half, 2].min * 1200) + ([half - 2, 0].max * 300) - clock_seconds # seconds elapsed in game, only works for NBA
|
78
|
+
end
|
70
79
|
|
71
80
|
def update(data, **opts)
|
72
81
|
@event_type = data['event_type'] # "lineupchange",
|
@@ -78,14 +87,29 @@ module Sportradar
|
|
78
87
|
@location = data['location'] # {"coord_x"=>"0", "coord_y"=>"0"},
|
79
88
|
@possession = data['possession'] # {"name"=>"Knicks", "market"=>"New York", "id"=>"583ec70e-fb46-11e1-82cb-f4ce4684ea4c"},
|
80
89
|
# @on_court = data['on_court'] # hash
|
90
|
+
|
91
|
+
handle_time(data, **opts)
|
81
92
|
parse_statistics(data) if data['statistics']
|
82
93
|
end
|
83
94
|
|
95
|
+
def handle_time(data, **opts)
|
96
|
+
@game_seconds = if opts[:quarter]
|
97
|
+
@quarter = opts[:quarter].sequence.to_i rescue opts[:quarter].to_i
|
98
|
+
@identifier = "#{quarter}_#{(720 - clock_seconds).to_s.rjust(3, '0')}".to_i
|
99
|
+
nba_game_seconds
|
100
|
+
elsif opts[:half]
|
101
|
+
@half = opts[:half].sequence.to_i rescue opts[:half].to_i
|
102
|
+
@identifier = "#{half}_#{(1200 - clock_seconds).to_s.rjust(4, '0')}".to_i
|
103
|
+
ncaa_game_seconds
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
84
107
|
def parse_statistics(data)
|
85
108
|
return unless data['statistics']
|
86
109
|
@statistics = data['statistics']
|
87
|
-
stat =
|
88
|
-
stat =
|
110
|
+
stat = @statistics.detect { |hash| hash['type'] == base_key }
|
111
|
+
# stat = data.dig('statistics', base_key) rescue data.dig('statistics', 0, base_key)
|
112
|
+
# stat = stat[0] if stat.is_a?(Array) # sometimes SR has an array of identical assist hashes
|
89
113
|
@team = stat['team']
|
90
114
|
@team_id = @team['id'] if @team
|
91
115
|
@player = stat['player']
|
@@ -3,7 +3,7 @@ module Sportradar
|
|
3
3
|
module Basketball
|
4
4
|
class EndPeriod < Play::Base
|
5
5
|
def display_type
|
6
|
-
|
6
|
+
''
|
7
7
|
end
|
8
8
|
|
9
9
|
def end_of_period?
|
@@ -14,7 +14,7 @@ module Sportradar
|
|
14
14
|
["End of 1st Quarter.", "End of 3rd Quarter."].include? description
|
15
15
|
end
|
16
16
|
def end_of_regulation?
|
17
|
-
description == "End of 4th Quarter."
|
17
|
+
description == "End of 4th Quarter." || description == "End of 2nd Half."
|
18
18
|
end
|
19
19
|
def end_of_ot?
|
20
20
|
description.start_with?("End of ") && description.end_with?(" OT.")
|
@@ -15,7 +15,9 @@ module Sportradar
|
|
15
15
|
false
|
16
16
|
end
|
17
17
|
def player_id
|
18
|
-
|
18
|
+
stats = @statistics
|
19
|
+
stats = stats[0] if stats.is_a?(Array)
|
20
|
+
@player_id ||= stats&.dig("player", "id") # safe operator is specifically for lane violations, which have no player_id and should not be assigned a player
|
19
21
|
end
|
20
22
|
end
|
21
23
|
end
|
@@ -16,7 +16,11 @@ module Sportradar
|
|
16
16
|
end
|
17
17
|
def parse_statistics(data)
|
18
18
|
super
|
19
|
-
|
19
|
+
stat = @statistics.dig(base_key)
|
20
|
+
stat = stat[0] if stat.is_a?(Array)
|
21
|
+
@rebound_type = stat['rebound_type']
|
22
|
+
rescue => e
|
23
|
+
# noop => bad data from SR
|
20
24
|
end
|
21
25
|
end
|
22
26
|
end
|
@@ -8,9 +8,10 @@ module Sportradar
|
|
8
8
|
end
|
9
9
|
def parse_statistics(data)
|
10
10
|
super
|
11
|
-
|
12
|
-
@
|
13
|
-
@
|
11
|
+
stat = @statistics.detect { |hash| hash['type'] == base_key }
|
12
|
+
@shot_type = stat['shot_type']
|
13
|
+
@shot_type_desc = stat['shot_type_desc']
|
14
|
+
@assist = Assist.new(data, quarter: @quarter, half: @half) if @statistics.detect { |hash| hash['type'] == 'assist' }
|
14
15
|
end
|
15
16
|
def made?
|
16
17
|
true
|
@@ -8,9 +8,10 @@ module Sportradar
|
|
8
8
|
end
|
9
9
|
def parse_statistics(data)
|
10
10
|
super
|
11
|
-
|
12
|
-
@
|
13
|
-
@
|
11
|
+
stat = @statistics.detect { |hash| hash['type'] == base_key }
|
12
|
+
@shot_type = stat['shot_type']
|
13
|
+
@shot_type_desc = stat['shot_type_desc']
|
14
|
+
@block = Block.new(data, quarter: @quarter, half: @half) if @statistics.detect { |hash| hash['type'] == 'block' }
|
14
15
|
end
|
15
16
|
def made?
|
16
17
|
false
|
@@ -11,7 +11,7 @@ module Sportradar
|
|
11
11
|
end
|
12
12
|
def parse_statistics(data)
|
13
13
|
super
|
14
|
-
@steal = Steal.new(data, quarter: @quarter) if @statistics['steal'
|
14
|
+
@steal = Steal.new(data, quarter: @quarter, half: @half) if @statistics.detect { |hash| hash['type'] == 'steal' }
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Sportradar
|
2
|
+
module Api
|
3
|
+
module Basketball
|
4
|
+
class Record < Data
|
5
|
+
attr_accessor :id, :team, :wins, :losses, :win_pct, :points_for, :points_against, :point_diff, :games_behind, :streak
|
6
|
+
|
7
|
+
def initialize(data, type: 'overall', **opts)
|
8
|
+
@team = opts[:team]
|
9
|
+
@type = type
|
10
|
+
|
11
|
+
update(data, **opts)
|
12
|
+
end
|
13
|
+
|
14
|
+
def profile
|
15
|
+
Profile.new(self)
|
16
|
+
end
|
17
|
+
|
18
|
+
def update(data, **opts)
|
19
|
+
@wins = data['wins'].to_i
|
20
|
+
@losses = data['losses'].to_i
|
21
|
+
@win_pct = data['win_pct'].to_f
|
22
|
+
@points_for = data['points_for']
|
23
|
+
@points_against = data['points_against']
|
24
|
+
@point_diff = data['point_diff']
|
25
|
+
@games_behind = data['games_behind']
|
26
|
+
@streak = data['streak']
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Sportradar
|
2
|
+
module Api
|
3
|
+
module Basketball
|
4
|
+
class Scoring < Data
|
5
|
+
attr_accessor :response, :api, :id, :home, :away, :scores
|
6
|
+
|
7
|
+
def initialize(data, **opts)
|
8
|
+
@api = opts[:api]
|
9
|
+
@game = opts[:game]
|
10
|
+
|
11
|
+
@scores = {}
|
12
|
+
@id = data['id']
|
13
|
+
|
14
|
+
update(data, **opts)
|
15
|
+
end
|
16
|
+
|
17
|
+
def update(data, source: nil, **opts)
|
18
|
+
new_scores = case source
|
19
|
+
when :box
|
20
|
+
parse_from_box(data)
|
21
|
+
when :pbp
|
22
|
+
parse_from_pbp(data)
|
23
|
+
when :summary
|
24
|
+
parse_from_box(data)
|
25
|
+
else
|
26
|
+
if data['quarter'] || data['half']
|
27
|
+
parse_from_pbp(data)
|
28
|
+
elsif data['team']
|
29
|
+
parse_from_box(data)
|
30
|
+
else # schedule requests
|
31
|
+
{}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
# parse data structure
|
35
|
+
# handle data from team (all quarters)
|
36
|
+
# handle data from quarter (both teams)
|
37
|
+
# handle data from game?
|
38
|
+
@scores.each { |k, v| v.merge!(new_scores.delete(k) || {} ) }
|
39
|
+
new_scores.each { |k, v| @scores.merge!(k => v) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def points(team_id)
|
43
|
+
@score[team_id].to_i
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def parse_from_box(data)
|
50
|
+
return {} if data.dig('team', 0, 'scoring').is_a?(String) || data.dig('team', 0, 'scoring').nil? # game hasn't started yet
|
51
|
+
period_name = data.dig('team', 0, 'scoring').keys.first
|
52
|
+
id = data.dig('team', 0, 'id')
|
53
|
+
da = data.dig('team', 0, 'scoring', period_name)
|
54
|
+
da_ot = data.dig('team', 0, 'scoring', 'overtime')
|
55
|
+
arr = [da].compact.flatten(1)
|
56
|
+
a = arr + [da_ot].compact.flatten(1)
|
57
|
+
a.each { |h| h[id] = h.delete('points').to_i }
|
58
|
+
id = data.dig('team', 1, 'id')
|
59
|
+
da = data.dig('team', 1, 'scoring', period_name)
|
60
|
+
da_ot = data.dig('team', 1, 'scoring', 'overtime')
|
61
|
+
arr = [da].compact.flatten(1)
|
62
|
+
b = arr + [da_ot].compact.flatten(1)
|
63
|
+
b.each { |h| h[id] = h.delete('points').to_i }
|
64
|
+
a.zip(b).map{ |a, b| [a['sequence'].to_i, a.merge(b)] }.sort{ |(a,_), (b,_)| a <=> b }.to_h
|
65
|
+
end
|
66
|
+
|
67
|
+
def parse_from_box(data)
|
68
|
+
id = data.dig('home', 'id')
|
69
|
+
# binding.pry if id
|
70
|
+
da = data.dig('home', 'scoring')
|
71
|
+
return {} unless da
|
72
|
+
da.each { |h| h[id] = h.delete('points').to_i }
|
73
|
+
id = data.dig('away', 'id')
|
74
|
+
db = data.dig('away', 'scoring')
|
75
|
+
return {} unless db
|
76
|
+
db.each { |h| h[id] = h.delete('points').to_i }
|
77
|
+
da.zip(db).map{ |a, b| [a['sequence'].to_i, a.merge(b)] }.sort{ |(a,_), (b,_)| a <=> b }.to_h
|
78
|
+
end
|
79
|
+
|
80
|
+
def parse_from_pbp(data)
|
81
|
+
period_name = data.key?('quarter') ? 'quarter' : 'half'
|
82
|
+
period_name = 'periods'
|
83
|
+
return {} unless data[period_name] # game hasn't started
|
84
|
+
quarters = data[period_name]#[1..-1]
|
85
|
+
overtimes = data['overtime']
|
86
|
+
overtimes = [overtimes] if !overtimes.is_a?(Array)
|
87
|
+
quarters = quarters[0] if (quarters.is_a?(Array) && (quarters.size == 1) && quarters.first.is_a?(Array))
|
88
|
+
data = (quarters + overtimes).compact.map{|q| q['scoring'] }
|
89
|
+
data.map.with_index(1) { |h, i| [i, { h.dig('home', 'id') => h.dig('home', 'points').to_i, h.dig('away', 'id') => h.dig('away', 'points').to_i }] }.to_h
|
90
|
+
end
|
91
|
+
|
92
|
+
# def period_name
|
93
|
+
# 'quarter'
|
94
|
+
# end
|
95
|
+
|
96
|
+
def parse_from_summary(data)
|
97
|
+
#
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module Sportradar
|
2
|
+
module Api
|
3
|
+
module Basketball
|
4
|
+
class Team < Data
|
5
|
+
attr_accessor :response, :id, :market, :name, :alias, :full_name, :venue, :records, :player_stats, :team_stats
|
6
|
+
@all_hash = {}
|
7
|
+
def self.new(data, **opts)
|
8
|
+
existing = @all_hash[data['id']]
|
9
|
+
if existing
|
10
|
+
existing.update(data, **opts)
|
11
|
+
existing
|
12
|
+
else
|
13
|
+
@all_hash[data['id']] = super
|
14
|
+
end
|
15
|
+
end
|
16
|
+
def self.all
|
17
|
+
@all_hash.values
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(data, **opts)
|
21
|
+
@response = data
|
22
|
+
@api = opts[:api]
|
23
|
+
|
24
|
+
@games_hash = {}
|
25
|
+
@players_hash = {}
|
26
|
+
@player_stats = {}
|
27
|
+
@records = {}
|
28
|
+
@stats = nil
|
29
|
+
@points = nil
|
30
|
+
|
31
|
+
case data
|
32
|
+
when String
|
33
|
+
@id = data
|
34
|
+
when Hash
|
35
|
+
@id = data['id']
|
36
|
+
update(data, **opts)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def profile
|
41
|
+
Profile.new(self)
|
42
|
+
end
|
43
|
+
|
44
|
+
def update(data, **opts)
|
45
|
+
handle_names(data)
|
46
|
+
@venue = Venue.new(data['venue']) if data['venue']
|
47
|
+
|
48
|
+
@alias = data['alias'] if data['alias']
|
49
|
+
@points = data['points'].to_i if data['points']
|
50
|
+
# @home = data['home'] == 'true' if data['home']
|
51
|
+
# @away = data['away'] == 'true' if data['away']
|
52
|
+
# @scoring = data.dig('scoring', 'quarter') if data.dig('scoring', 'quarter')
|
53
|
+
|
54
|
+
parse_records(data) if data['records']
|
55
|
+
# binding.pry if data['players']
|
56
|
+
parse_players(data.dig('players'), opts[:game]) if data.dig('players')
|
57
|
+
# parse_stats(data['statistics']) if data['statistics']
|
58
|
+
if opts[:game]
|
59
|
+
add_game(opts[:game])
|
60
|
+
opts[:game].update_score(id => @points) if @points
|
61
|
+
opts[:game].update_stats(self, data['statistics']) if data['statistics']
|
62
|
+
end
|
63
|
+
end
|
64
|
+
def handle_names(data)
|
65
|
+
# need to do some more work here
|
66
|
+
@name = data['name'] if data['name']
|
67
|
+
if data['name'] && !data.key?('market')
|
68
|
+
@full_name = data['name']
|
69
|
+
elsif data['name'] && data['market']
|
70
|
+
@market = data['market']
|
71
|
+
@full_name = [@market, data['name']].join(' ')
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def record(type = 'overall')
|
76
|
+
@records[type]
|
77
|
+
end
|
78
|
+
|
79
|
+
def games
|
80
|
+
@games_hash.values
|
81
|
+
end
|
82
|
+
def add_game(game)
|
83
|
+
@games_hash[game.id] = game.id if game
|
84
|
+
end
|
85
|
+
|
86
|
+
# def parse_stats(data)
|
87
|
+
# @stats = data
|
88
|
+
# end
|
89
|
+
|
90
|
+
def players
|
91
|
+
get_roster if @players_hash.empty?
|
92
|
+
@players_hash.values
|
93
|
+
end
|
94
|
+
alias :roster :players
|
95
|
+
def parse_players(data, game)
|
96
|
+
create_data(@players_hash, data, klass: player_class, api: api, team: self, game: game)
|
97
|
+
end
|
98
|
+
def update_player_stats(player, stats, game = nil)
|
99
|
+
game ? game.update_player_stats(player, stats) : @player_stats.merge!(player.id => stats.merge!(player: player))
|
100
|
+
end
|
101
|
+
|
102
|
+
def get_roster
|
103
|
+
data = api.get_data(path_roster)
|
104
|
+
ingest_roster(data)
|
105
|
+
end
|
106
|
+
def ingest_roster(data)
|
107
|
+
update(data)
|
108
|
+
data
|
109
|
+
end
|
110
|
+
|
111
|
+
def get_season_stats
|
112
|
+
data = api.get_data(path_season_stats)
|
113
|
+
ingest_season_stats(data)
|
114
|
+
end
|
115
|
+
def ingest_season_stats(data)
|
116
|
+
parse_season_stats(data)
|
117
|
+
end
|
118
|
+
|
119
|
+
def parse_records(data)
|
120
|
+
@records['overall'] = Record.new(data, type: 'overall')
|
121
|
+
data['records'].each { |record| @records[record['record_type']] = Record.new(record, type: record['record_type']) }
|
122
|
+
end
|
123
|
+
|
124
|
+
def parse_season_stats(data)
|
125
|
+
@team_stats = data.dig('own_record')
|
126
|
+
update(data)
|
127
|
+
player_data = data.dig('players')
|
128
|
+
create_data(@players_hash, player_data, klass: player_class, api: api, team: self)
|
129
|
+
data
|
130
|
+
end
|
131
|
+
|
132
|
+
def path_base
|
133
|
+
"teams/#{ id }"
|
134
|
+
end
|
135
|
+
def path_base_stats(season_year = api.default_year, default_season = api.default_season)
|
136
|
+
"seasontd/#{season_year}/#{default_season}/teams/#{id}"
|
137
|
+
end
|
138
|
+
def path_roster
|
139
|
+
"#{ path_base }/profile"
|
140
|
+
end
|
141
|
+
def path_season_stats
|
142
|
+
"#{ path_base_stats }/statistics"
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|