footballdata-api 2026.7.2 → 2026.7.12

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 285333f5aeaa22a6e531e570f7d41ba43970f73d735e1faca3885d1e0c3387f7
4
- data.tar.gz: 8e3fddb191ba66045b9c80526d80187b17ec05281603e17a8ea057c1779b6bd8
3
+ metadata.gz: 301b4ba1575a7275090ed592dce4a7da171d739d3fbd7599a4b19794d589df0d
4
+ data.tar.gz: 467180eeb2bdd379d40fe46021748ddfca3ed72dab132aacdf397c762b155b5e
5
5
  SHA512:
6
- metadata.gz: 54da6c832674994199a490fab7a047872f45c43b0e33f399a081e94ab38157168a6882e675fab18790f2a573011fdf8f698b9a8ac3a727af9626cd4794def106
7
- data.tar.gz: bcff64f55b28e0e62b08d3f7b5e4bf739755c5bf832e6026318935329aebbc82aca3d8e03d7b9a6a49e851b6a7213133a06d7ab4d9c9b5b6bdfddf36596258bc
6
+ metadata.gz: 0c103f1473a73a21a494b86e435182d6bfe22744b098b7f2a661b7ac2f66e4b6a81965f5d18ec0b726b950746496ce8ed44b96266a66256a625784d2f5b0ba86
7
+ data.tar.gz: 4d538238d329037c41ffbbe1cdfdb1f0c21309c50250c1e403855ed7599b02c459a5430d305b82e7929245c0c101d5781c3df41354b20fe233d2b496297a70f8
data/CHANGELOG.md CHANGED
@@ -1,4 +1,4 @@
1
- ### 2026.7.2
1
+ ### 2026.7.12
2
2
  ### 0.0.1 / 2024-07-03
3
3
 
4
4
  * Everything is new. First release.
data/Manifest.txt CHANGED
@@ -6,7 +6,10 @@ bin/fbdat
6
6
  config/leagues_tier1.csv
7
7
  lib/footballdata.rb
8
8
  lib/footballdata/convert-score.rb
9
- lib/footballdata/convert.rb
9
+ lib/footballdata/convert-stages.rb
10
+ lib/footballdata/convert-teams.rb
11
+ lib/footballdata/convert_csv.rb
12
+ lib/footballdata/convert_json.rb
10
13
  lib/footballdata/download.rb
11
14
  lib/footballdata/leagues.rb
12
15
  lib/footballdata/mods.rb
data/README.md CHANGED
@@ -84,10 +84,10 @@ require 'footballdata'
84
84
  'pt.1',
85
85
  'uefa.cl',
86
86
  ].each do |league|
87
- Footballdata.convert( league: league, season: '2020/21' )
87
+ Footballdata.convert_csv( league: league, season: '2020/21' )
88
88
  end
89
89
 
90
- Footballdata.convert( league: 'br.1', season: '2020' )
90
+ Footballdata.convert_csv( league: 'br.1', season: '2020' )
91
91
  ```
92
92
 
93
93
  Note: By default all datasets get written into the `./o`
data/bin/fbdat CHANGED
@@ -36,8 +36,10 @@ opts = {
36
36
  cached: false,
37
37
  convert: true,
38
38
  file: nil,
39
+ json: nil,
39
40
  }
40
41
 
42
+
41
43
  parser = OptionParser.new do |parser|
42
44
  parser.banner = "Usage: #{$PROGRAM_NAME} [options] [args]"
43
45
 
@@ -51,6 +53,11 @@ parser = OptionParser.new do |parser|
51
53
  opts[:convert] = convert
52
54
  end
53
55
 
56
+ parser.on( "--json",
57
+ "turn on conversion to .json in #{Footballdata.config.convert.out_dir}" ) do |json|
58
+ opts[:json] = json
59
+ end
60
+
54
61
  parser.on( "--print", "--pp",
55
62
  "pretty print cached data in #{Webcache.root}; no download & conversion") do |print|
56
63
  opts[:cached] = true
@@ -200,6 +207,11 @@ autofiller = ->(league_query) {
200
207
  datasets = if opts[:file]
201
208
  read_leagueset( opts[:file], autofill: autofiller )
202
209
  else
210
+
211
+ ### todo or fix/fix/fix - change to build_leagueset
212
+ ## and reserve parse_leagueset( txt ) for
213
+ ## read_leagueset with inline text
214
+
203
215
  parse_leagueset_args( args, autofill: autofiller )
204
216
  end
205
217
 
@@ -243,7 +255,13 @@ if opts[:convert]
243
255
  puts "==> converting to .csv"
244
256
  datasets.each do |league_key, seasons|
245
257
  seasons.each do |season|
246
- convert( league: league_key, season: season )
258
+ ### quick hack - for json only conversion
259
+ if opts[:json]
260
+ convert_json( league: league_key, season: season )
261
+ else
262
+ convert_csv( league: league_key, season: season )
263
+ convert_json( league: league_key, season: season )
264
+ end
247
265
  end
248
266
  end
249
267
  end
@@ -257,4 +275,3 @@ Footballdata.main( ARGV )
257
275
 
258
276
 
259
277
  puts "bye"
260
-
@@ -1,44 +1,44 @@
1
1
  key, code, seasons
2
2
 
3
- eng.1, PL, 2024/25 2023/24 2022/23 2021/22 2020/21
3
+ eng.1, PL, 2026/27 2025/26 2024/25 2023/24 2022/23 2021/22 2020/21
4
4
  # incl. team(s) from wales
5
- eng.2, ELC, 2024/25 2023/24 2022/23 2021/22 2020/21
5
+ eng.2, ELC, 2026/27 2025/26 2024/25 2023/24 2022/23 2021/22 2020/21
6
6
  # PL - Premier League, England 27 seasons | 2019-08-09 - 2020-07-25 / matchday 31
7
7
  # ELC - Championship, England 3 seasons | 2019-08-02 - 2020-07-22 / matchday 38
8
8
 
9
- es.1, PD, 2024/25 2023/24 2022/23 2021/22 2020/21
9
+ es.1, PD, 2026/27 2025/26 2024/25 2023/24 2022/23 2021/22 2020/21
10
10
  # PD - Primera Division, Spain 27 seasons | 2019-08-16 - 2020-07-19 / matchday 31
11
11
 
12
- pt.1, PPL, 2024/25 2023/24 2022/23 2021/22 2020/21
12
+ pt.1, PPL, 2026/27 2025/26 2024/25 2023/24 2022/23 2021/22 2020/21
13
13
  # PPL - Primeira Liga, Portugal 9 seasons | 2019-08-10 - 2020-07-26 / matchday 28
14
14
 
15
- de.1, BL1, 2024/25 2023/24 2022/23 2021/22 2020/21
15
+ de.1, BL1, 2026/27 2025/26 2024/25 2023/24 2022/23 2021/22 2020/21
16
16
  # BL1 - Bundesliga, Germany 24 seasons | 2019-08-16 - 2020-06-27 / matchday 34
17
17
 
18
- nl.1, DED, 2024/25 2023/24 2022/23 2021/22 2020/21
18
+ nl.1, DED, 2026/27 2025/26 2024/25 2023/24 2022/23 2021/22 2020/21
19
19
  # DED - Eredivisie, Netherlands 10 seasons | 2019-08-09 - 2020-03-08 / matchday 34
20
20
 
21
- fr.1, FL1, 2024/25 2023/24 2022/23 2021/22 2020/21
21
+ fr.1, FL1, 2026/27 2025/26 2024/25 2023/24 2022/23 2021/22 2020/21
22
22
  # incl. team(s) monaco
23
23
 
24
- it.1, SA, 2024/25 2023/24 2022/23 2021/22 2020/21
24
+ it.1, SA, 2026/27 2025/26 2024/25 2023/24 2022/23 2021/22 2020/21
25
25
  # SA - Serie A, Italy 15 seasons | 2019-08-24 - 2020-08-02 / matchday 27
26
26
 
27
- br.1, BSA, 2025 2024 2023 2022 2021 2020
27
+ br.1, BSA, 2026 2025 2024 2023 2022 2021 2020
28
28
 
29
29
 
30
30
  ########
31
31
  ## int'l cups
32
- uefa.cl, CL, 2024/25 2023/24 2022/23 2021/22 2020/21
32
+ uefa.cl, CL, 2025/26 2024/25 2023/24 2022/23 2021/22 2020/21
33
33
  ## note: cl is country code for chile!! - use champs - why? why not?
34
34
  ## was europe.cl / cl
35
35
  ## todo/check: use champs and NOT cl - why? why not?
36
36
 
37
- copa.l, CLI, 2025 2024 2023 2022 2021
37
+ copa.l, CLI, 2026 2025 2024 2023 2022 2021
38
38
  ## Copa Libertadores
39
39
 
40
40
 
41
41
  ############
42
42
  ## national teams
43
43
  euro, EC, 2024 2021
44
- world, WC, 2022
44
+ world, WC, 2026 2022
@@ -1,6 +1,15 @@
1
1
 
2
2
  module Footballdata
3
3
 
4
+ ##
5
+ ## note - "weirdo" rule
6
+ ## fullTime is only "classic" fulltime if REGULAR
7
+ ##
8
+ ## otherwise fullTime is aggregate of extraTime AND penalties!!
9
+ ##
10
+ ##
11
+
12
+
4
13
  def self.convert_score( score )
5
14
  ## duration: REGULAR · PENALTY_SHOOTOUT · EXTRA_TIME
6
15
  ft, ht, et, pen = ["","","",""]
@@ -41,4 +50,54 @@ def self.convert_score( score )
41
50
 
42
51
  [ft,ht,et,pen]
43
52
  end
53
+
54
+
55
+
56
+ def self.convert_score_to_hash( score )
57
+ ## duration: REGULAR · PENALTY_SHOOTOUT · EXTRA_TIME
58
+
59
+ if score['duration'] == 'REGULAR'
60
+ { ft: [score['fullTime']['home'],
61
+ score['fullTime']['away']],
62
+ ht: [score['halfTime']['home'],
63
+ score['halfTime']['away']]
64
+ }
65
+ elsif score['duration'] == 'EXTRA_TIME'
66
+ { et: [score['regularTime']['home']+score['extraTime']['home'],
67
+ score['regularTime']['away']+score['extraTime']['away']],
68
+ ft: [score['regularTime']['home'],
69
+ score['regularTime']['away']],
70
+ ht: [score['halfTime']['home'],
71
+ score['halfTime']['away']]
72
+ }
73
+
74
+ elsif score['duration'] == 'PENALTY_SHOOTOUT'
75
+ if score['extraTime']
76
+ ## quick & dirty hack - calc et via regulartime+extratime
77
+ { pen: [score['penalties']['home'],
78
+ score['penalties']['away']],
79
+ et: [score['regularTime']['home']+score['extraTime']['home'],
80
+ score['regularTime']['away']+score['extraTime']['away']],
81
+ ft: [score['regularTime']['home'],
82
+ score['regularTime']['away']],
83
+ ht: [score['halfTime']['home'],
84
+ score['halfTime']['away']]
85
+ }
86
+ else ### south american-style (no extra time)
87
+ ## quick & dirty hacke - calc ft via fullTime-penalties
88
+ { pen: [score['penalties']['home'],
89
+ score['penalties']['away']],
90
+ ft: [score['fullTime']['home']-score['penalties']['home'],
91
+ score['fullTime']['away']-score['penalties']['away']],
92
+ ht: [score['halfTime']['home'],
93
+ score['halfTime']['away']]
94
+ }
95
+ end
96
+ else
97
+ raise ArgumentError, "!! unknown score duration - #{score.inspect}"
98
+ end
99
+ end
100
+
101
+
102
+
44
103
  end # module Footballdata
@@ -0,0 +1,20 @@
1
+
2
+ class Stages
3
+ def initialize
4
+ @recs = {}
5
+ end
6
+
7
+ def add_matches( matches )
8
+ matches.each do |m|
9
+ name = m['stage']
10
+ rec = @recs[name] ||= { name: name,
11
+ count: 0 }
12
+ rec[:count] +=1
13
+ end
14
+ end
15
+
16
+ def as_json() @recs.values; end
17
+
18
+ def size() @recs.size; end
19
+
20
+ end # class Stages
@@ -0,0 +1,155 @@
1
+
2
+ =begin
3
+
4
+ note - for address "null null null",
5
+ is street + city + postal_code/
6
+
7
+
8
+ {"area"=>{"id"=>2061, "name"=>"Cyprus", "code"=>"CYP", "flag"=>nil},
9
+ "id"=>11034,
10
+ "name"=>"Paphos FC",
11
+ "shortName"=>"Paphos FC",
12
+ "tla"=>"AEP",
13
+ "address"=>"null null null",
14
+ "website"=>nil,
15
+ "founded"=>nil,
16
+ "clubColors"=>nil,
17
+ "venue"=>"Alphamega Stadium",
18
+
19
+ {"area"=>{"id"=>2119, "name"=>"Kazakhstan", "code"=>"KAZ", "flag"=>nil},
20
+ "id"=>10601,
21
+ "name"=>"FK Kairat",
22
+ "shortName"=>"FK Kairat",
23
+ "tla"=>"KAI",
24
+ "address"=>"null null null",
25
+ "website"=>nil,
26
+ "founded"=>nil,
27
+ "clubColors"=>nil,
28
+ "venue"=>nil,
29
+
30
+ {"area"=>{"id"=>2060, "name"=>"Curaçao", "code"=>"CUW", "flag"=>"https://crests.football-data.org/curacao.svg"},
31
+ "id"=>9460,
32
+ "name"=>"Curaçao",
33
+ "shortName"=>"Curaçao",
34
+ "tla"=>"CUW",
35
+ "address"=>"null null null",
36
+ "website"=>nil,
37
+ "founded"=>nil,
38
+ "clubColors"=>nil,
39
+ "venue"=>nil,
40
+
41
+
42
+
43
+ =end
44
+
45
+
46
+ def build_team( h )
47
+
48
+ ##
49
+ ## note: convert country code to "standard" fifa code
50
+ ## use official fifa country code
51
+ country_name = h['area']['name']
52
+
53
+ country = Fifa.world.find_by_name( country_name )
54
+ if country.nil?
55
+ raise ArgumentError, "[fifa world] no country record found for #{country_name} for: #{h.inspect}"
56
+ end
57
+
58
+ country_code = country.code
59
+
60
+
61
+ rec = { id: h['id'],
62
+ name: h['name'],
63
+ short_name: h['shortName'],
64
+ code: h['tla'],
65
+ address: h['address'], ### street / city / postal_code
66
+ founded: h['founded'],
67
+ ground: h['venue'], ### use stadium or venue ??
68
+
69
+ ##
70
+ ## or use country { code:, name }
71
+ ## change to cc (country code) - why? why not?
72
+ country: { name: country_name,
73
+ code: country_code,
74
+ code_bak: h['area']['code'] },
75
+
76
+ count: 0, ## track - match counts
77
+ }
78
+
79
+ rec
80
+ end
81
+
82
+
83
+ class Teams
84
+
85
+ def initialize
86
+ @recs = []
87
+ @by_name = {}
88
+ end
89
+
90
+
91
+ def add( recs )
92
+ recs.each do |h|
93
+ rec = build_team( h )
94
+ @recs << rec
95
+
96
+ ## note - assert/make sure team name is uniq
97
+ if @by_name.has_key?(rec[:name])
98
+ raise ArgumentError, "duplicate team name - #{h.inspect}"
99
+ else
100
+ @by_name[rec[:name]] = rec
101
+ end
102
+
103
+ if rec[:name] != rec[:short_name]
104
+ if @by_name.has_key?(rec[:short_name])
105
+ raise ArgumentError, "duplicate team (short) name - #{h.inspect}"
106
+ else
107
+ @by_name[rec[:short_name]] = rec
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+
114
+
115
+ def find_by!( name: )
116
+ rec = @by_name[ name ]
117
+ if rec.nil?
118
+ raise ArgumentError, "team >#{name}< not found; sorry"
119
+ end
120
+
121
+ rec
122
+ end
123
+
124
+
125
+ def add_matches( matches ) ## use/rename to add_matches - why? why not?
126
+ matches.each do |m|
127
+
128
+ ## note - skip teams without name (e.g. N.N.)
129
+ if m['homeTeam']['name']
130
+ rec = find_by!( name: m['homeTeam']['name'] )
131
+ rec[:count] += 1
132
+ end
133
+
134
+ if m['awayTeam']['name']
135
+ rec = find_by!( name: m['awayTeam']['name'] )
136
+ rec[:count] += 1
137
+ end
138
+ end
139
+ end
140
+
141
+ def each( &blk) @recs.each( &blk); end
142
+
143
+
144
+
145
+ def as_json( id: false )
146
+ if id
147
+ @recs
148
+ else
149
+ @recs.map { |rec| rec.except(:id ) }
150
+ end
151
+ end
152
+
153
+ def size() @recs.size; end
154
+
155
+ end # class Teams
@@ -1,73 +1,50 @@
1
1
 
2
2
  module Footballdata
3
3
 
4
- #######
5
- ## map round-like to higher-level stages
6
- ## fix - map by league codes
7
- ## or * (all)
4
+
5
+ ####
6
+ ## beautify stages constant names
8
7
  STAGES = {
9
- 'REGULAR_SEASON' => ['Regular'],
10
-
11
- 'QUALIFICATION' => ['Qualifying'],
12
- 'PRELIMINARY_ROUND' => ['Qualifying', 'Preliminary Round' ],
13
- 'PRELIMINARY_SEMI_FINALS' => ['Qualifying', 'Preliminary Semifinals' ],
14
- 'PRELIMINARY_FINAL' => ['Qualifying', 'Preliminary Final' ],
15
- '1ST_QUALIFYING_ROUND' => ['Qualifying', 'Round 1' ],
16
- '2ND_QUALIFYING_ROUND' => ['Qualifying', 'Round 2' ],
17
- '3RD_QUALIFYING_ROUND' => ['Qualifying', 'Round 3' ],
18
- 'QUALIFICATION_ROUND_1' => ['Qualifying', 'Round 1' ],
19
- 'QUALIFICATION_ROUND_2' => ['Qualifying', 'Round 2' ],
20
- 'QUALIFICATION_ROUND_3' => ['Qualifying', 'Round 3' ],
21
- 'ROUND_1' => ['Qualifying', 'Round 1'], ## use Qual. Round 1 - why? why not?
22
- 'ROUND_2' => ['Qualifying', 'Round 2'],
23
- 'ROUND_3' => ['Qualifying', 'Round 3'],
24
- 'PLAY_OFF_ROUND' => ['Qualifying', 'Playoff Round'],
25
- 'PLAYOFF_ROUND_1' => ['Qualifying', 'Playoff Round 1'],
26
-
27
- 'LEAGUE_STAGE' => ['League'],
28
- 'GROUP_STAGE' => ['Group'],
29
- 'PLAYOFFS' => ['Playoffs'],
30
-
31
- 'ROUND_OF_16' => ['Finals', 'Round of 16'],
32
- 'LAST_16' => ['Finals', 'Round of 16'], ## use Last 16 - why? why not?
33
- 'QUARTER_FINALS' => ['Finals', 'Quarterfinals'],
34
- 'SEMI_FINALS' => ['Finals', 'Semifinals'],
35
- 'THIRD_PLACE' => ['Finals', 'Third place play-off'],
36
- 'FINAL' => ['Finals', 'Final'],
8
+ 'REGULAR_SEASON' => 'Regular Season',
9
+
10
+ 'QUALIFICATION' => 'Qualification',
11
+ 'PRELIMINARY_ROUND' => 'Preliminary Round',
12
+ 'PRELIMINARY_SEMI_FINALS' => 'Preliminary Semifinals',
13
+ 'PRELIMINARY_FINAL' => 'Preliminary Final',
14
+ '1ST_QUALIFYING_ROUND' => '1st Qualifying Round',
15
+ '2ND_QUALIFYING_ROUND' => '2nd Qualifying Round',
16
+ '3RD_QUALIFYING_ROUND' => '3rd Qualifying Round',
17
+ 'QUALIFICATION_ROUND_1' => 'Qualification Round 1',
18
+ 'QUALIFICATION_ROUND_2' => 'Qualification Round 2',
19
+ 'QUALIFICATION_ROUND_3' => 'Qualification Round 3',
20
+ 'ROUND_1' => 'Round 1',
21
+ 'ROUND_2' => 'Round 2',
22
+ 'ROUND_3' => 'Round 3',
23
+ 'PLAY_OFF_ROUND' => 'Playoff Round',
24
+ 'PLAYOFF_ROUND_1' => 'Playoff Round 1',
25
+
26
+ 'LEAGUE_STAGE' => 'League', ## add Stage - why? why not?
27
+ 'GROUP_STAGE' => 'Group',
28
+ 'PLAYOFFS' => 'Playoffs', ## -- used in champs
29
+ 'PLAY_OFFS' => 'Playoffs', ## -- used in copa liber.
30
+
31
+ 'LAST_32' => 'Round of 32',
32
+ 'ROUND_OF_16' => 'Round of 16',
33
+ 'LAST_16' => 'Round of 16', ## use Last 16 - why? why not?
34
+ 'QUARTER_FINALS' => 'Quarterfinals',
35
+ 'SEMI_FINALS' => 'Semifinals',
36
+ 'THIRD_PLACE' => 'Third place',
37
+ 'FINAL' => 'Final',
37
38
  }
38
39
 
39
40
 
40
41
 
41
-
42
-
43
- def self.find_team_country_code( name, teams: )
44
- ## add (fifa) country code e.g.
45
- ## Liverpool FC => Liverpool FC (ENG)
46
- ## or Liverpool FC => Liverpool FC (URU)
47
- rec = teams[ name ]
48
- if rec.nil?
49
- puts "!! ERROR - no team record found in teams.json for #{name}"
50
- pp teams.keys
51
- exit 1
52
- end
53
-
54
- country_name = rec['area']['name']
55
- country = Fifa.world.find_by_name( country_name )
56
- if country.nil?
57
- puts "!! ERROR - no country record found for #{country_name}"
58
- exit 1
59
- end
60
-
61
- country.code
62
- end
63
-
64
-
65
-
66
- def self.convert( league:, season: )
42
+ def self.convert_csv( league:, season: )
67
43
 
68
44
  season = Season( season ) ## cast (ensure) season class (NOT string, integer, etc.)
69
45
 
70
- ### note - find_league returns the metal_league_code
46
+ ### note - find_league returns the metal_league_code
47
+ ## e.g. eng.1 => PL
71
48
  league_code = find_league!( league )
72
49
 
73
50
  matches_url = Metal.competition_matches_url( league_code, season.start_year )
@@ -77,6 +54,17 @@ def self.convert( league:, season: )
77
54
  data_teams = Webcache.read_json( teams_url )
78
55
 
79
56
 
57
+ ## build a team lookup by name (& short_name)
58
+
59
+ teams = Teams.new
60
+ teams.add( data_teams['teams'] )
61
+ puts "#{teams.size} team(s)"
62
+
63
+ ## add/update match counts
64
+ teams.add_matches( data['matches'] )
65
+
66
+
67
+
80
68
  ###
81
69
  ## todo/fix - use find_by! - add upstream!!!
82
70
  league_info = LeagueCodes.find_by( code: league, season: season )
@@ -93,135 +81,102 @@ def self.convert( league:, season: )
93
81
  'copa.l'].include?(league.downcase) ? true : false
94
82
 
95
83
 
84
+ mods = MODS[ league.downcase ] || {}
96
85
 
97
- ## build a (reverse) team lookup by name
98
- puts "#{data_teams['teams'].size} teams"
99
86
 
100
- teams_by_name = data_teams['teams'].reduce( {} ) do |h,rec|
101
- h[ rec['name'] ] = rec
102
- h
103
- end
104
-
105
- ## pp teams_by_name.keys
106
-
107
-
108
-
109
- mods = MODS[ league.downcase ] || {}
110
-
111
-
112
- recs = []
113
-
114
- teams = Hash.new( 0 )
115
87
 
88
+ recs = []
116
89
 
117
- # stat = Stat.new
118
-
90
+ # stat = Stat.new
119
91
  ## track stage, match status et
120
92
  stats = { 'status' => Hash.new(0),
121
93
  'stage' => Hash.new(0),
122
94
  }
123
95
 
124
96
 
125
-
126
-
127
97
  matches = data[ 'matches']
128
98
  matches.each do |m|
129
99
  # stat.update( m )
130
100
 
131
- ## use ? or N.N. or ? for nil - why? why not?
132
- team1 = m['homeTeam']['name'] || 'N.N.'
133
- team2 = m['awayTeam']['name'] || 'N.N.'
101
+ # use ? or N.N. or ? for nil - why? why not?
102
+ team1_name = m['homeTeam']['name']
103
+ team2_name = m['awayTeam']['name']
134
104
 
135
- ## auto-fix copa.l 2024
136
- ## !! ERROR: unsupported match status >IN_PLAY< - sorry:
137
- if m['status'] == 'IN_PLAY' &&
138
- team1 == 'Club Aurora' && team2 == 'FBC Melgar'
139
- m['status'] = 'FINISHED'
140
- end
105
+ team1 = team1_name ? teams.find_by!(name: team1_name ) : { name: 'N.N.' }
106
+ team2 = team2_name ? teams.find_by!(name: team2_name ) : { name: 'N.N.' }
141
107
 
142
108
 
109
+ ## use "normed" team names
110
+ team1_name = team1[:name]
111
+ team2_name = team2[:name]
143
112
 
144
- score = m['score']
113
+ ### mods - rename club names
114
+ unless mods.nil? || mods.empty?
115
+ team1_name = mods[ team1_name ] if mods[ team1_name ]
116
+ team2_name = mods[ team2_name ] if mods[ team2_name ]
117
+ end
145
118
 
146
- group = m['group']
147
- ## GROUP_A
148
- ## shorten group to A|B|C etc.
149
- if group && group =~ /^(GROUP_|Group )/
150
- group = group.sub( /^(GROUP_|Group )/, '' )
151
- else
152
- if group.nil?
153
- group = ''
154
- else
155
- puts "!! WARN - group defined with NON GROUP!? >#{group}< reset to empty"
156
- puts " and matchday to >#{m['matchday']}<"
157
- ## reset group to empty
158
- group = ''
159
- end
119
+ ####
120
+ # auto-add (fifa) country code if int'l club tournament
121
+ if clubs_intl
122
+ if team1_name != 'N.N.' ## note - ignore no team/placeholder (e.g. N.N)
123
+ team1_name = "#{team1_name} (#{team1[:country][:code]})"
124
+ end
125
+ if team2_name != 'N.N.' ## note - ignore no team/placeholder (e.g. N.N)
126
+ team2_name = "#{team2_name} (#{team2[:country][:code]})"
127
+ end
160
128
  end
161
129
 
162
130
 
131
+
132
+
133
+
134
+ group = m['group']
163
135
  stage_key = m['stage']
164
136
  stats['stage'][ stage_key ] += 1 ## track stage counts
137
+ ## add group stats?
165
138
 
166
139
 
167
- stage, stage_round = if group.empty?
168
- ## map stage to stage + round
169
- STAGES[ stage_key ]
170
- else
171
- ## if group defined ignore stage
172
- ## hard-core always to group for now
173
- if stage_key != 'GROUP_STAGE'
174
- puts "!! WARN - group defined BUT stage set to >#{stage_key}<"
175
- puts " and matchday to >#{m['matchday']}<"
176
- end
177
- ['Group', nil]
178
- end
140
+ ## GROUP_A
141
+ ## shorten group to A|B|C etc.
142
+ if group
143
+ ## assert group only used with GROUP_STAGE
144
+ if stage_key != 'GROUP_STAGE'
145
+ raise ArgumentError,
146
+ "group defined BUT stage set to >#{stage_key}< (expected GROUP_STAGE)" +
147
+ "and matchday to >#{m['matchday']}<"
148
+ end
179
149
 
180
- if stage.nil?
181
- puts "!! ERROR - no stage mapping found for stage >#{stage_key}<"
182
- exit 1
150
+ if group =~ /^(GROUP_|Group )/
151
+ group = group.sub( /^(GROUP_|Group )/, '' )
152
+ else
153
+ raise ArgumentError,
154
+ "group defined with NON GROUP!? >#{group}< reset to empty"+
155
+ " and matchday to >#{m['matchday']}<"
156
+ end
157
+ else ## assume group.nil?
158
+ group = ''
183
159
  end
184
160
 
185
- matchday_num = m['matchday']
186
- matchday_num = nil if matchday_num == 0 ## change 0 to nil (empty) too
187
-
188
- if stage_round.nil? ## e.g. Regular, League, Group, Playoffs
189
- ## keep/assume matchday number is matchday .e.g
190
- ## matchday 1, 2 etc.
191
- matchday = matchday_num.to_s
192
- else
193
- ## note - if matchday/round defined, use it
194
- ## note - ignore possible leg in matchday for now
195
- matchday = stage_round
161
+
162
+
163
+ ## map stage to stage + round
164
+ ## note - if no mapping defined; use (fallback to) upcase version
165
+ stage = STAGES[ stage_key ]
166
+
167
+ if stage.nil?
168
+ puts "!! WARN - no stage mapping found for stage >#{stage_key}<"
169
+ stage = stage_key
196
170
  end
197
171
 
198
172
 
199
- teams[ team1 ] += 1
200
- teams[ team2 ] += 1
201
173
 
174
+ matchday = m['matchday']
175
+ matchday = nil if matchday == 0 ## change 0 to nil (empty) too
202
176
 
203
- ### mods - rename club names
204
- unless mods.nil? || mods.empty?
205
- team1 = mods[ team1 ] if mods[ team1 ]
206
- team2 = mods[ team2 ] if mods[ team2 ]
207
- end
208
177
 
209
- ####
210
- # auto-add (fifa) country code if int'l club tournament
211
- if clubs_intl
212
- ## note - use "original" name (not moded) for lookup
213
- team1_org = m['homeTeam']['name']
214
- team2_org = m['awayTeam']['name']
215
- if team1_org ## note - ignore no team/placeholder (e.g. N.N)
216
- country_code = find_team_country_code( team1_org, teams: teams_by_name )
217
- team1 = "#{team1} (#{country_code})"
218
- end
219
- if team2_org ## note - ignore no team/placeholder (e.g. N.N)
220
- country_code = find_team_country_code( team2_org, teams: teams_by_name )
221
- team2 = "#{team2} (#{country_code})"
222
- end
223
- end
224
178
 
179
+ score = m['score']
225
180
 
226
181
 
227
182
  comments = ''
@@ -256,9 +211,8 @@ matches.each do |m|
256
211
  ht = ''
257
212
  comments = 'postponed'
258
213
  else
259
- puts "!! ERROR: unsupported match status >#{m['status']}< - sorry:"
260
- pp m
261
- exit 1
214
+ raise ArgumentError,
215
+ "unsupported match status >#{m['status']}< - sorry:"
262
216
  end
263
217
 
264
218
 
@@ -297,14 +251,14 @@ matches.each do |m|
297
251
 
298
252
  recs << [stage,
299
253
  group,
300
- matchday,
254
+ matchday.to_s, ## not convert nil or Integer to str e.g. "", "1"
301
255
  date,
302
256
  time,
303
257
  timezone,
304
- team1,
258
+ team1_name,
305
259
  ft,
306
260
  ht,
307
- team2,
261
+ team2_name,
308
262
  et,
309
263
  pen,
310
264
  comments,
@@ -333,7 +287,7 @@ dates = "#{start_date.strftime('%b %-d')} - #{end_date.strftime('%b %-d')}"
333
287
 
334
288
  buf = ''
335
289
  buf << "#{season.key} (#{dates}) - "
336
- buf << "#{teams.keys.size} teams, "
290
+ buf << "#{teams.size} teams, "
337
291
  buf << "#{recs.size} matches"
338
292
  # buf << "#{stat[:regular_season][:goals]} goals"
339
293
  buf << "\n"
@@ -420,27 +374,17 @@ recs, headers = vacuum( recs )
420
374
  headers: headers )
421
375
 
422
376
 
423
- teams.each do |name, count|
424
- rec = teams_by_name[ name ]
425
- print " #{count}x "
426
- print name
427
- if rec
428
- print " | #{rec['shortName']} " if name != rec['shortName']
429
- print " › #{rec['area']['name']}"
430
- print " - #{rec['address']}"
431
- else
432
- if name == 'N.N.'
433
- ## ignore missing record
434
- else
435
- puts "!! ERROR - no team record found in teams.json for #{name}"
436
- exit 1
437
- end
438
- end
377
+ teams.each do |rec|
378
+ print " #{rec[:count]}x "
379
+ print rec[:name]
380
+ print " | #{rec[:short_name]} " if rec[:name] != rec[:short_name]
381
+ print " › #{rec[:country][:name]} (#{rec[:country][:code]})"
382
+ print " - #{rec[:address]}"
439
383
  print "\n"
440
384
  end
441
385
 
442
386
  ## pp stat
443
- end # method convert
387
+ end # method convert_csv
444
388
 
445
389
 
446
390
 
@@ -471,6 +415,9 @@ MIN_HEADERS = [ ## always keep even if all empty
471
415
 
472
416
 
473
417
 
418
+ ###
419
+ ### move upstream to cocos!! (re)use vacuum_csv !!!!
420
+
474
421
  def self.vacuum( rows, headers: MAX_HEADERS, fixed_headers: MIN_HEADERS )
475
422
  ## check for unused columns and strip/remove
476
423
  counter = Array.new( MAX_HEADERS.size, 0 )
@@ -513,5 +460,3 @@ def self.vacuum( rows, headers: MAX_HEADERS, fixed_headers: MIN_HEADERS )
513
460
  [rows, headers]
514
461
  end
515
462
  end # module Footballdata
516
-
517
-
@@ -0,0 +1,160 @@
1
+
2
+ module Footballdata
3
+
4
+
5
+
6
+
7
+
8
+ def self.convert_json( league:, season: )
9
+
10
+ season = Season( season ) ## cast (ensure) season class (NOT string, integer, etc.)
11
+
12
+ ### note - find_league returns the metal_league_code
13
+ league_code = find_league!( league )
14
+
15
+ matches_url = Metal.competition_matches_url( league_code, season.start_year )
16
+ teams_url = Metal.competition_teams_url( league_code, season.start_year )
17
+
18
+ data = Webcache.read_json( matches_url )
19
+ data_teams = Webcache.read_json( teams_url )
20
+
21
+
22
+ ## note - for international club tournaments
23
+ ## auto-add (fifa) country code e.g.
24
+ ## Liverpool FC => Liverpool FC (ENG)
25
+ ##
26
+ ## todo/fix - move flag to league_info via .csv config - why? why not?
27
+ ## clubs_intl = ['uefa.cl',
28
+ ## 'copa.l'].include?(league.downcase) ? true : false
29
+
30
+ ## build a team lookup by name (& short_name)
31
+
32
+ teams = Teams.new
33
+ teams.add( data_teams['teams'] )
34
+ puts "#{teams.size} team(s)"
35
+
36
+ ## add/update match counts
37
+ teams.add_matches( data['matches'] )
38
+
39
+
40
+ stages = Stages.new
41
+ stages.add_matches( data['matches'] )
42
+
43
+
44
+ recs = []
45
+
46
+
47
+ ## track (meta stats) stage, match status et
48
+ stats = { status: Hash.new(0), ## e.g. TIMED,etc
49
+ score: Hash.new(0), ## e.g. REGULAR,etc.
50
+ }
51
+
52
+
53
+
54
+
55
+ data[ 'matches'].each do |m|
56
+
57
+ ## use ? or N.N. or ? for nil - why? why not?
58
+ team1_name = m['homeTeam']['name']
59
+ team2_name = m['awayTeam']['name']
60
+
61
+ team1 = team1_name ? teams.find_by!(name: team1_name ) : { name: 'N.N.' }
62
+ team2 = team2_name ? teams.find_by!(name: team2_name ) : { name: 'N.N.' }
63
+
64
+
65
+ stage = m['stage']
66
+ group = m['group']
67
+
68
+ matchday = m['matchday']
69
+ matchday = nil if matchday == 0 ## change 0 to nil (empty) too
70
+
71
+
72
+ stats[:status][m['status']] += 1 ## track status counts
73
+ stats[:score][m['score']['duration']] += 1
74
+
75
+ case m['status']
76
+ when 'SCHEDULED', 'TIMED',
77
+ 'PAUSED', 'IN_PLAY' ## IN_PLAY, PAUSED
78
+ score = []
79
+ when 'FINISHED'
80
+ score = convert_score_to_hash( m['score'] )
81
+ when 'AWARDED' # AWARDED
82
+ assert( m['score']['duration'] == 'REGULAR', 'score.duration REGULAR expected' )
83
+ score = [m['score']['fullTime']['home'],
84
+ m['score']['fullTime']['away']]
85
+ when 'CANCELLED'
86
+ ## note cancelled might have scores!! -- add/fix later!!!
87
+ ## ht only or ft+ht!!! (see fr 2021/22)
88
+ ## score = convert_score_to_hash( m['score'] )
89
+ ##
90
+ ## note - there is cancelled (e.g. not/played) AND
91
+ ## annulled/voided (e.g. played!! with score)
92
+ score = []
93
+ when 'POSTPONED'
94
+ score = []
95
+ else
96
+ raise ArgumentError,
97
+ "unsupported match status >#{m['status']}< - sorry: #{m.pretty_inspect}"
98
+ end
99
+
100
+
101
+
102
+ rec = {
103
+ status: m['status'],
104
+ stage: stage
105
+ }
106
+
107
+
108
+ utc = UTC.strptime( m['utcDate'], '%Y-%m-%dT%H:%M:%SZ' )
109
+ assert( utc.strftime( '%Y-%m-%dT%H:%M:%SZ' ) == m['utcDate'], 'utc time mismatch' )
110
+
111
+
112
+ ## do NOT add time if status is SCHEDULED
113
+ ## or POSTPONED for now
114
+ ## otherwise assume time always present - why? why not?
115
+ ##
116
+ ## assume NOT valid utc time if 00:00
117
+ if utc.hour == 0 && utc.min == 0 &&
118
+ ['SCHEDULED','POSTPONED'].include?( m['status'] )
119
+ ## assume local date - why? why not?
120
+ rec[:date] = utc.strftime( '%Y-%m-%d' )
121
+ else
122
+ rec[:date_utc] = utc.strftime( '%Y-%m-%dT%H:%M:%SZ' )
123
+ end
124
+
125
+
126
+
127
+ rec[:group] = group if group
128
+ rec[:matchday] = matchday if matchday
129
+
130
+ rec[:team1] = team1[:name]
131
+ rec[:team2] = team2[:name]
132
+
133
+ rec[:score] = score unless score.empty?
134
+
135
+
136
+ recs << rec
137
+ end
138
+
139
+
140
+ stats[:matches] = recs.size
141
+ stats[:teams] = teams.size
142
+
143
+ data = {
144
+ slug: league,
145
+ season: season.to_s,
146
+ meta: stats,
147
+ stages: stages.as_json,
148
+ teams: teams.as_json,
149
+ matches: recs
150
+ }
151
+
152
+
153
+ path = "#{config.convert.out_dir}/#{season.to_path}/#{league.downcase}.json"
154
+ ## note: change season_key from 2019/20 to 2019-20 (for path/directory!!!!)
155
+ puts " writing to >#{path}<"
156
+ write_json( path, data )
157
+ end # method convert_json
158
+
159
+
160
+ end # module Footballdata
@@ -53,6 +53,7 @@ class Metal
53
53
 
54
54
  request_headers = request_headers.merge( headers ) unless headers.empty?
55
55
 
56
+ ## pp request_headers
56
57
 
57
58
  ## note: add format: 'json' for pretty printing json (before) save in cache
58
59
  response = Webget.call( url, headers: request_headers )
@@ -117,10 +118,12 @@ class Metal
117
118
  get( "#{BASE_URL}/teams/#{id}" )
118
119
  end
119
120
 
120
- def self.match( id )
121
- get( "#{BASE_URL}/matches/#{id}" )
121
+
122
+ def self.match( id, headers: {} )
123
+ get( "#{BASE_URL}/matches/#{id}", headers: headers )
122
124
  end
123
125
 
126
+
124
127
  def self.person( id )
125
128
  get( "#{BASE_URL}/persons/#{id}" )
126
129
  end
@@ -1,3 +1,10 @@
1
+
2
+ ##
3
+ ## todo/fix cleanup
4
+ ## use a "generic" record for lookup
5
+ ##
6
+
7
+
1
8
  module Footballdata
2
9
 
3
10
  def self.find_league!( league )
@@ -48,4 +55,3 @@ module Footballdata
48
55
  seasons
49
56
  end
50
57
  end # module Footballdata
51
-
@@ -2,7 +2,7 @@
2
2
  module FootballdataApi
3
3
  MAJOR = 2026 ## todo: namespace inside version or something - why? why not??
4
4
  MINOR = 7
5
- PATCH = 2
5
+ PATCH = 12
6
6
  VERSION = [MAJOR,MINOR,PATCH].join('.')
7
7
 
8
8
  def self.version
data/lib/footballdata.rb CHANGED
@@ -36,8 +36,13 @@ require_relative 'footballdata/download'
36
36
  require_relative 'footballdata/prettyprint'
37
37
 
38
38
  require_relative 'footballdata/mods'
39
- require_relative 'footballdata/convert'
39
+ require_relative 'footballdata/convert_csv'
40
+ require_relative 'footballdata/convert_json'
41
+ require_relative 'footballdata/convert-stages'
42
+ require_relative 'footballdata/convert-teams'
40
43
  require_relative 'footballdata/convert-score'
44
+
45
+
41
46
  require_relative 'footballdata/teams'
42
47
 
43
48
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: footballdata-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 2026.7.2
4
+ version: 2026.7.12
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-07-02 00:00:00.000000000 Z
11
+ date: 2026-07-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: leagues
@@ -105,7 +105,10 @@ files:
105
105
  - config/leagues_tier1.csv
106
106
  - lib/footballdata.rb
107
107
  - lib/footballdata/convert-score.rb
108
- - lib/footballdata/convert.rb
108
+ - lib/footballdata/convert-stages.rb
109
+ - lib/footballdata/convert-teams.rb
110
+ - lib/footballdata/convert_csv.rb
111
+ - lib/footballdata/convert_json.rb
109
112
  - lib/footballdata/download.rb
110
113
  - lib/footballdata/leagues.rb
111
114
  - lib/footballdata/mods.rb