footty 2025.4.28 → 2026.5.25

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: 620119829b1dbd891a01c374c8996a264e347f329e0211387e18fdc12add6fe6
4
- data.tar.gz: cc1f485cc3bbfc12ab5f3846823df6c63f7295912045996a08c2ccb4e50eb7a5
3
+ metadata.gz: 75b4ca658d25b404bf5b4f1594da50c42564ad7ffc43e3190429a7c4627b8c94
4
+ data.tar.gz: 25eac464226fea4b31da270bc195c443e34bc4fa68ce9de1bd0646b8ec63639e
5
5
  SHA512:
6
- metadata.gz: a0394d72327b87ad90e207e48883bf90acace8e1183d85f9175b9a6484ab8c62e3e12759f77a3eac1e59cc526e0eeacdd70183418a29a920e47c10732f28e51e
7
- data.tar.gz: a0e274e21d9ca9620cbdda59ebdec84e92f5b8a99a4e747a177b076de6b830c94b66064c5b89c7ae6ae989b47803558aa7720fd51a321c089705987ad5196f52
6
+ metadata.gz: a1762d1389a13f0380267a7b8a0621b6df1e176eb847a4cac454fcfdfb87ece7321a950e80ad2c1ffedbd38f2966def3c63cda9d159063e568b1854bac0349b9
7
+ data.tar.gz: e791218fe6679d8c03545114cb60a7ce588b8f8dce4f8d80764aaa1da1168f6ab7552af00098ad6165a59d97ecea5fc1292cbb979689835f029bb4d06a1bc5cb
data/CHANGELOG.md CHANGED
@@ -1,4 +1,4 @@
1
- ### 2025.4.28
1
+ ### 2026.5.25
2
2
  ### 1.0.0 / 2018-06-09
3
3
 
4
4
  * Everything is new (again). First release.
data/Manifest.txt CHANGED
@@ -5,7 +5,9 @@ Rakefile
5
5
  bin/footty
6
6
  bin/ftty
7
7
  lib/footty.rb
8
- lib/footty/dataset.rb
9
- lib/footty/openfootball.rb
10
- lib/footty/print.rb
8
+ lib/footty/dataset/dataset.rb
9
+ lib/footty/dataset/openfootball.rb
10
+ lib/footty/main.rb
11
+ lib/footty/pp_matches.rb
12
+ lib/footty/pp_week.rb
11
13
  lib/footty/version.rb
data/README.md CHANGED
@@ -16,22 +16,22 @@ The footty (or ftty) command line tool lets you query the online football.db via
16
16
  for upcoming or past matches. For example:
17
17
 
18
18
  $ footty # Defaults to today's matches of top leagues
19
-
19
+
20
20
 
21
21
  prints on Sep 27, 2024:
22
22
 
23
23
  ==> English Premier League 2024/25
24
24
  Upcoming matches:
25
- Sat Sep/28 12:30 (in 1d) Newcastle United FC vs Manchester City FC Matchday 6
26
- Sat Sep/28 15:00 (in 1d) Arsenal FC vs Leicester City FC Matchday 6
27
- Sat Sep/28 15:00 (in 1d) Brentford FC vs West Ham United FC Matchday 6
28
- Sat Sep/28 15:00 (in 1d) Chelsea FC vs Brighton & Hove Albion Matchday 6
29
- Sat Sep/28 15:00 (in 1d) Everton FC vs Crystal Palace FC Matchday 6
30
- Sat Sep/28 15:00 (in 1d) Nottingham Forest FC vs Fulham FC Matchday 6
31
- Sat Sep/28 17:30 (in 1d) Wolverhampton Wanderers FC vs Liverpool FC Matchday 6
32
- Sun Sep/29 14:00 (in 2d) Ipswich Town FC vs Aston Villa FC Matchday 6
33
- Sun Sep/29 16:30 (in 2d) Manchester United FC vs Tottenham Hotspur FC Matchday 6
34
- Mon Sep/30 20:00 (in 3d) AFC Bournemouth vs Southampton FC Matchday 6
25
+ Sat Sep 28 12:30 (in 1d) Newcastle United FC vs Manchester City FC Matchday 6
26
+ Sat Sep 28 15:00 (in 1d) Arsenal FC vs Leicester City FC Matchday 6
27
+ Sat Sep 28 15:00 (in 1d) Brentford FC vs West Ham United FC Matchday 6
28
+ Sat Sep 28 15:00 (in 1d) Chelsea FC vs Brighton & Hove AlbionMatchday 6
29
+ Sat Sep 28 15:00 (in 1d) Everton FC vs Crystal Palace FC Matchday 6
30
+ Sat Sep 28 15:00 (in 1d) Nottingham Forest FC vs Fulham FC Matchday 6
31
+ Sat Sep 28 17:30 (in 1d) Wolverhampton Wanderers FC vs Liverpool FCMatchday 6
32
+ Sun Sep 29 14:00 (in 2d) Ipswich Town FC vs Aston Villa FC Matchday 6
33
+ Sun Sep 29 16:30 (in 2d) Manchester United FC vs Tottenham Hotspur FC Matchday 6
34
+ Mon Sep 30 20:00 (in 3d) AFC Bournemouth vs Southampton FC Matchday 6
35
35
 
36
36
 
37
37
 
@@ -81,7 +81,7 @@ More
81
81
  - `world` => World Cup
82
82
  - `euro` => "Euro" - European Championship
83
83
 
84
- See [footty/openfootball](https://github.com/sportdb/footty/blob/master/footty/lib/footty/openfootball.rb) for the complete built-in list of data sources (and league codes).
84
+ See [footty/openfootball](https://github.com/sportdb/footty/blob/master/footty/lib/footty/dataset/openfootball.rb) for the complete built-in list of data sources (and league codes).
85
85
 
86
86
 
87
87
 
@@ -110,4 +110,3 @@ Use it as you please with no restrictions whatsoever.
110
110
 
111
111
  Yes, you can. More than welcome.
112
112
  See [Help & Support »](https://github.com/openfootball/help)
113
-
data/Rakefile CHANGED
@@ -17,7 +17,7 @@ Hoe.spec 'footty' do
17
17
  self.history_file = 'CHANGELOG.md'
18
18
 
19
19
  self.extra_deps = [
20
- ['sportdb-quick', '>= 0.2.0'],
20
+ ['sportdb-quick', '>= 0.7.0'],
21
21
  ['webget'],
22
22
  ]
23
23
 
@@ -1,25 +1,26 @@
1
1
  module Footty
2
2
 
3
3
 
4
- class Dataset
4
+ ##
5
+ # note - assume "upstream" date is always date object or nil (NOT string)!!!
5
6
 
7
+ class Dataset
6
8
 
7
9
  def matches
8
10
  raise ArgumentError, "method matches must be implemented by concrete class!!"
9
11
  end
10
12
 
11
13
  def league_name
12
- raise ArgumentError, "method league_name must be implemented by concrete class!!"
14
+ raise ArgumentError, "method league_name must be implemented by concrete class!!"
13
15
  end
14
16
 
15
17
 
18
+
16
19
  def end_date
17
20
  @end_date ||= begin
18
21
  end_date = nil
19
22
  matches.each do |match|
20
- date = Date.strptime(match['date'], '%Y-%m-%d' )
21
- end_date = date if end_date.nil? ||
22
- date > end_date
23
+ end_date = match['date'] if end_date.nil? || match['date'] > end_date
23
24
  end
24
25
  end_date
25
26
  end
@@ -29,45 +30,29 @@ module Footty
29
30
  @start_date ||= begin
30
31
  start_date = nil
31
32
  matches.each do |match|
32
- date = Date.strptime(match['date'], '%Y-%m-%d' )
33
- start_date = date if start_date.nil? ||
34
- date < start_date
33
+ start_date = date = match['date'] if start_date.nil? || date = match['date'] < start_date
35
34
  end
36
35
  start_date
37
36
  end
38
37
  end
39
38
 
40
- def todays_matches( date: Date.today ) matches_for( date ); end
41
- def tomorrows_matches( date: Date.today ) matches_for( date+1 ); end
42
- def yesterdays_matches( date: Date.today ) matches_for( date-1 ); end
43
39
 
44
- def matches_for( date )
45
- matches = select_matches { |match| date == Date.parse( match['date'] ) }
46
- matches
47
- end
40
+ def todays_matches( date: Date.today ) _select_matches { |match| date == match['date'] }; end
41
+ def tomorrows_matches( date: Date.today ) _select_matches { |match| date+1 == match['date'] }; end
42
+ def yesterdays_matches( date: Date.today ) _select_matches { |match| date-1 == match['date'] }; end
48
43
 
49
44
 
50
- def query( q )
51
- ## query/check for team name match for now
52
- rx = /#{Regexp.escape(q)}/i ## use case-insensitive regex match
53
-
54
- matches = select_matches do |match|
55
- if rx.match( match['team1'] ) ||
56
- rx.match( match['team2'] )
57
- true
58
- else
59
- false
60
- end
61
- end
62
- matches
45
+ def weeks_matches( start_week, end_week )
46
+ _select_matches do |match|
47
+ match['date'] >= start_week && match['date'] <= end_week
48
+ end
63
49
  end
64
50
 
65
51
 
66
-
67
52
  def upcoming_matches( date: Date.today,
68
53
  limit: nil )
69
54
  ## note: includes todays matches for now
70
- matches = select_matches { |match| date <= Date.parse( match['date'] ) }
55
+ matches = _select_matches { |match| date <= match['date'] }
71
56
 
72
57
  if limit
73
58
  matches[0, limit] ## cut-off
@@ -76,16 +61,34 @@ module Footty
76
61
  end
77
62
  end
78
63
 
64
+
79
65
  def past_matches( date: Date.today )
80
- matches = select_matches { |match| date > Date.parse( match['date'] ) }
66
+ matches = _select_matches { |match| date > match['date'] }
81
67
  ## note reveserve matches (chronological order/last first)
82
68
  ## matches.reverse
83
69
  matches
84
70
  end
85
71
 
86
72
 
87
- private
88
- def select_matches( &blk)
73
+
74
+ def query( q )
75
+ ## query/check for team name match for now
76
+ rx = /#{Regexp.escape(q)}/i ## use case-insensitive regex match
77
+
78
+ _select_matches do |match|
79
+ if rx.match( match['team1'] ) ||
80
+ rx.match( match['team2'] )
81
+ true
82
+ else
83
+ false
84
+ end
85
+ end
86
+ end
87
+
88
+
89
+
90
+
91
+ def _select_matches( &blk )
89
92
  selected = []
90
93
  matches.each do |match|
91
94
  selected << match if blk.call( match )
@@ -3,7 +3,9 @@ module Footty
3
3
 
4
4
  class OpenfootballDataset < Dataset
5
5
  SOURCES = {
6
- 'world' => { '2022' => [ 'worldcup/2022--qatar/cup.txt',
6
+ 'world' => { '2026' => [ 'worldcup/2026--usa/cup.txt',
7
+ 'worldcup/2026--usa/cup_finals.txt'],
8
+ '2022' => [ 'worldcup/2022--qatar/cup.txt',
7
9
  'worldcup/2022--qatar/cup_finals.txt'],
8
10
  '2018' => [ 'worldcup/2018--russia/cup.txt',
9
11
  'worldcup/2018--russia/cup_finals.txt']
@@ -15,7 +17,7 @@ module Footty
15
17
  'de2' => 'deutschland/$season$/2-bundesliga2.txt',
16
18
  'de3' => 'deutschland/$season$/3-liga3.txt',
17
19
  'decup' => 'deutschland/$season$/cup.txt',
18
-
20
+
19
21
 
20
22
  'en' => 'england/$season$/1-premierleague.txt',
21
23
  'en2' => 'england/$season$/2-championship.txt',
@@ -23,13 +25,13 @@ module Footty
23
25
  ## use eflcup, facup - why? why not?
24
26
  'eneflcup' => 'england/$season$/eflcup.txt',
25
27
  'enfacup' => 'england/$season$/facup.txt',
26
-
28
+
27
29
 
28
30
  'es' => 'espana/$season$/1-liga.txt',
29
31
  'escup' => 'espana/$season$/cup.txt',
30
32
 
31
33
  'it'=> 'italy/$season$/1-seriea.txt',
32
-
34
+
33
35
  'at'=> 'austria/$season$/1-bundesliga.txt',
34
36
  'at2' => 'austria/$season$/2-liga2.txt',
35
37
  'at3o' => 'austria/$season$/3-regionalliga-ost.txt',
@@ -39,18 +41,24 @@ module Footty
39
41
  'nl'=> 'europe/netherlands/$season$_nl1.txt',
40
42
  'be'=> 'europe/belgium/$season$_be1.txt',
41
43
 
42
- 'champs'=> 'champions-league/$season$/cl.txt',
44
+ 'champs' => 'champions-league/$season$/cl.txt',
45
+ 'uefacl' => 'champions-league/$season$/cl.txt',
46
+ 'uefael' => 'champions-league/$season$/el.txt',
47
+ 'uefaconf' => 'champions-league/$season$/conf.txt',
48
+
43
49
 
44
50
  'br' => 'south-america/brazil/$year$_br1.txt',
45
51
  'ar' => 'south-america/argentina/$year$_ar1.txt',
46
52
  'co' => 'south-america/colombia/$year$_co1.txt',
47
-
53
+
48
54
  ## use a different code for copa libertadores? why? why not?
49
55
  'copa' => 'south-america/copa-libertadores/$year$_copal.txt',
50
56
 
51
57
  'mx' => 'world/north-america/mexico/$season$_mx1.txt',
52
58
  'mls' => 'world/north-america/major-league-soccer/$year$_mls.txt',
53
59
 
60
+ 'concacafcl' => 'world/north-america/champions-league/$year$_concacafcl.txt',
61
+
54
62
  'eg' => 'world/africa/egypt/$season$_eg1.txt',
55
63
  'ma' => 'world/africa/morocco/$season$_ma1.txt',
56
64
 
@@ -74,10 +82,10 @@ module Footty
74
82
 
75
83
  ## todo/fix - report error if no spec found
76
84
  season = if spec.is_a?( Hash ) ## assume lookup by year
77
- spec.keys[0]
85
+ spec.keys[0]
78
86
  else ## assume vanilla urls (no lookup by year)
79
- ## default to 2025 or 2024/25 for now
80
- spec.index( '$year$') ? '2025' : '2024/25'
87
+ ## default to 2026 or 2025/26 for now
88
+ spec.index( '$year$') ? '2026' : '2025/26'
81
89
  end
82
90
  season
83
91
  end
@@ -110,13 +118,27 @@ module Footty
110
118
  end
111
119
 
112
120
  matches = matches.map {|match| match.as_json } # convert to json
113
-
114
- ## note - sort by date/time
121
+
122
+ ## note - sort by date/time
115
123
  ## (assume stable sort; no reshuffle of matches if already sorted by date/time)
116
124
 
125
+
126
+ ###
127
+ ## note - convert date to date obj or nil !!!
128
+ matches = matches.map do |match|
129
+ if match['date'] && match['date'].empty? == false
130
+ match['date'] = Date.strptime( match['date'], '%Y-%m-%d' )
131
+ else
132
+ match['date'] = nil
133
+ end
134
+
135
+ match
136
+ end
137
+
138
+
117
139
  matches = matches.sort do |l,r|
118
140
  result = l['date'] <=> r['date']
119
- result = l['time'] <=> r['time'] if result == 0 &&
141
+ result = l['time'] <=> r['time'] if result == 0 &&
120
142
  (l['time'] && r['time'])
121
143
  result
122
144
  end
@@ -125,12 +147,13 @@ module Footty
125
147
  end
126
148
 
127
149
 
150
+
128
151
  def openfootball_url( path, season: )
129
152
  repo, local_path = path.split( '/', 2)
130
153
  url = "https://raw.githubusercontent.com/openfootball/#{repo}/master/#{local_path}"
131
154
  ## check for template vars too
132
155
  season = Season( season )
133
- url = url.gsub( '$year$', season.start_year.to_s )
156
+ url = url.gsub( '$year$', season.start_year.to_s )
134
157
  url = url.gsub( '$season$', season.to_path )
135
158
  url
136
159
  end
@@ -138,7 +161,7 @@ module Footty
138
161
 
139
162
  def matches() @matches; end
140
163
  def league_name() @league_name; end
141
-
164
+
142
165
 
143
166
 
144
167
  def get!( url )
@@ -0,0 +1,248 @@
1
+
2
+
3
+ module Footty
4
+
5
+
6
+ def self.main( args=ARGV )
7
+ puts banner # say hello
8
+
9
+
10
+ opts = { debug: false,
11
+ verbose: false, ## add more details
12
+ ## add cache/cache_dir - why? why not?
13
+
14
+ query: nil,
15
+
16
+ ## display format/mode - week/window/upcoming/past (default is today)
17
+ yesterday: nil,
18
+ tomorrow: nil,
19
+ upcoming: nil,
20
+ past: nil,
21
+
22
+ week: false,
23
+ # window: nil, ## 2 day plus/minus +2/-2
24
+ }
25
+
26
+
27
+ parser = OptionParser.new do |parser|
28
+ parser.banner = "Usage: #{$PROGRAM_NAME} [options] LEAGUES"
29
+
30
+ parser.on( "--verbose",
31
+ "turn on verbose output (default: #{opts[:verbose]})" ) do |verbose|
32
+ opts[:verbose] = true
33
+ end
34
+
35
+ parser.on( "-q NAME", "--query",
36
+ "query mode; display matches where team name matches query" ) do |query|
37
+ opts[:query] = query
38
+ end
39
+
40
+
41
+ parser.on( "-y", "--yesterday" ) do |yesterday|
42
+ opts[:yesterday] = true
43
+ end
44
+ parser.on( "-t", "--tomorrow" ) do |tomorrow|
45
+ opts[:tomorrow] = true
46
+ end
47
+ parser.on( "-p", "--past" ) do |past|
48
+ opts[:past] = true
49
+ end
50
+ parser.on( "-u", "--up", "--upcoming" ) do |upcoming|
51
+ opts[:upcoming] = true
52
+ end
53
+
54
+ parser.on( "-w", "--week",
55
+ "show matches of the (sport) week from tue to mon (default: #{opts[:week]})" ) do |week|
56
+ opts[:week] = true
57
+ end
58
+ end
59
+ parser.parse!( args )
60
+
61
+
62
+ puts "OPTS:"
63
+ p opts
64
+ puts "ARGV:"
65
+ p args
66
+
67
+
68
+ ###
69
+ ## use simple norm(alize) args (that is,) league codes for now
70
+ ## - downcase, strip dot (.) etc.)
71
+ ## e.g. en.facup => enfacup
72
+ ## at.cup => atcup etc.
73
+ args = args.map { |arg| arg.downcase.gsub( /[._-]/, '' ) }
74
+
75
+
76
+
77
+ ######################
78
+ ## note - first check for buil-in "magic" commands
79
+ ## e.g. leagues / codes - dump built-in league codes
80
+
81
+ if args.include?( 'leagues' )
82
+ puts "==> openfootball dataset sources:"
83
+ pp OpenfootballDataset::SOURCES
84
+
85
+ ## pretty print keys/codes only
86
+ puts
87
+ puts OpenfootballDataset::SOURCES.keys.join( ' ' )
88
+ puts " #{OpenfootballDataset::SOURCES.keys.size} league code(s)"
89
+
90
+ exit 1
91
+ end
92
+
93
+
94
+
95
+ top = [['world', '2026'], ## world cup (w/ national teams)
96
+ # ['euro', '2024'],
97
+ # ['mls', '2025'],
98
+ # ['concacafcl', '2025'],
99
+ # ['mx', '2024/25'],
100
+ ['en', '2025/26'],
101
+ ['es', '2025/26'],
102
+ ['it', '2025/26'],
103
+ ['fr', '2025/26'],
104
+ ['de', '2025/26'],
105
+ # ['decup', '2025/26'],
106
+ # ['at', '2025/26'],
107
+ # ['atcup', '2025/26'],
108
+ ['uefacl', '2025/26'],
109
+ # ['uefael', '2025/26'],
110
+ # ['uefaconf', '2025/26'],
111
+ ['br', '2026'],
112
+ ['copa', '2026'], ## copa libertadores (not copa america,etc.)
113
+ ]
114
+
115
+
116
+ leagues = if args.size == 0
117
+ top
118
+ else
119
+ ### auto-fill (latest) season/year
120
+ args.map do |arg|
121
+ [arg, OpenfootballDataset.latest_season( league: arg )]
122
+ end
123
+ end
124
+
125
+
126
+ ## fetch leagues
127
+ datasets = leagues.map do |league, season|
128
+ dataset = OpenfootballDataset.new( league: league, season: season )
129
+ ## parse matches
130
+ matches = dataset.matches
131
+ puts " #{league} #{season} - #{matches.size} match(es)"
132
+ dataset
133
+ end
134
+
135
+
136
+
137
+ ###################
138
+ ## check for query option to filter matches by query (team)
139
+ if opts[:query]
140
+ q = opts[:query]
141
+ puts
142
+ puts
143
+ datasets.each do |dataset|
144
+ matches = dataset.query( q )
145
+
146
+ if matches.size == 0
147
+ ## siltently skip for now
148
+ else ## assume matches found
149
+ print "==> #{dataset.league_name}"
150
+ print " #{dataset.start_date} - #{dataset.end_date}"
151
+ print " -- #{dataset.matches.size} match(es)"
152
+ print "\n"
153
+ print_matches( matches )
154
+ end
155
+ end
156
+ exit 1
157
+ end
158
+
159
+
160
+ # Dataset.new( league: 'euro', year: 2024 )
161
+ # dataset = Dataset.new( league: league, year: year )
162
+
163
+ ## in the future make today "configurable" as param - why? why not?
164
+ today = Date.today
165
+
166
+
167
+ what = if opts[:yesterday]
168
+ 'yesterday'
169
+ elsif opts[:tomorrow]
170
+ 'tomorrow'
171
+ elsif opts[:past]
172
+ 'past'
173
+ elsif opts[:upcoming]
174
+ 'upcoming'
175
+ elsif opts[:week]
176
+ 'week'
177
+ else
178
+ 'today'
179
+ end
180
+
181
+
182
+ ## if week get week number and start and end date (tuesday to mondey)
183
+ if what == 'week'
184
+ week_start, week_end = Footty.week_tue_to_mon( today)
185
+ puts
186
+ puts "=== " + Footty.fmt_week( week_start, week_end ) + " ==="
187
+ else
188
+ ## start with two empty lines - assume (massive) debug output before ;-)
189
+ puts
190
+ puts
191
+ end
192
+
193
+ datasets.each do |dataset|
194
+ print "==> #{dataset.league_name}"
195
+ print " #{dataset.start_date} - #{dataset.end_date}"
196
+ print " -- #{dataset.matches.size} match(es)"
197
+ print "\n"
198
+
199
+ if what == 'week'
200
+ matches = dataset.weeks_matches( week_start, week_end )
201
+ if matches.empty?
202
+ puts (' '*4) + "** No matches scheduled or played in week #{week_start.cweek}.\n"
203
+ end
204
+ elsif what == 'yesterday'
205
+ matches = dataset.yesterdays_matches
206
+ if matches.empty?
207
+ puts (' '*4) + "** No matches played yesterday.\n"
208
+ end
209
+ elsif what == 'tomorrow'
210
+ matches = dataset.tomorrows_matches
211
+ if matches.empty?
212
+ puts (' '*4) + "** No matches scheduled tomorrow.\n"
213
+ end
214
+ elsif what == 'past'
215
+ matches = dataset.past_matches
216
+ if matches.empty?
217
+ puts (' '*4) + "** No matches played yet.\n"
218
+ end
219
+ elsif what == 'upcoming'
220
+ matches = dataset.upcoming_matches
221
+ if matches.empty?
222
+ puts (' '*4) + "** No more matches scheduled.\n"
223
+ end
224
+ else ## assume today
225
+ matches = dataset.todays_matches
226
+
227
+ ## no matches today
228
+ if matches.empty?
229
+ puts (' '*4) + "** No matches scheduled today.\n"
230
+
231
+ if opts[:verbose]
232
+ ## note: was world cup 2018 - end date -- Date.new( 2018, 7, 11 )
233
+ ## note: was euro 2020 (in 2021) - end date -- Date.new( 2021, 7, 11 )
234
+ if Date.today > dataset.end_date ## tournament is over, look back
235
+ puts "Past matches:"
236
+ matches = dataset.past_matches
237
+ else ## world cup is upcoming /in-progress,look forward
238
+ puts "Upcoming matches:"
239
+ matches = dataset.upcoming_matches( limit: 18 )
240
+ end
241
+ end
242
+ end
243
+ end
244
+ print_matches( matches )
245
+ end
246
+
247
+ end # method self.main
248
+ end # module Footty
@@ -0,0 +1,135 @@
1
+
2
+ module Footty
3
+
4
+
5
+ ##
6
+ # note - assume "upstream" date is always date object or nil (NOT string)!!!
7
+
8
+ def self.print_matches( matches )
9
+
10
+ today = Date.today
11
+
12
+ matches.each do |match|
13
+ date = match['date']
14
+ print "#{date.strftime('%a %b %d')} " ## e.g. Thu Jun 14
15
+ print "#{match['time']} " if match['time']
16
+
17
+ if date > today
18
+ diff = (date - today).to_i
19
+ print "%10s" % "(in #{diff}d) "
20
+ end
21
+
22
+
23
+ if match['team1'].is_a?( Hash )
24
+ print "%22s" % "#{match['team1']['name']} (#{match['team1']['code']})"
25
+ else
26
+ print "%22s" % "#{match['team1']}"
27
+ end
28
+
29
+
30
+
31
+ if match['score'].is_a?( Hash )
32
+
33
+ if match['score']['et']
34
+ et = "#{match['score']['et'][0]}-#{match['score']['et'][1]} aet"
35
+ print " #{et}"
36
+ end
37
+
38
+ if match['score']['ft']
39
+ ft = "#{match['score']['ft'][0]}-#{match['score']['ft'][1]}"
40
+ if match['score']['et']
41
+ print " (#{ft})"
42
+ else
43
+ print " #{ft}"
44
+ end
45
+ end
46
+
47
+ if match['score']['p']
48
+ pen = "#{match['score']['p'][0]}-#{match['score']['p'][1]} pen"
49
+ print ", #{pen}"
50
+ end
51
+ print " "
52
+
53
+ elsif match['score'] &&
54
+ match['score'][0] && match['score'][1]
55
+ score = "#{match['score'][0]}-#{match['score'][1]}"
56
+ print " #{score} "
57
+ else
58
+ print " vs "
59
+ end
60
+
61
+ if match['team2'].is_a?( Hash )
62
+ print "%-22s" % "#{match['team2']['name']} (#{match['team2']['code']})"
63
+ else
64
+ print "%-22s" % "#{match['team2']}"
65
+ end
66
+
67
+
68
+ print "▪" ## note - add round marker!!
69
+
70
+
71
+ print " #{match['group']} /" if match['group'] ## (optional) group
72
+
73
+
74
+ print " #{match['round']} " ## knock out (k.o.) phase/stage
75
+
76
+
77
+ print "%-5s " % "(\##{match['num']}) " if match['num']
78
+
79
+ print " @ #{match['ground']}" if match['ground']
80
+
81
+
82
+ print "\n"
83
+
84
+
85
+ if match['goals1'] && match['goals2']
86
+ print " ("
87
+
88
+ print _pp_goals(match['goals1']) if match['goals1'].size > 0
89
+
90
+ print "; " if match['goals1'].size > 0 &&
91
+ match['goals2'].size > 0
92
+
93
+ print _pp_goals(match['goals2']) if match['goals2'].size > 0
94
+
95
+ print ")\n"
96
+ end
97
+ end
98
+ end
99
+
100
+
101
+
102
+ def self._pp_goals( recs )
103
+ players = {}
104
+
105
+ ## "fold" multiple goals of player
106
+ recs.each do |rec|
107
+
108
+ name = rec['name']
109
+
110
+ if rec['minute'].nil? || rec['minute'].empty?
111
+ puts "!! WARN - (goals) minute empty:"
112
+ pp rec
113
+ ## use '??'
114
+ rec['minute'] = '??'
115
+ ## raise ArgumentError, "minute empty"
116
+ end
117
+
118
+ goal = String.new
119
+ goal << "#{rec['minute']}'"
120
+ goal << '(og)' if rec['owngoal'] == true
121
+ goal << '(p)' if rec['penalty'] == true
122
+
123
+ player_rec = players[ name ] ||= { name: name, goals: [] }
124
+ player_rec[:goals] << goal
125
+ end
126
+
127
+
128
+ buf = players.map do |_,player|
129
+ "#{player[:name]} #{player[:goals].join(',')}"
130
+ end.join( ' ' )
131
+ buf
132
+ end
133
+
134
+
135
+ end # module Footty
@@ -0,0 +1,24 @@
1
+
2
+ module Footty
3
+
4
+
5
+ def self.week_tue_to_mon( today=Date.today )
6
+ ## Calculate the start of the (sport) week (tuesday)
7
+ ## note - wday starts counting sunday (0), monday (1), etc.
8
+ week_tue = today - (today.wday - 2) % 7
9
+ week_mon = week_tue + 6
10
+
11
+ [week_tue,week_mon]
12
+ end
13
+
14
+
15
+ def self.fmt_week( week_start, week_end )
16
+ buf = String.new
17
+ buf << "Week %02d" % week_start.cweek
18
+ buf << " - #{week_start.strftime( "%a %b %-d")}"
19
+ buf << " to #{week_end.strftime( "%a %b %-d %Y")}"
20
+ buf
21
+ end
22
+
23
+
24
+ end # module Footty
@@ -1,6 +1,6 @@
1
1
 
2
2
  module Footty
3
- VERSION = '2025.4.28'
3
+ VERSION = '2026.5.25'
4
4
 
5
5
  def self.banner
6
6
  "footty/#{VERSION} on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}] in (#{root})"
@@ -10,5 +10,3 @@ module Footty
10
10
  File.expand_path( File.dirname(File.dirname(File.dirname(__FILE__))) )
11
11
  end
12
12
  end
13
-
14
-
data/lib/footty.rb CHANGED
@@ -1,247 +1,33 @@
1
1
  require 'sportdb/quick' ## note - pulls in cocos et al
2
- require 'webget' ## add webcache support
3
-
4
- require 'optparse'
2
+ require 'webget' ## add webcache support
5
3
 
6
4
 
7
5
 
8
6
 
9
7
  # our own code
10
8
  require_relative 'footty/version' # let version always go first
11
- require_relative 'footty/dataset'
12
- require_relative 'footty/openfootball'
13
9
 
14
- require_relative 'footty/print'
10
+ require_relative 'footty/dataset/dataset'
11
+ require_relative 'footty/dataset/openfootball'
12
+
13
+ require_relative 'footty/pp_matches'
14
+ require_relative 'footty/pp_week'
15
+
16
+ require_relative 'footty/main'
17
+
18
+
15
19
 
16
20
 
17
21
 
18
22
  ## set cache to local .cache dir for now - why? why not?
19
23
  Webcache.root = './cache'
24
+
20
25
  # pp Webcache.root
21
26
  Webget.config.sleep = 1 ## set delay in secs (to 1 sec - default is/maybe 3)
22
27
 
23
28
 
24
- module Footty
25
- def self.main( args=ARGV )
26
- puts banner # say hello
27
-
28
-
29
- opts = { debug: false,
30
- verbose: false, ## add more details
31
- ## add cache/cache_dir - why? why not?
32
-
33
- query: nil,
34
-
35
- ## display format/mode - week/window/upcoming/past (default is today)
36
- yesterday: nil,
37
- tomorrow: nil,
38
- upcoming: nil,
39
- past: nil,
40
-
41
- # week: nil,
42
- # window: nil, ## 2 day plus/minus +2/-2
43
- }
44
-
45
-
46
- parser = OptionParser.new do |parser|
47
- parser.banner = "Usage: #{$PROGRAM_NAME} [options] LEAGUES"
48
-
49
- parser.on( "--verbose",
50
- "turn on verbose output (default: #{opts[:verbose]})" ) do |verbose|
51
- opts[:verbose] = true
52
- end
53
-
54
- parser.on( "-q NAME", "--query",
55
- "query mode; display matches where team name matches query" ) do |query|
56
- opts[:query] = query
57
- end
58
-
59
-
60
- parser.on( "-y", "--yesterday" ) do |yesterday|
61
- opts[:yesterday] = true
62
- end
63
- parser.on( "-t", "--tomorrow" ) do |tomorrow|
64
- opts[:tomorrow] = true
65
- end
66
- parser.on( "-p", "--past" ) do |past|
67
- opts[:past] = true
68
- end
69
- parser.on( "-u", "--up", "--upcoming" ) do |upcoming|
70
- opts[:upcoming] = true
71
- end
72
- end
73
- parser.parse!( args )
74
-
75
-
76
- puts "OPTS:"
77
- p opts
78
- puts "ARGV:"
79
- p args
80
-
81
-
82
- ###
83
- ## use simple norm(alize) args (that is,) league codes for now
84
- ## - downcase, strip dot (.) etc.)
85
- ## e.g. en.facup => enfacup
86
- ## at.cup => atcup etc.
87
- args = args.map { |arg| arg.downcase.gsub( /[._-]/, '' ) }
88
-
89
-
90
-
91
- ######################
92
- ## note - first check for buil-in "magic" commands
93
- ## e.g. leagues / codes - dump built-in league codes
94
-
95
- if args.include?( 'leagues' )
96
- puts "==> openfootball dataset sources:"
97
- pp OpenfootballDataset::SOURCES
98
-
99
- ## pretty print keys/codes only
100
- puts
101
- puts OpenfootballDataset::SOURCES.keys.join( ' ' )
102
- puts " #{OpenfootballDataset::SOURCES.keys.size} league code(s)"
103
-
104
- exit 1
105
- end
106
-
107
-
108
-
109
-
110
-
111
- top = [['world', '2022'],
112
- ['euro', '2024'],
113
- ['mx', '2024/25'],
114
- ['copa', '2025'], ## copa libertadores
115
- ['en', '2024/25'],
116
- ['es', '2024/25'],
117
- ['it', '2024/25'],
118
- ['fr', '2024/25'],
119
- ['de', '2024/25'],
120
- ['at', '2024/25'],
121
- ['champs', '2024/25'],
122
- ]
123
-
124
-
125
- leagues = if args.size == 0
126
- top
127
- else
128
- ### auto-fill (latest) season/year
129
- args.map do |arg|
130
- [arg, OpenfootballDataset.latest_season( league: arg )]
131
- end
132
- end
133
-
134
-
135
- ## fetch leagues
136
- datasets = leagues.map do |league, season|
137
- dataset = OpenfootballDataset.new( league: league, season: season )
138
- ## parse matches
139
- matches = dataset.matches
140
- puts " #{league} #{season} - #{matches.size} match(es)"
141
- dataset
142
- end
143
-
144
-
145
-
146
- ###################
147
- ## check for query option to filter matches by query (team)
148
- if opts[:query]
149
- q = opts[:query]
150
- puts
151
- puts
152
- datasets.each do |dataset|
153
- matches = dataset.query( q )
154
-
155
- if matches.size == 0
156
- ## siltently skip for now
157
- else ## assume matches found
158
- print "==> #{dataset.league_name}"
159
- print " #{dataset.start_date} - #{dataset.end_date}"
160
- print " -- #{dataset.matches.size} match(es)"
161
- print "\n"
162
- print_matches( matches )
163
- end
164
- end
165
- exit 1
166
- end
167
-
168
-
169
- # Dataset.new( league: 'euro', year: 2024 )
170
- # dataset = Dataset.new( league: league, year: year )
171
-
172
- ## in the future make today "configurable" as param - why? why not?
173
- today = Date.today
174
-
175
-
176
- what = if opts[:yesterday]
177
- 'yesterday'
178
- elsif opts[:tomorrow]
179
- 'tomorrow'
180
- elsif opts[:past]
181
- 'past'
182
- elsif opts[:upcoming]
183
- 'upcoming'
184
- else
185
- 'today'
186
- end
187
-
188
-
189
- ## start with two empty lines - assume (massive) debug output before ;-)
190
- puts
191
- puts
192
- datasets.each do |dataset|
193
- print "==> #{dataset.league_name}"
194
- print " #{dataset.start_date} - #{dataset.end_date}"
195
- print " -- #{dataset.matches.size} match(es)"
196
- print "\n"
197
-
198
- if what == 'yesterday'
199
- matches = dataset.yesterdays_matches
200
- if matches.empty?
201
- puts (' '*4) + "** No matches played yesterday.\n"
202
- end
203
- elsif what == 'tomorrow'
204
- matches = dataset.tomorrows_matches
205
- if matches.empty?
206
- puts (' '*4) + "** No matches scheduled tomorrow.\n"
207
- end
208
- elsif what == 'past'
209
- matches = dataset.past_matches
210
- if matches.empty?
211
- puts (' '*4) + "** No matches played yet.\n"
212
- end
213
- elsif what == 'upcoming'
214
- matches = dataset.upcoming_matches
215
- if matches.empty?
216
- puts (' '*4) + "** No more matches scheduled.\n"
217
- end
218
- else ## assume today
219
- matches = dataset.todays_matches
220
-
221
- ## no matches today
222
- if matches.empty?
223
- puts (' '*4) + "** No matches scheduled today.\n"
224
-
225
- if opts[:verbose]
226
- ## note: was world cup 2018 - end date -- Date.new( 2018, 7, 11 )
227
- ## note: was euro 2020 (in 2021) - end date -- Date.new( 2021, 7, 11 )
228
- if Date.today > dataset.end_date ## tournament is over, look back
229
- puts "Past matches:"
230
- matches = dataset.past_matches
231
- else ## world cup is upcoming /in-progress,look forward
232
- puts "Upcoming matches:"
233
- matches = dataset.upcoming_matches( limit: 18 )
234
- end
235
- end
236
- end
237
- end
238
- print_matches( matches )
239
- end
240
-
241
- end # method self.main
242
- end # module Footty
243
-
244
-
245
-
246
-
247
- Footty.main if __FILE__ == $0
29
+
30
+
31
+
32
+
33
+ Footty.main if __FILE__ == $0
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: footty
3
3
  version: !ruby/object:Gem::Version
4
- version: 2025.4.28
4
+ version: 2026.5.25
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gerald Bauer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-04-28 00:00:00.000000000 Z
11
+ date: 2026-05-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sportdb-quick
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.2.0
19
+ version: 0.7.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 0.2.0
26
+ version: 0.7.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: webget
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -91,9 +91,11 @@ files:
91
91
  - bin/footty
92
92
  - bin/ftty
93
93
  - lib/footty.rb
94
- - lib/footty/dataset.rb
95
- - lib/footty/openfootball.rb
96
- - lib/footty/print.rb
94
+ - lib/footty/dataset/dataset.rb
95
+ - lib/footty/dataset/openfootball.rb
96
+ - lib/footty/main.rb
97
+ - lib/footty/pp_matches.rb
98
+ - lib/footty/pp_week.rb
97
99
  - lib/footty/version.rb
98
100
  homepage: https://github.com/sportdb/footty
99
101
  licenses:
data/lib/footty/print.rb DELETED
@@ -1,102 +0,0 @@
1
-
2
- module Footty
3
-
4
- def self.print_matches( matches )
5
-
6
- today = Date.today
7
-
8
- matches.each do |match|
9
- print " %5s" % "\##{match['num']} " if match['num']
10
-
11
- date = Date.strptime( match['date'], '%Y-%m-%d' )
12
- print "#{date.strftime('%a %b/%d')} " ## e.g. Thu Jun/14
13
- print "#{match['time']} " if match['time']
14
-
15
- if date > today
16
- diff = (date - today).to_i
17
- print "%10s" % "(in #{diff}d) "
18
- end
19
-
20
-
21
- if match['team1'].is_a?( Hash )
22
- print "%22s" % "#{match['team1']['name']} (#{match['team1']['code']})"
23
- else
24
- print "%22s" % "#{match['team1']}"
25
- end
26
-
27
-
28
- if match['score'].is_a?( Hash ) &&
29
- match['score']['ft']
30
- if match['score']['ft']
31
- print " #{match['score']['ft'][0]}-#{match['score']['ft'][1]} "
32
- end
33
- if match['score']['et']
34
- print "aet #{match['score']['et'][0]}-#{match['score']['et'][1]} "
35
- end
36
- if match['score']['p']
37
- print "pen #{match['score']['p'][0]}-#{match['score']['p'][1]} "
38
- end
39
- elsif match['score1'] && match['score2']
40
- ## todo/fix: add support for knockout scores
41
- ## with score1et/score1p (extra time and penalty)
42
- print " #{match['score1']}-#{match['score2']} "
43
- print "(#{match['score1i']}-#{match['score2i']}) "
44
- else
45
- print " vs "
46
- end
47
-
48
- if match['team2'].is_a?( Hash )
49
- print "%-22s" % "#{match['team2']['name']} (#{match['team2']['code']})"
50
- else
51
- print "%-22s" % "#{match['team2']}"
52
- end
53
-
54
- if match['stage']
55
- print " #{match['stage']} /" ## stage
56
- end
57
-
58
- if match['group']
59
- print " #{match['group']} /" ## group phase
60
- end
61
-
62
- print " #{match['round']} " ## knock out (k.o.) phase/stage
63
-
64
- ## todo/fix - check for ground name in use???
65
- if match['stadium']
66
- print " @ #{match['stadium']['name']}, #{match['city']}"
67
- end
68
-
69
- print "\n"
70
-
71
-
72
- if match['goals1'] && match['goals2']
73
- print " ["
74
- match['goals1'].each_with_index do |goal,i|
75
- print " " if i > 0
76
- print "#{goal['name']}"
77
- print " #{goal['minute']}"
78
- print "+#{goal['offset']}" if goal['offset']
79
- print "'"
80
- print " (o.g.)" if goal['owngoal']
81
- print " (pen.)" if goal['penalty']
82
- end
83
- match['goals2'].each_with_index do |goal,i|
84
- if i == 0
85
- print "; "
86
- else
87
- print " "
88
- end
89
- print "#{goal['name']}"
90
- print " #{goal['minute']}"
91
- print "+#{goal['offset']}" if goal['offset']
92
- print "'"
93
- print " (o.g.)" if goal['owngoal']
94
- print " (pen.)" if goal['penalty']
95
- end
96
- print "]\n"
97
- end
98
- end
99
- end
100
-
101
-
102
- end # module Footty