35 Commits

Author SHA1 Message Date
kubab
eef9b788e4 Uzupełnienie danych zawodników 2025-06-07 20:53:35 +02:00
kubab
d304805006 Propozycje wykonania sekcji todo 2025-06-03 19:10:59 +02:00
kubab
cecc914935 dodano funkcje increment_stat 2025-06-03 19:05:44 +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
29 changed files with 2782 additions and 237 deletions

7
.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,157 @@
from lewy_db import baza as ldb
from lewy_globals import colors as c
import json
import lewy_globals
import requests
import time
def safe_traverse(obj: dict, path: list, default=None):
result = obj
try:
for x in path:
result = result[x]
except KeyError:
result = default
# print(f"error reading: {' -> '.join(path)} - returning: {default}")
finally:
return result
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 czy_mecz_istnieje(self, zewnetrzne_id_meczu: str):
# mecz = db.simple_select_all(ldb.mecze, zewnetrzne_id_meczu=zewnetrzne_id_meczu)
# if mecz is not None and mecz != []:
# return True
# else:
# return False
return self.__czy_x_istnieje("mecze", zewnetrzne_id_meczu=zewnetrzne_id_meczu)
def czy_klub_istnieje(self, id_klubu: str):
# mecz = db.simple_select_all(ldb.mecze, zewnetrzne_id_meczu=zewnetrzne_id_meczu)
# if mecz is not None and mecz != []:
# return True
# else:
# return False
return self.__czy_x_istnieje("kluby", id_klubu=id_klubu)
def aktualizuj_dane_sportowca(self, zewnetrzne_id_sportowca: str = "MVC8zHZD"):
stop_scraping = False
matches_to_add = []
# TODO: Sprawdź, czy sportowiec istnieje w bazie.
if not self.__czy_x_istnieje("sportowcy", zewnetrzne_id_sportowca=zewnetrzne_id_sportowca):
print(f"{c.OKCYAN}Dodaję nowego sportowca do bazy danych{c.ENDC}")
self.db.simple_insert_one("sportowcy",
zewnetrzne_id_sportowca=zewnetrzne_id_sportowca,
imie="Robert",
nazwisko="Lewandowski"
)
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")
if self.czy_mecz_istnieje(zewnetrzne_id_meczu=match_id):
stop_scraping = True
break
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: Zamień słownik match na obiekt mecz
mecz = {
"zewnetrzne_id_meczu": match_id,
"data": safe_traverse(match, ["startTimeTimestamp"], default=0),
"id_klubu_gospodarzy": home_club_id,
"id_klubu_gosci": away_club_id,
"gole_gospodarzy": safe_traverse(match, ["homeScore", "current"], default=0),
"gole_gosci": safe_traverse(match, ["awayScore", "current"], default=0),
"rozgrywki": safe_traverse(match, ["tournament", "name"], default="Brak"),
"zewnetrzne_id_sportowca": zewnetrzne_id_sportowca
}
# TODO: Dodaj obiekt mecz do bazy
self.db.simple_insert_one("mecze", **mecz)
print(f"{c.OKCYAN}Nowy mecz ({match_num}){c.ENDC}: {match_id}")
match_num += 1
# TODO: Zaktualizuj statystyki sportowca
# np. zlicz gole RL9
strzelcy = safe_traverse(match, ["goals"], default=[])
liczba_goli = 0
for gol in strzelcy:
if gol.get("playerId") == zewnetrzne_id_sportowca:
liczba_goli += 1
if liczba_goli > 0:
# zwiększamy liczbę goli zawodnika
print(f"{c.OKBLUE}Zwiększam gole Lewandowskiego o {liczba_goli}{c.ENDC}")
self.db.increment_stat("sportowcy", {"zewnetrzne_id_sportowca": zewnetrzne_id_sportowca}, "gole", liczba_goli)
page += 1
time.sleep(15)
def aktualizuj_dane(self):
"""
Pobiera mecze dla każdego sportowca wymienionego
w pliku konfiguracyjnym.
"""
for id_sportowca in lewy_globals.config['sportsmen']['tracked_ids']:
self.aktualizuj_dane_sportowca(zewnetrzne_id_sportowca=id_sportowca)
time.sleep(15)

View File

@@ -0,0 +1,173 @@
from argparse import ArgumentParser
from flask import Flask, Response, render_template
from flask_apscheduler import APScheduler
from fs_scraper import scraper as scr
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"
scrape = 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)
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('/historia', view_func=lewy_routes.historia)
# 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)
scraper = scr()
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
# ...
# scraper.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,148 @@
# 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]}", []
# 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):
"""
TODO: Zwraca mecze.
"""
pass
# 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':
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:
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 = "<p>Brak informacji o wersji skryptu</p>"
commit = getCommit()
if commit is not None:
repo = f"<p>Commit: <a href='https://gitea.7o7.cx/roberteam/lewangoalski/commit/{commit}'>{commit[:11]}</a></p>"
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,56 @@
from flask import render_template, request, make_response
import lewy_api_v1
import lewy_db
import lewy_globals
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
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)
def statystyki():
stats = {
'goals': 38,
'assists': 12,
'matches': 45,
}
return render_template('stats.html', stats=stats)
def historia():
selected_club = request.args.get("club","FC Barcelona")
history = [
{'club': 'FC Barcelona', 'goals': 22},
{'club': 'Bayern Monachium', 'goals': 132},
]
return render_template('history.html', history=history, selected_club=selected_club)
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

View File

@@ -1,140 +1,703 @@
/* 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: #05204A;
--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;
justify-content: center;
align-items: center;
padding: 20px 0;
background: var(--section-color);
color: white;
font-size: 15px;
z-index: -2;
position: relative;
/* box-shadow: 0px -5px 10px 2px rgba(0, 0, 0, 0.347); */
}
.header-content h1 {
border-bottom: 10px solid var(--barca-red);
border-radius: 10px;
padding: 5px;
animation: header-content 500ms ease;
transform: skewX(-5deg);
}
.header-content-special {
font-size: 50px;
color: var(--barca-gold);
}
.profile-image {
width: 40%;
padding: 20px;
position: relative;
}
.profile-image-cover {
background: linear-gradient(185deg, transparent 40% 60%, var(--section-color) 85%, var(--section-color) 90%);
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
}
.profile-image img {
width: 100%;
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);
}
@keyframes header-content {
0% {
opacity: 00%;
transform: skewX(30deg);
}
100% {
opacity: 100%;
}
}
header button {
border: none;
font-size: 16px;
cursor: pointer;
width: 100%;
height: 100%;
border-radius: var(--border-radius);
}
/* Styl nawigacji */
.navbar {
background: linear-gradient(to right, #002147, #A50044);
padding: 2.3rem 1rem;
height: 1.5rem;
position: sticky;
top: 0;
left: 0;
width: 100%;
z-index: 999;
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
color: white; align-items: center;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
} }
nav a, nav button { .navbar ul {
color: white; display: flex;
text-decoration: none; align-items: center;
}
.logo {
display: flex;
align-items: center;
}
.logo-text {
font-size: 1.5rem;
font-weight: bold; font-weight: bold;
background: none; 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; border: none;
cursor: pointer; cursor: pointer;
align-items: center;
text-decoration: none;
border-radius: var(--border-radius);
padding: 10px 20px;
height: 100%;
transition: 100ms ease;
position: relative;
} }
main { .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 {
display: block;
}
.nav-links li:has(button) {
padding: 20px; 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: 768px) {
.nav-links {
display: none;
flex-direction: column;
background-color: var(--barca-blue);
position: absolute;
top: 2.5rem;
right: 0rem;
padding: 0rem;
gap: 0;
width: 200px;
}
.nav-links li {
width: 100%;
}
.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.show {
display: flex;
}
.hamburger {
display: block;
}
}
/* 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 { table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin-top: 20px; margin-top: 20px;
} }
th, td { th,
td {
padding: 10px; padding: 10px;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
text-align: center; text-align: center;
} }
/* Styl dla zdjęcia */
.photo { .photo {
width: 200px; width: 100px;
height: 100px;
border-radius: 50%; border-radius: 50%;
display: block; display: block;
margin: 20px auto; margin: 0 auto;
/* Wyśrodkowanie obrazka */
} }
/* Styl dla trybu ciemnego */ .section__matches
body.dark-mode { {
background: #121212; background-color: white;
color: #e0e0e0; width: 80%;
border-radius: var(--border-radius);
max-width: 1000px;
}
.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;
} }
body.dark-mode nav { .section__matches td:last-child
background: #333; {
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;
} }
body.dark-mode table { /* Styl dla trybu polskiego */
color: #e0e0e0; body.poland-mode {
background-color: var(--polska-section-color);
} }
body.dark-mode h1 { body.poland-mode .navbar {
color: #eaeaea; background: linear-gradient(to bottom, #bd4148, #dc1414);
} }
body.dark-mode header button { body.poland-mode .logo {
background-color: #444; color: var(--polska-white)
color: #eaeaea;
} }
body.dark-mode header button:hover { body.poland-mode .logo-text {
background-color: #666; color: white;
} }
body.dark-mode ul li { body.poland-mode .nav-links li a {
background-color: #333; color: white;
} }
body.dark-mode ul li:hover {
background-color: #444; body.poland-mode .nav-links li a:hover {
color: #220000;
background-color: white;
} }
/* Dodatkowe style */ body.poland-mode .nav-links li button::before {
body.dark-mode h1 { background: none;
color: #eaeaea;
} }
body.dark-mode header button { body.poland-mode .nav-links li button {
background-color: #444; background-color: red;
color: #eaeaea; box-shadow: 0px 0px 6px 5px rgba(109, 0, 0, 0.219);
position: relative;
overflow: hidden;
} }
body.dark-mode header button:hover { body.poland-mode .nav-links li button::after {
background-color: #666; content: "";
display: block;
position: absolute;
top: 0;
left: 0;
background-color: white;
width: 100%;
height: 50%;
} }
body.dark-mode ul li { body.poland-mode .profile-image img {
background-color: #333; 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.dark-mode ul li:hover { body.poland-mode .header-content-special {
background-color: #444; 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: #220000
}
body.poland-mode .stat-box {
border-color: white;
}
body.poland-mode .stat-box h3 {
color: white;
} }
/* Przyciski i elementy */ /* Przyciski i elementy */
header button {
padding: 10px 15px;
border: none;
background-color: #007bff;
color: white;
font-size: 16px;
cursor: pointer;
border-radius: 5px;
transition: background-color 0.3s;
}
header button:hover {
background-color: #0056b3;
}
/* 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);
ul li { padding: 2rem 2rem;
background-color: #eaeaea; max-width: 1000px;
margin: 0 auto;
margin-bottom: 10px; margin-bottom: 10px;
padding: 10px;
border-radius: 5px;
transition: background-color 0.3s;
} }
ul li:hover { .section-stats h2 {
background-color: #d1d1d1; font-size: 2rem;
margin-bottom: 1rem;
color: var(--barca-red);
}
.stats {
display: flex;
justify-content: space-around;
text-align: center;
margin-top: 2rem;
flex-wrap: wrap;
gap: 2rem;
}
.stat-box {
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 button {
height: 50px;
width: 50px;
background-color: white;
border: none;
}
.choose-club button img {
height: 40px;
width: 40px;
}
.choose-club button img:hover {
height: 45px;
width: 45px;
background-color: #ffffff7e;
} }

View File

@@ -1,47 +1,74 @@
<!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> <body>
<nav> <header class="base-header">
<a href="/">🏠 Strona główna</a> | <nav class="navbar">
<a href="/mecze">📅 Mecze</a> | <a class="logo-link" href="/"><div class="logo-text">Robert Lewandowski</div></a>
<a href="/statystyki">📊 Statystyki</a> | <ul class="nav-links">
<button id="theme-toggle" onclick="toggleTheme()">🌙 / 🌞</button> <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="/historia">Osiągnięcia</a></li>
<li><button id="theme-toggle" onclick="toggleTheme()"></button></li>
</ul>
<div class="hamburger"></div>
</nav> </nav>
<header> <div class="header-content">
<img src="{{ url_for('static', filename='lewandowski.jpg') }}" alt="Robert Lewandowski" style="width: 200px; height: auto; border-radius: 50%;"> <div class="profile-image">
<h1>Statystyki Roberta Lewandowskiego</h1> <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,30 @@
{% extends "base.html" %}
{% block title %}Strona Główna{% endblock %}
{% block content %}
<section class="choose-club">
<a href="{{ url_for('historia', club='FC Barcelona') }}">
<button><img src="{{ url_for('static', filename='FC_Barcelona.png') }}"></button>
</a>
<a href="{{ url_for('historia', club='Bayern Monachium') }}">
<button><img src="{{ url_for('static', filename='FC_Bayern.png') }}"></button>
</a>
</section>
<!-- Wyświetlanie danych tylko dla wybranego klubu -->
{% for stats in history %}
{% if stats.club == selected_club %}
<section class="club-stats">
<h2>{{ stats.club }} - All time stats</h2>
<div class="stats">
Gole: {{ stats.goals }}
</div>
</section>
{% endif %}
{% endfor %}
{% endblock %}
{% block footer %}
{{ commit_in_html | safe }}
{% endblock %}

View File

@@ -3,16 +3,59 @@
{% 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">
<h2>Witaj na stronie poświęconej <br> statystykom Roberta Lewandowskiego!</h2>
<p>Tu znajdziesz najnowsze informacje o meczach, golach, asystach i innych statystykach.</p> <p>Tu znajdziesz najnowsze informacje o meczach, golach, asystach i innych statystykach.</p>
<div> <section class="about-section">
<h3>Ogólne statystyki:</h3> <article class="article__how-it-works">
<p>Gole: {{ goals }}</p> <h3>Jak to działa?</h3>
<p>Asysty: {{ assists }}</p> <h4>Pobieranie statystyk</h4>
<p>Liczba meczów: {{ matches }}</p> <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Ratione harum minus hic, voluptate perspiciatis laborum? Alias maxime, voluptate reprehenderit iusto dolorem officiis porro voluptatibus repellat dicta doloribus, blanditiis similique accusantium.</p>
<h4>Porównywanie zawodników</h4>
<p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Fuga, in perspiciatis. Sequi laborum et animi quas sit voluptatibus alias sed ad molestias nulla vel cum, consectetur commodi odio aliquam officia.</p>
</article>
<div class="about-section-image">
<img src="{{ url_for('static', filename='gigabuła.png') }}">
</div>
<article>
<h3>Mecze</h3>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Soluta ullam iusto ex? Quo amet officia aliquam odio sint harum nam eaque nihil ipsa quos aliquid, illum voluptatum, numquam, magnam omnis?</p>
<a href="/mecze">Zobacz mecze</a>
</article>
<article>
<h3>Statystyki</h3>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Temporibus dolore tenetur nulla sint recusandae illo dolores aspernatur ducimus, omnis vitae ipsam neque animi voluptates eos porro, nihil iusto veniam commodi!</p>
<a href="/statystyki">Zobacz statystyki</a>
</article>
<article>
<h3>Osiągnięcia</h3>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Quod dicta veritatis quibusdam eligendi corrupti. Expedita delectus assumenda ipsum illum molestias a voluptates, voluptas quia reprehenderit, quod non, eum veritatis tenetur!</p>
<a href="/historia">Zobacz osiągnięcia</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,6 +3,7 @@
{% block title %}Lista meczów{% endblock %} {% block title %}Lista meczów{% endblock %}
{% block content %} {% block content %}
<section class="section__matches">
<h2>📅 Mecze Roberta</h2> <h2>📅 Mecze Roberta</h2>
<table> <table>
<tr> <tr>
@@ -22,4 +23,5 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
</section>
{% endblock %} {% endblock %}

View File

@@ -3,10 +3,38 @@
{% block title %}Statystyki{% endblock %} {% block title %}Statystyki{% endblock %}
{% block content %} {% block content %}
<h2>Statystyki Roberta Lewandowskiego</h2> <section class="section-stats">
<ul> <h2>All time stats</h2>
<li>Gole: {{ stats.goals }}</li> <div class="stats">
<li>Asysty: {{ stats.assists }}</li> <div class="stat-box">
<li>Mecze: {{ stats.matches }}</li> <h3>{{ stats.goals }}</h3>
</ul> <p>Goals</p>
</div>
<div class="stat-box">
<h3>{{ stats.assists }}</h3>
<p>Assists</p>
</div>
<div class="stat-box">
<h3>{{ stats.matches }}</h3>
<p>Apps</p>
</div>
</div>
</section>
<section class="section-stats">
<h2>All time stats</h2>
<div class="stats">
<div class="stat-box">
<h3>{{ stats.goals }}</h3>
<p>Goals</p>
</div>
<div class="stat-box">
<h3>{{ stats.assists }}</h3>
<p>Assists</p>
</div>
<div class="stat-box">
<h3>{{ stats.matches }}</h3>
<p>Apps</p>
</div>
</div>
</section>
{% endblock %} {% endblock %}

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.