fotmob 0.1.0 → 0.2.0
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 +12 -42
- data/README.md +35 -51
- data/lib/fotmob/client.rb +49 -80
- data/lib/fotmob/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2db5d9b53f38dd3532d6db31d0442b64193ee9df642f0e27e5cafca07e7ca5e7
|
|
4
|
+
data.tar.gz: ec6df784201636a0f4e5cb2585e022fb10ccad33e5de8dc8ba3311f6d0ef3f7d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ea85bde7a3e7f9ca2e3a124fe0613a1e2df76e29311557ba69065bb7968833d343c5e43afe887c260552095d602677335aac8a1f3a0a45b865d3f4ee9514afbc
|
|
7
|
+
data.tar.gz: 3dab6a921e4a4b40b28f3d434c2e099760b69c4d0d2d8d291e41131c2b9013da8656a7d992de9a24b8acbb63d1da6f333cba52e8a10560aa4e2e1af7144e147e
|
data/CHANGELOG.md
CHANGED
|
@@ -5,51 +5,21 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [0.
|
|
8
|
+
## [0.2.0] - 2026-05-16
|
|
9
9
|
|
|
10
10
|
### Changed
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
11
|
+
- Updated all API endpoints to match FotMob's current backend (`/api/data/`)
|
|
12
|
+
- `get_matches` fully working — returns all matches across 150+ leagues for a date
|
|
13
|
+
- `get_match_details` reimplemented via Next.js SSR data (full lineups, stats, events)
|
|
14
|
+
- `get_match_details` now auto-refreshes build ID on stale deploys (transparent retry)
|
|
15
|
+
- Added `timezone` option to client (default: `Europe/Paris`)
|
|
16
|
+
- Removed `get_player` — endpoint now requires Cloudflare Turnstile (browser-only)
|
|
15
17
|
|
|
16
|
-
## [0.0
|
|
18
|
+
## [0.1.0] - 2026-01-15
|
|
17
19
|
|
|
18
20
|
### Added
|
|
19
|
-
- Initial release
|
|
20
|
-
-
|
|
21
|
-
- Custom error classes
|
|
22
|
-
- URI.open-based HTTP client with timeout support
|
|
23
|
-
- Comprehensive error handling with proper HTTP status codes
|
|
24
|
-
- YARD documentation for all public methods
|
|
25
|
-
- RSpec test suite with VCR for API response recording
|
|
26
|
-
- RuboCop linting with sensible defaults
|
|
27
|
-
- GitHub Actions CI for automated testing
|
|
28
|
-
- Gemfile and Rakefile for development workflow
|
|
29
|
-
- Comprehensive README with examples and badges
|
|
30
|
-
|
|
31
|
-
### Changed
|
|
32
|
-
- Refactored from single-file to modular structure
|
|
33
|
-
- Improved from basic API wrapper to production-ready gem
|
|
34
|
-
- Switched to symbolized JSON keys for better Ruby idioms
|
|
35
|
-
- Added timeout configuration support
|
|
36
|
-
|
|
37
|
-
### API Methods
|
|
38
|
-
- `get_team(team_id)` - Get team information and statistics
|
|
39
|
-
- `get_match_details(match_id)` - Get detailed match information
|
|
40
|
-
- `get_player(player_id)` - Get player profile and stats
|
|
41
|
-
- `get_league(league_id)` - Get league standings and details
|
|
42
|
-
- `get_matches(date)` - Get matches by date (limited availability)
|
|
43
|
-
|
|
44
|
-
### Technical Details
|
|
45
|
-
- Requires Ruby >= 2.7.0
|
|
46
|
-
- Uses standard library only (no runtime dependencies)
|
|
47
|
-
- Tested on Ruby 2.7, 3.0, 3.1, 3.2, 3.3
|
|
48
|
-
- Bot protection handling via URI.open
|
|
49
|
-
- Configurable timeout (default: 10 seconds)
|
|
50
|
-
|
|
51
|
-
### Known Limitations
|
|
52
|
-
- `get_matches` endpoint requires special authentication headers and may not work reliably
|
|
53
|
-
- API is unofficial and may change without notice
|
|
21
|
+
- Initial release
|
|
22
|
+
- `get_team`, `get_match_details`, `get_player`, `get_league`, `get_matches`
|
|
23
|
+
- Custom error classes, configurable timeout, VCR-based test suite
|
|
54
24
|
|
|
55
|
-
[0.0
|
|
25
|
+
[0.1.0]: https://github.com/bjrsti/fotmob/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -5,17 +5,16 @@
|
|
|
5
5
|
[](https://www.ruby-lang.org/)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
An unofficial Ruby wrapper for the [FotMob](https://www.fotmob.com/) API. Get football/soccer data including team stats, match details,
|
|
8
|
+
An unofficial Ruby wrapper for the [FotMob](https://www.fotmob.com/) API. Get football/soccer data including team stats, match details, and league standings.
|
|
9
9
|
|
|
10
10
|
## Features
|
|
11
11
|
|
|
12
12
|
- 🏆 **Team Data** - Get comprehensive team information and statistics
|
|
13
|
-
- ⚽ **Match Details** - Detailed match information and live scores
|
|
14
|
-
-
|
|
15
|
-
- 📊 **League
|
|
13
|
+
- ⚽ **Match Details** - Detailed match information including lineups, stats, and live scores
|
|
14
|
+
- 📅 **Matches by Date** - All matches for a given day across 150+ leagues
|
|
15
|
+
- 📊 **League Data** - League tables, fixtures, and competition details
|
|
16
16
|
- 🛡️ **Error Handling** - Custom error classes for different scenarios
|
|
17
|
-
- ⏱️ **
|
|
18
|
-
- 📝 **Well Documented** - YARD documentation included
|
|
17
|
+
- ⏱️ **Configurable** - Timeout and timezone support
|
|
19
18
|
|
|
20
19
|
## Installation
|
|
21
20
|
|
|
@@ -36,80 +35,70 @@ gem install fotmob
|
|
|
36
35
|
```ruby
|
|
37
36
|
require 'fotmob'
|
|
38
37
|
|
|
39
|
-
# Create a client
|
|
40
38
|
client = Fotmob.new
|
|
41
39
|
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
# All matches for today
|
|
41
|
+
matches = client.get_matches("20260516")
|
|
42
|
+
matches[:leagues].each { |l| puts l[:name] }
|
|
45
43
|
|
|
46
|
-
#
|
|
47
|
-
match = client.get_match_details("
|
|
44
|
+
# Match details (lineups, stats, events)
|
|
45
|
+
match = client.get_match_details("5315746")
|
|
48
46
|
puts "#{match[:general][:homeTeam][:name]} vs #{match[:general][:awayTeam][:name]}"
|
|
47
|
+
puts match[:header][:status][:scoreStr]
|
|
49
48
|
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
puts
|
|
49
|
+
# Team info
|
|
50
|
+
team = client.get_team("8455") # Chelsea
|
|
51
|
+
puts team[:details][:name]
|
|
53
52
|
|
|
54
|
-
#
|
|
53
|
+
# League standings
|
|
55
54
|
league = client.get_league("47") # Premier League
|
|
56
55
|
puts league[:details][:name]
|
|
57
56
|
```
|
|
58
57
|
|
|
59
58
|
## API Methods
|
|
60
59
|
|
|
61
|
-
### `
|
|
60
|
+
### `get_matches(date)`
|
|
62
61
|
|
|
63
|
-
|
|
62
|
+
All matches for a given date (150+ leagues).
|
|
64
63
|
|
|
65
64
|
```ruby
|
|
66
|
-
|
|
67
|
-
# Returns:
|
|
65
|
+
matches = client.get_matches("20260516")
|
|
66
|
+
# Returns: { leagues: [...], date: "..." }
|
|
67
|
+
# Each league has a :matches array with scores, teams, status
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
### `get_match_details(match_id)`
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
Full match data — lineups, stats, events, shotmap.
|
|
73
73
|
|
|
74
74
|
```ruby
|
|
75
|
-
match = client.get_match_details("
|
|
76
|
-
# Returns:
|
|
75
|
+
match = client.get_match_details("5315746")
|
|
76
|
+
# Returns: { general:, header:, content: { stats:, lineup:, shotmap:, ... } }
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
### `
|
|
79
|
+
### `get_team(team_id)`
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
Team overview, fixtures, and squad.
|
|
82
82
|
|
|
83
83
|
```ruby
|
|
84
|
-
|
|
85
|
-
# Returns:
|
|
84
|
+
team = client.get_team("8455")
|
|
85
|
+
# Returns: { details:, overview:, fixtures:, ... }
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
### `get_league(league_id)`
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
League table, fixtures, and stats.
|
|
91
91
|
|
|
92
92
|
```ruby
|
|
93
93
|
league = client.get_league("47")
|
|
94
|
-
# Returns:
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### `get_matches(date)` ⚠️
|
|
98
|
-
|
|
99
|
-
**Note:** This endpoint currently requires special authentication headers and may not work reliably.
|
|
100
|
-
|
|
101
|
-
```ruby
|
|
102
|
-
matches = client.get_matches("20250114")
|
|
103
|
-
# Returns: Hash with matches for the specified date
|
|
94
|
+
# Returns: { details:, table:, fixtures:, stats:, ... }
|
|
104
95
|
```
|
|
105
96
|
|
|
106
97
|
## Configuration
|
|
107
98
|
|
|
108
|
-
### Custom Timeout
|
|
109
|
-
|
|
110
99
|
```ruby
|
|
111
|
-
#
|
|
112
|
-
client = Fotmob.new(timeout: 30)
|
|
100
|
+
# Defaults: timeout 10s, timezone Europe/Paris
|
|
101
|
+
client = Fotmob.new(timeout: 30, timezone: "Europe/Paris")
|
|
113
102
|
```
|
|
114
103
|
|
|
115
104
|
## Error Handling
|
|
@@ -179,16 +168,11 @@ bundle exec rspec
|
|
|
179
168
|
|
|
180
169
|
## Finding IDs
|
|
181
170
|
|
|
182
|
-
|
|
171
|
+
IDs are in the fotmob.com URL for each resource:
|
|
183
172
|
|
|
184
|
-
- **Teams**:
|
|
185
|
-
|
|
186
|
-
- **
|
|
187
|
-
- Example: `fotmob.com/players/961995/kylian-mbappe` → Player ID: `961995`
|
|
188
|
-
- **Matches**: Browse matches, ID is in the match URL
|
|
189
|
-
- Example: `fotmob.com/match/4193741` → Match ID: `4193741`
|
|
190
|
-
- **Leagues**: Browse leagues, ID is in the league URL
|
|
191
|
-
- Example: `fotmob.com/leagues/47/overview/premier-league` → League ID: `47`
|
|
173
|
+
- **Teams**: `fotmob.com/teams/8455/overview/chelsea` → `8455`
|
|
174
|
+
- **Matches**: `fotmob.com/matches/chelsea-vs-manchester-city/abc123#5315746` → `5315746`
|
|
175
|
+
- **Leagues**: `fotmob.com/leagues/47/overview/premier-league` → `47`
|
|
192
176
|
|
|
193
177
|
## Disclaimer
|
|
194
178
|
|
data/lib/fotmob/client.rb
CHANGED
|
@@ -6,73 +6,46 @@ require "uri"
|
|
|
6
6
|
require "timeout"
|
|
7
7
|
|
|
8
8
|
module Fotmob
|
|
9
|
-
# Main client for interacting with the FotMob API
|
|
10
9
|
class Client
|
|
11
|
-
BASE_URL = "
|
|
12
|
-
DEFAULT_TIMEOUT = 10
|
|
10
|
+
BASE_URL = "https://www.fotmob.com/api/data"
|
|
11
|
+
DEFAULT_TIMEOUT = 10
|
|
12
|
+
DEFAULT_TIMEZONE = "Europe/Paris"
|
|
13
|
+
UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " \
|
|
14
|
+
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
13
15
|
|
|
14
|
-
attr_reader :timeout
|
|
16
|
+
attr_reader :timeout, :timezone
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
#
|
|
18
|
-
# @param timeout [Integer] Request timeout in seconds (default: 10)
|
|
19
|
-
def initialize(timeout: DEFAULT_TIMEOUT)
|
|
18
|
+
def initialize(timeout: DEFAULT_TIMEOUT, timezone: DEFAULT_TIMEZONE)
|
|
20
19
|
@timeout = timeout
|
|
20
|
+
@timezone = timezone
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
# Get league/competition data
|
|
24
|
-
#
|
|
25
|
-
# @param league_id [String] The league ID
|
|
26
|
-
# @return [Hash] League data with symbolized keys
|
|
27
|
-
# @raise [Fotmob::Error] If the request fails
|
|
28
23
|
def get_league(league_id)
|
|
29
|
-
get("/leagues", id: league_id)
|
|
24
|
+
get("/leagues", id: league_id, tab: "overview")
|
|
30
25
|
end
|
|
31
26
|
|
|
32
|
-
# Get matches for a specific date
|
|
33
|
-
#
|
|
34
|
-
# @param date [String] Date in YYYYMMDD format (e.g., "20221030")
|
|
35
|
-
# @return [Hash] Match data with symbolized keys
|
|
36
|
-
# @raise [Fotmob::Error] If the request fails
|
|
37
27
|
def get_matches(date)
|
|
38
|
-
get("/matches", date: date)
|
|
28
|
+
get("/matches", date: date, timezone: @timezone)
|
|
39
29
|
end
|
|
40
30
|
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
# @param match_id [String] The match ID
|
|
44
|
-
# @return [Hash] Match details with symbolized keys
|
|
45
|
-
# @raise [Fotmob::Error] If the request fails
|
|
31
|
+
# Returns pageProps with keys: general, header, content (stats, lineup, etc.)
|
|
32
|
+
# Resolves the match slug via a redirect, with automatic buildId refresh on stale deploys.
|
|
46
33
|
def get_match_details(match_id)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# @raise [Fotmob::Error] If the request fails
|
|
55
|
-
def get_player(player_id)
|
|
56
|
-
get("/playerData", id: player_id)
|
|
34
|
+
@retried_build_id = false
|
|
35
|
+
build_id = fetch_build_id
|
|
36
|
+
slug = fetch_match_slug(match_id, build_id)
|
|
37
|
+
body = fetch_with_timeout(URI("https://www.fotmob.com/_next/data/#{build_id}/matches/#{slug}.json"))
|
|
38
|
+
parse_json(body)[:pageProps]
|
|
39
|
+
rescue OpenURI::HTTPError => e
|
|
40
|
+
handle_http_error(e)
|
|
57
41
|
end
|
|
58
42
|
|
|
59
|
-
# Get team data and statistics
|
|
60
|
-
#
|
|
61
|
-
# @param team_id [String] The team ID
|
|
62
|
-
# @return [Hash] Team data with symbolized keys
|
|
63
|
-
# @raise [Fotmob::Error] If the request fails
|
|
64
43
|
def get_team(team_id)
|
|
65
44
|
get("/teams", id: team_id)
|
|
66
45
|
end
|
|
67
46
|
|
|
68
47
|
private
|
|
69
48
|
|
|
70
|
-
# Make a GET request to the FotMob API
|
|
71
|
-
#
|
|
72
|
-
# @param path [String] API endpoint path
|
|
73
|
-
# @param params [Hash] Query parameters
|
|
74
|
-
# @return [Hash] Parsed JSON response with symbolized keys
|
|
75
|
-
# @raise [Fotmob::Error] If the request fails
|
|
76
49
|
def get(path, **params)
|
|
77
50
|
uri = build_uri(path, params)
|
|
78
51
|
response_body = fetch_with_timeout(uri)
|
|
@@ -81,30 +54,47 @@ module Fotmob
|
|
|
81
54
|
handle_http_error(e)
|
|
82
55
|
end
|
|
83
56
|
|
|
84
|
-
|
|
57
|
+
def fetch_build_id(force: false)
|
|
58
|
+
@fetch_build_id = nil if force
|
|
59
|
+
@fetch_build_id ||= begin
|
|
60
|
+
html = fetch_with_timeout(URI("https://www.fotmob.com/"))
|
|
61
|
+
html.match(/"buildId":"([^"]+)"/)[1]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def fetch_match_slug(match_id, build_id)
|
|
66
|
+
redirect_body = fetch_with_timeout(
|
|
67
|
+
URI("https://www.fotmob.com/_next/data/#{build_id}/match/#{match_id}.json")
|
|
68
|
+
)
|
|
69
|
+
redirect = parse_json(redirect_body).dig(:pageProps, :__N_REDIRECT)
|
|
70
|
+
raise NotFoundError.new("Match #{match_id} not found", status_code: 404) unless redirect
|
|
71
|
+
|
|
72
|
+
redirect.sub(%r{^/matches/}, "").sub(/#.*$/, "")
|
|
73
|
+
rescue OpenURI::HTTPError => e
|
|
74
|
+
raise unless e.io.status[0].to_i == 404 && !@retried_build_id
|
|
75
|
+
|
|
76
|
+
@retried_build_id = true
|
|
77
|
+
fetch_match_slug(match_id, fetch_build_id(force: true))
|
|
78
|
+
end
|
|
79
|
+
|
|
85
80
|
def build_uri(path, params)
|
|
86
|
-
|
|
87
|
-
url = BASE_URL + path
|
|
88
|
-
uri = URI(url)
|
|
81
|
+
uri = URI(BASE_URL + path)
|
|
89
82
|
uri.query = URI.encode_www_form(params) unless params.empty?
|
|
90
83
|
uri
|
|
91
84
|
end
|
|
92
85
|
|
|
93
|
-
# Fetch URL with timeout using URI.open (avoids bot detection)
|
|
94
86
|
def fetch_with_timeout(uri)
|
|
95
87
|
Timeout.timeout(@timeout) do
|
|
96
|
-
URI.open(uri.to_s).read
|
|
88
|
+
URI.open(uri.to_s, "User-Agent" => UA).read
|
|
97
89
|
end
|
|
98
90
|
rescue Timeout::Error => e
|
|
99
91
|
raise TimeoutError, "Request timed out after #{@timeout} seconds: #{e.message}"
|
|
100
92
|
rescue OpenURI::HTTPError
|
|
101
|
-
# Let HTTP errors bubble up to be handled by the caller
|
|
102
93
|
raise
|
|
103
94
|
rescue StandardError => e
|
|
104
95
|
raise Error, "Network error: #{e.message}"
|
|
105
96
|
end
|
|
106
97
|
|
|
107
|
-
# Handle HTTP errors from URI.open
|
|
108
98
|
def handle_http_error(error)
|
|
109
99
|
status = error.io.status[0].to_i
|
|
110
100
|
body = begin
|
|
@@ -115,39 +105,18 @@ module Fotmob
|
|
|
115
105
|
|
|
116
106
|
case status
|
|
117
107
|
when 404
|
|
118
|
-
raise NotFoundError.new(
|
|
119
|
-
"Resource not found",
|
|
120
|
-
status_code: 404,
|
|
121
|
-
response_body: body
|
|
122
|
-
)
|
|
108
|
+
raise NotFoundError.new("Resource not found", status_code: 404, response_body: body)
|
|
123
109
|
when 429
|
|
124
|
-
raise RateLimitError.new(
|
|
125
|
-
"Rate limit exceeded. Please try again later.",
|
|
126
|
-
status_code: 429,
|
|
127
|
-
response_body: body
|
|
128
|
-
)
|
|
110
|
+
raise RateLimitError.new("Rate limit exceeded. Please try again later.", status_code: 429, response_body: body)
|
|
129
111
|
when 400..499
|
|
130
|
-
raise APIError.new(
|
|
131
|
-
"Client error: #{status}",
|
|
132
|
-
status_code: status,
|
|
133
|
-
response_body: body
|
|
134
|
-
)
|
|
112
|
+
raise APIError.new("Client error: #{status}", status_code: status, response_body: body)
|
|
135
113
|
when 500..599
|
|
136
|
-
raise APIError.new(
|
|
137
|
-
"Server error: #{status}",
|
|
138
|
-
status_code: status,
|
|
139
|
-
response_body: body
|
|
140
|
-
)
|
|
114
|
+
raise APIError.new("Server error: #{status}", status_code: status, response_body: body)
|
|
141
115
|
else
|
|
142
|
-
raise APIError.new(
|
|
143
|
-
"HTTP Error: #{status}",
|
|
144
|
-
status_code: status,
|
|
145
|
-
response_body: body
|
|
146
|
-
)
|
|
116
|
+
raise APIError.new("HTTP Error: #{status}", status_code: status, response_body: body)
|
|
147
117
|
end
|
|
148
118
|
end
|
|
149
119
|
|
|
150
|
-
# Parse JSON response with symbolized keys
|
|
151
120
|
def parse_json(body)
|
|
152
121
|
JSON.parse(body, symbolize_names: true)
|
|
153
122
|
rescue JSON::ParserError => e
|
data/lib/fotmob/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fotmob
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stian Bjørkelo
|
|
@@ -117,7 +117,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
117
117
|
- !ruby/object:Gem::Version
|
|
118
118
|
version: '0'
|
|
119
119
|
requirements: []
|
|
120
|
-
rubygems_version: 4.0.
|
|
120
|
+
rubygems_version: 4.0.6
|
|
121
121
|
specification_version: 4
|
|
122
122
|
summary: An unofficial FotMob API wrapper for Ruby
|
|
123
123
|
test_files: []
|