| |
| import streamlit as st |
| import requests |
| import webbrowser |
| import secrets |
| import threading |
| import time |
| from http.server import HTTPServer, BaseHTTPRequestHandler |
| from urllib.parse import urlparse, parse_qs |
| from dotenv import load_dotenv |
| import os |
|
|
| load_dotenv() |
|
|
| |
| CLIENT_ID = os.getenv("MAL_CLIENT_ID") |
| CLIENT_SECRET = os.getenv("MAL_CLIENT_SECRET") |
| REDIRECT_URI = os.getenv("MAL_REDIRECT_URI", "http://localhost:8000/callback") |
| PORT = int(os.getenv("MAL_PORT", 8000)) |
|
|
| class MALAuth: |
| def __init__(self): |
| self.code_verifier = secrets.token_urlsafe(64) |
| self.code_challenge = self.code_verifier |
| self.auth_code = None |
| self.error = None |
|
|
| class OAuthHandler(BaseHTTPRequestHandler): |
| auth_code = None |
| error = None |
|
|
| def do_GET(self): |
| parsed = urlparse(self.path) |
| params = parse_qs(parsed.query) |
| if "code" in params: |
| MALAuth.OAuthHandler.auth_code = params["code"][0] |
| self.send_response(200) |
| self.end_headers() |
| self.wfile.write(b"Authorization successful! Return to the app.") |
| elif "error" in params: |
| MALAuth.OAuthHandler.error = params["error"][0] |
| self.send_response(400) |
| self.end_headers() |
| self.wfile.write(b"Authorization failed. Check your settings.") |
| else: |
| self.send_response(400) |
| self.end_headers() |
| self.wfile.write(b"Invalid request") |
|
|
| def run_server(self): |
| server = HTTPServer(('localhost', PORT), self.OAuthHandler) |
| server.timeout = 120 |
| server.handle_request() |
|
|
| def start_oauth_flow(self): |
| server_thread = threading.Thread(target=self.run_server) |
| server_thread.daemon = True |
| server_thread.start() |
|
|
| auth_url = ( |
| "https://myanimelist.net/v1/oauth2/authorize?" |
| f"response_type=code&" |
| f"client_id={CLIENT_ID}&" |
| f"code_challenge={self.code_challenge}&" |
| f"redirect_uri={REDIRECT_URI}" |
| ) |
| webbrowser.open(auth_url) |
|
|
| start_time = time.time() |
| while not self.OAuthHandler.auth_code and not self.OAuthHandler.error: |
| if time.time() - start_time > 120: |
| return None, "Authorization timed out" |
| time.sleep(0.5) |
|
|
| return self.OAuthHandler.auth_code, self.OAuthHandler.error |
|
|
| def get_access_token(self, auth_code): |
| token_url = "https://myanimelist.net/v1/oauth2/token" |
| data = { |
| "client_id": CLIENT_ID, |
| "client_secret": CLIENT_SECRET, |
| "code": auth_code, |
| "code_verifier": self.code_verifier, |
| "grant_type": "authorization_code", |
| "redirect_uri": REDIRECT_URI |
| } |
| try: |
| response = requests.post(token_url, data=data) |
| response.raise_for_status() |
| return response.json()["access_token"], None |
| except requests.exceptions.HTTPError as e: |
| return None, f"Token exchange failed: {e.response.status_code} {e.response.text}" |
|
|
| def login_button(): |
| """Streamlit component for handling MAL login""" |
| if st.button("Login with MyAnimeList"): |
| with st.spinner("Authenticating..."): |
| auth = MALAuth() |
| auth_code, error = auth.start_oauth_flow() |
| |
| if error: |
| st.error(error) |
| return False |
| |
| access_token, error = auth.get_access_token(auth_code) |
| if error: |
| st.error(error) |
| return False |
| |
| st.session_state.access_token = access_token |
| st.rerun() |
| return True |
|
|