footty 2024.9.27 → 2025.4.28

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: 2254b1fa1cf0832e403b6bc0e6509d1ffee06e0f552fec2b805b124c0a867c0d
4
- data.tar.gz: 8c8182dabe0e72801b580e46bed8471baacd01080fedac360b2532b63673d9cb
3
+ metadata.gz: 620119829b1dbd891a01c374c8996a264e347f329e0211387e18fdc12add6fe6
4
+ data.tar.gz: cc1f485cc3bbfc12ab5f3846823df6c63f7295912045996a08c2ccb4e50eb7a5
5
5
  SHA512:
6
- metadata.gz: 3e7421b9f2bf8ec572a55075970301343604a4dbaaabfeceec10827df9a712184fa9372a72c34e0f79811a4ae47b8a773115916e0e195d46def89a9e509b509d
7
- data.tar.gz: 4ef82377e9caead4e3ec0048b8eac6120fe77c8de4018e69e74faec2842f33909626234a5bc2ccef137661f999c7825f9ef834c64cab292bcf4e9c357467d645
6
+ metadata.gz: a0394d72327b87ad90e207e48883bf90acace8e1183d85f9175b9a6484ab8c62e3e12759f77a3eac1e59cc526e0eeacdd70183418a29a920e47c10732f28e51e
7
+ data.tar.gz: a0e274e21d9ca9620cbdda59ebdec84e92f5b8a99a4e747a177b076de6b830c94b66064c5b89c7ae6ae989b47803558aa7720fd51a321c089705987ad5196f52
data/CHANGELOG.md CHANGED
@@ -1,4 +1,4 @@
1
- ### 2024.9.27
1
+ ### 2025.4.28
2
2
  ### 1.0.0 / 2018-06-09
3
3
 
4
4
  * Everything is new (again). First release.
data/Manifest.txt CHANGED
@@ -6,4 +6,6 @@ bin/footty
6
6
  bin/ftty
7
7
  lib/footty.rb
8
8
  lib/footty/dataset.rb
9
+ lib/footty/openfootball.rb
10
+ lib/footty/print.rb
9
11
  lib/footty/version.rb
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # footty - football.db command line tool for national & int'l football club leagues (& cups) from around the world (bonus - incl. world cup, euro and more)
1
+ # footty/ftty - football.db command line tool for national & int'l football club leagues (& cups) from around the world (bonus - incl. world cup, euro and more)
2
2
 
3
3
 
4
4
  * home :: [github.com/sportdb/footty](https://github.com/sportdb/footty)
@@ -12,13 +12,15 @@
12
12
 
13
13
  ## Usage - Who's playing today?
14
14
 
15
- The footty command line tool lets you query the online football.db via HTTP
15
+ The footty (or ftty) command line tool lets you query the online football.db via HTTP
16
16
  for upcoming or past matches. For example:
17
17
 
18
- $ footty # Defaults to today's English Premier League 2024/25 matches
18
+ $ footty # Defaults to today's matches of top leagues
19
+
19
20
 
20
21
  prints on Sep 27, 2024:
21
22
 
23
+ ==> English Premier League 2024/25
22
24
  Upcoming matches:
23
25
  Sat Sep/28 12:30 (in 1d) Newcastle United FC vs Manchester City FC Matchday 6
24
26
  Sat Sep/28 15:00 (in 1d) Arsenal FC vs Leicester City FC Matchday 6
@@ -33,25 +35,25 @@ prints on Sep 27, 2024:
33
35
 
34
36
 
35
37
 
36
- Use `tomorrow` or `t` or `+1` to print tomorrow's matches e.g.:
38
+ Use `--tomorrow` or `-t` ` to print tomorrow's matches e.g.:
37
39
 
38
- $ footty tomorrow # -or-
39
- $ footty t
40
+ $ footty --tomorrow # -or-
41
+ $ footty -t
40
42
 
41
- Use `yesterday` or `y` or `-1` to print yesterday's matches e.g.:
43
+ Use `--yesterday` or `-y` to print yesterday's matches e.g.:
42
44
 
43
- $ footty yesterday # -or-
44
- $ footty y
45
+ $ footty --yesterday # -or-
46
+ $ footty -y
45
47
 
46
- Use `upcoming` or `up` or `u` to print all upcoming matches e.g.:
48
+ Use `--upcoming` or `--up` or `-u` to print all upcoming matches e.g.:
47
49
 
48
- $ footty upcoming # -or-
49
- $ footty up
50
+ $ footty --upcoming # -or-
51
+ $ footty --up
50
52
 
51
- Use `past` or `p` to print all past matches e.g.:
53
+ Use `--past` or `-p` to print all past matches e.g.:
52
54
 
53
- $ footty past # -or-
54
- $ footty p
55
+ $ footty --past # -or-
56
+ $ footty -p
55
57
 
56
58
 
57
59
 
@@ -60,7 +62,7 @@ That's it. Enjoy the beautiful game.
60
62
 
61
63
  ## Bonus - More Leagues & Cups - Bundesliga, Serie A, Ligue 1, La Liga & More
62
64
 
63
- Pass in the league code to display the Germand Bundesliga, Spanish La Liga, etc:
65
+ Pass in the league code to display the German Bundesliga, Spanish La Liga, etc:
64
66
 
65
67
  $ footty de # who's playing in the bundesliga today?
66
68
  $ footty es # who's playing in la liga today?
@@ -79,7 +81,7 @@ More
79
81
  - `world` => World Cup
80
82
  - `euro` => "Euro" - European Championship
81
83
 
82
- See [footty/dataset](https://github.com/sportdb/footty/blob/master/lib/footty/dataset.rb) for the complete built-in list of data sources (and league keys).
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).
83
85
 
84
86
 
85
87
 
@@ -2,98 +2,14 @@ module Footty
2
2
 
3
3
 
4
4
  class Dataset
5
- SOURCES = {
6
- 'world' => { '2022' => [ 'worldcup/2022--qatar/cup.txt',
7
- 'worldcup/2022--qatar/cup_finals.txt'],
8
- '2018' => [ 'worldcup/2018--russia/cup.txt',
9
- 'worldcup/2018--russia/cup_finals.txt']
10
- },
11
- 'euro' => { '2024' => 'euro/2024--germany/euro.txt',
12
- '2021' => 'euro/2021--europe/euro.txt'
13
- },
14
- 'de' => 'deutschland/$season$/1-bundesliga.txt',
15
- 'de2' => 'deutschland/$season$/2-bundesliga2.txt',
16
- 'en'=> 'england/$season$/1-premierleague.txt',
17
- 'es'=> 'espana/$season$/1-liga.txt',
18
- 'it'=> 'italy/$season$/1-seriea.txt',
19
- 'at'=> 'austria/$season$/1-bundesliga.txt',
20
- 'at2' => 'austria/$season$/2-liga2.txt',
21
- 'at3o' => 'austria/$season$/3-regionalliga-ost.txt',
22
- 'atcup' => 'austria/$season$/cup.txt',
23
-
24
- 'fr'=> 'europe/france/$season$/1-ligue1.txt',
25
- 'nl'=> 'europe/netherlands/$season$/1-eredivisie.txt',
26
- 'be'=> 'europe/belgium/$season$/1-firstdivisiona.txt',
27
-
28
- 'champs'=> 'champions-league/$season$/cl.txt',
29
-
30
- 'br' => 'south-america/brazil/$year$/1-seriea.txt',
31
- 'ar' => 'south-america/argentina/$year$/1-primeradivision.txt',
32
- 'co' => 'south-america/colombia/$year$/1-primeraa.txt',
33
- ## use a different code for copa libertadores? why? why not?
34
- 'copa' => 'south-america/copa-libertadores/$year$/libertadores.txt',
35
-
36
- 'mx' => 'mexico/$season$/1-ligamx.txt',
37
-
38
- 'eg' => 'africa/egypt/$season$/1-premiership.txt',
39
- 'ma' => 'africa/morocco/$season$/1-botolapro1.txt',
40
- }
41
-
42
-
43
- ## return built-in league keys
44
- def self.leagues() SOURCES.keys; end
45
-
46
-
47
- def initialize( league:, year: nil )
48
- @league = league
49
-
50
- spec = SOURCES[ @league.downcase ]
51
- urls = if spec.is_a?( Hash ) ## assume lookup by year
52
- if year.nil? ## lookup default first entry
53
- year = spec.keys[0].to_i
54
- spec.values[0]
55
- else
56
- spec[ year.to_s ]
57
- end
58
- else ## assume vanilla urls (no lookup by year)
59
- ## default to 2024 for now
60
- year = 2024 if year.nil?
61
- spec
62
- end
63
- raise ArgumentError, "no dataset (source) for league #{league} found" if urls.nil?
64
-
65
- ## wrap single sources (strings) in array
66
- urls = urls.is_a?( Array ) ? urls : [urls]
67
- ## expand shortened url and fill-in template vars
68
- @urls = urls.map { |url| openfootball_url( url, year: year ) }
69
- end
70
5
 
71
6
 
72
- def openfootball_url( path, year: )
73
- repo, local_path = path.split( '/', 2)
74
- url = "https://raw.githubusercontent.com/openfootball/#{repo}/master/#{local_path}"
75
- ## check for template vars too
76
- season = Season( "#{year}/#{year+1}" )
77
- url = url.gsub( '$year$', year.to_s )
78
- url = url.gsub( '$season$', season.to_path )
79
- url
7
+ def matches
8
+ raise ArgumentError, "method matches must be implemented by concrete class!!"
80
9
  end
81
10
 
82
-
83
- ### note:
84
- ## cache ALL methods - only do one web request for match schedule & results
85
- def matches
86
- @data ||= begin
87
- matches = []
88
- @urls.each do |url|
89
- txt = get!( url ) ## use "memoized" / cached result
90
- matches += SportDb::QuickMatchReader.parse( txt )
91
- end
92
- data = matches.map {|match| match.as_json } # convert to json
93
- ## quick hack to get keys as strings not symbols!!
94
- ## fix upstream
95
- JSON.parse( JSON.generate( data ))
96
- end
11
+ def league_name
12
+ raise ArgumentError, "method league_name must be implemented by concrete class!!"
97
13
  end
98
14
 
99
15
 
@@ -131,6 +47,23 @@ module Footty
131
47
  end
132
48
 
133
49
 
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
63
+ end
64
+
65
+
66
+
134
67
  def upcoming_matches( date: Date.today,
135
68
  limit: nil )
136
69
  ## note: includes todays matches for now
@@ -162,28 +95,5 @@ private
162
95
  ## sort matches here; might not be chronologicial (by default)
163
96
  selected
164
97
  end # method select_matches
165
-
166
-
167
-
168
- def get!( url )
169
- ## use cached urls for 12h by default
170
- ## if expired in cache (or not present) than get/fetch
171
- if Webcache.expired_in_12h?( url )
172
- response = Webget.text( url )
173
-
174
- if response.status.ok?
175
- response.text # note - return text (utf-8)
176
- else
177
- ## dump headers
178
- response.headers.each do |key,value|
179
- puts " #{key}: #{value}"
180
- end
181
- puts "!! HTTP ERROR - #{response.status.code} #{response.status.message}"
182
- exit 1
183
- end
184
- else
185
- Webcache.read( url )
186
- end
187
- end # method get_txt!
188
98
  end # class Dataset
189
99
  end # module Footty
@@ -0,0 +1,166 @@
1
+ module Footty
2
+
3
+
4
+ class OpenfootballDataset < Dataset
5
+ SOURCES = {
6
+ 'world' => { '2022' => [ 'worldcup/2022--qatar/cup.txt',
7
+ 'worldcup/2022--qatar/cup_finals.txt'],
8
+ '2018' => [ 'worldcup/2018--russia/cup.txt',
9
+ 'worldcup/2018--russia/cup_finals.txt']
10
+ },
11
+ 'euro' => { '2024' => 'euro/2024--germany/euro.txt',
12
+ '2021' => 'euro/2021--europe/euro.txt'
13
+ },
14
+ 'de' => 'deutschland/$season$/1-bundesliga.txt',
15
+ 'de2' => 'deutschland/$season$/2-bundesliga2.txt',
16
+ 'de3' => 'deutschland/$season$/3-liga3.txt',
17
+ 'decup' => 'deutschland/$season$/cup.txt',
18
+
19
+
20
+ 'en' => 'england/$season$/1-premierleague.txt',
21
+ 'en2' => 'england/$season$/2-championship.txt',
22
+ ## add alternate codes!!!
23
+ ## use eflcup, facup - why? why not?
24
+ 'eneflcup' => 'england/$season$/eflcup.txt',
25
+ 'enfacup' => 'england/$season$/facup.txt',
26
+
27
+
28
+ 'es' => 'espana/$season$/1-liga.txt',
29
+ 'escup' => 'espana/$season$/cup.txt',
30
+
31
+ 'it'=> 'italy/$season$/1-seriea.txt',
32
+
33
+ 'at'=> 'austria/$season$/1-bundesliga.txt',
34
+ 'at2' => 'austria/$season$/2-liga2.txt',
35
+ 'at3o' => 'austria/$season$/3-regionalliga-ost.txt',
36
+ 'atcup' => 'austria/$season$/cup.txt',
37
+
38
+ 'fr'=> 'europe/france/$season$_fr1.txt',
39
+ 'nl'=> 'europe/netherlands/$season$_nl1.txt',
40
+ 'be'=> 'europe/belgium/$season$_be1.txt',
41
+
42
+ 'champs'=> 'champions-league/$season$/cl.txt',
43
+
44
+ 'br' => 'south-america/brazil/$year$_br1.txt',
45
+ 'ar' => 'south-america/argentina/$year$_ar1.txt',
46
+ 'co' => 'south-america/colombia/$year$_co1.txt',
47
+
48
+ ## use a different code for copa libertadores? why? why not?
49
+ 'copa' => 'south-america/copa-libertadores/$year$_copal.txt',
50
+
51
+ 'mx' => 'world/north-america/mexico/$season$_mx1.txt',
52
+ 'mls' => 'world/north-america/major-league-soccer/$year$_mls.txt',
53
+
54
+ 'eg' => 'world/africa/egypt/$season$_eg1.txt',
55
+ 'ma' => 'world/africa/morocco/$season$_ma1.txt',
56
+
57
+ 'au' => 'world/pacific/australia/$season$_au1.txt',
58
+ 'jp' => 'world/asia/japan/$year$_jp1.txt',
59
+ 'cn' => 'world/asia/china/$year$_cn1.txt',
60
+
61
+ }
62
+
63
+
64
+ ## return built-in league keys
65
+ def self.leagues() SOURCES.keys; end
66
+
67
+
68
+
69
+ ### auto-fill latest season
70
+ def self.latest_season( league: )
71
+ spec = SOURCES[ league.downcase ]
72
+
73
+ raise ArgumentError, "no dataset (source) for league #{league} found" if spec.nil?
74
+
75
+ ## todo/fix - report error if no spec found
76
+ season = if spec.is_a?( Hash ) ## assume lookup by year
77
+ spec.keys[0]
78
+ else ## assume vanilla urls (no lookup by year)
79
+ ## default to 2025 or 2024/25 for now
80
+ spec.index( '$year$') ? '2025' : '2024/25'
81
+ end
82
+ season
83
+ end
84
+
85
+
86
+
87
+ def initialize( league:, season: )
88
+ spec = SOURCES[ league.downcase ]
89
+
90
+ urls = if spec.is_a?( Hash ) ## assume lookup by year
91
+ spec[ season ]
92
+ else ## assume vanilla urls (no lookup by year)
93
+ spec
94
+ end
95
+ raise ArgumentError, "no dataset (source) for league #{league} found" if urls.nil?
96
+
97
+ ## wrap single sources (strings) in array
98
+ urls = urls.is_a?( Array ) ? urls : [urls]
99
+ ## expand shortened url and fill-in template vars
100
+ urls = urls.map { |url| openfootball_url( url, season: season ) }
101
+
102
+ matches = []
103
+ urls.each do |url|
104
+ txt = get!( url ) ## use "memoized" / cached result
105
+ parser = SportDb::QuickMatchReader.new( txt )
106
+ matches += parser.parse
107
+ ### for multiple source file use latest name as "definitive"
108
+ @league_name = parser.league_name
109
+ ### todo/fix - report errors
110
+ end
111
+
112
+ matches = matches.map {|match| match.as_json } # convert to json
113
+
114
+ ## note - sort by date/time
115
+ ## (assume stable sort; no reshuffle of matches if already sorted by date/time)
116
+
117
+ matches = matches.sort do |l,r|
118
+ result = l['date'] <=> r['date']
119
+ result = l['time'] <=> r['time'] if result == 0 &&
120
+ (l['time'] && r['time'])
121
+ result
122
+ end
123
+
124
+ @matches = matches
125
+ end
126
+
127
+
128
+ def openfootball_url( path, season: )
129
+ repo, local_path = path.split( '/', 2)
130
+ url = "https://raw.githubusercontent.com/openfootball/#{repo}/master/#{local_path}"
131
+ ## check for template vars too
132
+ season = Season( season )
133
+ url = url.gsub( '$year$', season.start_year.to_s )
134
+ url = url.gsub( '$season$', season.to_path )
135
+ url
136
+ end
137
+
138
+
139
+ def matches() @matches; end
140
+ def league_name() @league_name; end
141
+
142
+
143
+
144
+ def get!( url )
145
+ ## use cached urls for 12h by default
146
+ ## if expired in cache (or not present) than get/fetch
147
+ if Webcache.expired_in_12h?( url )
148
+ response = Webget.text( url )
149
+
150
+ if response.status.ok?
151
+ response.text # note - return text (utf-8)
152
+ else
153
+ ## dump headers
154
+ response.headers.each do |key,value|
155
+ puts " #{key}: #{value}"
156
+ end
157
+ puts "!! HTTP ERROR - #{response.status.code} #{response.status.message}"
158
+ exit 1
159
+ end
160
+ else
161
+ Webcache.read( url )
162
+ end
163
+ end # method get_txt!
164
+
165
+ end # class OpenfootballDaset
166
+ end # module Footty
@@ -0,0 +1,102 @@
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
@@ -1,6 +1,6 @@
1
1
 
2
2
  module Footty
3
- VERSION = '2024.9.27'
3
+ VERSION = '2025.4.28'
4
4
 
5
5
  def self.banner
6
6
  "footty/#{VERSION} on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}] in (#{root})"
data/lib/footty.rb CHANGED
@@ -1,9 +1,17 @@
1
1
  require 'sportdb/quick' ## note - pulls in cocos et al
2
2
  require 'webget' ## add webcache support
3
3
 
4
+ require 'optparse'
5
+
6
+
7
+
8
+
4
9
  # our own code
5
- require_relative 'footty/version' # let it always go first
10
+ require_relative 'footty/version' # let version always go first
6
11
  require_relative 'footty/dataset'
12
+ require_relative 'footty/openfootball'
13
+
14
+ require_relative 'footty/print'
7
15
 
8
16
 
9
17
 
@@ -17,176 +25,223 @@ module Footty
17
25
  def self.main( args=ARGV )
18
26
  puts banner # say hello
19
27
 
20
- league = 'en'
21
- year = nil
22
28
 
23
- leagues = Dataset.leagues ## e.g. ['world','euro',
24
- ## 'de','en','at']
29
+ opts = { debug: false,
30
+ verbose: false, ## add more details
31
+ ## add cache/cache_dir - why? why not?
25
32
 
26
- ## filter args for league or year
27
- args = args.select do |arg|
28
- if arg =~ /^\d{4}$/
29
- year = arg.to_i(10)
30
- false ## eat-up
31
- elsif leagues.include?( arg )
32
- league = arg
33
- false
34
- else
35
- true
36
- end
37
- end
33
+ query: nil,
38
34
 
35
+ ## display format/mode - week/window/upcoming/past (default is today)
36
+ yesterday: nil,
37
+ tomorrow: nil,
38
+ upcoming: nil,
39
+ past: nil,
39
40
 
40
- what = args[0] || 'today'
41
- what = what.downcase
41
+ # week: nil,
42
+ # window: nil, ## 2 day plus/minus +2/-2
43
+ }
42
44
 
45
+
46
+ parser = OptionParser.new do |parser|
47
+ parser.banner = "Usage: #{$PROGRAM_NAME} [options] LEAGUES"
43
48
 
44
- # Dataset.new( league: 'euro', year: 2024 )
45
- dataset = Dataset.new( league: league, year: year )
49
+ parser.on( "--verbose",
50
+ "turn on verbose output (default: #{opts[:verbose]})" ) do |verbose|
51
+ opts[:verbose] = true
52
+ end
46
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
47
58
 
48
- ## in the future make today "configurable" as param - why? why not?
49
- today = Date.today
50
59
 
51
- if ['yesterday', 'y', '-1'].include?( what )
52
- matches = dataset.yesterdays_matches
53
- if matches.empty?
54
- puts "** No matches played yesterday.\n"
60
+ parser.on( "-y", "--yesterday" ) do |yesterday|
61
+ opts[:yesterday] = true
55
62
  end
56
- elsif ['tomorrow', 't', '+1', '1'].include?( what )
57
- matches = dataset.tomorrows_matches
58
- if matches.empty?
59
- puts "** No matches scheduled tomorrow.\n"
63
+ parser.on( "-t", "--tomorrow" ) do |tomorrow|
64
+ opts[:tomorrow] = true
60
65
  end
61
- elsif ['past', 'p', 'prev'].include?( what )
62
- matches = dataset.past_matches
63
- if matches.empty?
64
- puts "** No matches played yet.\n"
66
+ parser.on( "-p", "--past" ) do |past|
67
+ opts[:past] = true
65
68
  end
66
- elsif ['upcoming', 'up', 'u', 'next', 'n'].include?( what )
67
- matches = dataset.upcoming_matches
68
- if matches.empty?
69
- puts "** No more matches scheduled.\n"
69
+ parser.on( "-u", "--up", "--upcoming" ) do |upcoming|
70
+ opts[:upcoming] = true
70
71
  end
71
- else
72
- matches = dataset.todays_matches
73
-
74
- ## no matches today
75
- if matches.empty?
76
- puts "** No matches scheduled today.\n"
77
-
78
- ## note: was world cup 2018 - end date -- Date.new( 2018, 7, 11 )
79
- ## note: was euro 2020 (in 2021) - end date -- Date.new( 2021, 7, 11 )
80
- if Date.today > dataset.end_date ## tournament is over, look back
81
- puts "Past matches:"
82
- matches = dataset.past_matches
83
- else ## world cup is upcoming /in-progress,look forward
84
- puts "Upcoming matches:"
85
- matches = dataset.upcoming_matches( limit: 18 )
86
- end
87
- end
88
72
  end
73
+ parser.parse!( args )
89
74
 
90
- print_matches( matches )
91
- end
92
75
 
76
+ puts "OPTS:"
77
+ p opts
78
+ puts "ARGV:"
79
+ p args
93
80
 
94
- def self.print_matches( matches )
95
- ## print games
96
81
 
97
- today = Date.today
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( /[._-]/, '' ) }
98
88
 
99
- matches.each do |match|
100
- print " %5s" % "\##{match['num']} " if match['num']
101
89
 
102
- date = Date.strptime( match['date'], '%Y-%m-%d' )
103
- print "#{date.strftime('%a %b/%d')} " ## e.g. Thu Jun/14
104
- print "#{match['time']} " if match['time']
105
90
 
106
- if date > today
107
- diff = (date - today).to_i
108
- print "%10s" % "(in #{diff}d) "
109
- end
91
+ ######################
92
+ ## note - first check for buil-in "magic" commands
93
+ ## e.g. leagues / codes - dump built-in league codes
110
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)"
111
103
 
112
- if match['team1'].is_a?( Hash )
113
- print "%22s" % "#{match['team1']['name']} (#{match['team1']['code']})"
114
- else
115
- print "%22s" % "#{match['team1']}"
116
- end
104
+ exit 1
105
+ end
117
106
 
118
107
 
119
- if match['score'].is_a?( Hash ) &&
120
- match['score']['ft']
121
- if match['score']['ft']
122
- print " #{match['score']['ft'][0]}-#{match['score']['ft'][1]} "
123
- end
124
- if match['score']['et']
125
- print "aet #{match['score']['et'][0]}-#{match['score']['et'][1]} "
126
- end
127
- if match['score']['p']
128
- print "pen #{match['score']['p'][0]}-#{match['score']['p'][1]} "
129
- end
130
- elsif match['score1'] && match['score2']
131
- ## todo/fix: add support for knockout scores
132
- ## with score1et/score1p (extra time and penalty)
133
- print " #{match['score1']}-#{match['score2']} "
134
- print "(#{match['score1i']}-#{match['score2i']}) "
135
- else
136
- print " vs "
137
- end
138
108
 
139
- if match['team2'].is_a?( Hash )
140
- print "%-22s" % "#{match['team2']['name']} (#{match['team2']['code']})"
141
- else
142
- print "%-22s" % "#{match['team2']}"
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
143
164
  end
165
+ exit 1
166
+ end
144
167
 
145
168
 
146
- if match['group']
147
- print " #{match['group']} /" ## group phase/stage
148
- end
169
+ # Dataset.new( league: 'euro', year: 2024 )
170
+ # dataset = Dataset.new( league: league, year: year )
149
171
 
150
- print " #{match['round']} " ## knock out (k.o.) phase/stage
172
+ ## in the future make today "configurable" as param - why? why not?
173
+ today = Date.today
151
174
 
152
- if match['stadium']
153
- print " @ #{match['stadium']['name']}, #{match['city']}"
154
- end
155
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)"
156
196
  print "\n"
157
197
 
158
-
159
- if match['goals1'] && match['goals2']
160
- print " ["
161
- match['goals1'].each_with_index do |goal,i|
162
- print " " if i > 0
163
- print "#{goal['name']}"
164
- print " #{goal['minute']}"
165
- print "+#{goal['offset']}" if goal['offset']
166
- print "'"
167
- print " (o.g.)" if goal['owngoal']
168
- print " (pen.)" if goal['penalty']
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"
169
212
  end
170
- match['goals2'].each_with_index do |goal,i|
171
- if i == 0
172
- print "; "
173
- else
174
- print " "
175
- end
176
- print "#{goal['name']}"
177
- print " #{goal['minute']}"
178
- print "+#{goal['offset']}" if goal['offset']
179
- print "'"
180
- print " (o.g.)" if goal['owngoal']
181
- print " (pen.)" if goal['penalty']
213
+ elsif what == 'upcoming'
214
+ matches = dataset.upcoming_matches
215
+ if matches.empty?
216
+ puts (' '*4) + "** No more matches scheduled.\n"
182
217
  end
183
- print "]\n"
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 )
184
239
  end
185
- end
186
- end
187
240
 
241
+ end # method self.main
188
242
  end # module Footty
189
243
 
190
244
 
191
245
 
246
+
192
247
  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: 2024.9.27
4
+ version: 2025.4.28
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gerald Bauer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-27 00:00:00.000000000 Z
11
+ date: 2025-04-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sportdb-quick
@@ -64,14 +64,14 @@ dependencies:
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '4.1'
67
+ version: '4.2'
68
68
  type: :development
69
69
  prerelease: false
70
70
  version_requirements: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: '4.1'
74
+ version: '4.2'
75
75
  description: footty - football.db command line tool for national & int'l football
76
76
  club leagues (& cups) from around the world (bonus - incl. world cup, euro and more)
77
77
  email: gerald.bauer@gmail.com
@@ -92,6 +92,8 @@ files:
92
92
  - bin/ftty
93
93
  - lib/footty.rb
94
94
  - lib/footty/dataset.rb
95
+ - lib/footty/openfootball.rb
96
+ - lib/footty/print.rb
95
97
  - lib/footty/version.rb
96
98
  homepage: https://github.com/sportdb/footty
97
99
  licenses:
@@ -114,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
114
116
  - !ruby/object:Gem::Version
115
117
  version: '0'
116
118
  requirements: []
117
- rubygems_version: 3.4.10
119
+ rubygems_version: 3.5.22
118
120
  signing_key:
119
121
  specification_version: 4
120
122
  summary: footty - football.db command line tool for national & int'l football club