footty 2024.8.28 → 2025.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -1
- data/Manifest.txt +3 -0
- data/README.md +58 -19
- data/Rakefile +4 -4
- data/lib/footty/datalib.rb +20 -0
- data/lib/footty/dataset.rb +16 -76
- data/lib/footty/openfootball.rb +145 -0
- data/lib/footty/print.rb +102 -0
- data/lib/footty/version.rb +1 -1
- data/lib/footty.rb +149 -147
- metadata +26 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e3101d3bc2cbc21aae12dcc1a108f7972211d55c86415a06c48e0a8db945ac3f
|
4
|
+
data.tar.gz: afaa93ffb303fd4205acdaa1aee1c80ec810a7bf723dabb99960025edb03076b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d23098b1142d238c5f18bec351b6aff504682cb21e724df04dace6a9e40294267463037313c0fafae9a51e46ae80f88e976b681fb6d79fd51832282610957b7
|
7
|
+
data.tar.gz: 3906e217d2f73536b52295c6502eed1b96af0d4cd436a331c075ecc1780ab8c005ebbad260ef3856a70009cd5a4befd037f700e4a3f1d050b7bc3842b70a5b90
|
data/CHANGELOG.md
CHANGED
data/Manifest.txt
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
# footty - football.db command line
|
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
|
* home :: [github.com/sportdb/footty](https://github.com/sportdb/footty)
|
4
5
|
* bugs :: [github.com/sportdb/footty/issues](https://github.com/sportdb/footty/issues)
|
@@ -11,39 +12,78 @@
|
|
11
12
|
|
12
13
|
## Usage - Who's playing today?
|
13
14
|
|
14
|
-
The footty command line tool lets you query the online football.db HTTP
|
15
|
+
The footty (or ftty) command line tool lets you query the online football.db via HTTP
|
15
16
|
for upcoming or past matches. For example:
|
16
17
|
|
17
|
-
$ footty
|
18
|
+
$ footty # Defaults to today's matches of top leagues
|
19
|
+
|
20
|
+
|
21
|
+
prints on Sep 27, 2024:
|
22
|
+
|
23
|
+
==> English Premier League 2024/25
|
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
|
35
|
+
|
18
36
|
|
19
|
-
prints on Jun/14 2024:
|
20
37
|
|
21
|
-
|
22
|
-
|
23
|
-
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.:
|
24
39
|
|
25
|
-
$ footty tomorrow # -or-
|
26
|
-
$ footty t
|
40
|
+
$ footty --tomorrow # -or-
|
41
|
+
$ footty -t
|
27
42
|
|
28
|
-
Use
|
43
|
+
Use `--yesterday` or `-y` to print yesterday's matches e.g.:
|
29
44
|
|
30
|
-
$ footty yesterday # -or-
|
31
|
-
$ footty y
|
45
|
+
$ footty --yesterday # -or-
|
46
|
+
$ footty -y
|
32
47
|
|
33
|
-
Use
|
48
|
+
Use `--upcoming` or `--up` or `-u` to print all upcoming matches e.g.:
|
34
49
|
|
35
|
-
$ footty upcoming # -or-
|
36
|
-
$ footty up
|
50
|
+
$ footty --upcoming # -or-
|
51
|
+
$ footty --up
|
37
52
|
|
38
|
-
Use
|
53
|
+
Use `--past` or `-p` to print all past matches e.g.:
|
54
|
+
|
55
|
+
$ footty --past # -or-
|
56
|
+
$ footty -p
|
39
57
|
|
40
|
-
$ footty past # -or-
|
41
|
-
$ footty p
|
42
58
|
|
43
59
|
|
44
60
|
That's it. Enjoy the beautiful game.
|
45
61
|
|
46
62
|
|
63
|
+
## Bonus - More Leagues & Cups - Bundesliga, Serie A, Ligue 1, La Liga & More
|
64
|
+
|
65
|
+
Pass in the league code to display the German Bundesliga, Spanish La Liga, etc:
|
66
|
+
|
67
|
+
$ footty de # who's playing in the bundesliga today?
|
68
|
+
$ footty es # who's playing in la liga today?
|
69
|
+
...
|
70
|
+
|
71
|
+
League codes include:
|
72
|
+
|
73
|
+
- `de` => Bundesliga
|
74
|
+
- `es` => La Liga
|
75
|
+
- `it` => Serie A
|
76
|
+
- `fr` => Ligue 1
|
77
|
+
- ...
|
78
|
+
|
79
|
+
More
|
80
|
+
|
81
|
+
- `world` => World Cup
|
82
|
+
- `euro` => "Euro" - European Championship
|
83
|
+
|
84
|
+
See [footty/openfootball](https://github.com/sportdb/footty/blob/master/lib/footty/openfootball.rb) for the complete built-in list of data sources (and league codes).
|
85
|
+
|
86
|
+
|
47
87
|
|
48
88
|
|
49
89
|
## Trivia
|
@@ -66,7 +106,6 @@ Use it as you please with no restrictions whatsoever.
|
|
66
106
|
|
67
107
|
|
68
108
|
|
69
|
-
|
70
109
|
## Questions? Comments?
|
71
110
|
|
72
111
|
Yes, you can. More than welcome.
|
data/Rakefile
CHANGED
@@ -2,23 +2,23 @@ require 'hoe'
|
|
2
2
|
require './lib/footty/version.rb'
|
3
3
|
|
4
4
|
Hoe.spec 'footty' do
|
5
|
-
|
6
5
|
self.version = Footty::VERSION
|
7
6
|
|
8
|
-
self.summary =
|
7
|
+
self.summary = "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)"
|
9
8
|
self.description = summary
|
10
9
|
|
11
10
|
self.urls = { home: 'https://github.com/sportdb/footty' }
|
12
11
|
|
13
12
|
self.author = 'Gerald Bauer'
|
14
|
-
self.email
|
13
|
+
self.email = 'gerald.bauer@gmail.com'
|
15
14
|
|
16
15
|
# switch extension to .markdown for gihub formatting
|
17
16
|
self.readme_file = 'README.md'
|
18
17
|
self.history_file = 'CHANGELOG.md'
|
19
18
|
|
20
19
|
self.extra_deps = [
|
21
|
-
['sportdb-quick']
|
20
|
+
['sportdb-quick', '>= 0.2.0'],
|
21
|
+
['webget'],
|
22
22
|
]
|
23
23
|
|
24
24
|
self.licenses = ['Public Domain']
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Footty
|
2
|
+
|
3
|
+
class Datalib ## Library (Selection) of datasets
|
4
|
+
## nameing - use datasel(ection) or datacol(lection)
|
5
|
+
## or databox or dataman(ger) or
|
6
|
+
## or datacache or
|
7
|
+
## or datacomposite/view
|
8
|
+
## or dtataportal or ???
|
9
|
+
|
10
|
+
|
11
|
+
def initialize( datasets=[] )
|
12
|
+
@datasets = datasets
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
end # class Datalib
|
20
|
+
end # module Footty
|
data/lib/footty/dataset.rb
CHANGED
@@ -1,49 +1,22 @@
|
|
1
1
|
module Footty
|
2
2
|
|
3
3
|
|
4
|
-
|
5
4
|
class Dataset
|
6
5
|
|
7
|
-
def initialize( league:, year: )
|
8
|
-
@league = league
|
9
|
-
@year = year
|
10
|
-
@season = Season( "#{year}/#{year+1}" )
|
11
|
-
end
|
12
|
-
|
13
6
|
|
14
|
-
APIS = {
|
15
|
-
world: 'https://raw.githubusercontent.com/openfootball/worldcup.json/master/$year$/worldcup.json',
|
16
|
-
euro: 'https://raw.githubusercontent.com/openfootball/euro.json/master/$year$/euro.json',
|
17
|
-
de: 'https://raw.githubusercontent.com/openfootball/deutschland/master/$season$/1-bundesliga.txt',
|
18
|
-
en: 'https://raw.githubusercontent.com/openfootball/england/master/$season$/1-premierleague.txt',
|
19
|
-
at: 'https://raw.githubusercontent.com/openfootball/austria/master/$season$/1-bundesliga-i.txt',
|
20
|
-
}
|
21
|
-
|
22
|
-
### note:
|
23
|
-
## cache ALL methods - only do one web request for match schedule & results
|
24
7
|
def matches
|
25
|
-
|
26
|
-
url = APIS[ @league.downcase.to_sym ]
|
27
|
-
url = url.gsub( '$year$', @year.to_s )
|
28
|
-
url = url.gsub( '$season$', @season.to_path )
|
29
|
-
|
30
|
-
res = get!( url ) ## use "memoized" / cached result
|
31
|
-
if url.end_with?( '.json' )
|
32
|
-
JSON.parse( res.text )
|
33
|
-
else ## assume football.txt format
|
34
|
-
matches = SportDb::QuickMatchReader.parse( res.text )
|
35
|
-
data = matches.map {|match| match.as_json } # convert to json
|
36
|
-
## quick hack to get keys as strings not symbols!!
|
37
|
-
## fix upstream
|
38
|
-
JSON.parse( JSON.generate( data ))
|
39
|
-
end
|
40
|
-
end
|
8
|
+
raise ArgumentError, "method matches must be implemented by concrete class!!"
|
41
9
|
end
|
42
10
|
|
11
|
+
def league_name
|
12
|
+
raise ArgumentError, "method league_name must be implemented by concrete class!!"
|
13
|
+
end
|
14
|
+
|
15
|
+
|
43
16
|
def end_date
|
44
17
|
@end_date ||= begin
|
45
18
|
end_date = nil
|
46
|
-
each do |match|
|
19
|
+
matches.each do |match|
|
47
20
|
date = Date.strptime(match['date'], '%Y-%m-%d' )
|
48
21
|
end_date = date if end_date.nil? ||
|
49
22
|
date > end_date
|
@@ -55,7 +28,7 @@ module Footty
|
|
55
28
|
def start_date
|
56
29
|
@start_date ||= begin
|
57
30
|
start_date = nil
|
58
|
-
each do |match|
|
31
|
+
matches.each do |match|
|
59
32
|
date = Date.strptime(match['date'], '%Y-%m-%d' )
|
60
33
|
start_date = date if start_date.nil? ||
|
61
34
|
date < start_date
|
@@ -89,54 +62,21 @@ module Footty
|
|
89
62
|
def past_matches( date: Date.today )
|
90
63
|
matches = select_matches { |match| date > Date.parse( match['date'] ) }
|
91
64
|
## note reveserve matches (chronological order/last first)
|
92
|
-
matches.reverse
|
65
|
+
## matches.reverse
|
66
|
+
matches
|
93
67
|
end
|
94
68
|
|
95
69
|
|
96
70
|
private
|
97
|
-
def
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
round['matches'].each do |match|
|
102
|
-
## hack: add (outer) round to match
|
103
|
-
match['round'] = round['name']
|
104
|
-
yield( match )
|
105
|
-
end
|
106
|
-
end
|
107
|
-
else ## assume flat matches in Array
|
108
|
-
data.each do |match|
|
109
|
-
yield( match )
|
110
|
-
end
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
|
115
|
-
def select_matches
|
116
|
-
matches = []
|
117
|
-
each do |match|
|
118
|
-
matches << match if yield( match )
|
71
|
+
def select_matches( &blk)
|
72
|
+
selected = []
|
73
|
+
matches.each do |match|
|
74
|
+
selected << match if blk.call( match )
|
119
75
|
end
|
120
76
|
|
121
77
|
## todo/fix:
|
122
78
|
## sort matches here; might not be chronologicial (by default)
|
123
|
-
|
79
|
+
selected
|
124
80
|
end # method select_matches
|
125
|
-
|
126
|
-
|
127
|
-
def get!( url )
|
128
|
-
response = Webclient.get( url )
|
129
|
-
|
130
|
-
if response.status.ok?
|
131
|
-
response
|
132
|
-
else
|
133
|
-
puts "!! HTTP ERROR - #{response.status.code} #{response.status.message}"
|
134
|
-
## dump headers
|
135
|
-
response.headers.each do |key,value|
|
136
|
-
puts " #{key}: #{value}"
|
137
|
-
end
|
138
|
-
exit 1
|
139
|
-
end
|
140
|
-
end # method get!
|
141
|
-
end # class Client
|
81
|
+
end # class Dataset
|
142
82
|
end # module Footty
|
@@ -0,0 +1,145 @@
|
|
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
|
+
|
17
|
+
'en'=> 'england/$season$/1-premierleague.txt',
|
18
|
+
'es'=> 'espana/$season$/1-liga.txt',
|
19
|
+
'it'=> 'italy/$season$/1-seriea.txt',
|
20
|
+
|
21
|
+
'at'=> 'austria/$season$/1-bundesliga.txt',
|
22
|
+
'at2' => 'austria/$season$/2-liga2.txt',
|
23
|
+
'at3o' => 'austria/$season$/3-regionalliga-ost.txt',
|
24
|
+
'atcup' => 'austria/$season$/cup.txt',
|
25
|
+
|
26
|
+
'fr'=> 'europe/france/$season$_fr1.txt',
|
27
|
+
'nl'=> 'europe/netherlands/$season$_nl1.txt',
|
28
|
+
'be'=> 'europe/belgium/$season$_be1.txt',
|
29
|
+
|
30
|
+
'champs'=> 'champions-league/$season$/cl.txt',
|
31
|
+
|
32
|
+
'br' => 'south-america/brazil/$year$_br1.txt',
|
33
|
+
'ar' => 'south-america/argentina/$year$_ar1.txt',
|
34
|
+
'co' => 'south-america/colombia/$year$_co1.txt',
|
35
|
+
## use a different code for copa libertadores? why? why not?
|
36
|
+
'copa' => 'south-america/copa-libertadores/$year$_copal.txt',
|
37
|
+
|
38
|
+
'mx' => 'world/north-america/mexico/$season$_mx1.txt',
|
39
|
+
|
40
|
+
'eg' => 'world/africa/egypt/$season$_eg1.txt',
|
41
|
+
'ma' => 'world/africa/morocco/$season$_ma1.txt',
|
42
|
+
}
|
43
|
+
|
44
|
+
|
45
|
+
## return built-in league keys
|
46
|
+
def self.leagues() SOURCES.keys; end
|
47
|
+
|
48
|
+
|
49
|
+
|
50
|
+
### auto-fill latest season
|
51
|
+
def self.latest_season( league: )
|
52
|
+
spec = SOURCES[ league.downcase ]
|
53
|
+
|
54
|
+
## todo/fix - report error if no spec found
|
55
|
+
season = if spec.is_a?( Hash ) ## assume lookup by year
|
56
|
+
spec.keys[0]
|
57
|
+
else ## assume vanilla urls (no lookup by year)
|
58
|
+
## default to 2025 or 2024/25 for now
|
59
|
+
spec.index( '$year$') ? '2025' : '2024/25'
|
60
|
+
end
|
61
|
+
season
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
|
66
|
+
def initialize( league:, season: )
|
67
|
+
spec = SOURCES[ league.downcase ]
|
68
|
+
|
69
|
+
urls = if spec.is_a?( Hash ) ## assume lookup by year
|
70
|
+
spec[ season ]
|
71
|
+
else ## assume vanilla urls (no lookup by year)
|
72
|
+
spec
|
73
|
+
end
|
74
|
+
raise ArgumentError, "no dataset (source) for league #{league} found" if urls.nil?
|
75
|
+
|
76
|
+
## wrap single sources (strings) in array
|
77
|
+
urls = urls.is_a?( Array ) ? urls : [urls]
|
78
|
+
## expand shortened url and fill-in template vars
|
79
|
+
urls = urls.map { |url| openfootball_url( url, season: season ) }
|
80
|
+
|
81
|
+
matches = []
|
82
|
+
urls.each do |url|
|
83
|
+
txt = get!( url ) ## use "memoized" / cached result
|
84
|
+
parser = SportDb::QuickMatchReader.new( txt )
|
85
|
+
matches += parser.parse
|
86
|
+
### for multiple source file use latest name as "definitive"
|
87
|
+
@league_name = parser.league_name
|
88
|
+
### todo/fix - report errors
|
89
|
+
end
|
90
|
+
|
91
|
+
matches = matches.map {|match| match.as_json } # convert to json
|
92
|
+
|
93
|
+
## note - sort by date/time
|
94
|
+
## (assume stable sort; no reshuffle of matches if already sorted by date/time)
|
95
|
+
|
96
|
+
matches = matches.sort do |l,r|
|
97
|
+
result = l['date'] <=> r['date']
|
98
|
+
result = l['time'] <=> r['time'] if result == 0 &&
|
99
|
+
(l['time'] && r['time'])
|
100
|
+
result
|
101
|
+
end
|
102
|
+
|
103
|
+
@matches = matches
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
def openfootball_url( path, season: )
|
108
|
+
repo, local_path = path.split( '/', 2)
|
109
|
+
url = "https://raw.githubusercontent.com/openfootball/#{repo}/master/#{local_path}"
|
110
|
+
## check for template vars too
|
111
|
+
season = Season( season )
|
112
|
+
url = url.gsub( '$year$', season.start_year.to_s )
|
113
|
+
url = url.gsub( '$season$', season.to_path )
|
114
|
+
url
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
def matches() @matches; end
|
119
|
+
def league_name() @league_name; end
|
120
|
+
|
121
|
+
|
122
|
+
|
123
|
+
def get!( url )
|
124
|
+
## use cached urls for 12h by default
|
125
|
+
## if expired in cache (or not present) than get/fetch
|
126
|
+
if Webcache.expired_in_12h?( url )
|
127
|
+
response = Webget.text( url )
|
128
|
+
|
129
|
+
if response.status.ok?
|
130
|
+
response.text # note - return text (utf-8)
|
131
|
+
else
|
132
|
+
## dump headers
|
133
|
+
response.headers.each do |key,value|
|
134
|
+
puts " #{key}: #{value}"
|
135
|
+
end
|
136
|
+
puts "!! HTTP ERROR - #{response.status.code} #{response.status.message}"
|
137
|
+
exit 1
|
138
|
+
end
|
139
|
+
else
|
140
|
+
Webcache.read( url )
|
141
|
+
end
|
142
|
+
end # method get_txt!
|
143
|
+
|
144
|
+
end # class OpenfootballDaset
|
145
|
+
end # module Footty
|
data/lib/footty/print.rb
ADDED
@@ -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
|
data/lib/footty/version.rb
CHANGED
data/lib/footty.rb
CHANGED
@@ -1,187 +1,189 @@
|
|
1
1
|
require 'sportdb/quick' ## note - pulls in cocos et al
|
2
|
+
require 'webget' ## add webcache support
|
2
3
|
|
4
|
+
require 'optparse'
|
3
5
|
|
4
|
-
# our own code
|
5
|
-
require_relative 'footty/version' # let it always go first
|
6
|
-
require_relative 'footty/dataset'
|
7
6
|
|
8
7
|
|
9
|
-
module Footty
|
10
8
|
|
11
|
-
|
12
|
-
|
9
|
+
# our own code
|
10
|
+
require_relative 'footty/version' # let version always go first
|
11
|
+
require_relative 'footty/dataset'
|
12
|
+
require_relative 'footty/openfootball'
|
13
|
+
## require_relative 'footty/datalib'
|
13
14
|
|
14
|
-
|
15
|
-
year = 2024
|
15
|
+
require_relative 'footty/print'
|
16
16
|
|
17
|
-
## filter ARGV for league or year
|
18
|
-
args = ARGV.select do |arg|
|
19
|
-
if arg =~ /^\d{4}$/
|
20
|
-
year = arg.to_i(10)
|
21
|
-
false ## eat-up
|
22
|
-
elsif ['world',
|
23
|
-
'euro',
|
24
|
-
'de',
|
25
|
-
'en',
|
26
|
-
'at'].include?( arg )
|
27
|
-
league = arg
|
28
|
-
false
|
29
|
-
else
|
30
|
-
true
|
31
|
-
end
|
32
|
-
end
|
33
17
|
|
34
18
|
|
35
|
-
|
36
|
-
|
19
|
+
## set cache to local .cache dir for now - why? why not?
|
20
|
+
Webcache.root = './cache'
|
21
|
+
# pp Webcache.root
|
22
|
+
Webget.config.sleep = 1 ## set delay in secs (to 1 sec - default is/maybe 3)
|
37
23
|
|
38
24
|
|
39
|
-
|
40
|
-
|
25
|
+
module Footty
|
26
|
+
def self.main( args=ARGV )
|
27
|
+
puts banner # say hello
|
41
28
|
|
42
29
|
|
43
|
-
|
44
|
-
|
30
|
+
opts = { debug: false,
|
31
|
+
verbose: false, ## add more details
|
32
|
+
## add cache/cache_dir - why? why not?
|
45
33
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
elsif ['tomorrow', 't', '+1', '1'].include?( what )
|
52
|
-
matches = dataset.tomorrows_matches
|
53
|
-
if matches.empty?
|
54
|
-
puts "** No matches scheduled tomorrow.\n"
|
55
|
-
end
|
56
|
-
elsif ['past', 'p', 'prev'].include?( what )
|
57
|
-
matches = dataset.past_matches
|
58
|
-
if matches.empty?
|
59
|
-
puts "** No matches played yet.\n"
|
60
|
-
end
|
61
|
-
elsif ['upcoming', 'up', 'u', 'next', 'n'].include?( what )
|
62
|
-
matches = dataset.upcoming_matches
|
63
|
-
if matches.empty?
|
64
|
-
puts "** No more matches scheduled.\n"
|
65
|
-
end
|
66
|
-
else
|
67
|
-
matches = dataset.todays_matches
|
68
|
-
|
69
|
-
## no matches today
|
70
|
-
if matches.empty?
|
71
|
-
puts "** No matches scheduled today.\n"
|
72
|
-
|
73
|
-
## note: was world cup 2018 - end date -- Date.new( 2018, 7, 11 )
|
74
|
-
## note: was euro 2020 (in 2021) - end date -- Date.new( 2021, 7, 11 )
|
75
|
-
if Date.today > dataset.end_date ## tournament is over, look back
|
76
|
-
puts "Past matches:"
|
77
|
-
matches = dataset.past_matches
|
78
|
-
else ## world cup is upcoming /in-progress,look forward
|
79
|
-
puts "Upcoming matches:"
|
80
|
-
matches = dataset.upcoming_matches( limit: 18 )
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
34
|
+
## display format/mode - week/window/upcoming/past (default is today)
|
35
|
+
yesterday: nil,
|
36
|
+
tomorrow: nil,
|
37
|
+
upcoming: nil,
|
38
|
+
past: nil,
|
84
39
|
|
85
|
-
|
86
|
-
|
40
|
+
# week: nil,
|
41
|
+
# window: nil, ## 2 day plus/minus +2/-2
|
42
|
+
}
|
87
43
|
|
44
|
+
|
45
|
+
parser = OptionParser.new do |parser|
|
46
|
+
parser.banner = "Usage: #{$PROGRAM_NAME} [options] LEAGUES"
|
88
47
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
48
|
+
parser.on( "--verbose",
|
49
|
+
"turn on verbose output (default: #{opts[:verbose]})" ) do |verbose|
|
50
|
+
opts[:verbose] = true
|
51
|
+
end
|
93
52
|
|
94
|
-
matches.each do |match|
|
95
|
-
print " %5s" % "\##{match['num']} " if match['num']
|
96
53
|
|
97
|
-
|
98
|
-
|
99
|
-
print "#{match['time']} " if match['time']
|
100
|
-
|
101
|
-
if date > today
|
102
|
-
diff = (date - today).to_i
|
103
|
-
print "%10s" % "(in #{diff}d) "
|
54
|
+
parser.on( "-y", "--yesterday" ) do |yesterday|
|
55
|
+
opts[:yesterday] = true
|
104
56
|
end
|
105
|
-
|
106
|
-
|
107
|
-
if match['team1'].is_a?( Hash )
|
108
|
-
print "%22s" % "#{match['team1']['name']} (#{match['team1']['code']})"
|
109
|
-
else
|
110
|
-
print "%22s" % "#{match['team1']}"
|
57
|
+
parser.on( "-t", "--tomorrow" ) do |tomorrow|
|
58
|
+
opts[:tomorrow] = true
|
111
59
|
end
|
112
|
-
|
113
|
-
|
114
|
-
if match['score'].is_a?( Hash ) &&
|
115
|
-
match['score']['ft']
|
116
|
-
if match['score']['ft']
|
117
|
-
print " #{match['score']['ft'][0]}-#{match['score']['ft'][1]} "
|
118
|
-
end
|
119
|
-
if match['score']['et']
|
120
|
-
print "aet #{match['score']['et'][0]}-#{match['score']['et'][1]} "
|
121
|
-
end
|
122
|
-
if match['score']['p']
|
123
|
-
print "pen #{match['score']['p'][0]}-#{match['score']['p'][1]} "
|
124
|
-
end
|
125
|
-
elsif match['score1'] && match['score2']
|
126
|
-
## todo/fix: add support for knockout scores
|
127
|
-
## with score1et/score1p (extra time and penalty)
|
128
|
-
print " #{match['score1']}-#{match['score2']} "
|
129
|
-
print "(#{match['score1i']}-#{match['score2i']}) "
|
130
|
-
else
|
131
|
-
print " vs "
|
60
|
+
parser.on( "-p", "--past" ) do |past|
|
61
|
+
opts[:past] = true
|
132
62
|
end
|
133
|
-
|
134
|
-
|
135
|
-
print "%-22s" % "#{match['team2']['name']} (#{match['team2']['code']})"
|
136
|
-
else
|
137
|
-
print "%-22s" % "#{match['team2']}"
|
63
|
+
parser.on( "-u", "--up", "--upcoming" ) do |upcoming|
|
64
|
+
opts[:upcoming] = true
|
138
65
|
end
|
66
|
+
end
|
67
|
+
parser.parse!( args )
|
68
|
+
|
69
|
+
|
70
|
+
puts "OPTS:"
|
71
|
+
p opts
|
72
|
+
puts "ARGV:"
|
73
|
+
p args
|
74
|
+
|
75
|
+
|
76
|
+
top = [['world', '2022'],
|
77
|
+
['euro', '2024'],
|
78
|
+
['mx', '2024/25'],
|
79
|
+
['copa', '2025'], ## copa libertadores
|
80
|
+
['en', '2024/25'],
|
81
|
+
['es', '2024/25'],
|
82
|
+
['it', '2024/25'],
|
83
|
+
['fr', '2024/25'],
|
84
|
+
['de', '2024/25'],
|
85
|
+
['at', '2024/25'],
|
86
|
+
['champs', '2024/25'],
|
87
|
+
]
|
88
|
+
|
89
|
+
|
90
|
+
leagues = if args.size == 0
|
91
|
+
top
|
92
|
+
else
|
93
|
+
### auto-fill (latest) season/year
|
94
|
+
args.map do |arg|
|
95
|
+
[arg, OpenfootballDataset.latest_season( league: arg )]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
## fetch leagues
|
101
|
+
datasets = leagues.map do |league, season|
|
102
|
+
dataset = OpenfootballDataset.new( league: league, season: season )
|
103
|
+
## parse matches
|
104
|
+
matches = dataset.matches
|
105
|
+
puts " #{league} #{season} - #{matches.size} match(es)"
|
106
|
+
dataset
|
107
|
+
end
|
108
|
+
|
109
|
+
|
139
110
|
|
140
111
|
|
141
|
-
|
142
|
-
|
143
|
-
end
|
112
|
+
# Dataset.new( league: 'euro', year: 2024 )
|
113
|
+
# dataset = Dataset.new( league: league, year: year )
|
144
114
|
|
145
|
-
|
115
|
+
## in the future make today "configurable" as param - why? why not?
|
116
|
+
today = Date.today
|
146
117
|
|
147
|
-
if match['stadium']
|
148
|
-
print " @ #{match['stadium']['name']}, #{match['city']}"
|
149
|
-
end
|
150
118
|
|
119
|
+
what = if opts[:yesterday]
|
120
|
+
'yesterday'
|
121
|
+
elsif opts[:tomorrow]
|
122
|
+
'tomorrow'
|
123
|
+
elsif opts[:past]
|
124
|
+
'past'
|
125
|
+
elsif opts[:upcoming]
|
126
|
+
'upcoming'
|
127
|
+
else
|
128
|
+
'today'
|
129
|
+
end
|
130
|
+
|
131
|
+
## start with two empty lines - assume (massive) debug output before ;-)
|
132
|
+
puts
|
133
|
+
puts
|
134
|
+
datasets.each do |dataset|
|
135
|
+
print "==> #{dataset.league_name}"
|
136
|
+
print " #{dataset.start_date} - #{dataset.end_date}"
|
137
|
+
print " -- #{dataset.matches.size} match(es)"
|
151
138
|
print "\n"
|
152
139
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
print " (o.g.)" if goal['owngoal']
|
163
|
-
print " (pen.)" if goal['penalty']
|
140
|
+
if what == 'yesterday'
|
141
|
+
matches = dataset.yesterdays_matches
|
142
|
+
if matches.empty?
|
143
|
+
puts (' '*4) + "** No matches played yesterday.\n"
|
144
|
+
end
|
145
|
+
elsif what == 'tomorrow'
|
146
|
+
matches = dataset.tomorrows_matches
|
147
|
+
if matches.empty?
|
148
|
+
puts (' '*4) + "** No matches scheduled tomorrow.\n"
|
164
149
|
end
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
print " "
|
170
|
-
end
|
171
|
-
print "#{goal['name']}"
|
172
|
-
print " #{goal['minute']}"
|
173
|
-
print "+#{goal['offset']}" if goal['offset']
|
174
|
-
print "'"
|
175
|
-
print " (o.g.)" if goal['owngoal']
|
176
|
-
print " (pen.)" if goal['penalty']
|
150
|
+
elsif what == 'past'
|
151
|
+
matches = dataset.past_matches
|
152
|
+
if matches.empty?
|
153
|
+
puts (' '*4) + "** No matches played yet.\n"
|
177
154
|
end
|
178
|
-
|
155
|
+
elsif what == 'upcoming'
|
156
|
+
matches = dataset.upcoming_matches
|
157
|
+
if matches.empty?
|
158
|
+
puts (' '*4) + "** No more matches scheduled.\n"
|
159
|
+
end
|
160
|
+
else ## assume today
|
161
|
+
matches = dataset.todays_matches
|
162
|
+
|
163
|
+
## no matches today
|
164
|
+
if matches.empty?
|
165
|
+
puts (' '*4) + "** No matches scheduled today.\n"
|
166
|
+
|
167
|
+
if opts[:verbose]
|
168
|
+
## note: was world cup 2018 - end date -- Date.new( 2018, 7, 11 )
|
169
|
+
## note: was euro 2020 (in 2021) - end date -- Date.new( 2021, 7, 11 )
|
170
|
+
if Date.today > dataset.end_date ## tournament is over, look back
|
171
|
+
puts "Past matches:"
|
172
|
+
matches = dataset.past_matches
|
173
|
+
else ## world cup is upcoming /in-progress,look forward
|
174
|
+
puts "Upcoming matches:"
|
175
|
+
matches = dataset.upcoming_matches( limit: 18 )
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
print_matches( matches )
|
179
181
|
end
|
180
|
-
end
|
181
|
-
end
|
182
182
|
|
183
|
+
end # method self.main
|
183
184
|
end # module Footty
|
184
185
|
|
185
186
|
|
186
187
|
|
188
|
+
|
187
189
|
Footty.main if __FILE__ == $0
|
metadata
CHANGED
@@ -1,17 +1,31 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: footty
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2025.4.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gerald Bauer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-04-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sportdb-quick
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: webget
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
16
30
|
requirements:
|
17
31
|
- - ">="
|
@@ -50,16 +64,16 @@ dependencies:
|
|
50
64
|
requirements:
|
51
65
|
- - "~>"
|
52
66
|
- !ruby/object:Gem::Version
|
53
|
-
version: '4.
|
67
|
+
version: '4.2'
|
54
68
|
type: :development
|
55
69
|
prerelease: false
|
56
70
|
version_requirements: !ruby/object:Gem::Requirement
|
57
71
|
requirements:
|
58
72
|
- - "~>"
|
59
73
|
- !ruby/object:Gem::Version
|
60
|
-
version: '4.
|
61
|
-
description: footty - football.db command line
|
62
|
-
|
74
|
+
version: '4.2'
|
75
|
+
description: footty - football.db command line tool for national & int'l football
|
76
|
+
club leagues (& cups) from around the world (bonus - incl. world cup, euro and more)
|
63
77
|
email: gerald.bauer@gmail.com
|
64
78
|
executables:
|
65
79
|
- footty
|
@@ -77,7 +91,10 @@ files:
|
|
77
91
|
- bin/footty
|
78
92
|
- bin/ftty
|
79
93
|
- lib/footty.rb
|
94
|
+
- lib/footty/datalib.rb
|
80
95
|
- lib/footty/dataset.rb
|
96
|
+
- lib/footty/openfootball.rb
|
97
|
+
- lib/footty/print.rb
|
81
98
|
- lib/footty/version.rb
|
82
99
|
homepage: https://github.com/sportdb/footty
|
83
100
|
licenses:
|
@@ -100,9 +117,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
100
117
|
- !ruby/object:Gem::Version
|
101
118
|
version: '0'
|
102
119
|
requirements: []
|
103
|
-
rubygems_version: 3.
|
120
|
+
rubygems_version: 3.5.22
|
104
121
|
signing_key:
|
105
122
|
specification_version: 4
|
106
|
-
summary: footty - football.db command line
|
107
|
-
|
123
|
+
summary: footty - football.db command line tool for national & int'l football club
|
124
|
+
leagues (& cups) from around the world (bonus - incl. world cup, euro and more)
|
108
125
|
test_files: []
|