├── .gitignore
├── favicon-16x16.png
├── README.md
├── get_fixture.py
├── .github
└── workflows
│ └── build.yaml
├── ladder.py
├── gen_html.py
└── thttp.py
/.gitignore:
--------------------------------------------------------------------------------
1 | matches.json
2 | __pycache__
3 | *.pyc
4 | _build
--------------------------------------------------------------------------------
/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sesh/footyfixture/main/favicon-16x16.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Fetch the AFL fixture from the official AFL website, then generate a version that only takes a couple of KBs of bandwidth, doesn't include any tracking or betting ads, and includes the whole fixture on one page.
2 |
3 | Scores for games in the past are included if they are available. This is _absolutely not_ intended to provide live scores.
4 |
--------------------------------------------------------------------------------
/get_fixture.py:
--------------------------------------------------------------------------------
1 | import time
2 | import json
3 | import sys
4 | from thttp import request
5 |
6 | headers = {
7 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:97.0) Gecko/20100101 Firefox/97.0",
8 | "Accept": "*/*",
9 | "Accept-Language": "en-US,en;q=0.5",
10 | "Accept-Encoding": "gzip, deflate, br",
11 | "Account": "afl",
12 | }
13 |
14 | matches = []
15 |
16 | for r in range(1, 24):
17 | print(f"Fetching Round {r}")
18 | response = request(
19 | f"https://aflapi.afl.com.au/afl/v2/matches?competitionId=1&compSeasonId=43&pageSize=50&roundNumber={r}"
20 | )
21 |
22 | try:
23 | matches.extend(response.json["matches"])
24 | except:
25 | print(response)
26 | sys.exit(1)
27 |
28 | print(f"{len(matches)} matches retrieved so far")
29 | time.sleep(5)
30 |
31 | with open("matches.json", "w") as f:
32 | f.write(json.dumps(matches))
33 |
--------------------------------------------------------------------------------
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------
1 | name: Build Footy Calendar
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | schedule:
7 | - cron: "0 */12 * * *"
8 | workflow_dispatch:
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 |
17 | - uses: actions/setup-python@v3
18 | with:
19 | python-version: '3.10'
20 |
21 | - name: Create the build directory
22 | run: mkdir _build
23 |
24 | - name: Get the latest fixture
25 | run: python3 get_fixture.py
26 |
27 | - name: Generate the calendar html file
28 | run: python3 gen_html.py
29 |
30 | - name: Copy the favicon
31 | run: cp favicon-16x16.png ./_build/
32 |
33 | - name: Deploy to Github Pages
34 | uses: peaceiris/actions-gh-pages@v3
35 | with:
36 | github_token: ${{ secrets.GITHUB_TOKEN }}
37 | publish_dir: ./_build
38 |
--------------------------------------------------------------------------------
/ladder.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 |
4 | def get_ladder():
5 | teams = {}
6 |
7 | matches = json.load(open("matches.json"))
8 |
9 | for m in matches:
10 | if m["status"] == "CONCLUDED":
11 | home_score = m["home"]["score"]["totalScore"]
12 | away_score = m["away"]["score"]["totalScore"]
13 |
14 | print(
15 | m["home"]["team"]["abbreviation"],
16 | m["away"]["team"]["abbreviation"],
17 | home_score,
18 | away_score,
19 | )
20 |
21 | for t in ["home", "away"]:
22 | if m[t]["team"]["abbreviation"] not in teams:
23 | teams[m[t]["team"]["abbreviation"]] = {
24 | "wins": 0,
25 | "losses": 0,
26 | "draws": 0,
27 | "for": 0,
28 | "against": 0,
29 | "results": [],
30 | }
31 |
32 | teams[m["home"]["team"]["abbreviation"]]["for"] += home_score
33 | teams[m["home"]["team"]["abbreviation"]]["against"] += away_score
34 | teams[m["away"]["team"]["abbreviation"]]["for"] += away_score
35 | teams[m["away"]["team"]["abbreviation"]]["against"] += home_score
36 |
37 | if home_score > away_score:
38 | teams[m["home"]["team"]["abbreviation"]]["wins"] += 1
39 | teams[m["home"]["team"]["abbreviation"]]["results"].append("W")
40 |
41 | teams[m["away"]["team"]["abbreviation"]]["losses"] += 1
42 | teams[m["away"]["team"]["abbreviation"]]["results"].append("L")
43 | elif away_score > home_score:
44 | teams[m["home"]["team"]["abbreviation"]]["losses"] += 1
45 | teams[m["home"]["team"]["abbreviation"]]["results"].append("L")
46 |
47 | teams[m["away"]["team"]["abbreviation"]]["wins"] += 1
48 | teams[m["away"]["team"]["abbreviation"]]["results"].append("W")
49 | else:
50 | teams[m["home"]["team"]["abbreviation"]]["draws"] += 1
51 | teams[m["home"]["team"]["abbreviation"]]["results"].append("D")
52 |
53 | teams[m["away"]["team"]["abbreviation"]]["draws"] += 1
54 | teams[m["away"]["team"]["abbreviation"]]["results"].append("D")
55 |
56 | teams = {
57 | k: teams[k]
58 | for k in sorted(
59 | teams, key=lambda x: teams[x]["for"] / teams[x]["against"], reverse=True
60 | )
61 | }
62 | teams = teams = {
63 | k: teams[k]
64 | for k in sorted(
65 | teams,
66 | key=lambda x: teams[x]["wins"] * 4 + teams[x]["draws"] * 2,
67 | reverse=True,
68 | )
69 | }
70 |
71 | html = ""
72 | html += "Ladder
"
73 | html += '- WLDLast 5
'
74 |
75 | for team in teams:
76 | html += "- "
77 | html += f'{team}'
78 | html += f'{teams[team]["wins"]}'
79 | html += f'{teams[team]["losses"]}'
80 | html += f'{teams[team]["draws"]}'
81 | html += f'{teams[team]["for"] / teams[team]["against"] * 100:.1f}%'
82 | html += f'{"".join(teams[team]["results"][-5:])}'
83 | html += "
"
84 |
85 | html += "
"
86 | return html
87 |
--------------------------------------------------------------------------------
/gen_html.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | import json
3 | from thttp import request
4 | from datetime import datetime
5 | from zoneinfo import ZoneInfo
6 |
7 | from ladder import get_ladder
8 |
9 |
10 | matches = json.loads(open("matches.json").read())
11 |
12 | html = ""
13 | roundNumber = 0
14 |
15 | for m in matches:
16 | if m["round"]["roundNumber"] != roundNumber:
17 | # is new round
18 | roundNumber = m["round"]["roundNumber"]
19 | if html:
20 | html += ""
21 |
22 | html += f"Round {roundNumber}
"
23 |
24 | home = m["home"]["team"]["abbreviation"]
25 | away = m["away"]["team"]["abbreviation"]
26 | venue = m["venue"]["abbreviation"]
27 | start_time_utc = datetime.fromisoformat(
28 | m["utcStartTime"].replace("+0000", "+00:00")
29 | )
30 | start_time_local = start_time_utc.astimezone(tz=ZoneInfo(m["venue"]["timezone"]))
31 |
32 | if m["status"] == "CONCLUDED":
33 | home_score = m["home"]["score"]["totalScore"]
34 | away_score = m["away"]["score"]["totalScore"]
35 | home_win = home_score > away_score
36 | if home_score == away_score:
37 | home_win = None # draw
38 | else:
39 | home_score, away_score, home_win = None, None, None
40 |
41 | html += "- "
42 | html += f''
43 | html += f'{home}'
44 |
45 | if home_score:
46 | html += f'{home_score} - {away_score}'
47 | else:
48 | html += 'vs'
49 |
50 | html += (
51 | f'{away}'
52 | )
53 | html += f'{venue}'
54 | html += "
"
55 |
56 | now = datetime.utcnow()
57 | now_melbourne = now.astimezone(tz=ZoneInfo("Australia/Melbourne"))
58 |
59 | html += "
"
60 |
61 | template = (
62 | request("https://basehtml.xyz")
63 | .content.decode()
64 | .split("")[0]
65 | )
66 | template = template.replace(
67 | "
Minimal base.html", "Footy Fixture - 2022 Edition"
68 | )
69 |
70 | template = template.replace("width=device-width", "width=440")
71 | template = template.replace("initial-scale=1.0", "maximum-scale=1.2")
72 |
73 | template = template.replace(
74 | "",
75 | """
76 |
77 |
134 | """,
135 | )
136 |
137 | template += get_ladder()
138 | template += html
139 | template += '