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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc5115b39ae5263043d1efc3a66205b9e005c978c442d7aa9c246065b80fdbfb
4
- data.tar.gz: 02461ba1b0750f0614df5ad2baf18eba734deb810da9a6f99619c0dfa61fefe0
3
+ metadata.gz: 2db5d9b53f38dd3532d6db31d0442b64193ee9df642f0e27e5cafca07e7ca5e7
4
+ data.tar.gz: ec6df784201636a0f4e5cb2585e022fb10ccad33e5de8dc8ba3311f6d0ef3f7d
5
5
  SHA512:
6
- metadata.gz: 0cfe6fc4a515cb3ef0431a3c7d62ce94227c2c4eb78d925489e2051e1f8fe3b0af84ad399a845272860e5449a868e991e54058dc6a6393934fbf9c2d8e3ca000
7
- data.tar.gz: 82c1c31b13f588eff307b5283f1b6fb9c571bf2ae5e3844f6feb0a296fd51bcbff9940aff9f185d6a4c939be76f3fe62c9a65ed05eed8004c1fcde7c7cc55901
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.1.0] - 2026-01-15
8
+ ## [0.2.0] - 2026-05-16
9
9
 
10
10
  ### Changed
11
- - Version bump for first stable release
12
- - Repository cleanup (removed build artifacts and test files)
13
- - Added MIT LICENSE file
14
- - Added CI status badge to README
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.1] - 2026-01-15
18
+ ## [0.1.0] - 2026-01-15
17
19
 
18
20
  ### Added
19
- - Initial release of modernized fotmob gem
20
- - Modern project structure with separate modules
21
- - Custom error classes (Error, APIError, NotFoundError, RateLimitError, TimeoutError, InvalidResponseError)
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.1]: https://github.com/bjrsti/fotmob/releases/tag/v0.0.1
25
+ [0.1.0]: https://github.com/bjrsti/fotmob/releases/tag/v0.1.0
data/README.md CHANGED
@@ -5,17 +5,16 @@
5
5
  [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%202.7-ruby.svg)](https://www.ruby-lang.org/)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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, player information, and league standings.
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
- - 👤 **Player Stats** - Player profiles and performance data
15
- - 📊 **League Standings** - League tables and competition data
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
- - ⏱️ **Timeout Support** - Configurable request timeouts
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
- # Get team information
43
- team = client.get_team("8540") # Palermo
44
- puts team[:details][:name]
40
+ # All matches for today
41
+ matches = client.get_matches("20260516")
42
+ matches[:leagues].each { |l| puts l[:name] }
45
43
 
46
- # Get match details
47
- match = client.get_match_details("4193741")
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
- # Get player data
51
- player = client.get_player("961995") # Mbappé
52
- puts player[:name]
49
+ # Team info
50
+ team = client.get_team("8455") # Chelsea
51
+ puts team[:details][:name]
53
52
 
54
- # Get league standings
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
- ### `get_team(team_id)`
60
+ ### `get_matches(date)`
62
61
 
63
- Get comprehensive team information.
62
+ All matches for a given date (150+ leagues).
64
63
 
65
64
  ```ruby
66
- team = client.get_team("8540")
67
- # Returns: Hash with team details, fixtures, squad, etc.
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
- Get detailed match information.
72
+ Full match data — lineups, stats, events, shotmap.
73
73
 
74
74
  ```ruby
75
- match = client.get_match_details("4193741")
76
- # Returns: Hash with match details, lineups, events, stats
75
+ match = client.get_match_details("5315746")
76
+ # Returns: { general:, header:, content: { stats:, lineup:, shotmap:, ... } }
77
77
  ```
78
78
 
79
- ### `get_player(player_id)`
79
+ ### `get_team(team_id)`
80
80
 
81
- Get player profile and statistics.
81
+ Team overview, fixtures, and squad.
82
82
 
83
83
  ```ruby
84
- player = client.get_player("961995")
85
- # Returns: Hash with player info and stats
84
+ team = client.get_team("8455")
85
+ # Returns: { details:, overview:, fixtures:, ... }
86
86
  ```
87
87
 
88
88
  ### `get_league(league_id)`
89
89
 
90
- Get league/competition information.
90
+ League table, fixtures, and stats.
91
91
 
92
92
  ```ruby
93
93
  league = client.get_league("47")
94
- # Returns: Hash with league details and standings
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
- # Default timeout is 10 seconds
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
- To use the API, you need IDs for teams, players, matches, and leagues:
171
+ IDs are in the fotmob.com URL for each resource:
183
172
 
184
- - **Teams**: Visit [fotmob.com](https://www.fotmob.com/), search for a team, the ID is in the URL
185
- - Example: `fotmob.com/teams/8540/overview/palermo` → Team ID: `8540`
186
- - **Players**: Search for a player, ID is in the URL
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 = "http://www.fotmob.com/api"
12
- DEFAULT_TIMEOUT = 10 # seconds
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
- # Initialize a new FotMob API client
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
- # Get detailed information about a specific match
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
- get("/matchDetails", matchId: match_id)
48
- end
49
-
50
- # Get player data and statistics
51
- #
52
- # @param player_id [String] The player ID
53
- # @return [Hash] Player data with symbolized keys
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
- # Build URI with query parameters
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
- # Use string concatenation like old version - URI.join doesn't work with /api/path
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fotmob
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
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.1.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.3
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: []