openliga 0.0.1

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.
@@ -0,0 +1,172 @@
1
+ module Openliga
2
+
3
+
4
+
5
+ def self.build_score( h )
6
+ ht = nil
7
+ ft = nil
8
+ et = nil
9
+ pen = nil
10
+
11
+ maybe_ft = nil
12
+ maybe_et = nil
13
+
14
+ h.each do |result|
15
+ ## note - assume extra time for both options now
16
+ ## assert ft is nil - why? why not?
17
+
18
+ ### resultName check - why? why not?
19
+ ### are legacy or not?
20
+
21
+ ###
22
+ ## todo - check copa america
23
+ ## if extra time excluded on penalties???
24
+
25
+ case result['resultTypeKind']
26
+ when 'HalfTime'
27
+ ## result['resultName'] == 'Halbzeitergebnis' ||
28
+ ## result['resultName'] == 'Halbzeit'
29
+ ## desc = >Ergebnis nach Ende der ersten Halbzeit
30
+ ht = [
31
+ result['pointsTeam1'],
32
+ result['pointsTeam2'],
33
+ ]
34
+ ##
35
+ ## note if resultName is Endergebnis this might actually
36
+ ## be after penalty shootout or extra time!!!
37
+ ## check if "resultName": "nach 90 Minuten"
38
+ ## is present too!!
39
+ when 'After90Minutes'
40
+ ## result['resultName'] == 'Endergebnis' ## ||
41
+ ## result['resultName'] == 'nach Nachspielzeit' ## !!!!
42
+ ## desc => Ergebnis nach Ende der offiziellen Spielzeit
43
+ ft = [
44
+ result['pointsTeam1'],
45
+ result['pointsTeam2'],
46
+ ]
47
+ when 'AfterExtraTime'
48
+ ## result['resultName'] == 'Verlängerung' ||
49
+ ## result['resultName'] == 'nach Verlängerung'
50
+ ## desc: Ergebnis nach Verlängerung
51
+ et = [
52
+ result['pointsTeam1'],
53
+ result['pointsTeam2'],
54
+ ]
55
+ when 'AfterPenalties'
56
+ ## result['resultName'] == 'Elfmeterschießen' ||
57
+ ## result['resultName'] == 'nach Elfmeterschießen'
58
+ ## desc: Ergebnis nach Elfmeterschießen
59
+ pen = [
60
+ result['pointsTeam1'],
61
+ result['pointsTeam2'],
62
+ ]
63
+ when 'Unknown'
64
+ ### note:
65
+ ## for now resultName: "nach 90 Minutes" overwrites
66
+ ## first After90Minutes!!! (assuming this is kind of Endergebnis really incl. extra-time or penalty shootout)
67
+ if result['resultName'] == 'nach 90 Minuten'
68
+ puts "!! warn - weirdo Unknown/nach 90 Minuten result found"
69
+ ft = [
70
+ result['pointsTeam1'],
71
+ result['pointsTeam2'],
72
+ ]
73
+ elsif result['resultName'] == 'Endergebnis'
74
+ puts "!! warn - weirdo Unknown/Endergebnis result found"
75
+ maybe_ft = [
76
+ result['pointsTeam1'],
77
+ result['pointsTeam2'],
78
+ ]
79
+ elsif result['resultName'] == 'Nachspielzeit'
80
+ puts "!! warn - weirdo Unknown/Nachspielzeit result found"
81
+ maybe_et = [
82
+ result['pointsTeam1'],
83
+ result['pointsTeam2'],
84
+ ]
85
+ else
86
+ puts "!! ERROR - unknown <Unknown> result type:"
87
+ pp result
88
+ exit 1
89
+ end
90
+ else
91
+ puts "!! ERROR - unknown result type:"
92
+ pp result
93
+ exit 1
94
+ end
95
+ end
96
+
97
+ ## note - DFB Pokal 2025/2026
98
+ ### requires hack
99
+ ## if Nachspielzeit (et) present
100
+ ## only accept if different from ft or pen is present!!!
101
+
102
+ if maybe_et
103
+ if pen || maybe_et != ft
104
+ et = maybe_et
105
+ else
106
+ puts "!! WARN - ignoring et (Unknown/Nachspielzeit) result in:"
107
+ pp h
108
+ end
109
+ end
110
+
111
+ if maybe_ft
112
+ if ft.nil?
113
+ ft = maybe_ft
114
+ else
115
+ puts "!! WARN - ignoring ft (Unknown/Endergebnis) result in:"
116
+ pp h
117
+ end
118
+ end
119
+
120
+ Score.new( ht: ht, ft: ft, et: et, pen: pen )
121
+ end
122
+
123
+
124
+ end ## module Openliga
125
+
126
+
127
+
128
+ =begin
129
+ "matchResults": [
130
+ {
131
+ "resultID": 110926,
132
+ "resultName": "Halbzeitergebnis",
133
+ "pointsTeam1": 3,
134
+ "pointsTeam2": 0,
135
+ "resultOrderID": 1,
136
+ "resultTypeID": 1,
137
+ "resultDescription": "Ergebnis zur Halbzeitpause"
138
+ },
139
+ {
140
+ "resultID": 110927,
141
+ "resultName": "Endergebnis",
142
+ "pointsTeam1": 5,
143
+ "pointsTeam2": 1,
144
+ "resultOrderID": 2,
145
+ "resultTypeID": 2,
146
+ "resultDescription": "Ergebnis nach Ende der offiziellen Spielzeit"
147
+ }
148
+
149
+ {"resultID"=>127243,
150
+ "resultName"=>"Halbzeit",
151
+ "pointsTeam1"=>0,
152
+ "pointsTeam2"=>10,
153
+ "resultOrderID"=>1,
154
+ "resultTypeID"=>1,
155
+ "resultTypeKind"=>"HalfTime",
156
+ "resultDescription"=>"Ergebnis nach Ende der ersten Halbzeit"}
157
+
158
+
159
+ or
160
+ !! ERROR - unknown result type:
161
+ {"resultID"=>118424,
162
+ "resultName"=>"Nachspielzeit",
163
+ "pointsTeam1"=>1,
164
+ "pointsTeam2"=>2,
165
+ "resultOrderID"=>3,
166
+ "resultTypeID"=>3,
167
+ "resultTypeKind"=>"Unknown",
168
+ "resultDescription"=>"Ergebnis nach Nachspielzeit"}
169
+
170
+
171
+ DFB Pokal 2025/2026
172
+ =end
@@ -0,0 +1,68 @@
1
+ module Openliga
2
+
3
+ def self.convert( league:, season: )
4
+
5
+ season = Season( season ) ## cast (ensure) season class (NOT string, integer, etc.)
6
+
7
+
8
+ info = (LEAGUES[ league.downcase ]||{})[ season.to_key ]
9
+ if info.nil?
10
+ puts "!! ERROR - no openliga info found for #{league} #{season}"
11
+ exit 1
12
+ end
13
+ pp info
14
+
15
+
16
+ data = Webcache.read_json( Metal.matches_url( info.shortcut, info.season ))
17
+ puts " #{data.size} match(es)"
18
+
19
+ ## data_teams = Webcache.read_json( Metal.teams_url( LEAGUES[league.downcase], season.start_year ))
20
+ ## puts " #{data_teams.size} team(s)"
21
+
22
+
23
+ body = _convert( data )
24
+
25
+ ###
26
+ ### auto-add header
27
+ header =<<TXT
28
+ ###
29
+ # converted from openligadb.de json to Football.TXT
30
+ # for source, see https://api.openligadb.de/getmatchdata/#{info.shortcut}/#{info.season}
31
+
32
+ = #{info.name}
33
+
34
+ TXT
35
+
36
+ header + body
37
+ end
38
+
39
+
40
+
41
+ def self._convert( data )
42
+
43
+ recs = []
44
+
45
+ matches = data
46
+ matches.each do |h|
47
+ m = build_match( h )
48
+
49
+ ## note - if match is broken e.g. has no team1 or team2
50
+ ## than skip for now
51
+
52
+ if m.team1.nil? || m.team2.nil?
53
+ puts "!! warn - skipping match with team missing:"
54
+ pp h
55
+ else
56
+ recs << m
57
+ end
58
+ end
59
+
60
+ ## pp recs
61
+
62
+ pp_matches( recs )
63
+ end # method self._convert
64
+
65
+
66
+
67
+
68
+ end # module Openliga
@@ -0,0 +1,73 @@
1
+ module Openliga
2
+
3
+
4
+ def self._clean( str )
5
+ ## note - accept nil
6
+ ## turn blank strings e.g "" or " " into nil
7
+ ## remove leading and trailing spaces
8
+ return nil if str.nil?
9
+
10
+ str = str.strip
11
+ if str.empty?
12
+ nil
13
+ else
14
+ str
15
+ end
16
+ end
17
+
18
+ =begin
19
+ @ Sportpark Ronhof | Thomas Sommer, Fürth
20
+ @ Sportforum "Sojus 31", Zwickau
21
+ @ "Allianz Arena, München
22
+
23
+ or add into protected_name token - why? why not?
24
+ e.g.
25
+ @ ‹Sportpark Ronhof | Thomas Sommer›, Fürth
26
+ @ ‹Sportforum "Sojus 31"›, Zwickau
27
+ @ ‹"Allianz Arena›, München -- really better FIX typo
28
+ =end
29
+
30
+ def self._clean_geo( str )
31
+ return nil if str.nil?
32
+
33
+ str = str.gsub( %r{["|]}, '' ) ## remove "|
34
+ str = str.gsub( %r{[ ]{2,}}, ' ' ) ## squish spaces (maybe move "upstream" to clean)
35
+
36
+ _clean( str )
37
+ end
38
+
39
+
40
+ def self._clean_name( str ) ## player name
41
+ ## e.g.
42
+ ## Poulsen, Yussuf => Poulsen Yussuf
43
+ ## Günther-Schmidt, Julian
44
+ ## Hercher, Philipp
45
+ ## maybe auto-turn around later - why? why not?
46
+ ##
47
+ ## E. N‘Dicka => E. N'Dicka
48
+
49
+
50
+ return nil if str.nil?
51
+
52
+ ## move asciify "upstream" - why? why not?
53
+ str = str.gsub( /[‘]/, "'" ) ## asciify "smart/unicode" quotes
54
+
55
+
56
+ ## str = str.gsub( /[,]/, '' ) ## remove "," -- todo - add a warn(ing) on replace
57
+ ## tr = str.sub( 'Poulsen, Yussuf', 'Yussuf Poulsen' )
58
+ ##
59
+ ## turn "Poulsen, Yussuf" into "Yussuf Poulsen"
60
+ if str.include?(',')
61
+ last_name, first_name = str.split( ',', 2 )
62
+ str = "#{first_name} #{last_name}"
63
+ end
64
+
65
+
66
+ str = str.gsub( /[ ]{2,}/, ' ' ) ## squish spaces (maybe move "upstream" to clean)
67
+
68
+ _clean( str )
69
+ end
70
+
71
+
72
+
73
+ end ## module Openliga
@@ -0,0 +1,81 @@
1
+ module Openliga
2
+
3
+
4
+ def self.download( league:, season: )
5
+
6
+ season = Season( season ) ## cast (ensure) season class (NOT string, integer, etc.)
7
+
8
+ info = (LEAGUES[ league.downcase ]||{})[ season.to_key ]
9
+ if info.nil?
10
+ puts "!! ERROR - no openliga info found for #{league} #{season}"
11
+ exit 1
12
+ end
13
+ pp info
14
+
15
+
16
+ Metal.matches( info.shortcut, info.season )
17
+ Metal.teams( info.shortcut, info.season )
18
+
19
+ true ## return true on success (instead of latest response json data/payload)
20
+ end
21
+
22
+
23
+ ##################
24
+ ## plumbing metal "helpers"
25
+
26
+ class Metal
27
+
28
+ BASE_URL = 'https://api.openligadb.de'
29
+
30
+
31
+ ### api call - /getavailableleagues
32
+ def self.leagues
33
+ get( "#{BASE_URL}/getavailableleagues" )
34
+ end
35
+
36
+ ## api call - /getavailableteams/{code}/{year}
37
+ def self.teams( code, year )
38
+ get( teams_url( code, year ) )
39
+ end
40
+ def self.teams_url( code, year )
41
+ "#{BASE_URL}/getavailableteams/#{code}/#{year}"
42
+ end
43
+
44
+ ## api call - /getmatchdata/{code}/{year}
45
+ def self.matches( code, year )
46
+ get( matches_url( code, year ) )
47
+ end
48
+ def self.matches_url( code, year )
49
+ "#{BASE_URL}/getmatchdata/#{code}/#{year}"
50
+ end
51
+
52
+ ## api call - /getgoalgetters/{code}/{year}
53
+ def self.goalgetters( code, year )
54
+ get( "#{BASE_URL}/getgoalgetters/#{code}/#{year}" )
55
+ end
56
+
57
+
58
+
59
+
60
+
61
+
62
+ def self.get( url )
63
+ headers = {}
64
+ headers['User-Agent'] = 'ruby'
65
+ headers['Accept'] = '*/*'
66
+
67
+ ## note: add format: 'json' for pretty printing json (before) save in cache
68
+ response = Webget.call( url, headers: headers )
69
+
70
+ exit 1 if response.status.nok? # e.g. HTTP status code != 200
71
+
72
+
73
+ data = response.json
74
+ ## for debugging print pretty printed json first 400 chars
75
+ puts data.pretty_inspect[0..400]
76
+
77
+
78
+ data ## note - return json data response (on success)
79
+ end
80
+ end # class Metal
81
+ end # module Footballdata
@@ -0,0 +1,45 @@
1
+ module Openliga
2
+
3
+
4
+ def self.config_dir() "#{File.dirname(File.dirname(__dir__))}/config"; end
5
+
6
+
7
+ ## code, season, league_name, league_season, league_shortcut
8
+ ##
9
+ ## "leagueName": "Champions League 2026/2027",
10
+ ## "leagueShortcut": "ucl",
11
+ ## "leagueSeason": 2026,
12
+ ##
13
+ ## from https://api.openligadb.de
14
+ ## Name Description
15
+ ## leagueShortcut - string - der Shortcut der Liga, z.B. 'bl1' für die erste Bundesliga
16
+ ## leagueSeason - integer($int32) - die Saison der Liga, z.B. 2019 für die Saison 2019/2020
17
+
18
+
19
+
20
+
21
+
22
+ League = Struct.new( :name, :season, :shortcut)
23
+
24
+
25
+ LEAGUES = begin
26
+ leagues = {}
27
+ ['leagues_de.csv',
28
+ 'leagues_more.csv'].each do |name|
29
+ path = "#{config_dir}/#{name}"
30
+ rows = read_csv( path )
31
+ ## pp rows
32
+ rows.each do |row|
33
+ seasons = leagues[row['code']] ||= {}
34
+ ## note - normalize season!!
35
+ season = Season.parse( row['season'] )
36
+ seasons[ season.to_key ] = League.new( name: row['league_name'],
37
+ season: row['league_season'].to_i(10),
38
+ shortcut: row['league_shortcut'] )
39
+ end
40
+ end
41
+ leagues
42
+ end
43
+
44
+
45
+ end # module Openliga
@@ -0,0 +1,153 @@
1
+ module Openliga
2
+
3
+
4
+ Match = Struct.new( :team1,
5
+ :team2,
6
+ :date, # assumes Date class
7
+ :time, # assumes String class (00:00)
8
+ :timezone, ## use tz ?
9
+ ### :datetime_utc,
10
+ :score,
11
+ :goals,
12
+ :round, ## is stage/round/group
13
+ :city,
14
+ :ground, ## use stadium (or venue?)
15
+ :att, ## attendance (numberOfViewers)
16
+ :finished ### todo/check - use status (is really only END or false)
17
+ ) do
18
+ end
19
+
20
+
21
+
22
+
23
+ Goal = Struct.new( :team, ## expect 1|2 or nil
24
+ :name,
25
+ :score, ## expect array e.g. [0,1]
26
+ :m, :stoppage,
27
+ :pen, :og,
28
+ :comment ) do
29
+
30
+ def pen?() @pen == true; end
31
+ def og?() @og == true; end
32
+
33
+
34
+ ## print goal as single line
35
+ ## e.g.
36
+ ## (0-1 Yorbe Vertessen 37'
37
+ ## 0-2 Yorbe Vertessen 48')
38
+
39
+ def calc_stoppage( m )
40
+ if m >= 120 then [120, m%120]
41
+ elsif m >= 105 then [105, m%105]
42
+ elsif m >= 90 then [90, m%90]
43
+ else [45, m%45] ## assume 45
44
+ end
45
+ end
46
+
47
+
48
+
49
+ def to_s
50
+ buf = String.new
51
+
52
+ ## score required !!!
53
+ buf << "#{score[0]}-#{score[1]} " ## note - use double (two) space for now!!
54
+ buf << (name ? name : 'N.N.') ## note - if no name use N.N. for now!!
55
+ if m
56
+ if stoppage
57
+ _m,_stoppage = calc_stoppage(m)
58
+ puts "!! INFO - auto-calc stoppage #{m}+ => #{_m}+#{_stoppage}"
59
+ buf << " #{_m}+#{_stoppage}'"
60
+ else
61
+ buf << " #{m}'"
62
+ end
63
+ end
64
+
65
+ buf << " (og)" if og
66
+ buf << " (p)" if pen
67
+
68
+ buf
69
+ end
70
+
71
+ end
72
+
73
+
74
+
75
+ ######
76
+ ## todo/fix - reuse a "generic" score (and/or result) class
77
+
78
+ class Score
79
+ ##
80
+ ## for generic add add reported (score) too
81
+ ## use if score line undefined/unknown
82
+
83
+ attr_reader :ht, :ft, :et, :pen
84
+
85
+ def initialize( ht: [], ft:[], et: [], pen: [])
86
+ @ht, @ft, @et, @pen = ht, ft, et, pen
87
+
88
+ ## ( ..., extra_time: nil)
89
+ ## extra_time flag - e.g. copa america has penalty shootouts
90
+ ## BUT no extra time (only in final)
91
+ ##
92
+ ## add a reg (90min) flag too -
93
+ ## @extra_time = extra_time
94
+ end
95
+
96
+
97
+ def pen?() @pen.is_a?( Array ) && @pen.size == 2; end
98
+ def et?() @et.is_a?( Array ) && @et.size == 2; end
99
+ def ft?() @ft.is_a?( Array ) && @ft.size == 2; end
100
+ def ht?() @ht.is_a?( Array ) && @ht.size == 2; end
101
+
102
+
103
+ def pretty_print( q )
104
+ q.group(1, "<Score", ">") do
105
+ q.text( " ht: #{@ht}") if ht?
106
+ q.text( " ft: #{@ft}") if ft?
107
+ q.text( " et: #{@et}") if et?
108
+ q.text( " pen: #{@pen}") if pen?
109
+ end
110
+ end
111
+
112
+
113
+
114
+ def to_s
115
+ buf = String.new
116
+ if pen?
117
+ if et?
118
+ buf << "#{@et[0]}-#{@et[1]} aet"
119
+ if ft?
120
+ buf << " (#{@ft[0]}-#{@ft[1]}"
121
+ buf << ", #{@ht[0]}-#{@ht[1]}" if ht?
122
+ buf << ")"
123
+ end
124
+ elsif ft?
125
+ ### pen without et e.g. in copa
126
+ ## use 2-2 (1-1), 5-3 pen. or such
127
+ buf << " " unless buf.empty?
128
+ buf << "#{@ft[0]}-#{@ft[1]}"
129
+ buf << " (#{@ht[0]}-#{@ht[1]})" if ht?
130
+ end
131
+ buf << ", " unless buf.empty?
132
+ buf << "#{@pen[0]}-#{@pen[1]} pen"
133
+ elsif et?
134
+ buf << "#{@et[0]}-#{@et[1]} aet"
135
+ if ft?
136
+ buf << " (#{@ft[0]}-#{@ft[1]}"
137
+ buf << ", #{@ht[0]}-#{@ht[1]}" if ht?
138
+ buf << ")"
139
+ end
140
+ elsif ft?
141
+ buf << "#{@ft[0]}-#{@ft[1]}"
142
+ buf << " (#{@ht[0]}-#{@ht[1]})" if ht?
143
+ else
144
+ ## note - return empty string on no score for now
145
+ end
146
+
147
+ buf
148
+ end
149
+ end # class Score
150
+
151
+
152
+
153
+ end ## module Openliga