fifadat 2026.9.23 → 2026.9.30
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/CHANGELOG.md +1 -1
- data/Manifest.txt +5 -0
- data/lib/fifadat/api.rb +59 -0
- data/lib/fifadat/helper.rb +52 -0
- data/lib/fifadat/pp/ppdebug.rb +69 -0
- data/lib/fifadat/pp/ppmatch.rb +78 -0
- data/lib/fifadat/pp/ppstats.rb +97 -0
- data/lib/fifadat/prepare.rb +48 -12
- data/lib/fifadat/types.rb +202 -0
- data/lib/fifadat/version.rb +1 -1
- data/lib/fifadat.rb +5 -2
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7785bb2b01f91bf0eca6a9d47ec73e0a7a6b98c6d11c23dc150843a3e2d4502d
|
|
4
|
+
data.tar.gz: c7d838da7303b761ec6b5fcd0d46459e17df1d9513ebb4c418724f22d2846622
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2076c18eb49a0b5a22fddf58ac082eb81cf83cd1c47cba048278b22f77bae6fb4c6e3c268f5069fc943b800f13ed3be0c5301f03f9cb23a53ef1d2a62e9a55ae
|
|
7
|
+
data.tar.gz: 12554947e6f9105fb1c965bfa56c7d9685287bd60119661928ff2e9a356c5d9d2ef9dfdbf361d7bbe61f40948c50845845190b13e96446399f6a26ee3531b6fb
|
data/CHANGELOG.md
CHANGED
data/Manifest.txt
CHANGED
|
@@ -63,6 +63,11 @@ config/seasons/za.csv
|
|
|
63
63
|
lib/fifadat.rb
|
|
64
64
|
lib/fifadat/api.rb
|
|
65
65
|
lib/fifadat/config.rb
|
|
66
|
+
lib/fifadat/helper.rb
|
|
66
67
|
lib/fifadat/json.rb
|
|
68
|
+
lib/fifadat/pp/ppdebug.rb
|
|
69
|
+
lib/fifadat/pp/ppmatch.rb
|
|
70
|
+
lib/fifadat/pp/ppstats.rb
|
|
67
71
|
lib/fifadat/prepare.rb
|
|
72
|
+
lib/fifadat/types.rb
|
|
68
73
|
lib/fifadat/version.rb
|
data/lib/fifadat/api.rb
CHANGED
|
@@ -74,11 +74,19 @@ class Metal
|
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
|
|
77
78
|
def self.squads_url( idSeason:, idCompetition: )
|
|
78
79
|
## API_ROOT/teams/squads/all/108/278491
|
|
79
80
|
"#{BASE_URL}/teams/squads/all/#{idCompetition}/#{idSeason}"
|
|
80
81
|
end
|
|
81
82
|
|
|
83
|
+
def self.teams_url( idSeason: )
|
|
84
|
+
## /competitions/teams/{season}
|
|
85
|
+
# Teams participating in a competition/season
|
|
86
|
+
"#{BASE_URL}/competitions/teams/#{idSeason}" +
|
|
87
|
+
"?language=en"
|
|
88
|
+
end
|
|
89
|
+
|
|
82
90
|
def self.matches_url( idSeason: )
|
|
83
91
|
## note - may add &idCompetition=17
|
|
84
92
|
|
|
@@ -101,10 +109,61 @@ class Metal
|
|
|
101
109
|
"#{BASE_URL}/live/football/#{idCompetition}/#{idSeason}/#{idStage}/#{idMatch}?language=en"
|
|
102
110
|
end
|
|
103
111
|
|
|
112
|
+
=begin
|
|
104
113
|
def self.timeline_url( idCompetition:, idSeason:,
|
|
105
114
|
idStage:, idMatch: )
|
|
106
115
|
#API_ROOT/timelines/108/278491/278493/300424860?language=en-GB
|
|
116
|
+
|
|
107
117
|
"#{BASE_URL}/timelines/#{idCompetition}/#{idSeason}/#{idStage}/#{idMatch}?language=en"
|
|
108
118
|
end
|
|
119
|
+
=end
|
|
120
|
+
|
|
121
|
+
def self.timeline_url( idMatch: )
|
|
122
|
+
#API_ROOT/timelines/108/278491/278493/300424860?language=en-GB
|
|
123
|
+
|
|
124
|
+
##
|
|
125
|
+
## todo/check
|
|
126
|
+
## only use idMatch ??
|
|
127
|
+
## e.g. GET https://api.fifa.com/api/v3/timelines/{matchId}?language=en
|
|
128
|
+
|
|
129
|
+
"#{BASE_URL}/timelines/#{idMatch}?language=en"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
#############
|
|
133
|
+
## more support urls
|
|
134
|
+
|
|
135
|
+
# get_team
|
|
136
|
+
# Get a national team or club by its idTeam: name, abbreviation, country,
|
|
137
|
+
# home city, and stadium.
|
|
138
|
+
# Surfaces idStadium so you can chain to get_stadium.
|
|
139
|
+
def self.team_url( idTeam: ) ## note - singular (NOT plural)
|
|
140
|
+
"#{BASE_URL}/teams/#{idTeam}?language=en"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
## note - singular (NOT plural)
|
|
145
|
+
# Squad/player information
|
|
146
|
+
def self.squad_url( idTeam:, idCompetition:, idSeason: )
|
|
147
|
+
"#{BASE_URL}/teams/#{idTeam}/squad?idCompetition=#{idCompetition}&idSeason=#{idSeason}&language=en"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# list_countries
|
|
152
|
+
# List FIFA countries and member associations (reference data),
|
|
153
|
+
# each with its idCountry
|
|
154
|
+
|
|
155
|
+
def self.countries_url
|
|
156
|
+
"#{BASE_URL}/countries?count=500&language=en"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# list_confederations
|
|
160
|
+
# List the six FIFA confederations
|
|
161
|
+
# (UEFA, CONMEBOL, CONCACAF, CAF, AFC, OFC), each with its idConfederation
|
|
162
|
+
|
|
163
|
+
def self.confederations_url
|
|
164
|
+
"#{BASE_URL}/confederations?language=en"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
|
|
109
168
|
end ## class Metal
|
|
110
169
|
end # class Fifa
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
def assert( test, msg )
|
|
4
|
+
if test
|
|
5
|
+
else
|
|
6
|
+
puts "!! ASSERT FAILED - #{msg}"
|
|
7
|
+
exit 1
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
### fix - change/rename to datetime!!!
|
|
12
|
+
def parse_date( date_str )
|
|
13
|
+
date = DateTime.strptime( date_str, '%Y-%m-%dT%H:%M:%S%z' )
|
|
14
|
+
|
|
15
|
+
assert( date_str == date.strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
16
|
+
"date parse expected #{date_str} - got #{date.inspect}" )
|
|
17
|
+
date
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_date_utc( str )
|
|
22
|
+
date = DateTime.strptime( str, '%Y-%m-%dT%H:%M:%S%z' )
|
|
23
|
+
|
|
24
|
+
## note: check for seconds is 00
|
|
25
|
+
## AND timezone is Z (zulu) (shortkey for utc!)
|
|
26
|
+
## e.g. hard code ":00Z" in strftime!!!
|
|
27
|
+
|
|
28
|
+
str_exp = date.strftime('%Y-%m-%dT%H:%M:00Z')
|
|
29
|
+
assert( str == str_exp,
|
|
30
|
+
"date parse expected #{str_exp} - got #{str} #{date.inspect}" )
|
|
31
|
+
date
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
=begin
|
|
37
|
+
- [ ] add desc json samples here
|
|
38
|
+
|
|
39
|
+
=end
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def desc( data ) ## get description
|
|
44
|
+
return nil if data.nil? || (data.is_a?(Array) && data.empty?)
|
|
45
|
+
|
|
46
|
+
row = data[0] ## assume first entry is en-GB
|
|
47
|
+
locale = row['Locale']
|
|
48
|
+
assert( locale == 'en-GB' || locale == 'en-gb',
|
|
49
|
+
"locale en-GB expected in data[0] - got #{data}")
|
|
50
|
+
|
|
51
|
+
row['Description']
|
|
52
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def pp_debug( data )
|
|
5
|
+
|
|
6
|
+
errors = []
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
matches = data['Results'] ## only use results (match) array
|
|
10
|
+
|
|
11
|
+
## get SeasonName from first match
|
|
12
|
+
m = matches[0]
|
|
13
|
+
competitionName = desc( m['CompetitionName'])
|
|
14
|
+
seasonName = desc( m['SeasonName'])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## pp data
|
|
18
|
+
puts " #{matches.size} match(es) in #{competitionName} / #{seasonName}"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
buf = String.new
|
|
22
|
+
|
|
23
|
+
matches.each_with_index do |m, i|
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## todo -- log match details
|
|
27
|
+
## if matchstatus not 0,1
|
|
28
|
+
## or result type not 0,1,2,3,4,5 !!!
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
buf << "==> [#{i+1}/#{matches.size}] "
|
|
32
|
+
buf << pp_match(m)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
idMatch = m['IdMatch']
|
|
36
|
+
|
|
37
|
+
matchStatus = m['MatchStatus']
|
|
38
|
+
resultType = m['ResultType']
|
|
39
|
+
|
|
40
|
+
## add aggregate score too
|
|
41
|
+
score = [m['HomeTeamScore'], m['AwayTeamScore']].compact
|
|
42
|
+
penScore = [m['HomeTeamPenaltyScore'], m['AwayTeamPenaltyScore']].compact
|
|
43
|
+
aggScore = [m['AggregateHomeTeamScore'],m['AggregateAwayTeamScore']].compact
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if resultType == 0 && !score.empty?
|
|
47
|
+
## pachuca vs salzburg in cwc 2025??
|
|
48
|
+
## double check if score present
|
|
49
|
+
## raise ArgumentError,
|
|
50
|
+
## " resultType == 0 but score present idMatch #{m['IdMatch']} #{m['HomeTeamScore']}-#{m['AwayTeamScore']}"
|
|
51
|
+
|
|
52
|
+
msg = " resultType == 0 but score present idMatch #{idMatch} #{score.inspect}"
|
|
53
|
+
errors << msg
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if ![0,1,9].include?(matchStatus)
|
|
57
|
+
msg = "matchStatus 0-END,1-SCHED,9-AWD expected; got #{matchStatus} in: #{m.pretty_inspect}"
|
|
58
|
+
errors << msg
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if ![0,1,2,3,4,5,8,12].include?(resultType)
|
|
62
|
+
msg = "resultType 0,1,2,3,4,5,8,12 expected; got #{resultType} in: #{m.pretty_inspect}"
|
|
63
|
+
errors << msg
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
[buf, errors]
|
|
69
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
## todo/check:
|
|
4
|
+
## use: fomat: full/fuller, short/long or such - why? why not?
|
|
5
|
+
def pp_match( m, season: false )
|
|
6
|
+
|
|
7
|
+
competitionName = desc( m['CompetitionName'])
|
|
8
|
+
seasonName = desc( m['SeasonName'])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
idCompetition = m['IdCompetition']
|
|
12
|
+
idSeason = m['IdSeason']
|
|
13
|
+
idStage = m['IdStage']
|
|
14
|
+
idMatch = m['IdMatch']
|
|
15
|
+
|
|
16
|
+
stageName = desc( m['StageName'] )
|
|
17
|
+
groupName = desc( m['GroupName'] ) # optional
|
|
18
|
+
matchDay = m['MatchDay'] # optional
|
|
19
|
+
|
|
20
|
+
matchNumber = m['MatchNumber'] # optional
|
|
21
|
+
|
|
22
|
+
team1 = m['Home'] ? { name: desc( m['Home']['TeamName'] ),
|
|
23
|
+
abbrev: m['Home']['Abbreviation'],
|
|
24
|
+
country: m['Home']['IdCountry'],
|
|
25
|
+
}
|
|
26
|
+
: { name: '?', abbrev: '?', country: '?' }
|
|
27
|
+
|
|
28
|
+
team2 = m['Away'] ? { name: desc( m['Away']['TeamName'] ),
|
|
29
|
+
abbrev: m['Away']['Abbreviation'],
|
|
30
|
+
country: m['Away']['IdCountry'],
|
|
31
|
+
}
|
|
32
|
+
: { name: '?', abbrev: '?', country: '?' }
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
matchStatus = m['MatchStatus']
|
|
36
|
+
resultType = m['ResultType']
|
|
37
|
+
|
|
38
|
+
## add aggregate score too
|
|
39
|
+
score = [m['HomeTeamScore'], m['AwayTeamScore']].compact
|
|
40
|
+
penScore = [m['HomeTeamPenaltyScore'], m['AwayTeamPenaltyScore']].compact
|
|
41
|
+
aggScore = [m['AggregateHomeTeamScore'],m['AggregateAwayTeamScore']].compact
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
buf = String.new
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if season
|
|
49
|
+
## beautify
|
|
50
|
+
## possible rm competitionName from season
|
|
51
|
+
|
|
52
|
+
seasonNameAlt = seasonName.sub( competitionName, '').gsub( /[ ]{2,}/,' ').strip
|
|
53
|
+
buf << "== #{competitionName}"
|
|
54
|
+
buf << " | #{seasonNameAlt}"
|
|
55
|
+
buf << "\n"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
buf << "#{matchStatus}-#{MATCH_STATUS[matchStatus]||'??'} "
|
|
60
|
+
buf << " ▪ #{stageName}"
|
|
61
|
+
buf << ", #{groupName}" if groupName
|
|
62
|
+
buf << " - #{matchDay}" if matchDay
|
|
63
|
+
buf << " (#{matchNumber})" if matchNumber
|
|
64
|
+
|
|
65
|
+
buf << " #{team1[:name]} | #{team1[:abbrev]} (#{team1[:country]})"
|
|
66
|
+
buf << " v "
|
|
67
|
+
buf << "#{team2[:name]} | #{team2[:abbrev]} (#{team2[:country]}) "
|
|
68
|
+
buf << "\n"
|
|
69
|
+
|
|
70
|
+
buf << " "
|
|
71
|
+
buf << " (#{resultType}-#{RESULT_TYPE[resultType]||'??'})"
|
|
72
|
+
buf << " score: #{score.inspect}" unless score.empty?
|
|
73
|
+
buf << " pen: #{penScore.inspect}" unless penScore.empty?
|
|
74
|
+
buf << " agg: #{aggScore.inspect}" unless aggScore.empty?
|
|
75
|
+
buf << "\n"
|
|
76
|
+
|
|
77
|
+
buf
|
|
78
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Counter
|
|
5
|
+
def initialize( types )
|
|
6
|
+
@types = types
|
|
7
|
+
@counter = Hash.new(0)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def [](key) @counter[key]; end
|
|
11
|
+
def []=(key,value) @counter[key]=value; end
|
|
12
|
+
|
|
13
|
+
def pretty_print(q)
|
|
14
|
+
q.group(1, "{", "}") do
|
|
15
|
+
## sort by keys
|
|
16
|
+
pairs = @counter.sort_by { |key,_| key }
|
|
17
|
+
pairs.each_with_index do |(key,count),i|
|
|
18
|
+
q.breakable(', ') if i != 0
|
|
19
|
+
q.text( "#{key}-#{(@types[key]||'??')}=>#{count}")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end ## class Counter
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def mk_stats
|
|
29
|
+
stats = {
|
|
30
|
+
## match stats
|
|
31
|
+
'MatchStatus' => Counter.new( MATCH_STATUS ),
|
|
32
|
+
'ResultType' => Counter.new( RESULT_TYPE ),
|
|
33
|
+
'TimeDefined' => Hash.new(0),
|
|
34
|
+
|
|
35
|
+
'MatchDay' => Hash.new(0),
|
|
36
|
+
'MatchNumber' => Hash.new(0),
|
|
37
|
+
'Leg' => Hash.new(0),
|
|
38
|
+
'IsHomeMatch' => Hash.new(0),
|
|
39
|
+
|
|
40
|
+
'Attendance' => Hash.new(0),
|
|
41
|
+
'Weather' => Hash.new(0),
|
|
42
|
+
|
|
43
|
+
## add stage & group
|
|
44
|
+
## add agg score and pen score!!
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
## team stats
|
|
48
|
+
## 'TeamType' => Hash.new(0),
|
|
49
|
+
## 'AgeType' => Hash.new(0),
|
|
50
|
+
## 'FootballType' => Hash.new(0),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
stats
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def collect_stats( data, stats: )
|
|
59
|
+
|
|
60
|
+
## use results (match) array
|
|
61
|
+
data['Results'].each_with_index do |m, i|
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
##
|
|
65
|
+
## check usage
|
|
66
|
+
stats[ 'MatchStatus' ][m['MatchStatus']] +=1
|
|
67
|
+
stats[ 'ResultType'][m['ResultType']] +=1
|
|
68
|
+
|
|
69
|
+
stats[ 'TimeDefined'][m['TimeDefined']] +=1
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
stats[ 'MatchDay'][!m['MatchDay'].nil?] +=1
|
|
73
|
+
stats[ 'MatchNumber'][!m['MatchNumber'].nil?] +=1
|
|
74
|
+
|
|
75
|
+
stats[ 'Leg'][m['Leg']] +=1
|
|
76
|
+
stats[ 'IsHomeMatch'][m['IsHomeMatch']] +=1
|
|
77
|
+
|
|
78
|
+
stats[ 'Attendance'][!m['MatchDay'].nil?] +=1
|
|
79
|
+
stats[ 'Weather'][!m['Weather'].nil?] +=1
|
|
80
|
+
|
|
81
|
+
=begin
|
|
82
|
+
if m['Home']
|
|
83
|
+
stats[ 'TeamType'][m['Home']['TeamType']] +=1
|
|
84
|
+
stats[ 'AgeType'][m['Home']['AgeType']] +=1
|
|
85
|
+
stats[ 'FootballType'][m['Home']['FootballType']] +=1
|
|
86
|
+
end
|
|
87
|
+
if m['Away']
|
|
88
|
+
stats[ 'TeamType'][m['Away']['TeamType']] +=1
|
|
89
|
+
stats[ 'AgeType'][m['Away']['AgeType']] +=1
|
|
90
|
+
stats[ 'FootballType'][m['Away']['FootballType']] +=1
|
|
91
|
+
end
|
|
92
|
+
=end
|
|
93
|
+
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
stats
|
|
97
|
+
end
|
data/lib/fifadat/prepare.rb
CHANGED
|
@@ -22,6 +22,9 @@ def prepare( name:,
|
|
|
22
22
|
fetch_json_if( Fifa::Metal.matches_url( idSeason: idSeason ),
|
|
23
23
|
"#{outdir}/#{name}/#{season.to_path}_matches.json", force: force )
|
|
24
24
|
|
|
25
|
+
fetch_json_if( Fifa::Metal.teams_url( idSeason: idSeason ),
|
|
26
|
+
"#{outdir}/#{name}/misc/#{season.to_path}_teams.json", force: force )
|
|
27
|
+
|
|
25
28
|
fetch_json_if( Fifa::Metal.stages_url( idSeason: idSeason ),
|
|
26
29
|
"#{outdir}/#{name}/misc/#{season.to_path}_stages.json", force: force )
|
|
27
30
|
|
|
@@ -45,7 +48,8 @@ def prepare_reports( name:,
|
|
|
45
48
|
matches = data['Results']
|
|
46
49
|
|
|
47
50
|
|
|
48
|
-
puts "
|
|
51
|
+
puts "==> prepare reports - #{matches.size} match(es) in #{name} #{season}"
|
|
52
|
+
|
|
49
53
|
|
|
50
54
|
|
|
51
55
|
matches.each_with_index do |m, i|
|
|
@@ -54,8 +58,6 @@ def prepare_reports( name:,
|
|
|
54
58
|
idStage = m['IdStage']
|
|
55
59
|
idMatch = m['IdMatch']
|
|
56
60
|
|
|
57
|
-
stageName = desc( m['StageName'] )
|
|
58
|
-
|
|
59
61
|
|
|
60
62
|
## note - skip if teams not yet know
|
|
61
63
|
## e.g. Home & Away is null
|
|
@@ -66,14 +68,34 @@ def prepare_reports( name:,
|
|
|
66
68
|
## e.g. MatchStatus = 1 -- future
|
|
67
69
|
## ResultType = 0 -- not played yet
|
|
68
70
|
|
|
71
|
+
## if not yet played!!!
|
|
72
|
+
# 1-scheduled and
|
|
73
|
+
# 2-live
|
|
74
|
+
# 7-postponed !!!! (too)
|
|
75
|
+
# return empty score!!!
|
|
76
|
+
if m['MatchStatus'] == 1 ||
|
|
77
|
+
m['MatchStatus'] == 2 ||
|
|
78
|
+
m['MatchStatus'] == 7
|
|
79
|
+
## skip scheduled/live matches (not yet played)
|
|
80
|
+
next
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
69
86
|
if m['Home'].nil? || m['Away'].nil?
|
|
70
87
|
puts "[#{i+1}/#{matches.size}] ?? ??, #{stageName} (SKIPPED - TO BE DONE)"
|
|
71
88
|
next
|
|
72
89
|
end
|
|
73
90
|
|
|
74
91
|
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
### note - date is always utc without timezone even if local!!
|
|
93
|
+
## local is utc (plus/minus offset)!!!
|
|
94
|
+
## e.g.
|
|
95
|
+
## "Date": "2026-08-02T15:00:00Z",
|
|
96
|
+
## "LocalDate": "2026-08-02T17:00:00Z",
|
|
97
|
+
dateTime = parse_date_utc( m['Date'] ) ## utc
|
|
98
|
+
localDateTime = parse_date_utc( m['LocalDate'] )
|
|
77
99
|
|
|
78
100
|
|
|
79
101
|
## pp m['Home']
|
|
@@ -84,8 +106,21 @@ def prepare_reports( name:,
|
|
|
84
106
|
teamName2 = desc( m['Away']['TeamName'] )
|
|
85
107
|
teamCode2 = m['Away']['Abbreviation']
|
|
86
108
|
|
|
109
|
+
stageName = desc( m['StageName'] )
|
|
110
|
+
## "MatchDay": null,
|
|
111
|
+
matchday = m['MatchDay'] || '∅'
|
|
112
|
+
groupName = desc( m['GroupName'] )
|
|
113
|
+
|
|
114
|
+
print "[#{i+1}/#{matches.size}] "
|
|
115
|
+
print MATCH_STATUS[m['MatchStatus']]||"???-#{m['MatchStatus']}"
|
|
116
|
+
print "/"
|
|
117
|
+
print RESULT_TYPE[m['ResultType']]||"???-#{m['ResultType']}"
|
|
118
|
+
print " #{teamName1} (#{teamCode1}) v #{teamName2} (#{teamCode2})"
|
|
119
|
+
print " | #{stageName} - #{matchday}"
|
|
120
|
+
print ", #{groupName}" if groupName
|
|
121
|
+
print " | #{localDateTime}"
|
|
122
|
+
print "\n"
|
|
87
123
|
|
|
88
|
-
puts "[#{i+1}/#{matches.size}] #{teamName1} #{teamName2}, #{stageName}, #{localDateTime}"
|
|
89
124
|
|
|
90
125
|
|
|
91
126
|
outpath = "#{outdir}/#{name}/matches/#{season.to_path}/#{localDateTime.strftime('%Y-%m-%d')}_#{teamCode1}-#{teamCode2}__#{idMatch}.json"
|
|
@@ -93,7 +128,7 @@ def prepare_reports( name:,
|
|
|
93
128
|
url = Fifa::Metal.live_url( idCompetition: idCompetition,
|
|
94
129
|
idSeason: idSeason,
|
|
95
130
|
idStage: idStage,
|
|
96
|
-
|
|
131
|
+
idMatch: idMatch )
|
|
97
132
|
|
|
98
133
|
fetch_json_if( url, outpath, force: force )
|
|
99
134
|
|
|
@@ -101,15 +136,16 @@ def prepare_reports( name:,
|
|
|
101
136
|
###
|
|
102
137
|
## add timeline (only) if score incl. penalty shoot-out
|
|
103
138
|
resultType = m['ResultType']
|
|
104
|
-
if resultType == 2
|
|
139
|
+
if resultType == 2 ## WIN_ON_PENS
|
|
105
140
|
|
|
106
141
|
## download timeline
|
|
107
142
|
outpath = "#{outdir}/#{name}/timelines/#{season.to_path}/#{localDateTime.strftime('%Y-%m-%d')}_#{teamCode1}-#{teamCode2}__#{idMatch}.json"
|
|
108
143
|
|
|
109
|
-
url = Fifa::Metal.timeline_url( idCompetition: idCompetition,
|
|
110
|
-
idSeason: idSeason,
|
|
111
|
-
idStage: idStage,
|
|
112
|
-
idMatch: idMatch )
|
|
144
|
+
# url = Fifa::Metal.timeline_url( idCompetition: idCompetition,
|
|
145
|
+
# idSeason: idSeason,
|
|
146
|
+
# idStage: idStage,
|
|
147
|
+
# idMatch: idMatch )
|
|
148
|
+
url = Fifa::Metal.timeline_url( idMatch:idMatch )
|
|
113
149
|
|
|
114
150
|
fetch_json_if( url, outpath, force: force )
|
|
115
151
|
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
##
|
|
3
|
+
## help - yes, you can.
|
|
4
|
+
## if you have any more info about the fifa types,
|
|
5
|
+
## please tell and help fill-in the details.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
MATCH_STATUS = {
|
|
10
|
+
0 => 'END', # finished
|
|
11
|
+
1 => 'SCHEDULED', # scheduled (with or without time - see timed flag)
|
|
12
|
+
2 => 'LIVE',
|
|
13
|
+
|
|
14
|
+
## ???
|
|
15
|
+
|
|
16
|
+
7 => 'POSTPONED', # postponed - use PPD ??
|
|
17
|
+
|
|
18
|
+
9 => 'AWD', # awarded !!!
|
|
19
|
+
# check if 9/0 is possibly cancelled/annulled/walkover (if no score)
|
|
20
|
+
# 9/12 (AWD 3-0 or such)
|
|
21
|
+
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
## "MatchStatus"=>9, cancelled!!!!
|
|
27
|
+
##
|
|
28
|
+
## "ResultType"=>12,
|
|
29
|
+
## 21:30 CD Independiente Medellín (COL) v CR Flamengo (BRA) [cancelled]
|
|
30
|
+
##
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
## Result: Al Duhail awarded a 3–0 walkover victory.
|
|
34
|
+
## Reason: Auckland City FC withdrew from the tournament
|
|
35
|
+
## due to strict COVID-19 pandemic quarantine and isolation measures
|
|
36
|
+
## enforced by New Zealand authorities.
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
## check 2 is only win on pens!!!
|
|
40
|
+
## might be WITHOUT extra time e.g. copa liber.
|
|
41
|
+
## [28/142] 0-FIN 3rd Round Sporting Cristal | SCL (PER) v Carabobo FC | CRB (VEN)
|
|
42
|
+
## (2-AET_WIN_ON_PENS) score: [1, 2] pen: [3, 2] agg: [2, 2]
|
|
43
|
+
## and others!!!
|
|
44
|
+
##
|
|
45
|
+
## how to check if extra time was played?? (use period??)
|
|
46
|
+
|
|
47
|
+
# resultType
|
|
48
|
+
# 0 => no result / not played yet
|
|
49
|
+
# 1 => regular (90 mins)
|
|
50
|
+
# 2 => aet (120 mins), win on pens
|
|
51
|
+
# 3 => aet (120 mins)
|
|
52
|
+
# 8 => same as 3? -aet with golden goal/silver goal in 1998 FRA-PAR
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
RESULT_TYPE = {
|
|
57
|
+
0 => 'NO',
|
|
58
|
+
1 => 'REGULAR',
|
|
59
|
+
## note - cannot tell if
|
|
60
|
+
## win on penalties
|
|
61
|
+
## is after extra-time!!!
|
|
62
|
+
## used many times in copa libertadores
|
|
63
|
+
## check gold cup too
|
|
64
|
+
## change to WIN_ON_PENS only - why? why not?
|
|
65
|
+
2 => 'WIN_ON_PENS', ## AET_WIN_ON_PENS or REG_WIN_ON_PENS or AGG_WIN_ON_PENS !!!!!!
|
|
66
|
+
3 => 'AET',
|
|
67
|
+
##
|
|
68
|
+
## note - agg - used for 1st & 2nd leg
|
|
69
|
+
4 => 'REGULAR/AGG', ## aggregate (1st/2nd leg) - regular
|
|
70
|
+
5 => 'AET/AGG', ## aggregate - after extra-time
|
|
71
|
+
## e.g.
|
|
72
|
+
## 2nd Leg Feb 25, 2026 Juventus 3–2 (AET) Galatasaray
|
|
73
|
+
##
|
|
74
|
+
8 => 'AET/GG', ## after extra-time/golden goal !!!
|
|
75
|
+
|
|
76
|
+
12 => 'AWD', ## AWARDED or ???
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
GOAL_TYPE = {
|
|
81
|
+
1 => 'PENALTY',
|
|
82
|
+
2 => 'REGULAR',
|
|
83
|
+
3 => 'OWN_GOAL',
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
## use PERIOD only - why? why not?
|
|
88
|
+
PERIOD_TYPE = {
|
|
89
|
+
3 => '1ST_HALF', ## "MatchMinute": "0'", "1'", "45'+2'",
|
|
90
|
+
4 => 'HALF', ## "MatchMinute": "46'", "90'+2'",
|
|
91
|
+
5 => '2ND_HALF', ## "MatchMinute": "91'", "105'+2'",
|
|
92
|
+
6 => 'EXTRA_TIME', ## or end of regular time??
|
|
93
|
+
7 => 'EXTRA_TIME_1ST_HALF', ## "MatchMinute": "106'", "120'+2'",
|
|
94
|
+
8 => 'EXTRA_TIME_HALF',
|
|
95
|
+
9 => 'EXTRA_TIME_2ND_HALF',
|
|
96
|
+
10 => 'END', ## or use MATCH_END ???
|
|
97
|
+
## (generic incl. optional extra time and/or penalty shootout)
|
|
98
|
+
11 => 'PENALTY_SHOOTOUT', ## "MatchMinute": "120'", "121'", "122'", "123'",
|
|
99
|
+
## "MatchMinute": "127'", "130'",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
=begin
|
|
103
|
+
"MatchMinute": "1'",
|
|
104
|
+
"Period": 0, !!!!
|
|
105
|
+
"MatchMinute": "91'",
|
|
106
|
+
"Period": 0, !!!
|
|
107
|
+
=end
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
EVENT_TYPE = {
|
|
111
|
+
0 => 'Goal!', ## e.g. DI MARÍA (Argentina) scores!!
|
|
112
|
+
1 => 'Assist', ## e.g. Assisted by MAC ALLISTER.
|
|
113
|
+
2 => 'Yellow card', ## e.g. FERNÁNDEZ (Argentina) is booked by the referee.
|
|
114
|
+
3 => 'Red card', ## e.g. Zinedine ZIDANE (France) is sent off
|
|
115
|
+
|
|
116
|
+
5 => 'Substitution', ## e.g. ACUÑA (in) comes off the bench to replace DI MARÍA (out) (Argentina)
|
|
117
|
+
6 => 'Penalty Awarded',
|
|
118
|
+
7 => 'Start Time', ## e.g. The referee signals the start of the first period.
|
|
119
|
+
8 => 'End Time', ## e.g. 'The referee brings the first period to an end.
|
|
120
|
+
|
|
121
|
+
12 => 'Attempt at Goal',
|
|
122
|
+
|
|
123
|
+
15 => 'Offside',
|
|
124
|
+
16 => 'Corner',
|
|
125
|
+
18 => 'Foul',
|
|
126
|
+
19 => 'Coin Toss',
|
|
127
|
+
|
|
128
|
+
23 => 'Dropped Ball',
|
|
129
|
+
24 => 'Throw In',
|
|
130
|
+
25 => 'Clearance',
|
|
131
|
+
26 => 'Match end',
|
|
132
|
+
27 => 'Aerial Duel',
|
|
133
|
+
|
|
134
|
+
41 => 'Penalty Goal',
|
|
135
|
+
46 => 'Penalty missed', ## David TREZEGUET (France) rattles the crossbar from the spot!
|
|
136
|
+
51 => 'Penalty missed', ## JULIO CESAR (Brazil) hits the post from the spot!
|
|
137
|
+
60 => 'Penalty missed', ## COMAN (France) sees his penalty saved by the goalkeeper.
|
|
138
|
+
65 => 'Penalty missed', ## TCHOUAMENI (France) misses from the penalty spot!
|
|
139
|
+
## Michel PLATINI (France) misses from the penalty spot!
|
|
140
|
+
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
__END__
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
TeamType
|
|
150
|
+
0 => club
|
|
151
|
+
1 => nati(onal) team
|
|
152
|
+
AgeType
|
|
153
|
+
0 => ??
|
|
154
|
+
1 => U17 U-17 World Cup / U17 National Team Friendlies
|
|
155
|
+
MLS Generation Cup U17
|
|
156
|
+
2 => U18 U18 National Team Friendlies
|
|
157
|
+
U18 Premier League Cup
|
|
158
|
+
3 => U19 U19 Championship Qualification
|
|
159
|
+
4 => U20 U-20 World Cup / CONMEBOL U20
|
|
160
|
+
|
|
161
|
+
5 => ?? Olympic Football Tournament / Olympics Intercontinental Play-offs /
|
|
162
|
+
AFC U23 Asian Cup
|
|
163
|
+
|
|
164
|
+
10 => U15 U15 National Team Friendlies
|
|
165
|
+
MLS Generation Cup U15
|
|
166
|
+
11 => U16 U16 National Team Friendlies
|
|
167
|
+
12 => U21 U21 Championship / Tournoi Maurice Revello /
|
|
168
|
+
U21 National Team Friendlies
|
|
169
|
+
Premier League 2
|
|
170
|
+
|
|
171
|
+
14 => ?? U23 U23 National Team Friendlies / Asian Games
|
|
172
|
+
|
|
173
|
+
FootballType
|
|
174
|
+
0 => "classic"
|
|
175
|
+
1 => Futsal
|
|
176
|
+
2 => Beach Soccer
|
|
177
|
+
Gender
|
|
178
|
+
1 => m
|
|
179
|
+
2 => f
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
note - matchStatus for 12-AWD should also be 9-AWD!!!
|
|
183
|
+
|
|
184
|
+
resultType 12
|
|
185
|
+
The Clausura 2022 match between Querétaro and Atlas on March 5, 2022,
|
|
186
|
+
was officially suspended in the 62nd minute
|
|
187
|
+
and later recorded as a 3–0 victory for Atlas
|
|
188
|
+
: Atlas was leading 1–0 at the time of suspension.
|
|
189
|
+
Forfeit Victory: Atlas was officially awarded a 3–0 walkover win.
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
0 => no result / not played yet
|
|
193
|
+
1 => regular (90 mins)
|
|
194
|
+
2 => aet (120 mins), win on pens
|
|
195
|
+
3 => aet (120 mins)
|
|
196
|
+
|
|
197
|
+
8 => same as 3? -aet with golden goal/silver goal in 1998 FRA-PAR
|
|
198
|
+
|
|
199
|
+
4 => aggregate leg 1/2 ?? e.g.
|
|
200
|
+
"AggregateHomeTeamScore": 2,
|
|
201
|
+
"AggregateAwayTeamScore": 4,
|
|
202
|
+
=end
|
data/lib/fifadat/version.rb
CHANGED
data/lib/fifadat.rb
CHANGED
|
@@ -18,8 +18,11 @@ require_relative 'fifadat/json' ## read_json_v2, fetch_json, fetch_json_if, e
|
|
|
18
18
|
## our own code
|
|
19
19
|
require_relative 'fifadat/version'
|
|
20
20
|
|
|
21
|
-
require_relative 'fifadat/
|
|
22
|
-
require_relative 'fifadat/
|
|
21
|
+
require_relative 'fifadat/helper'
|
|
22
|
+
require_relative 'fifadat/config'
|
|
23
|
+
require_relative 'fifadat/api'
|
|
24
|
+
require_relative 'fifadat/types'
|
|
25
|
+
|
|
23
26
|
require_relative 'fifadat/prepare' ## "all-in-one" prepare (download cache) helpers etc.
|
|
24
27
|
|
|
25
28
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fifadat
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2026.9.
|
|
4
|
+
version: 2026.9.30
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gerald Bauer
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: cocos
|
|
@@ -160,8 +160,13 @@ files:
|
|
|
160
160
|
- lib/fifadat.rb
|
|
161
161
|
- lib/fifadat/api.rb
|
|
162
162
|
- lib/fifadat/config.rb
|
|
163
|
+
- lib/fifadat/helper.rb
|
|
163
164
|
- lib/fifadat/json.rb
|
|
165
|
+
- lib/fifadat/pp/ppdebug.rb
|
|
166
|
+
- lib/fifadat/pp/ppmatch.rb
|
|
167
|
+
- lib/fifadat/pp/ppstats.rb
|
|
164
168
|
- lib/fifadat/prepare.rb
|
|
169
|
+
- lib/fifadat/types.rb
|
|
165
170
|
- lib/fifadat/version.rb
|
|
166
171
|
homepage: https://github.com/sportdb/sport.db
|
|
167
172
|
licenses:
|