65 Commits

Author SHA1 Message Date
56e68ed751 fix: show only relevant matches for a given year 2025-06-13 00:31:19 +02:00
00bbe05b1c feat: include club's full name in matches view 2025-06-12 22:35:04 +02:00
e373c6b274 hotfix: fixes /mecze endpoint not working (cause of a wrong API call) 2025-06-12 20:43:22 +02:00
e80ee6bf8f feat: return matches with respect to received sportsman id
also include a comment in the database model to include a relation
2025-06-12 20:36:10 +02:00
a25be482b0 matches from DB 2025-06-12 16:23:14 +02:00
4e8cb48c82 fix: safe_traverse renaming issue 2025-06-11 00:38:55 +02:00
bc82fcd6b8 chore: depend on safe_traverse from lewy_globals, add scraper cron job 2025-06-11 00:35:48 +02:00
5d238cb6b8 fix: general fixes to scraper, and db model
Co-authored-by: Noicei <kuba.baj@interia.pl>
2025-06-11 00:15:30 +02:00
fe4c9aab3d Merge branch 'ScraperV2' 2025-06-06 00:50:21 +02:00
9e8a60dfa1 chore: simplify expression, add an important todo note 2025-06-06 00:50:05 +02:00
7fe4e03945 fix: matches deafult data set to 2025 2025-06-05 22:12:48 +02:00
ffcfa12d4c static data for page 2025-06-05 22:07:29 +02:00
Pc
f7e80811a0 Updated Scraper 2025-06-05 21:41:31 +02:00
c86a93c153 fix: i forgor to add some waiting time to avoid rate limiting 2025-06-05 19:22:05 +02:00
43eb1d421d Merge branch 'ScraperTest' 2025-06-05 19:18:49 +02:00
c2568c86ef fix: scraper fixes, list matches 2025-06-05 19:18:31 +02:00
cf46fdfbb7 data for stats 2025-06-05 18:47:30 +02:00
791d20139d chore: offload some shared functionality to other methods
should also fix unexpected db behavior (hangs and disconnects)
2025-06-05 16:31:45 +02:00
c3a6626d6f fix: display commit info properly 2025-06-05 11:40:56 +02:00
5960e44b17 Merge pull request 'merge new front-end' (#3) from frontend into master
Reviewed-on: #3
2025-06-05 10:30:28 +02:00
919d64ca5e Merge branch 'master' into frontend 2025-06-05 10:30:04 +02:00
2cfa5f1fa4 poland-style changes 2025-06-05 10:27:11 +02:00
9b45a3f26f polandmode statbox color 2025-06-05 00:47:59 +02:00
3dfc40cdb0 trophies update and hamburger fix 2025-06-05 00:40:47 +02:00
Pc
13f6e2e3b9 Added rest of player updating code 2025-06-04 20:34:26 +02:00
Pc
9a007f504c Wins update 2025-06-04 19:56:32 +02:00
Pc
b5fdbb3230 Updating matches
Updating players stats(not finished)
Not tested
2025-06-04 19:50:18 +02:00
be951d296f representation changes 2025-06-04 17:02:57 +02:00
6e1e8ccc7d stats maches club style changes 2025-06-04 16:55:31 +02:00
03463905ef fixing responsive 2025-06-04 15:32:03 +02:00
35db71b8cc feat: get sportsmen full name and birthday from id 2025-06-04 00:19:57 +02:00
f65a174089 skeleton to all sites (i hope) 2025-06-03 23:26:30 +02:00
bdfa31c8ea fix: check for id in simple_insert_one() to avoid breaking autoincrement 2025-06-03 21:46:59 +02:00
206f7d6fb3 Merge branch 'frontend' of https://gitea.7o7.cx/roberteam/lewangoalski into frontend 2025-06-03 09:10:08 +02:00
df0e47c610 base page style 2025-06-03 09:09:59 +02:00
3b9aa8150b feat: new last_goal_for endpoint, simple_select_all improvements
also introduces sample usage for the new endpoint in lewy_routes
2025-06-03 02:38:13 +02:00
bc557b35af header style and font changes 2025-06-03 02:01:19 +02:00
4987dc4cf7 . 2025-06-02 00:34:03 +02:00
48825185b8 begining of history section 2025-06-02 00:32:27 +02:00
42c60f9db5 style poland-mode 2025-06-01 17:55:19 +02:00
504702700c Responsive navigation menu 2025-06-01 00:06:10 +02:00
ca961320e7 feat: scraper usage example, fixed session handling 2025-05-31 05:45:27 +02:00
56f90efe40 feat: orm support for db calls
sample call from debugger_halt():
`getDb().simple_select_all("mecze", id_meczu=1)`
2025-05-30 01:45:35 +02:00
72141768d4 feat: add annotations about functionalities of some endpoints 2025-05-28 14:47:49 +02:00
67307f216f fix: proper(?) db support, add examples of data insertion and selection 2025-05-28 03:11:21 +02:00
65ec7ff73d feat: wrap the db object around baza, which will provide helper methods
also adds the list of tracked sportsmen
2025-05-27 12:28:51 +02:00
b6ae33861e feat: differentiate between proxied and non-proxied instances in logs
will show IP either from remote address, or X-Forwarded-For if proxied
2025-05-27 02:00:05 +02:00
69e911b1b8 fix: increment bad requests when trying to access authed endpoint
without a token
2025-05-27 01:33:04 +02:00
4893715118 fix: db revamp, add relations, annotate the db
also allows for request retrieval, and adds decorator for authed access
2025-05-27 01:28:40 +02:00
00a30695b7 feat: add a basic example of scraper usage 2025-05-17 00:41:36 +02:00
8689426ae3 fix: make the project runnable again from visual studio 2025-05-11 02:10:36 +02:00
a372459298 fix: another typo, and updated readme 2025-05-11 01:54:13 +02:00
8a0c9ae9b8 fix: db_prefix typo 2025-05-11 01:43:52 +02:00
c1facf00fb feat: major rewrite of the webserver
gets rid of __init__ and runserver in favor of new modular design
also introduces db model, first api endpoints, as well as their wrappers
2025-05-11 01:30:32 +02:00
96e2c53484 chore: update dependencies to known, working versions 2025-05-11 00:44:15 +02:00
0b52d5b527 chore: introduce new required dependencies 2025-04-30 16:03:13 +02:00
5f13949cd4 feat: differentiate between commit and html-formatted commit 2025-04-16 12:07:04 +02:00
c35da4a043 update readme 2025-04-10 12:03:37 +02:00
4eb107d3ad chore: add project files to visual studio project, fix horizontal separator 2025-04-10 09:18:38 +02:00
dc845cf884 Merge branch 'pr-3' 2025-04-10 09:12:18 +02:00
76db85765c Embedy fix (mam nadzieję) 2025-04-09 22:45:52 +02:00
a707edcb30 Embedy discordowe 2025-04-09 22:28:52 +02:00
6175f7171f add todo to readme
Also tests a new webhook.
2025-04-09 18:36:09 +02:00
Pc
ca58821361 Centered Style 2025-04-09 18:26:13 +02:00
b5ebcfbe68 Merge pull request 'Basic Flask app' (#2) from pr-3 into master
Reviewed-on: #2
2025-04-09 16:46:53 +02:00
35 changed files with 15073 additions and 251 deletions

9
.gitignore vendored
View File

@@ -364,6 +364,13 @@ FodyWeavers.xsd
# Wirtualne środowisko pythona # Wirtualne środowisko pythona
FlaskWebProject/env FlaskWebProject/env
.venv
# Wersja pythona # Wersja pythona
FlaskWebProject/FlaskWebProject.pyproj FlaskWebProject/FlaskWebProject.pyproj
# Baza sqlite
FlaskWebProject/FlaskWebProject/instance
# Poufne dane
config.toml

View File

@@ -7,18 +7,30 @@
<ProjectGuid>89df7a6e-dc87-40f3-8b4b-f609a6e889d1</ProjectGuid> <ProjectGuid>89df7a6e-dc87-40f3-8b4b-f609a6e889d1</ProjectGuid>
<ProjectHome>.</ProjectHome> <ProjectHome>.</ProjectHome>
<ProjectTypeGuids>{789894c7-04a9-4a11-a6b5-3f4435165112};{1b580a1a-fdb3-4b32-83e1-6407eb2722e6};{349c5851-65df-11da-9384-00065b846f21};{888888a0-9f3d-457c-b088-3a5042f75d52}</ProjectTypeGuids> <ProjectTypeGuids>{789894c7-04a9-4a11-a6b5-3f4435165112};{1b580a1a-fdb3-4b32-83e1-6407eb2722e6};{349c5851-65df-11da-9384-00065b846f21};{888888a0-9f3d-457c-b088-3a5042f75d52}</ProjectTypeGuids>
<StartupFile>runserver.py</StartupFile> <StartupFile>FlaskWebProject\lewy.py</StartupFile>
<SearchPath> <SearchPath>
</SearchPath> </SearchPath>
<WorkingDirectory>.</WorkingDirectory> <WorkingDirectory>.\FlaskWebProject\</WorkingDirectory>
<LaunchProvider>Web launcher</LaunchProvider> <LaunchProvider>Web launcher</LaunchProvider>
<WebBrowserUrl>http://localhost</WebBrowserUrl> <WebBrowserUrl>http://127.0.0.1</WebBrowserUrl>
<OutputPath>.</OutputPath> <OutputPath>.</OutputPath>
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles> <SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
<Name>FlaskWebProject</Name> <Name>FlaskWebProject</Name>
<RootNamespace>FlaskWebProject</RootNamespace> <RootNamespace>FlaskWebProject</RootNamespace>
<InterpreterId> <InterpreterId>MSBuild|env|$(MSBuildProjectFullPath)</InterpreterId>
</InterpreterId> <PythonRunWebServerCommandEnvironment>
</PythonRunWebServerCommandEnvironment>
<PythonDebugWebServerCommandEnvironment>
</PythonDebugWebServerCommandEnvironment>
<IsWindowsApplication>False</IsWindowsApplication>
<WebBrowserPort>5000</WebBrowserPort>
<Environment>
</Environment>
<PythonRunWebServerCommandArguments>
</PythonRunWebServerCommandArguments>
<PythonDebugWebServerCommandArguments>
</PythonDebugWebServerCommandArguments>
<CommandLineArguments>-i 127.0.0.1 -p 5000</CommandLineArguments>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
@@ -29,8 +41,14 @@
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging> <EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="FlaskWebProject\fs_scraper.py" />
<Compile Include="FlaskWebProject\lewy.py" />
<Compile Include="FlaskWebProject\lewy_api.py" />
<Compile Include="FlaskWebProject\lewy_api_v1.py" />
<Compile Include="FlaskWebProject\lewy_db.py" />
<Compile Include="FlaskWebProject\lewy_globals.py" />
<Compile Include="FlaskWebProject\lewy_routes.py" />
<Compile Include="runserver.py" /> <Compile Include="runserver.py" />
<Compile Include="FlaskWebProject\__init__.py" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="FlaskWebProject\" /> <Folder Include="FlaskWebProject\" />
@@ -41,6 +59,7 @@
<Folder Include="FlaskWebProject\templates\" /> <Folder Include="FlaskWebProject\templates\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="FlaskWebProject\config.toml" />
<Content Include="FlaskWebProject\static\script.js" /> <Content Include="FlaskWebProject\static\script.js" />
<Content Include="FlaskWebProject\static\style.css" /> <Content Include="FlaskWebProject\static\style.css" />
<Content Include="FlaskWebProject\templates\base.html" /> <Content Include="FlaskWebProject\templates\base.html" />
@@ -71,6 +90,17 @@
<Content Include="FlaskWebProject\static\scripts\_references.js" /> <Content Include="FlaskWebProject\static\scripts\_references.js" />
<Content Include="FlaskWebProject\templates\index.html" /> <Content Include="FlaskWebProject\templates\index.html" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Interpreter Include="env\">
<Id>env</Id>
<Version>3.13</Version>
<Description>env (Python 3.13 (64-bit))</Description>
<InterpreterPath>Scripts\python.exe</InterpreterPath>
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
<Architecture>X64</Architecture>
</Interpreter>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.Web.targets" /> <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.Web.targets" />
<!-- Specify pre- and post-build commands in the BeforeBuild and <!-- Specify pre- and post-build commands in the BeforeBuild and
AfterBuild targets below. --> AfterBuild targets below. -->

View File

@@ -1,41 +0,0 @@
from flask import Flask, render_template
from .lewy_globals import *
app = Flask(__name__)
@app.route('/')
def index():
stats = {
'goals': 38,
'assists': 12,
'matches': 45,
'matches_list': [
{'date': '2024-10-12', 'opponent': 'Real Madrid', 'goals': 2, 'assists': 1, 'minutes': 90},
{'date': '2024-10-19', 'opponent': 'Valencia', 'goals': 1, 'assists': 0, 'minutes': 85},
# Możesz dodać więcej meczów...
]
}
return render_template('index.html', goals=stats['goals'], assists=stats['assists'],
matches=stats['matches'], matches_list=stats['matches_list'],
commit=lewy_globals.getCommit())
@app.route('/mecze')
def mecze():
# Możesz dostarczyć szczegóły dotyczące meczów
matches = [
{'date': '2024-10-12', 'opponent': 'Real Madrid', 'goals': 2, 'assists': 1, 'minutes': 90},
{'date': '2024-10-19', 'opponent': 'Valencia', 'goals': 1, 'assists': 0, 'minutes': 85},
]
return render_template('matches.html', matches=matches)
@app.route('/statystyki')
def statystyki():
stats = {
'goals': 38,
'assists': 12,
'matches': 45,
}
return render_template('stats.html', stats=stats)
if __name__ == '__main__':
app.run(debug=True)

View File

@@ -0,0 +1,31 @@
[general]
db_path_url = "postgresql+psycopg2://user:password@hostname/database_name"
db_prefix = "" # What (if any) prefix will be appended to table names.
is_proxied = false # Will ignore discrepancies between retrieved IP and public-facing URL.
public_facing_url = "http://127.0.0.1:5000/" # Used for URL rewriting. Note the trailing forward slash /.
[api]
# Leave empty to automatically generate API key every launch (insecure).
api_key = ""
[scraper]
user-agent = "" # Leave empty for default (Firefox ESR).
[sportsmen]
tracked_ids = [
"MVC8zHZD", # Robert Lewandowski
"WGOY4FSt", # Cristiano Ronaldo
"vgOOdZbd", # Lionel Messi
"Wn6E2SED", # Kylian Mbappe
"AiH2zDve", # Zlatan Ibrahimovic
"dUShzrBp", # Luis Suarez
"UmV9iQmE", # Erling Haaland
"tpV0VX0S", # Karim Benzema
"vw8ZV7HC", # Sergio Aguero
"Qgx2trzH", # Edinson Cavani
"2oMimkAU", # Radamel Falcao
"WfXv1DCa", # Wayne Rooney
"0vgcq6un", # Robin van Persie
"v5HSlEAa", # Harry Kane
"4S9fNUYh" # Ciro Immobile
]

View File

@@ -0,0 +1,269 @@
from flask import session
from lewy_db import baza as ldb
from lewy_globals import colors as c
from lewy_globals import safeTraverse as safe_traverse
import json
import lewy_globals
import requests
import time
from sqlalchemy import func
class scraper:
headers = {
'x-fsign': 'SW9D1eZo'
}
db = None
def __init__(self):
self.db = lewy_globals.getDb()
pass
def pobierzDaneNajlepszegoSportowcaNaSwiecie(self) -> dict:
response = requests.get('https://3.flashscore.ninja/3/x/feed/plm_MVC8zHZD_0', headers=headers)
return json.loads(response.text)
def pobierz_pojedyncza_strone(self, zewnetrzne_id_sportowca: str = "MVC8zHZD", nr_strony: int = 0) -> dict:
if len(zewnetrzne_id_sportowca) != 8:
raise ValueError("Zewnętrzne ID sportowca powinno być długości 8!")
response = requests.get(f'https://3.flashscore.ninja/3/x/feed/plm_{zewnetrzne_id_sportowca}_{nr_strony}', headers=self.headers)
return json.loads(response.text)
def __czy_x_istnieje(self, typ, **id):
rekord = self.db.simple_select_all(typ, **id)
if rekord is not None and rekord != []:
return True
else:
return False
def to_iso_compatible_date(self, dmy_date: str):
"""
Zamienia datę z formatu DD.MM.YY na YYYY-MM-DD
:param dmy_date: Data w formacie DD.MM.YY
:type dmy_date: str
"""
day, month, year = dmy_date.split(".")
return f"{2000 + int(year)}-{month}-{day}"
def czy_mecz_istnieje(self, zewnetrzne_id_meczu: str):
return self.__czy_x_istnieje("mecze", zewnetrzne_id_meczu=zewnetrzne_id_meczu)
def czy_klub_istnieje(self, id_klubu: str):
return self.__czy_x_istnieje("kluby", id_klubu=id_klubu)
def czy_nie_trzeba_pobierac_nowych_meczy_zawodnika(self, id_zawodnika: int, zewnetrzne_id_meczu: str):
return self.__czy_x_istnieje("sportowcy_w_meczach", id_zawodnika=id_zawodnika, zewnetrzne_id_meczu=zewnetrzne_id_meczu)
def id_na_imie_nazwisko_urodziny(self, zewnetrzne_id_sportowca: str = "MVC8zHZD"):
"""
Scraper z dykty xD
Pobiera imiona, nazwiska i dni urodzin sportowców z zewnętrznego id.
Działa na słowo honoru.
:param zewnetrzne_id_sportowca: Zewnętrzne id sportowca
:type zewnetrzne_id_sportowca: str
"""
if len(zewnetrzne_id_sportowca) != 8:
raise ValueError("Zewnętrzne ID sportowca powinno być długości 8!")
r = requests.get(f'https://www.flashscore.pl/?r=4:{zewnetrzne_id_sportowca}')
page = r.text
name_start_pos = page.find("data-testid=\"wcl-scores-heading-02\">") + 36
name_end_pos = page.find("</", name_start_pos)
name = page[name_start_pos:name_end_pos].strip().split(' ')
# Tak wiem... można by było użyć beautifulsoup4, ale nie ma sensu dodawać nowych zależności dla tylko jednej metody.
birthday_start_pos_1 = page.find("data-testid=\"wcl-scores-simpleText-01\">", name_end_pos) + 39
birthday_start_pos_2 = page.find("data-testid=\"wcl-scores-simpleText-01\">", birthday_start_pos_1) + 39
birthday_start_pos_3 = page.find("data-testid=\"wcl-scores-simpleText-01\">", birthday_start_pos_2) + 39
birthday_start_pos = page.find("data-testid=\"wcl-scores-simpleText-01\">", birthday_start_pos_3) + 39
birthday_end_pos = page.find("</", birthday_start_pos) - 1
birthday = None if birthday_end_pos - birthday_start_pos > 20 else page[birthday_start_pos:birthday_end_pos].strip(" ()")
return name, birthday
def aktualizuj_dane_sportowca(self, zewnetrzne_id_sportowca: str = "MVC8zHZD"):
stop_scraping = False
matches_to_add = []
# TODO: Sprawdź, czy sportowiec istnieje w bazie.
# Jeśli nie, dodaj go w podobny sposób, jak
# w sample_data_init() (w lewy_db.py).
# -- Jednak tego tak nie robimy, ponieważ nie uzyskamy wielu informacji.
id_zawodnika = self.db.get_id_zawodnika_by_zewnetrzne_id(zewnetrzne_id_sportowca)
zawodnik = self.db.simple_select_all("sportowcy", zewnetrzne_id_zawodnika=zewnetrzne_id_sportowca)[0]
page = 0
match_num = 0
while not stop_scraping:
retrieved_page = self.pobierz_pojedyncza_strone(zewnetrzne_id_sportowca=zewnetrzne_id_sportowca, nr_strony=page)
if not safe_traverse(retrieved_page, ["hasMoreLastMatches"], default=False):
stop_scraping = True
break
print(f"{c.WARNING}Pobrano nową stronę {page}{c.ENDC}:\n{retrieved_page}")
retrieved_matches = safe_traverse(retrieved_page, ["lastMatches"], default=[])
for match in retrieved_matches:
match_id = safe_traverse(match, ["eventEncodedId"], default="non-existent-match-id")
home_club_id = safe_traverse(match, ["homeParticipantUrl"], default="non-existent-club-id")
away_club_id = safe_traverse(match, ["awayParticipantUrl"], default="non-existent-club-id")
# ~Sprawdź, czy mecz nie znajduje się już w bazie~
#
# UWAGA! Nie powinniśmy tego sprawdzać jak w komentarzu poniżej!
# To sprawdzenie powinno jedynie służyć zapobieganiu dodania istniejących meczy,
# natomiast istniejący mecz nie oznacza, że sportowiec ma już statystykę z niego!
# Przerwać scrapowanie należy wtedy, gdy znajdzie się statystykę sportowca
# z bieżącego meczu, a nie kiedy znajdzie się bieżący mecz w bazie!
#
# if self.czy_mecz_istnieje(zewnetrzne_id_meczu=match_id):
# stop_scraping = True
# break
#
# Rozwiązanie jest mocno nieefektywne przy scrapowaniu całej bazy od zera,
# ale rozwiąże przypadki, w których zawodnicy, których śledzimy, grali przeciwko sobie.
if self.czy_nie_trzeba_pobierac_nowych_meczy_zawodnika(id_zawodnika=id_zawodnika, zewnetrzne_id_meczu=match_id):
stop_scraping = True
break
if self.czy_mecz_istnieje(zewnetrzne_id_meczu=match_id):
# Nie scrapuj istniejących meczy.
# Naturalnie, istniejący mecz nie musi oznaczać potrzeby zakończenia scrapowania.
continue
# Sprawdź, czy klub znajduje się już w bazie. Jeśli nie,
# trzeba go dodać przed meczem.
if not self.czy_klub_istnieje(id_klubu=home_club_id):
print(f"{c.OKCYAN}Nowy klub{c.ENDC}: {home_club_id}")
self.db.simple_insert_one("kluby",
id_klubu=home_club_id,
pelna_nazwa=safe_traverse(match, ["homeParticipantName"]),
skrocona_nazwa=safe_traverse(match, ["homeParticipant3CharName"]))
if not self.czy_klub_istnieje(id_klubu=away_club_id):
print(f"{c.OKCYAN}Nowy klub{c.ENDC}: {away_club_id}")
self.db.simple_insert_one("kluby",
id_klubu=away_club_id,
pelna_nazwa=safe_traverse(match, ["awayParticipantName"]),
skrocona_nazwa=safe_traverse(match, ["awayParticipant3CharName"]))
# TODO: dodaj obiekt mecz do bazy (simple_insert_one(), simple_insert_many())
print(f"{c.OKCYAN}Nowy mecz ({match_num}){c.ENDC}: {match}")
iso_converted_date = self.to_iso_compatible_date(safe_traverse(match, ["eventStartTime"], default="1970-01-01"))
self.db.simple_insert_one("mecze",
zewnetrzne_id_meczu = safe_traverse(match, ["eventEncodedId"], default=""),
data = iso_converted_date,
gospodarze_id = home_club_id,
gospodarze = self.db.simple_select_all("kluby", id_klubu=home_club_id)[0],
goscie_id = away_club_id,
goscie = self.db.simple_select_all("kluby", id_klubu=away_club_id)[0],
gosp_wynik = safe_traverse(match, ["homeScore"], default=0),
gosc_wynik = safe_traverse(match, ["awayScore"], default=0),
sezon = safe_traverse(match, ["tournamentSeason"], default=""),
nazwa_turnieju = safe_traverse(match, ["tournamentTitle"], default=""),
skrocona_nazwa_turnieju = safe_traverse(match, ["tournamentTemplateShortCode"], default=""),
flaga = safe_traverse(match, ["flagId"], default=0),
)
match_num += 1
stats = safe_traverse(match, ["stats"], default="")
zewnetrzne_id_meczu = safe_traverse(match, ["eventEncodedId"], default="")
if stats != False: # gdy sportowiec był aktywny w meczu
# print("todo :)")
self.db.simple_insert_one("sportowcy_w_meczach",
id_zawodnika = id_zawodnika,
zawodnik = zawodnik,
zewnetrzne_id_meczu = zewnetrzne_id_meczu,
# Uwaga! Czasami przygłupy z flashscore zwracają puste pole '' zamiast zera, np. do liczby strzelonych goli.
# Dlatego int("0" + "") = int("0"), co zapobiegnie wysypaniu się przy int("").
czas_gry = int("0" + safe_traverse(stats, ["595", "value"], default="0").rstrip("'?")),
goli = int("0" + safe_traverse(stats, ["596", "value"], default="0")),
asyst = int("0" + safe_traverse(stats, ["541", "value"], default="0")),
interwencje_bramkarza = 0,
suma_interwencji_na_bramke = 0,
zolte_kartki = int("0" + safe_traverse(stats, ["599", "value"], default="0")),
czerwone_kartki = int("0" + safe_traverse(stats, ["600", "value"], default="0")),
wygrana = {"Z": 1, "R": 0, "P": -1}.get(safe_traverse(match, ["winLoseShort"], default=""), 0),
wynik = safe_traverse(match, ["rating"], default=0) or 0
)
# # analogicznie zinkrementuj statystyki_sportowcow:
# # aby to zrobić, najpierw pobierz najnowszą statystykę sportowca
# # ...
#
#
# # a następnie użyj funkcji w lewy_db do inkrementowania danych
# # (póki co jeszcze takiej nie ma)
# self.db.simple_increment_data("statystyki_sportowcow",
# id = ... # pewnie id się przyda
# sportowiec = zawodnik,
# ostatni_mecz = self.db.get_id_meczu_by_zewnetrzne_id(zewnetrzne_id_meczu),
# ilosc_wystapien = 1 if int(safe_traverse(stats, ["595", "value"], default="0").rstrip("'")) > 0 else 0,
# minut_gry = int(safe_traverse(stats, ["595", "value"], default="0").rstrip("'")),
# gier_sum = 1 if int(safe_traverse(stats, ["595", "value"], default="0").rstrip("'")) > 0 else 0,
# goli_sum = int(safe_traverse(stats, ["596", "value"], default="0")),
# asyst_sum = int(safe_traverse(stats, ["541", "value"], default="0")),
# interwencji_sum = 0,
# nieobronionych_interwencji_sum = 0,
# zoltych_kartek_sum = int(safe_traverse(stats, ["599", "value"], default="0")),
# czerwonych_kartek_sum = int(safe_traverse(stats, ["600", "value"], default="0")),
# wygranych_sum = 1 if safe_traverse(match, ["winloseshort"], default="") == "z" else 0,
# wynik_sum = safe_traverse(match, ["rating"], default=0),
# meczow_do_wynikow_sum = 1 if safe_traverse(match, ["rating"], default=0) not in (0, none) else none
# )
else:
# print("też todo :)")
# # TODO: TU TEŻ TRZEBA POPRAWIĆ ANALOGICZNIE DO TEGO, CO JEST WEWNĄTRZ IF'A
self.db.simple_insert_one("sportowcy_w_meczach",
id_zawodnika = id_zawodnika,
zawodnik = zawodnik,
zewnetrzne_id_meczu = zewnetrzne_id_meczu,
czas_gry = 0, #(lambda v: int(str(v).rstrip("'")) if isinstance(v, (str, int, float)) and str(v).rstrip("'").lstrip("-").isdigit() else 0)(safe_traverse(stats if isinstance(stats, dict) else {}, ["595", "value"], default="0")),
goli = int("0" + safe_traverse(stats, ["596", "value"], default="0")),
asyst = int("0" + safe_traverse(stats, ["541", "value"], default="0")),
interwencje_bramkarza = 0,
suma_interwencji_na_bramke = 0,
zolte_kartki = int("0" + safe_traverse(stats, ["599", "value"], default="0")),
czerwone_kartki = int("0" + safe_traverse(stats, ["600", "value"], default="0")),
wygrana = {"Z": 1, "R": 0, "P": -1}.get(safe_traverse(match, ["winLoseShort"], default=""), 0),
wynik = safe_traverse(match, ["rating"], default=0) or 0
)
# TODO: Zaktualizuj statystyki sportowca
# Opcjonalnie: odczekaj kilka sekund (?)
# Problem w tym, że time.sleep() jest blokujące,
# a asyncio i flask nie idą ze sobą w parze.
# Można to załatwić osobnym skryptem, ale
# martwmy się tym dopiero, gdy dostaniemy
# rate limita. - sherl
page += 1
#time.sleep(5)
def aktualizuj_dane(self):
"""
Pobiera mecze dla każdego sportowca wymienionego
w pliku konfiguracyjnym.
"""
start_time = time.time()
for id_sportowca in lewy_globals.config['sportsmen']['tracked_ids']:
self.aktualizuj_dane_sportowca(zewnetrzne_id_sportowca=id_sportowca)
time.sleep(5)
end_time = time.time()
print(f"Scrapowanie trwało {end_time - start_time}s.")

View File

@@ -0,0 +1,177 @@
from argparse import ArgumentParser
from flask import Flask, Response, render_template
from flask_apscheduler import APScheduler
from fs_scraper import scraper
from lewy_globals import colors as c
import lewy_api
import lewy_db
import lewy_globals
import lewy_routes
import os
import time
app = Flask(__name__)
app_host = "None"
app_port = "None"
scr = None
def setup():
# sanity check: make sure config is set
# required to make `flask --app lewy run --debug` work
global config, app_host, app_port, scrape
try:
if not config['general']:
lewy_globals.setConfig(lewy_globals.configfile)
config = lewy_globals.config
except:
lewy_globals.setConfig(lewy_globals.configfile)
config = lewy_globals.config
# setting all the variables
lewy_globals.starttime = int(time.time())
lewy_globals.realUptime = 0
lewy_globals.apiRequests = 0
lewy_globals.apiFailedRequests = 0
lewy_globals.isProxied = config['general']['is_proxied']
lewy_globals.outsideApiHits = 0
are_we_sure_of_host_and_port = True
if app_host == "None":
app_host = "127.0.0.1"
are_we_sure_of_host_and_port = False
if app_port == "None":
app_port = "5000"
are_we_sure_of_host_and_port = False
public_facing_url = config['general']['public_facing_url']
if len(public_facing_url) >= 4 and public_facing_url[0:5].lower() == "https":
https_str = f"{c.OKBLUE}INFO:{c.ENDC} You're trying to run this web server on HTTPS, but currently it's not possible to do that!\n"
https_str += f" Please consider running this service behind a reverse proxy if you need HTTPS.\n"
print(https_str)
rewrite_sanity_check = public_facing_url.replace(f"{app_host}:{app_port}", "")
if not config['general']['is_proxied'] and public_facing_url == rewrite_sanity_check:
sanity_string = f"{c.OKBLUE}INFO:{c.ENDC} Public facing URL does not match the IP and port the server is running on.\n"
sanity_string += f" Expected: {c.OKCYAN}{config['general']['public_facing_url']}{c.ENDC}, but"
if not are_we_sure_of_host_and_port: sanity_string += " (assuming it's)"
sanity_string += f" running on: {c.OKCYAN}{app_host}:{app_port}{c.ENDC}.\n"
sanity_string += f" This is just a sanity check and may not neccessarily mean bad configuration.\n"
sanity_string += f" If you're running a reverse proxy, set {c.OKCYAN}is_proxied{c.ENDC} to true to silence this message.\n"
print(sanity_string)
# Should fix disconnects: https://stackoverflow.com/a/61739721
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {"pool_pre_ping": True}
app.config['SQLALCHEMY_DATABASE_URI'] = f"{config['general']['db_path_url']}"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# Widoki widoczne dla "normalnego" użytkownika:
app.add_url_rule('/', view_func=lewy_routes.index)
app.add_url_rule('/index.html', view_func=lewy_routes.index)
app.add_url_rule('/mecze', view_func=lewy_routes.mecze)
app.add_url_rule('/statystyki', view_func=lewy_routes.statystyki)
app.add_url_rule('/toggle_dark_mode', view_func=lewy_routes.toggle_dark_mode)
app.add_url_rule('/club', view_func=lewy_routes.clubs)
app.add_url_rule('/representation', view_func=lewy_routes.representation)
app.add_url_rule('/compare', view_func=lewy_routes.compare)
app.add_url_rule('/trophies', view_func=lewy_routes.trophies)
# API:
app.add_url_rule('/api/', view_func=lewy_api.api_greeting)
app.add_url_rule('/api/<path:received_request>', view_func=lewy_api.api_global_catchall)
db = lewy_globals.setupDb(app, config)
scr = scraper()
with app.app_context():
db.create_all()
# job scheduler for repetetive tasks
scheduler = APScheduler()
scheduler.add_job(func=every5seconds, trigger='interval', id='5sec', seconds=5)
scheduler.add_job(func=every2hours, trigger='interval', id='2hr', hours=2)
scheduler.start()
# gets called every 5 seconds
def every5seconds():
# update the "real" uptime counter
lewy_globals.realUptime += 5
def every2hours():
# zaktualizuj bazę danych scrapując FS
scr.aktualizuj_dane()
return
@app.route('/<string:val>', methods=['GET'])
def blank(val):
return Response(f"{val}: not implemented in lewangoalski {lewy_globals.getCommitWithFailsafe()}", mimetype="text/plain")
def main(args):
print(f"{c.BOLD + c.HEADER}Witaj w webaplikacji 'lewangoalski' ({lewy_globals.getCommitWithFailsafe()})!{c.ENDC}")
print(f"Aby uruchomić w trybie deweloperskim (aby włączyć automatyczne przeładowanie zmian), użyj: {c.OKCYAN}flask --app lewy run --debug{c.ENDC}.")
print( "Aby uruchomić lokalnie, użyj adresu IP 127.0.0.1. Aby uruchomić na każdym z interfejsów, użyj 0.0.0.0.\n")
global config, app_host, app_port
try:
# if specified, use custom config file
lewy_globals.configfile = args.config
lewy_globals.setConfig(lewy_globals.configfile)
except:
# if not, try using the default "config.toml"
if os.path.exists("config.toml"):
lewy_globals.configfile = "config.toml"
else:
# unless it's not there, if that's the case then use the dummy file
lewy_globals.configfile = ""
# but try to set the API secret if provided by the user
if args.secret:
lewy_globals.randomly_generated_passcode = args.secret
lewy_globals.setConfig(lewy_globals.configfile)
config = lewy_globals.config
try:
host = args.ip
port = args.port
if not host or not port:
raise Exception
except:
config_ip, config_port = lewy_globals.extractIpAndPortFromPublicUrl()
print(f"Wpisz nazwę_hosta:port, na których należy uruchomić serwer Flask [domyślnie: {config_ip}:{config_port}]:")
try:
host_port = input('> ').split(':')
if host_port == ['']:
host_port = [config_ip, config_port] # defaults
except KeyboardInterrupt:
print(" ...wychodzę z programu."), quit() # handle Ctrl+C
host = host_port[0]
port = host_port[1]
print()
app_host = host
app_port = port
setup()
app.run(host=host, port=int(port))
if __name__ == "__main__":
#app.run(host="127.0.0.1", port=5000)
#app.run(host="0.0.0.0", port=5000)
parser = ArgumentParser(description='Aplikacja webowa do śledzenia statystyk Roberta Lewandowskiego.')
parser.add_argument("-i", "--ip", dest="ip", help="ip address/interface to bind to")
parser.add_argument("-p", "--port", dest="port", help="port on which the flask web backend should be ran")
parser.add_argument("-c", "--config", dest="config", help="path to TOML config file")
parser.add_argument("-s", "--secret", dest="secret", help="API key for resource access") # NOT tested
args = parser.parse_args()
main(args)
else:
app_host = os.getenv("FLASK_RUN_HOST", "None")
app_port = os.getenv("FLASK_RUN_PORT", "None")
setup()

View File

@@ -0,0 +1,43 @@
from flask import Response, request
from lewy_globals import colors as c
from markupsafe import escape
import json
import lewy_globals
import lewy_api_v1
import requests
import time
import traceback
def api_greeting():
string = {'status': 200, 'msg': f"ok (lewangoalski {lewy_globals.version})", 'latest_api': f"v{lewy_globals.apiVersion}"}
string = json.dumps(string)
return Response(string, mimetype='application/json')
def api_global_catchall(received_request):
lewy_globals.apiRequests += 1
if request.environ['REMOTE_ADDR'] != "127.0.0.1" or (lewy_globals.isProxied and request.environ['X-Forwarded-For'] != "127.0.0.1"):
lewy_globals.outsideApiHits += 1
request_list = received_request.split('/')
api_version = request_list[0]
if request_list[0] == 'v1':
# use v1 api
del request_list[0]
# if list is empty, aka /api/v1/, or /api/v1
if request_list == [''] or request_list == []:
resp = api_greeting()
try:
status, received, data = lewy_api_v1.lookup(request_list, request)
except Exception as e:
lewy_globals.apiFailedRequests += 1
stripped_filename = __file__[max(__file__.rfind("/"), __file__.rfind("\\")) + 1:]
print(f"\n{c.FAIL}Error! /api/{received_request} -> {stripped_filename}:L{e.__traceback__.tb_lineno} -> {type(e).__name__}{c.ENDC}:")
print(traceback.format_exc())
status, received, data = 500, f"internal server error: call ended in failure: {e} ({stripped_filename}:L{e.__traceback__.tb_lineno})", []
resp = Response(json.dumps({'status': status, 'msg': received, 'data': data}), mimetype='application/json', status=status)
else:
lewy_globals.apiFailedRequests += 1
status, received, data = 405, f'error: unsupported api version: "{request_list[0]}". try: "v{lewy_globals.apiVersion}".', []
resp = Response(json.dumps({'status': status, 'msg': received, 'data': data}), mimetype='application/json', status=status)
return resp

View File

@@ -0,0 +1,190 @@
# API is expected to return a tuple of:
# - HTTP status code,
# - human-readable status message,
# - json with appropriate data
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
from fs_scraper import scraper
from functools import wraps
from lewy_globals import getDb, colors as c
import flask, json, time
import lewy_db as ldb
import lewy_globals
def require_authentication(func):
"""
Ten dekorator służy do wymuszenia parametru "token"
podczas obsługi zapytania. Powinien on zostać doklejony
do żądania, np. /api/v1/halt?token=XXX...
Wartość tokenu jest pobierana z pola api_key w config.toml.
Jeżeli skrypt jej tam nie znajdzie, jest generowana losowo
na starcie i drukowana w terminalu.
"""
@wraps(func)
def wrapper(*args, **kwargs):
token = kwargs["r"].args.get('token')
if token == lewy_globals.config['api']['api_key']:
try:
status, received, data = func(*args, **kwargs)
return status, received, data
except:
raise AssertionError(f"Function \"{func.__name__}\" does not return status, code, and data as it should!")
else:
increment_bad_requests()
return 401, "error", {'error_msg': "Unauthorized"}
return wrapper
def increment_bad_requests():
"""
Zwiększa globalny, tymczasowy licznik nieprawidłowych zapytań.
"""
lewy_globals.apiFailedRequests += 1
def not_implemented(data):
"""
Zwraca kod 501 wraz z endpointem, który wywołał błąd.
:param data: Ścieżka zapytania
:type data: list
"""
# TODO: change list to string -> data, not data[0]
return 501, f"not recognised/implemented: {data[0]}", []
def __czy_x_istnieje(typ, **id):
rekord = getDb().simple_select_all(typ, **id)
if rekord is not None and rekord != []:
return True
else:
return False
def czy_sportowiec_istnieje(id_zawodnika: str):
return __czy_x_istnieje("sportowcy", id_zawodnika=id_zawodnika)
# GET /api/v1
def stub_hello():
"""
Prosta funkcja witająca użytkowników w /api/v1
"""
return 200, 'hello from v1! stats are at /api/v1/stats', []
def epoch_to_date(epoch):
"""
Zamienia Unix'owy epoch na lokalny czas,
w formacie przypominającym format ISO.
:param epoch: Epoch - sekundy po 1. stycznia 1970
:type epoch: int
"""
return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(epoch))
# GET /api/v1/
# GET /api/v1/stats
def stats():
"""
Zwraca ogólne statystyki serwera.
"""
data_to_send = {
"start_time": lewy_globals.starttime,
"uptime": lewy_globals.getUptime(),
"real_uptime": lewy_globals.realUptime,
"total_api_requests": lewy_globals.apiRequests,
"failed_api_requests": lewy_globals.apiFailedRequests,
"outside_api_requests": lewy_globals.outsideApiHits,
"local_api_requests": lewy_globals.apiRequests - lewy_globals.outsideApiHits
}
return 200, "ok", data_to_send
# GET /api/v1/matches
def get_matches(r = None, id_zawodnika: str | None = None, rok: int | None = None):
"""
Zwraca mecze.
Przykład wywołania:
get_matches(r, id_zawodnika=1), tożsame z GET /api/v1/matches?id_zawodnika=1
get_matches(r, rok=2024), tożsame z GET /api/v1/matches?rok=2024
get_matches(r), tożsame z GET /api/v1/matches
"""
response_json = []
mecze = None
if id_zawodnika is None:
# Gdy nie podano id wprost, sprawdź, czy podano je przez parametr.
id_zawodnika = r.args.get('id_zawodnika', -1)
if rok is None:
# Gdy nie podano roku wprost, sprawdź, czy podano je przez parametr.
# Jeśli nie, przyjmij None (2025).
rok = r.args.get('rok', None)
# Sprawdź, czy podano jakiekolwiek ID sportowca. Jeżeli nie, wypisz wszystkie mecze.
if id_zawodnika == -1:
mecze = getDb().get_sportsman_matches(year=rok)
# Sprawdź, czy sportowiec o podanym (lub niepodanym) id istnieje.
# Jeśli nie istnieje, wypisz wszystkie mecze.
elif not czy_sportowiec_istnieje(id_zawodnika=id_zawodnika):
return 404, "error", {"error_msg": "This sportsman has not been found in the database. Try: id_zawodnika=1"}
# Gdy sportowiec istnieje, wypisz jego mecze.
else:
mecze = getDb().get_sportsman_matches(id_zawodnika=id_zawodnika, year=rok)
for mecz in mecze:
response_json.append(mecz.jsonify())
# print(f"zwracam mecze: {response_json}")
return 200, "ok", response_json
# GET /api/v1/debugger_halt?token=XXX...
@require_authentication
def debugger_halt(r):
"""
Zatrzymuje wykonywanie skryptu, aby pozwolić
administratorowi na wykonywanie dowolnego polecenia z konsoli.
"""
if lewy_globals.config['general']['is_proxied']:
print(f"{c.WARNING}[{epoch_to_date(time.time())}]{c.ENDC} {r.headers['X-Forwarded-For']} triggered a debugger halt!")
else:
print(f"{c.WARNING}[{epoch_to_date(time.time())}]{c.ENDC} {r.remote_addr} triggered a debugger halt!")
breakpoint()
return 200, "ok", []
def last_goal_for(sportowiec: str = "MVC8zHZD"):
"""
Pobierz klub, dla którego sportowiec (domyślnie Robert) oddał ostatni gol.
:returns: Klub, lub None
:rtype: kluby | None
"""
sportowiec = getDb().simple_select_all("sportowcy", zewnetrzne_id_zawodnika=sportowiec)
if sportowiec != []:
return sportowiec[0].ostatni_gol_dla
return None
def lookup(data, request):
"""
Obsługuje zapytania zwrócone do /api/v1/...
:param data: Lista ze ścieżką zapytania
:type data: list
:param request: Zapytanie
:type request: flask.request
:returns: Wartość zwróconą przez którąś z przywołanych funkcji.
"""
if data == []:
return stub_hello()
match data[0].lower():
case 'stats' | '':
return stats()
case 'user':
return stub_hello()
case 'info':
return stub_hello()
case 'halt':
return debugger_halt(r = request)
case 'matches':
return get_matches(r = request)
case _:
increment_bad_requests()
return not_implemented(data)

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,158 @@
from git import Repo # hash ostatniego commitu #from git import Repo # hash ostatniego commitu
import os
import time
import toml
import lewy_db
def getCommit(): global db, config, randomly_generated_passcode
repo = "<p>Brak informacji o wersji skryptu</p>"
class colors:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
ENDL = '\n'
def safeTraverse(obj: dict, path: list, default=None):
result = obj
try: try:
repo = Repo(search_parent_directories=True).head.object.hexsha for x in path:
repo = f"<p>Commit: <a href='https://gitea.7o7.cx/roberteam/lewangoalski/commit/{repo}' style='width: 50%'>{repo[:11]}</a></p>" result = result[x]
except (KeyError, TypeError):
result = default
print(f"error reading: {' -> '.join(path)} - returning: {default}")
finally:
return result
def getCommit() -> str | None:
try:
return Repo(search_parent_directories=True).head.object.hexsha
except Exception as e:
return None
def getCommitInFormattedHTML():
repo = "<center><p style=\"color: white;\">Brak informacji o wersji skryptu</p></center>"
commit = getCommit()
if commit is not None:
repo = f"<center><p style=\"color: white;\">Commit: <a href='https://gitea.7o7.cx/roberteam/lewangoalski/commit/{commit}'>{commit[:11]}</a></p></center>"
return repo
def getCommitWithFailsafe():
commit = getCommit()
if commit is None:
commit = "(unknown commit)"
else:
commit = "#" + commit
return commit[:12]
def ensureRandomlyGeneratedPassword():
global randomly_generated_passcode
# iff the passcode is 0, as we manually set it elsewhere!
if randomly_generated_passcode == 0:
# generate a pseudorandom one and use it in the temporary config
randomly_generated_passcode = str(int(time.time() * 1337 % 899_999 + 100_000))
print(f"{colors.WARNING}WARNING{colors.ENDC}: Default config populated with one-time, insecure pseudorandom API key: {colors.OKCYAN}{randomly_generated_passcode}{colors.ENDC}.\n"
f" The API key is not the Flask debugger PIN. You need to provide a config file for persistence!{colors.ENDL}")
def getConfig(configfile: str) -> dict:
global randomly_generated_passcode
if not os.path.exists(configfile):
dummy_config = {'general': {'db_path_url': 'sqlite:///lewangoalski.sqlite', 'is_proxied': False, 'public_facing_url': 'http://127.0.0.1:5000/', 'db_prefix': 'lewy_sqlite'}, 'api': {'api_key': 'CHANGEME'}, 'scraper': {'user-agent': ''}, 'sportsmen': {'tracked_ids': ["MVC8zHZD", "WGOY4FSt", "vgOOdZbd", "Wn6E2SED", "AiH2zDve", "dUShzrBp", "UmV9iQmE", "tpV0VX0S", "vw8ZV7HC", "Qgx2trzH", "2oMimkAU", "WfXv1DCa", "0vgcq6un", "v5HSlEAa", "4S9fNUYh"]}}
# if a passcode has not been provided by the user (config file doesn't exist, and user didn't specify it using an argument)
print(f"{colors.WARNING}WARNING{colors.ENDC}: Using default, baked in config data. {colors.ENDL}"
f" Consider copying and editing the provided example file ({colors.OKCYAN}config.example.toml{colors.ENDC}).")
ensureRandomlyGeneratedPassword()
dummy_config['api']['api_key'] = str(randomly_generated_passcode)
return dummy_config
else:
return toml.load(configfile)
def setupDb(app, config) -> lewy_db.baza:
global db
db = lewy_db.baza(app, config)
return db
def getDb() -> lewy_db.baza:
"""
Akcesor dla wrappera bazy danych wspólnego dla całego projektu
(klasy baza z lewy_db)
"""
return db
def setConfig(configfile):
"""
Zapewnia, że konfiguracja nie jest pusta,
nawet, gdy sam plik jest pusty.
:param configfile: Ścieżka do pliku
:type configfile: str
"""
global config
config = getConfig(configfile)
if safeTraverse(config['api']['api_key'], []) is None or not config['api']['api_key']:
ensureRandomlyGeneratedPassword()
config['api']['api_key'] = str(randomly_generated_passcode)
def getHeaders():
"""
Zwraca hardkodowane nagłówki do scrapowania, bądź te,
z config.toml (o ile użytkownik jakieś podał).
"""
# NOTE: use ESR user-agent
# user_agent = 'Mozilla/5.0 (Windows NT 10.0; rv:130.0) Gecko/20100101 Firefox/130.0'
user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0'
if safeTraverse(config[scraper]['user-agent'], []) is not None:
user_agent = config[scraper]['user-agent']
return user_agent
def getUptime():
"""
Zwraca informację o czasie działania serwera.
"""
return int(time.time()) - starttime
def extractIpAndPortFromPublicUrl() -> tuple:
"""
Pobiera dane z konfiguracji i zwraca
krotkę: adres IP i port.
"""
ip, port = "127.0.0.1", "5000"
try:
url = config['general']['public_facing_url'].replace(":/", "")
url_parts = url.split('/')
ip_and_port = url_parts[1]
ip, port = ip_and_port.split(':')
except: except:
pass pass
return repo
return ip, port
# Please leave at the bottom of this file.
config = {}
configfile = "config.toml"
version = getCommitWithFailsafe()
apiVersion = "1"
randomly_generated_passcode = 0

View File

@@ -0,0 +1,190 @@
from flask import render_template, request, make_response
import lewy_api_v1
import lewy_db
import lewy_globals
import json
from lewy_api_v1 import get_matches
def get_lewy_stats():
return {
'all_time_stats': {
'goals': 589+85,
'assists':154+35,
'matches': 791+158,
},
'club_stats': {
'goals': 589,
'assists': 154,
'matches': 791,
},
'nation_stats': {
'goals': 85,
'assists': 35,
'matches': 158,
},
'international_cups': {
'goals': 110,
'assists': 19,
'matches': 152,
},
'national_cups': {
'goals': 58,
'assists': 4,
'matches': 74,
},
'cards': {
'yellow': 86,
'red': 2,
}
}
def index():
dark_mode = request.cookies.get('darkMode', 'disabled')
# Przykładowe użycie endpointu last_goal_for():
# roberts_last_goals_club = lewy_api_v1.last_goal_for()
# print(roberts_last_goals_club.id_klubu)
stats = {
'goals': 38,
'assists': 12,
'matches': 45,
'matches_list': [
{'date': '2024-10-12', 'opponent': 'Real Madrid', 'goals': 2, 'assists': 1, 'minutes': 90},
{'date': '2024-10-19', 'opponent': 'Valencia', 'goals': 1, 'assists': 0, 'minutes': 85},
# Możesz dodać więcej meczów...
]
}
return render_template('index.html', goals=stats['goals'], assists=stats['assists'],
matches=stats['matches'], matches_list=stats['matches_list'],
commit_in_html=lewy_globals.getCommitInFormattedHTML(),
dark_mode=dark_mode)
def mecze():
# Możesz dostarczyć szczegóły dotyczące meczów
selected_date = request.args.get("date", '2025')
try:
selected_date = int(selected_date)
except:
selected_date = 2025
#with open("static/lewandowski_matches.json", "r") as file:
# data = json.load(file)
status, msg, matches = get_matches(None, id_zawodnika=1, rok=selected_date)
return render_template('matches.html', matches=matches, selected_date=selected_date)
def statystyki():
dane=get_lewy_stats()
return render_template('stats.html', **dane)
def clubs():
selected_club = request.args.get("club","FC Barcelona")
clubs = [
{'club': 'FC Barcelona', 'goals': 101,'assist':20, 'matches':147,'minutes_played': 11684,'yellow_card':12,'red_card': 1, 'wins':101, 'draws': 14,'lost': 32},
{'club': 'Bayern Monachium', 'goals': 344,'assist':73,'matches':375,'minutes_played': 31759,'yellow_card':36,'red_card': 0, 'wins':307, 'draws': 35,'lost': 33},
{'club': 'Borussia Dortmund', 'goals': 103,'assist':42,'matches':187,'minutes_played': 14374,'yellow_card':19,'red_card': 1, 'wins':120, 'draws': 40,'lost': 27},
{'club': 'Lech Poznan', 'goals': 41,'assist':19,'matches':82,'minutes_played': 6858,'yellow_card':9,'red_card': 0, 'wins':'-', 'draws': '-','lost': '-'},
]
return render_template('club.html', clubs=clubs, selected_club=selected_club)
def representation():
nation_stats = {
'goals': 85,
'assists': 35,
'matches': 158,
'minutes_played': 12108,
'yellow_card':10,
'red_card': 0,
'wins':75,
'draws': 35,
'lost': 48
}
return render_template('representation.html', nation_stats=nation_stats)
def compare():
selected_player = request.args.get("player","Leo Messi")
lewy=get_lewy_stats()
player2 = [
{'name':'Leo Messi','goals': 865,'assists': 384,'matches':1103},
{'name':'Cristiano Ronaldo','goals': 937,'assists': 257,'matches':1280},
{'name':'Kylian Mbappé','goals': 330,'assists': 187,'matches':436},
{'name':'Zlatan Ibrahimović','goals': 573,'assists': 200,'matches':988},
{'name':'Luis Suárez','goals': 511,'assists': 277,'matches':858},
{'name':'Erling Haaland','goals': 276,'assists': 49,'matches':342},
{'name':'Karim Benzema','goals': 438,'assists': 160,'matches':820},
{'name':'Sergio Agüero','goals': 385,'assists': 118,'matches':685},
]
return render_template('compare.html',player2=player2, selected_player=selected_player,**lewy, )
def trophies():
trophy = [
{'name': 'Piłkarz Roku FIFA', 'year':'2021'},
{'name': 'Piłkarz Roku FIFA', 'year':'2020'},
{'name': 'UEFA Best Player in Europe', 'year':'2020'},
{'name': 'Zdobywca Złotego Buta (Europe)', 'sezon':'2021/2022'},
{'name': 'Zdobywca Złotego Buta (Europe)', 'sezon':'2020/2021'},
{'name': 'Piłkarz roku', 'sezon':'2021'},
{'name': 'Piłkarz roku', 'sezon':'2021'},
{'name': 'Piłkarz roku', 'sezon':'2020'},
{'name': 'Piłkarz roku', 'sezon':'2020'},
{'name': 'Piłkarz roku', 'sezon':'2019'},
{'name': 'Piłkarz roku', 'sezon':'2017'},
{'name': 'Piłkarz roku', 'sezon':'2016'},
{'name': 'Piłkarz roku', 'sezon':'2015'},
{'name': 'Piłkarz roku', 'sezon':'2014'},
{'name': 'Piłkarz roku', 'sezon':'2013'},
{'name': 'Piłkarz roku', 'sezon':'2012'},
{'name': 'Piłkarz roku', 'sezon':'2011'},
{'name': 'Król strzelców', 'sezon':'2022/2023'},
{'name': 'Król strzelców', 'sezon':'2021/2022'},
{'name': 'Król strzelców', 'sezon':'2020/2021'},
{'name': 'Król strzelców', 'sezon':'2019/2020'},
{'name': 'Król strzelców', 'sezon':'2019/2020'},
{'name': 'Król strzelców', 'sezon':'2018/2019'},
{'name': 'Król strzelców', 'sezon':'2018/2019'},
{'name': 'Król strzelców', 'sezon':'2017/2018'},
{'name': 'Król strzelców', 'sezon':'2017/2018'},
{'name': 'Król strzelców', 'sezon':'2016/2017'},
{'name': 'Król strzelców', 'sezon':'2016/2017'},
{'name': 'Król strzelców', 'sezon':'2015/2016'},
{'name': 'Król strzelców', 'sezon':'2015/2016'},
{'name': 'Król strzelców', 'sezon':'2013/2014'},
{'name': 'Król strzelców', 'sezon':'2011/2012'},
{'name': 'Król strzelców', 'sezon':'2009/2010'},
{'name': 'Zdobywca Ligi Mistrzów', 'sezon':'2019/2020'},
{'name': 'Zdobywca Klubowych Mistrzostw Świata', 'sezon':'2021'},
{'name': 'Mistrz Hiszpanii', 'sezon':'2024/2025'},
{'name': 'Mistrz Hiszpanii', 'sezon':'2022/2023'},
{'name': 'Mistrz Niemiec', 'sezon':'2021/2022'},
{'name': 'Mistrz Niemiec', 'sezon':'2020/2021'},
{'name': 'Mistrz Niemiec', 'sezon':'2019/2020'},
{'name': 'Mistrz Niemiec', 'sezon':'2018/2019'},
{'name': 'Mistrz Niemiec', 'sezon':'2017/2018'},
{'name': 'Mistrz Niemiec', 'sezon':'2016/2017'},
{'name': 'Mistrz Niemiec', 'sezon':'2015/2016'},
{'name': 'Mistrz Niemiec', 'sezon':'2014/2015'},
{'name': 'Mistrz Niemiec', 'sezon':'2011/2012'},
{'name': 'Mistrz Niemiec', 'sezon':'2010/2011'},
{'name': 'Mistrz Niemiec', 'sezon':'2020/2021'},
{'name': 'Zdobywca Superpucharu UEFA', 'sezon':'2020/2021'},
{'name': 'Zdobywca Pucharu Niemiec', 'sezon':'2019/2020'},
{'name': 'Zdobywca Pucharu Niemiec', 'sezon':'2018/2019'},
{'name': 'Zdobywca Pucharu Niemiec', 'sezon':'2015/2016'},
{'name': 'Zdobywca Pucharu Niemiec', 'sezon':'2011/2012'},
{'name': 'Zdobywca Pucharu Hiszpanii', 'sezon':'2024/2025'},
{'name': 'Zdobywca Superpucharu Niemiec', 'sezon':'2021/2022'},
{'name': 'Zdobywca Superpucharu Niemiec', 'sezon':'2020/2021'},
{'name': 'Zdobywca Superpucharu Niemiec', 'sezon':'2018/2019'},
{'name': 'Zdobywca Superpucharu Niemiec', 'sezon':'2017/2018'},
{'name': 'Zdobywca Superpucharu Niemiec', 'sezon':'2016/2017'},
{'name': 'Zdobywca Superpucharu Niemiec', 'sezon':'2013/2014'},
{'name': 'Zdobywca Superpucharu Hiszpanii', 'sezon':'2024/2025'},
{'name': 'Zdobywca Superpucharu Hiszpanii', 'sezon':'2022/2023'},
{'name': 'Mistrz Polski', 'sezon':'2009/2010'},
{'name': 'Zdobywca Superpucharu Polski', 'sezon': '2009/2010'},
{'name': 'Zdobywca Pucharu Polski', 'sezon': '2008/2009'},
]
return render_template('trophies.html',trophy=trophy)
def toggle_dark_mode():
# Przełącz tryb i zapisz w ciasteczku
dark_mode = request.cookies.get('darkMode', 'disabled')
new_mode = 'enabled' if dark_mode == 'disabled' else 'disabled'
response = make_response("OK")
response.set_cookie('darkMode', new_mode, max_age=31536000) # Ustawienie ciasteczka na 1 rok
return response

View File

@@ -1,17 +0,0 @@
from flask import render_template, request, make_response
from FlaskWebProject import app
@app.route('/')
def index():
# Odczyt ciasteczka "darkMode" <20> domy<6D>lnie "disabled"
dark_mode = request.cookies.get('darkMode', 'disabled')
return render_template('index.html', dark_mode=dark_mode)
@app.route('/toggle_dark_mode')
def toggle_dark_mode():
# Prze<7A><65>cz tryb i zapisz w ciasteczku
dark_mode = request.cookies.get('darkMode', 'disabled')
new_mode = 'enabled' if dark_mode == 'disabled' else 'disabled'
response = make_response("OK")
response.set_cookie('darkMode', new_mode, max_age=31536000) # Ustawienie ciasteczka na 1 rok
return response

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

View File

@@ -1,140 +1,881 @@
/* Podstawowy styl */ * {
box-sizing: border-box;
}
@font-face {
font-family: 'Exo2SemiBold';
src: url('fonts/Exo2-SemiBold.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Exo2ExtraBold';
src: url('fonts/Exo2-ExtraBold.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
:root {
--barca-blue: #002147;
--barca-red: #A50044;
--barca-gold: #FDB913;
--polska-red-dark: #DC143C;
--polska-red: #E30B17;
--polska-white: #FFFFFF;
--polska-section-color: #121623;
--section-color: #051839;
--pink-highlight: #E1317E;
--blue-highlight: #00B9BF;
--yellow-highlight: #FFD23F;
--border-radius: 5px;
}
/* Podstawowy styl */
body { body {
font-family: 'Arial', sans-serif; font-family: 'Exo2ExtraBold', sans-serif;
margin: 0; margin: 0;
padding: 0; padding: 0;
background: #f7f7f7; background-color: var(--section-color);
color: #222; color: black;
transition: all 0.3s ease; transition: all 0s ease;
display: flex;
flex-direction: column;
align-items: stretch;
/* Wyśrodkowanie elementów w poziomie */
justify-content: flex-start;
/* Ustalamy początek na górze */
min-height: 100vh;
} }
nav { /* Header */
background: #d32f2f; .header-content {
padding: 10px; width: 100%;
display: flex; display: flex;
justify-content: space-around; justify-content: center;
color: white; align-items: center;
padding: 20px 0;
background: var(--section-color);
color: white;
font-size: 15px;
z-index: -2;
position: relative;
margin-top: 60px;
/* box-shadow: 0px -5px 10px 2px rgba(0, 0, 0, 0.347); */
} }
nav a, nav button {
color: white;
text-decoration: none;
font-weight: bold;
background: none;
border: none;
cursor: pointer;
}
main { .header-content h1 {
padding: 20px; border-bottom: 10px solid var(--barca-red);
border-radius: 10px;
padding: 5px;
animation: header-content 500ms ease;
transform: skewX(-5deg);
} }
table { .header-content-special {
width: 100%; font-size: 50px;
border-collapse: collapse; color: var(--barca-gold);
margin-top: 20px;
} }
th, td { .profile-image {
padding: 10px; width: 40%;
border-bottom: 1px solid #ccc; padding: 20px;
text-align: center; position: relative;
} }
.photo { .profile-image-cover {
width: 200px; background: linear-gradient(185deg, transparent 40% 60%, var(--section-color) 85%, var(--section-color) 90%);
border-radius: 50%; position: absolute;
display: block; width: 100%;
margin: 20px auto; height: 100%;
left: 0;
top: 0;
} }
/* Styl dla trybu ciemnego */
body.dark-mode { .profile-image img {
background: #121212; width: 100%;
color: #e0e0e0; max-width: 600px;
animation: header-content 400ms ease;
background: linear-gradient(90deg, var(--barca-blue) 50%, var(--barca-red) 50% 100%);
border-radius: var(--border-radius);
transform: skewX(2deg) skewY(0deg);
} }
body.dark-mode nav { @keyframes header-content {
background: #333; 0% {
} opacity: 00%;
transform: skewX(30deg);
}
body.dark-mode table { 100% {
color: #e0e0e0; opacity: 100%;
} }
}
body.dark-mode h1 {
color: #eaeaea;
}
body.dark-mode header button {
background-color: #444;
color: #eaeaea;
}
body.dark-mode header button:hover {
background-color: #666;
}
body.dark-mode ul li {
background-color: #333;
}
body.dark-mode ul li:hover {
background-color: #444;
}
/* Dodatkowe style */
body.dark-mode h1 {
color: #eaeaea;
}
body.dark-mode header button {
background-color: #444;
color: #eaeaea;
}
body.dark-mode header button:hover {
background-color: #666;
}
body.dark-mode ul li {
background-color: #333;
}
body.dark-mode ul li:hover {
background-color: #444;
}
/* Przyciski i elementy */
header button { header button {
padding: 10px 15px; border: none;
border: none; font-size: 16px;
background-color: #007bff; cursor: pointer;
color: white; width: 100%;
font-size: 16px; height: 100%;
cursor: pointer; border-radius: var(--border-radius);
border-radius: 5px;
transition: background-color 0.3s;
} }
header button:hover {
background-color: #0056b3; /* Styl nawigacji */
} .navbar {
background: linear-gradient(to right, #002147, #A50044);
padding: 2.3rem 1rem;
height: 1.5rem;
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 999;
display: flex;
justify-content: space-around;
align-items: center;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}
.navbar ul {
display: flex;
align-items: center;
}
.logo {
display: flex;
align-items: center;
}
.logo-text {
font-size: 1.5rem;
font-weight: bold;
color: var(--barca-gold);
}
.logo-icon {
font-size: 2.8em;
}
.logo-link {
text-decoration: none;
}
.nav-links {
display: flex;
gap: 0rem;
list-style: none;
height: 100%;
}
.nav-links li a {
display: block;
color: white;
/* dopasowane do .navbar padding */
font-weight: 500;
border: none;
cursor: pointer;
align-items: center;
text-decoration: none;
border-radius: var(--border-radius);
padding: 10px;
height: 100%;
transition: 100ms ease;
position: relative;
}
.nav-links li a::before {
display: flex;
justify-content: center;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0%;
font-size: 25px;
transition: 100ms ease;
}
.nav-links li:hover a::before {
opacity: 100%;
transform: translateY(-20px);
}
.nav-links li:nth-child(1) a::before {
content: "🏠";
}
.nav-links li:nth-child(2) a::before {
content: "📅";
}
.nav-links li:nth-child(3) a::before {
content: "📊";
}
.nav-links li:nth-child(4) a::before {
content: "🤝";
}
.nav-links li:nth-child(5) a::before {
content: "⚽";
}
.nav-links li:nth-child(6) a::before {
content: "🏆";
}
.nav-links li:nth-child(7) a::before {
content: "🔎";
}
.nav-links li {
display: block;
}
.nav-links li:has(button) {
padding: 20px;
}
.nav-links li button {
width: 1.5em;
height: 1.5em;
padding: 20px;
font-size: 30px;
border-radius: 50%;
padding: 0;
margin: 0;
background-color: var(--bg-color);
border: none;
}
.nav-links li button {
background-color: var(--barca-blue);
position: relative;
overflow: hidden;
box-shadow: 0px 0px 6px 1px #FDB913;
transition: 100ms ease;
}
.nav-links li button:hover {
transform: scale(1.1, 1.1);
}
.nav-links li button::after {
content: "";
display: block;
position: absolute;
top: 0;
right: -1px;
background-color: var(--barca-red);
width: 50%;
height: 100%;
}
.nav-links li button::before {
content: "";
background: url('FC_Barcelona.png');
background-size: contain;
z-index: 1;
display: block;
position: absolute;
top: 5px;
left: 5px;
width: 80%;
height: 80%;
}
.nav-links a:hover {
background-color: var(--barca-gold, #FDB913);
color: var(--barca-blue, #002147);
}
.hamburger {
display: none;
font-size: 2rem;
color: var(--barca-gold);
cursor: pointer;
}
@media (max-width: 1090px) {
/* .base-header .navbar:not(:has(.show))
{
margin-bottom: 400px;
} */
.nav-links {
position: absolute;
flex-direction: column;
top: 57px;
right: 0px;
padding: 0rem;
gap: 0;
height: auto;
backdrop-filter: blur(4px);
width: 30%;
justify-content: center;
box-shadow: 0px 0px 5px 5px rgba(0, 0, 0, 0.105);
}
.nav-links li {
width: 100%;
margin: 0;
display: flex;
justify-content: center;
}
.nav-links li a,
.nav-links li button {
display: block;
width: 100%;
text-align: left;
padding: 1rem;
margin: 0;
border-radius: 0;
/* brak zaokrągleń w mobilnym menu */
}
.nav-links li button
{
background-color: none;
width: 45px;
border-radius: 50%;
}
.nav-links li button::before,.nav-links li button::after
{
background-color: none
;
}
.nav-links li button:hover
{
transform: none;
}
.nav-links.show {
display: none;
}
.hamburger {
display: block;
}
}
@media (max-width: 1000px)
{
.header-content
{
flex-direction: column;
}
.header-content .profile-image
{
display: flex;
justify-content: center;
width: 100%;
}
.header-content .profile-image img{
width: 100%;
}
.about-section-image
{
display: none;
}
}
/* Wyśrodkowanie głównej zawartości */
main {
display: flex;
align-items: center;
flex-direction: column;
}
.main-index {
border-radius: var(--border-radius);
margin-top: 20px;
padding: 20px;
width: 80%;
max-width: 1200px;
background-color: rgba(255, 255, 255, 0.086);
color: white;
}
.about-section {
border-radius: var(--border-radius);
background-color: var(--barca-blue);
padding: 20px;
color: white;
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 20px;
grid-template-areas:
"how how"
"- about-section-image"
"- about-section-image"
"- about-section-image";
}
.about-section article h3 {
background-color: var(--barca-gold);
color: black;
text-align: center;
border-radius: var(--border-radius);
padding: 10px;
}
.about-section article h4 {
background-color: #00B9BF;
width: fit-content;
padding: 10px;
color: black;
border-radius: (--border-radius)
}
.article__how-it-works {
grid-area: how;
}
.about-section article a {
display: block;
width: 100%;
color: var(--barca-gold);
text-align: center;
}
.about-section-image {
grid-area: about-section-image;
z-index: 0;
position: relative;
}
.about-section-image::after {
content: "";
display: block;
position: absolute;
right: 0;
bottom: 0;
width: 90%;
height: 90%;
z-index: -1;
background-color: #051839;
border-radius: 20px;
}
.about-section-image img {
width: 100%;
z-index: 3;
}
.general-stats-section {
border-radius: var(--border-radius);
display: flex;
flex-direction: column;
align-items: center;
}
.general-stats-section h2 {
width: 100%;
background-color: #002147;
padding: 20px;
text-align: center;
}
.general-stats-section .grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
height: 150px;
gap: 20px;
width: 100%;
}
.general-stats-section .grid article {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 100%;
font-size: 1.3em;
}
.general-stats-section .grid article:nth-child(1){background-color: var(--blue-highlight);}
.general-stats-section .grid article:nth-child(2){background-color: var(--yellow-highlight);}
.general-stats-section .grid article:nth-child(3){background-color: var(--pink-highlight);}
.general-stats-section .grid p
{
font-size: 40px;
}
.general-stats-section .grid h3, .general-stats-section .grid p
{
margin: 0;
padding: 0;
}
/* Styl dla tabeli */
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th,
td {
padding: 10px;
border-bottom: 1px solid #ccc;
text-align: center;
}
/* Styl dla zdjęcia */
.photo {
width: 100px;
height: 100px;
border-radius: 50%;
display: block;
margin: 0 auto;
/* Wyśrodkowanie obrazka */
}
.section__matches
{
background-color: white;
width: 80%;
border-radius: var(--border-radius);
max-width: 1000px;
margin-bottom: 50px;
}
.section__matches h2
{
margin: 0;
border-radius: var(--border-radius);
background-color: var(--barca-gold);
padding: 20px;
text-align: center;
}
.section__matches td:nth-child(1)
{
border-radius: 25px 0 0px 25px;
}
.section__matches td:last-child
{
border-radius: 0 25px 25px 0;
}
.section__matches th
{
font-size: 1.3em;
color: var(--barca-red)
}
.section__matches tr
{
transition: 100ms ease;
}
.section__matches tr:hover:has(:not(th))
{
transform: scale(1.05,1.05);
background-color: #00B9BF;
}
/* Styl dla trybu polskiego */
body.poland-mode {
background-color: var(--polska-section-color);
}
body.poland-mode .navbar {
background: linear-gradient(to bottom, #bd4148, #dc1414);
}
body.poland-mode .logo {
color: var(--polska-white)
}
body.poland-mode .logo-text {
color: white;
}
body.poland-mode .nav-links li a {
color: white;
}
body.poland-mode .nav-links li a:hover {
color: #220000;
background-color: white;
}
body.poland-mode .nav-links li button::before {
background: none;
}
body.poland-mode .nav-links li button {
background-color: red;
box-shadow: 0px 0px 6px 5px rgba(109, 0, 0, 0.219);
position: relative;
overflow: hidden;
}
body.poland-mode .nav-links li button::after {
content: "";
display: block;
position: absolute;
top: 0;
left: 0;
background-color: white;
width: 100%;
height: 50%;
}
body.poland-mode .profile-image img {
width: 100%;
animation: header-content 300ms ease;
background: linear-gradient(rgba(255, 255, 255, 0.534) 50%, rgba(255, 0, 0, 0.551) 50% 100%);
border-radius: var(--border-radius);
}
body.poland-mode .header-content-special {
font-size: 50px;
color: red;
}
body.poland-mode .header-content {
background-color: var(--polska-section-color)
}
body.poland-mode .header-content h1 {
border-bottom-color: red;
}
body.poland-mode .profile-image-cover {
background: linear-gradient(185deg, transparent 40% 60%, var(--polska-section-color) 85%, var(--polska-section-color) 90%);
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
}
body.poland-mode .hamburger {
color: white;
}
@media (max-width: 768px) {
/* body.poland-mode .nav-links {
background-color: var(--polska-red);
/*Ale oczopląs*/
}
body.poland-mode .section-stats {
background: linear-gradient(to bottom, #bd4148, #dc1414);
}
body.poland-mode .section-stats h2 {
color: white;
}
body.poland-mode .stat-box {
border-color: white;
background: linear-gradient(to bottom, #ff0000,#231212);
}
body.poland-mode .stat-box h3 {
color: white;
}
body.poland-mode .club-stats h2{
background: linear-gradient(to bottom, #ff0000,#231212);
border: 4px solid white;
color: white;
}
body.poland-mode .about-section {
background-color: var(--polska-section-color);
color: white;
}
body.poland-mode .about-section article h3 {
background-color: var(--polska-red-dark);
color: white;
}
body.poland-mode .about-section article h4 {
background-color: #ffcaca;
color: black;
border-radius: 5px;
}
body.poland-mode .general-stats-section h2 {
background: var(--polska-section-color);
}
body.poland-mode .general-stats-section .grid article:nth-child(1){background-color: var(--polska-red-dark);}
body.poland-mode .general-stats-section .grid article:nth-child(2){background-color: var(--yellow-highlight);}
body.poland-mode .general-stats-section .grid article:nth-child(3){background-color: var(--barca-blue);}
body.poland-mode .about-section-image::after{
background:var(--polska-red-dark);
}
body.poland-mode .section__matches
{
background-color: white;
}
body.poland-mode .section__matches h2
{
background-color: var(--polska-red);
color: white;
}
body.poland-mode .section__matches th
{
color: var(--barca-red)
}
body.poland-mode .section__matches tr:hover:has(:not(th))
{
background-color: #ff5959;
}
body.poland-mode select{
background:var(--section-color);
}
/* Przyciski i elementy */
/* Style dla listy meczów */ /* Style dla listy meczów */
ul { .section-stats {
list-style: none; background: linear-gradient(135deg, #002147, #A50044);
padding: 0; color: white;
border-radius: 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
padding: 2rem 2rem;
max-width: 1000px;
margin: 0 auto;
text-align: center;
margin-bottom: 10px;
width: 100%;
}
.section-stats h1{
font-size: 34px;
color: var(--barca-red);
}
body.poland-mode .section-stats h1{
color: white;
}
.section-stats-center
{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
justify-content: center;
}
@media (max-width: 1400px) {
.section-stats-center
{
grid-template-columns: 1fr !important;
}
}
.section-stats h2 {
font-size: 2rem;
margin-bottom: 1rem;
color: var(--barca-red);
} }
ul li { .stats {
background-color: #eaeaea; display: flex;
margin-bottom: 10px; justify-content: space-around;
padding: 10px; text-align: center;
border-radius: 5px; margin-top: 2rem;
transition: background-color 0.3s; gap: 2rem;
} }
ul li:hover { .stat-box {
background-color: #d1d1d1; background-color: rgba(255, 255, 255, 0.1);
} border: 2px solid var(--barca-gold);
color: white;
padding: 2rem;
border-radius: 15px;
width: 160px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
transition: transform 0.3s ease;
}
.stat-box:hover {
transform: scale(1.1, 1.1);
}
.stat-box h3 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
color: var(--barca-gold);
}
.stat-box p {
font-size: 1.1rem;
}
.choose-club
{
margin: 10px;
padding: 20px;
display: flex;
gap: 20px;
justify-content: space-around;
}
.choose-club button {
width: 100px;
border: none;
background-color:transparent;
border-radius: 50%;
}
.choose-club button img {
width: 100%;
height: 100%;
background-color:transparent;
transition: transform 100ms ease;
}
.choose-club button img:hover {
transform: scale(1.3,1.3) translateY(-10px);
}
.club-stats h2{
color: var(--barca-gold);
text-align: center;
border: 5px solid var(--barca-red);
background-color: var(--barca-blue);
padding: 20px;
border-radius: var(--border-radius);
}
.stat-box
{
padding: 20px;
background-color: var(--barca-blue);
display: grid;
grid-template-rows: 1fr 1fr;
text-align: center;
}
.stat-box-special
{text-align: center;
display: block;
font-size: 40px;
color: var(--barca-gold);
}
.club-stats-grid
{
display: grid;
margin-bottom: 50px;
gap: 10px;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr;
}
select
{
color: white;
padding: 20px;
margin: 10px;
font-size: 24px;
background-color: var(--barca-blue);
border-radius: 10px;
border: 2px solid white;
}

View File

@@ -1,47 +1,77 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="pl"> <html lang="pl">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Lewandowski Stats{% endblock %}</title> <title>{% block title %}Lewandowski Stats{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<meta property="og:title" content="Robert Lewandowski Stats">
<meta property="og:type" content="website">
<meta property="og:url" content="https://lewy.7o7.cx/">
<meta property="og:description" content="Najnowsze informacje o meczach, golach, asystach i innych statystykach">
<meta property="og:image" content="{{ url_for('static', filename='lewandowski.jpg') }}">
<meta property="og:image:height" content="143">
<meta property="og:image:width" content="100">
</head> </head>
<body>
<nav>
<a href="/">🏠 Strona główna</a> |
<a href="/mecze">📅 Mecze</a> |
<a href="/statystyki">📊 Statystyki</a> |
<button id="theme-toggle" onclick="toggleTheme()">🌙 / 🌞</button>
</nav>
<header> <body>
<img src="{{ url_for('static', filename='lewandowski.jpg') }}" alt="Robert Lewandowski" style="width: 200px; height: auto; border-radius: 50%;"> <header class="base-header">
<h1>Statystyki Roberta Lewandowskiego</h1> <nav class="navbar">
<a class="logo-link" href="/"><div class="logo-text">Robert Lewandowski</div></a>
<ul class="nav-links">
<li><a href="/">Strona główna</a></li>
<li><a href="/mecze">Mecze</a></li>
<li><a href="/statystyki">Statystyki</a></li>
<li><a href="/club">Kluby</a></li>
<li><a href="/representation">Reprezentacja</a></li>
<li><a href="/trophies">Trofea</a></li>
<li><a href="/compare">Porównaj</a></li>
<li><button id="theme-toggle" onclick="toggleTheme()"></button></li>
</ul>
<div class="hamburger"></div>
</nav>
<div class="header-content">
<div class="profile-image">
<img src="{{ url_for('static', filename='lewandowski_no_bg.png') }}" alt="Robert Lewandowski">
<div class="profile-image-cover"></div>
</div>
<h1>Statystyki <br><span class="header-content-special"> Roberta <br> Lewandowskiego</span></h1>
</div>
<div></div>
</header> </header>
<main> <main>
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
<script>
const hamburger = document.querySelector('.hamburger');
const navLinks = document.querySelector('.nav-links');
hamburger.addEventListener('click', () => {
navLinks.classList.toggle('show');
});
</script>
<script> <script>
function toggleTheme() { function toggleTheme() {
const currentMode = document.body.classList.contains('dark-mode') ? 'dark' : 'light'; const currentMode = document.body.classList.contains('poland-mode') ? 'poland' : 'fcb';
const newMode = currentMode === 'light' ? 'dark' : 'light'; const newMode = currentMode === 'fcb' ? 'poland' : 'fcb';
document.body.classList.toggle('dark-mode'); document.body.classList.toggle('poland-mode');
localStorage.setItem('theme', newMode); localStorage.setItem('theme', newMode);
} }
window.onload = function () { window.onload = function () {
const savedTheme = localStorage.getItem('theme'); const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') { if (savedTheme === 'poland') {
document.body.classList.add('dark-mode'); document.body.classList.add('poland-mode');
} }
} }
</script> </script>
<!--!>Footer<--> <!--!>Footer<-->
<hr/> <hr style='width: 50%' />
{% block footer %}{% endblock %} {% block footer %}{% endblock %}
</body> </body>
</html>
</html>

View File

@@ -0,0 +1,64 @@
{% extends "base.html" %}
{% block title %}Strona Główna{% endblock %}
{% block content %}
<section class="choose-club">
<a href="{{ url_for('clubs', club='FC Barcelona') }}">
<button><img src="{{ url_for('static', filename='FC_Barcelona.png') }}"></button>
</a>
<a href="{{ url_for('clubs', club='Bayern Monachium') }}">
<button><img src="{{ url_for('static', filename='FC_Bayern.png') }}"></button>
</a>
<a href="{{ url_for('clubs', club='Borussia Dortmund') }}">
<button><img src="{{ url_for('static', filename='Borussia_Dortmund.png') }}"></button>
</a>
<!--Jak nie będzie statysytk dla lecha to usunać-->
<a href="{{ url_for('clubs', club='Lech Poznan') }}">
<button><img src="{{ url_for('static', filename='Lech_Poznan.png') }}"></button>
</a>
</section>
<!-- Wyświetlanie danych tylko dla wybranego klubu -->
{% for stats in clubs %}
{% if stats.club == selected_club %}
<section class="club-stats">
<h2>Statystyki dla {{selected_club}}</h2>
<div class="wybrany{{selected_club}}"></div>
<div class="club-stats-grid">
<div class="stat-box">
<p>Gole:</p> <span class="stat-box-special"> {{ stats.goals }} </span>
</div>
<div class="stat-box">
<p>Asysty:</p> <span class="stat-box-special"> {{ stats.assist }} </span>
</div>
<div class="stat-box">
<p>Występy:</p> <span class="stat-box-special"> {{ stats.matches }} </span>
</div>
<div class="stat-box">
<p>Łączny czas gry:</p> <span class="stat-box-special"> {{ stats.minutes_played }}</span>
</div>
<div class="stat-box">
<p>Żółte kartki:</p> <span class="stat-box-special"> {{ stats.yellow_card }}</span>
</div>
<div class="stat-box">
<p>Czerwone kartki:</p> <span class="stat-box-special"> {{ stats.red_card }}</span>
</div>
<div class="stat-box">
<p>Zwycięstwa:</p> <span class="stat-box-special"> {{ stats.wins }}</span>
</div>
<div class="stat-box">
<p>Remisy:</p> <span class="stat-box-special"> {{ stats.draws }}</span>
</div>
<div class="stat-box">
<p>Porażki:</p> <span class="stat-box-special"> {{ stats.lost }}</span>
</div>
</div>
</section>
{% endif %}
{% endfor %}
{% endblock %}
{% block footer %}
{{ commit_in_html | safe }}
{% endblock %}

View File

@@ -0,0 +1,63 @@
{% extends "base.html" %}
{% block title %}Statystyki{% endblock %}
{% block content %}
<select onchange="location = this.value;">
<option disabled selected>Wybierz zawodnika</option>
<option value="{{ url_for('compare', player='Leo Messi') }}">Leo Messi</option>
<option value="{{ url_for('compare', player='Cristiano Ronaldo') }}">Cristiano Ronaldo</option>
<option value="{{ url_for('compare', player='Kylian Mbappé') }}">Kylian Mbappé</option>
<option value="{{ url_for('compare', player='Zlatan Ibrahimović') }}">Zlatan Ibrahimović</option>
<option value="{{ url_for('compare', player='Luis Suárez') }}">Luis Suárez</option>
<option value="{{ url_for('compare', player='Erling Haaland') }}">Erling Haaland</option>
<option value="{{ url_for('compare', player='Karim Benzema') }}">Karim Benzema</option>
<option value="{{ url_for('compare', player='Sergio Agüero') }}">Sergio Agüero</option>
</select>
{%for player in player2 %}
{% if player.name == selected_player %}
<section class="section-stats">
<h1>Robert Lewandowski VS {{selected_player}}</h1>
<h2>Gole</h2>
<div class="stats">
<div class="stat-box">
<h3>{{ all_time_stats.goals }}</h3>
</div>
<div class="stat-box">
<h3>{{ player.goals}}</h3>
</div>
</div>
<h2>Asysty</h2>
<div class="stats">
<div class="stat-box">
<h3>{{ all_time_stats.assists }}</h3>
</div>
<div class="stat-box">
<h3>{{ player.assists}}</h3>
</div>
</div>
<h2>Wystąpienia</h2>
<div class="stats">
<div class="stat-box">
<h3>{{ all_time_stats.matches }}</h3>
</div>
<div class="stat-box">
<h3>{{ player.assists}}</h3>
</div>
</div>
</section>
{% endif %}
{% endfor%}
{% endblock %}

View File

@@ -3,16 +3,61 @@
{% block title %}Strona Główna{% endblock %} {% block title %}Strona Główna{% endblock %}
{% block content %} {% block content %}
<h2>Witaj na stronie poświęconej statystykom Roberta Lewandowskiego!</h2> <div class="main-index">
<p>Tu znajdziesz najnowsze informacje o meczach, golach, asystach i innych statystykach.</p> <h2>Witaj na stronie poświęconej <br> statystykom Roberta Lewandowskiego!</h2>
<div> <p>Tu znajdziesz najnowsze informacje o meczach, golach, asystach i innych statystykach.</p>
<h3>Ogólne statystyki:</h3> <section class="about-section">
<p>Gole: {{ goals }}</p> <article class="article__how-it-works">
<p>Asysty: {{ assists }}</p> <h3>Jak to działa?</h3>
<p>Liczba meczów: {{ matches }}</p> <h4>Pobieranie statystyk</h4>
<p>Tu znajdziesz najświeższe statystyki, aktualizowane zaraz po zakończeniu meczu Lewego</p>
<h4>Porównywanie zawodników</h4>
<p>Robert Lewandowski jest uznawany za jednego z najlepszych zawodników na świecie, zobacz jak wypada w porównaniu do innych czołowych zawodników i sprawdź czy zasługuje na miano jednego z najlepszych napastników świata.</p>
</article>
<div class="about-section-image">
<img src="{{ url_for('static', filename='gigabuła.png') }}">
</div>
<article>
<h3>Mecze</h3>
<p>Nie oglądałeś ostatniego meczu Barcelony? Wypadło Ci coś i nie widziałeś najnowszego spotkania reprezentacji Polski? Wejdź i sprawdź jak poradził sobie Robert.</p>
<a href="/mecze">Zobacz mecze</a>
</article>
<article>
<h3>Statystyki</h3>
<p>Jesteś zainteresowany jak Robert radzi sobie na przestrzeni tylu lat gry? Jesteś ciekaw czy napastnik oprócz strzelania bramek lubi kolekcjonować kartki? Sprawdź u nas jak wygląda kariera Roberta Lewandowskiego na podstawie jego gry w kadrze Polski, klubach oraz Pucharach ligowych i międzynarodowych.</p>
<a href="/statystyki">Zobacz statystyki</a>
</article>
<article>
<h3>Trofea</h3>
<p>Zastanawia Cie jakie było ostatnie trofeum Roberta? A może z kolegami sprawdzacie, który lepiej pamięta złote chwile w karierze naszego idola? Rozwiejcie wasze wątpliwości i zobaczcie wielką kolekcji zdobyczy pucharowych najlepszego polskiego zawodnika.</p>
<a href="/club">Zobacz trofea</a>
</article>
</section>
<!--
<section class="general-stats-section">
<h2>Ogólne statystyki:</h3>
<div class="grid">
<article>
<h3>Gole:</h3>
<p>{{ goals }}</p>
</article>
<article>
<h3>Asysty</h3>
<p>{{ assists }}</p>
</article>
<article>
<h3>Liczba meczów</h3>
<p>{{ matches }}</p>
</article>
</div>
</section>
-->
</div> </div>
{% endblock %} {% endblock %}
{% block footer %} {% block footer %}
{{ commit | safe }} <!--
{{ commit_in_html | safe }}
-->
{% endblock %} {% endblock %}

View File

@@ -3,23 +3,52 @@
{% block title %}Lista meczów{% endblock %} {% block title %}Lista meczów{% endblock %}
{% block content %} {% block content %}
<h2>📅 Mecze Roberta</h2> <select onchange="location = this.value;">
<table> <option disabled selected>Wybierz rok</option>
<tr> <option value="{{ url_for('mecze', date='2025') }}">2025</option>
<th>Data</th> <option value="{{ url_for('mecze', date='2024') }}">2024</option>
<th>Przeciwnik</th> <option value="{{ url_for('mecze', date='2023') }}">2023</option>
<th>Gole</th> <option value="{{ url_for('mecze', date='2022') }}">2022</option>
<th>Asysty</th> <option value="{{ url_for('mecze', date='2021') }}">2021</option>
<th>Minuty</th> <option value="{{ url_for('mecze', date='2020') }}">2020</option>
</tr> <option value="{{ url_for('mecze', date='2019') }}">2019</option>
{% for match in matches %} <option value="{{ url_for('mecze', date='2018') }}">2018</option>
<tr> <option value="{{ url_for('mecze', date='2017') }}">2017</option>
<td>{{ match.date }}</td> <option value="{{ url_for('mecze', date='2016') }}">2016</option>
<td>{{ match.opponent }}</td> <option value="{{ url_for('mecze', date='2015') }}">2015</option>
<td>{{ match.goals }}</td> <option value="{{ url_for('mecze', date='2014') }}">2014</option>
<td>{{ match.assists }}</td> </select>
<td>{{ match.minutes }}</td> <section class="section__matches">
</tr> <h2>📅 Mecze Roberta</h2>
{% endfor %} <table>
</table> <tr>
<th>Data</th>
<!--
<th>Przeciwnik</th>
<th>Gole</th>
<th>Asysty</th>
<th>Minuty</th>
-->
<th>Gospodarze</th>
<th>Wynik</th>
<th>Goście</th>
<th></th>
</tr>
{% for match in matches %}
<tr>
<td>{{ match.data }}</td>
<td>{{ match.gospodarze_pelna_nazwa }}</td>
<td>{{ match.gosp_wynik }} : {{ match.gosc_wynik }}</td>
<td>{{ match.goscie_pelna_nazwa }}</td>
<!--
<td>{{ match.goals }}</td>
<td>{{ match.assists }}</td>
<td>{{ match.minutes }}</td>
-->
</tr>
{% endfor %}
</table>
</section>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block title %}Statystyki{% endblock %}
{% block content %}
<section class="club-stats club-stats-poland">
<h2>Statystyki w reprezentacji Polski</h2>
<div class="wybrany{{selected_club}}"></div>
<div class="club-stats-grid">
<div class="stat-box">
<p>Gole:</p> <span class="stat-box-special"> {{ nation_stats.goals }} </span>
</div>
<div class="stat-box">
<p>Asysty:</p> <span class="stat-box-special"> {{ nation_stats.assists }} </span>
</div>
<div class="stat-box">
<p>Występy:</p> <span class="stat-box-special"> {{ nation_stats.matches }} </span>
</div>
<div class="stat-box">
<p>Łączny czas gry:</p> <span class="stat-box-special"> {{ nation_stats.minutes_played }}</span>
</div>
<div class="stat-box">
<p>Żółte kartki:</p> <span class="stat-box-special"> {{ nation_stats.yellow_card }}</span>
</div>
<div class="stat-box">
<p>Czerwone kartki:</p> <span class="stat-box-special"> {{ nation_stats.red_card }}</span>
</div>
<div class="stat-box">
<p>Zwycięstwa:</p> <span class="stat-box-special"> {{ nation_stats.wins }}</span>
</div>
<div class="stat-box">
<p>Remisy:</p> <span class="stat-box-special"> {{ nation_stats.draws }}</span>
</div>
<div class="stat-box">
<p>Porażki:</p> <span class="stat-box-special"> {{ nation_stats.lost }}</span>
</div>
</div>
</section>
{% endblock %}

View File

@@ -3,10 +3,104 @@
{% block title %}Statystyki{% endblock %} {% block title %}Statystyki{% endblock %}
{% block content %} {% block content %}
<h2>Statystyki Roberta Lewandowskiego</h2> <div class="section-stats-center">
<ul>
<li>Gole: {{ stats.goals }}</li> <section class="section-stats">
<li>Asysty: {{ stats.assists }}</li> <h2>Ogólne statystyki</h2>
<li>Mecze: {{ stats.matches }}</li> <div class="stats">
</ul> <div class="stat-box">
{% endblock %} <h3>{{ all_time_stats.goals }}</h3>
<p>Gole</p>
</div>
<div class="stat-box">
<h3>{{ all_time_stats.assists }}</h3>
<p>Asysty</p>
</div>
<div class="stat-box">
<h3>{{ all_time_stats.matches }}</h3>
<p>Występy</p>
</div>
</div>
</section>
<section class="section-stats">
<h2>Klubowe statystyki</h2>
<div class="stats">
<div class="stat-box">
<h3>{{ club_stats.goals }}</h3>
<p>Gole</p>
</div>
<div class="stat-box">
<h3>{{ club_stats.assists }}</h3>
<p>Asysty</p>
</div>
<div class="stat-box">
<h3>{{ club_stats.matches }}</h3>
<p>Występy</p>
</div>
</div>
</section>
<section class="section-stats">
<h2>Reprezentacja statystyki</h2>
<div class="stats">
<div class="stat-box">
<h3>{{ nation_stats.goals }}</h3>
<p>Gole</p>
</div>
<div class="stat-box">
<h3>{{ nation_stats.assists }}</h3>
<p>Asysty</p>
</div>
<div class="stat-box">
<h3>{{ nation_stats.matches }}</h3>
<p>Występy</p>
</div>
</section>
<section class="section-stats">
<h2>Puchary międzynarodowe</h2>
<div class="stats">
<div class="stat-box">
<h3>{{ international_cups.goals }}</h3>
<p>Gole</p>
</div>
<div class="stat-box">
<h3>{{ international_cups.assists }}</h3>
<p>Asysty</p>
</div>
<div class="stat-box">
<h3>{{ international_cups.matches }}</h3>
<p>Występy</p>
</div>
</div>
</section>
<section class="section-stats">
<h2>Puchary krajowe</h2>
<div class="stats">
<div class="stat-box">
<h3>{{ national_cups.goals }}</h3>
<p>Gole</p>
</div>
<div class="stat-box">
<h3>{{ national_cups.assists }}</h3>
<p>Asysty</p>
</div>
<div class="stat-box">
<h3>{{ national_cups.matches }}</h3>
<p>Występy</p>
</div>
</div>
</section>
<section class="section-stats">
<h2>Kartki</h2>
<div class="stats">
<div class="stat-box">
<h3>{{ cards.yellow }}</h3>
<p>Żółte</p>
</div>
<div class="stat-box">
<h3>{{ cards.red }}</h3>
<p>Czerwone</p>
</div>
</div>
</div>
</section>
{% endblock %}

View File

@@ -0,0 +1,28 @@
{% extends "base.html" %}
{% block title %}Statystyki{% endblock %}
{% block content %}
<section class="section__matches">
<h2>🏆 Trofea</h2>
<table>
<tr>
<th>Nazwa</th>
<th>Data/Sezon</th>
</tr>
{% for trophy in trophy %}
<tr>
<td>{{ trophy.name }}</td>
{% if trophy.year == NULL %}
<td>{{ trophy.sezon }}</td>
{% endif %}
{% if trophy.sezon == NULL %}
<td>{{ trophy.year }}</td>
{% endif %}
</tr>
{% endfor %}
</table>
</section>
{% endblock %}

Binary file not shown.

View File

@@ -1,2 +1,7 @@
Flask>=2.2.3 Flask~=2.2.3
gitpython gitpython~=3.1.44
Flask-SQLAlchemy~=3.1.1
psycopg2~=2.9.10
Flask-APScheduler~=1.13.1
requests~=2.32.3
toml~=0.10.2

View File

@@ -1,14 +1,5 @@
""" """
This script runs the FlaskWebProject application using a development server. Please see README.md for more tips on how to get your server running.
""" """
from os import environ print("runserver.py is obsolete. Please run your server with lewy.py.")
from FlaskWebProject import app
if __name__ == '__main__':
HOST = environ.get('SERVER_HOST', 'localhost')
try:
PORT = int(environ.get('SERVER_PORT', '5555'))
except ValueError:
PORT = 5555
app.run(HOST, PORT)

View File

@@ -1,2 +1,56 @@
# lewangoalski # lewangoalski
## Uruchamianie projektu:
- Zacznij od pobrania plików projektu lub sklonuj repozytorium, używając git:
```
git clone https://gitea.7o7.cx/roberteam/lewangoalski.git
```
- Przejdź do katalogu z plikami projektu:
```
cd lewangoalski
```
- Stwórz wirtualne środowisko:
```
python -m venv .venv
```
Powyższe polecenie stworzy ukryty folder o nazwie *.venv*.
- Aby aktywować to wirtualne środowisko należy użyć:
- na Linuxie (bash):
```
source .venv/bin/activate
```
- na Windowsie (cmd):
```
.venv\Scripts\activate
```
- Zainstaluj niezbędne pakiety w świeżo utworzonym środowisku wirtualnym:
```
pip install -r FlaskWebProject\requirements.txt
```
- Wejdź do katalogu ze skryptem:
```
cd FlaskWebProject/FlaskWebProject
```
- Uruchom skrypt:
- w sposób ciągły:
```
python lewy.py
```
- z automatycznym przeładowaniem (kod zostanie przeładowany po zmianie w bazie kodu):
```
flask --app lewy run --debug
```
## Przydatne do rozruchu parametry:
```
python lewy.py -h
```
## Konfiguracja
Przykładowa konfiguracja została umieszczona w pliku config.example.toml. Skopiuj go, a następnie zmień jego nazwę na config.toml, aby móc swobodnie nanieść swoje zmiany.