Files
google-labs-jules[bot] 6f811d2028 ✨ FEATURE: Implemented User Account, Watch History, and Watchlist System (#14)
This major update transforms the platform into a personalized anime experience.

Key Changes:
- Database: Added 'users', 'watch_history', and 'watchlists' tables with appropriate relationships and indices.
- Backend: Created 'blueprints/user.py' for authentication (Register/Login/Logout) and User Data API (History/Watchlist).
- Security: Implemented password hashing using werkzeug.security and enabled Flask sessions.
- Frontend Architecture: Refactored all templates to use a new 'base.html' for global navigation and consistent styling.
- UI/UX: Added 'Continue Watching' and 'My Watchlist' sections to the home page.
- Tracking: Integrated real-time watch progress tracking in the video player.

This moves the platform from a static database to a dynamic, user-centric streaming site.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-01-16 07:45:49 +00:00

318 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from flask import Blueprint, request, jsonify, render_template, redirect, url_for
from typing import Dict, Any, cast
import requests
import time
import threading
import db
from config import JIKAN_API_BASE, JIKAN_RATE_LIMIT
ui_bp = Blueprint('ui', __name__)
# ─────────────────────────────────────────────────────────────────────────────
# JIKAN API - SEZON ANIME LİSTESİ (RATE LIMIT: 3/saniye, 60/dakika)
# ─────────────────────────────────────────────────────────────────────────────
# Rate limiter için global değişkenler
_jikan_last_request_time = 0.0
_jikan_rate_lock = threading.Lock()
def _apply_jikan_rate_limit():
"""Jikan API rate limit - saniyede max 3 istek."""
global _jikan_last_request_time
with _jikan_rate_lock:
now = time.time()
elapsed = now - _jikan_last_request_time
if elapsed < 0.35: # ~3 istek/saniye için güvenli aralık
time.sleep(0.35 - elapsed)
_jikan_last_request_time = time.time()
def fetch_season_anime(year: int, season: str, page: int = 1, max_retries: int = 5) -> Dict[str, Any] | None:
"""
Jikan API'den sezon anime listesini çek.
429 (Too Many Requests) hatası aldığında bekleyip tekrar dener.
season: winter, spring, summer, fall
"""
for attempt in range(max_retries):
try:
_apply_jikan_rate_limit() # Rate limit uygula
url = f"{JIKAN_API_BASE}/seasons/{year}/{season}"
params = {"page": page, "limit": 25}
response = requests.get(url, params=params, timeout=30)
# 429 Too Many Requests - bekle ve tekrar dene
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 5))
wait_time = max(retry_after, 2 ** attempt + 1)
print(f"[Jikan] Rate limit! {wait_time}s bekleniyor... (deneme {attempt+1}/{max_retries})")
time.sleep(wait_time)
continue
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
if hasattr(e, 'response') and e.response is not None and e.response.status_code == 429:
wait_time = 2 ** attempt + 2
print(f"[Jikan] Rate limit! {wait_time}s bekleniyor... (deneme {attempt+1}/{max_retries})")
time.sleep(wait_time)
continue
print(f"[Jikan] Sezon çekme hatası ({year}/{season}): {e}")
if attempt < max_retries - 1:
time.sleep(2 ** attempt)
continue
return None
except requests.exceptions.Timeout:
print(f"[Jikan] Timeout ({year}/{season} s.{page}), tekrar deneniyor...")
if attempt < max_retries - 1:
time.sleep(2)
continue
return None
except Exception as e:
print(f"[Jikan] Sezon çekme hatası ({year}/{season}): {e}")
return None
print(f"[Jikan] Max deneme aşıldı ({year}/{season} s.{page})")
return None
def fetch_all_season_anime(year: int, season: str, save_to_db: bool = True) -> list:
"""
Bir sezonun tüm anime'lerini çek (tüm sayfalar).
Args:
year: Yıl (ör: 2026)
season: Sezon (winter, spring, summer, fall)
save_to_db: True ise eksik anime'leri DB'ye kaydet
Returns:
Anime listesi
"""
all_anime = []
page = 1
# Mevcut DB'deki MAL ID'ler
existing_ids = set(db.get_all_mal_ids()) if save_to_db else set()
saved_count = 0
while True:
print(f"[Jikan] {year}/{season} sayfa {page} çekiliyor...")
result = fetch_season_anime(year, season, page)
if not result:
break
data = result.get("data", [])
if not data:
break
all_anime.extend(data)
pagination = result.get("pagination", {})
if not pagination.get("has_next_page", False):
break
page += 1
time.sleep(JIKAN_RATE_LIMIT) # Rate limit
return all_anime
@ui_bp.route("/")
def home():
"""Ana sayfa."""
from flask import session
# İstatistikleri çek
conn = db.get_connection()
stats = {"anime_count": 0, "episode_count": 0, "video_count": 0}
if conn:
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT COUNT(*) as count FROM animes")
res = cursor.fetchone()
if res: stats["anime_count"] = int(cast(Dict[str, Any], res)["count"])
cursor.execute("SELECT COUNT(*) as count FROM episodes")
res = cursor.fetchone()
if res: stats["episode_count"] = int(cast(Dict[str, Any], res)["count"])
cursor.execute("SELECT COUNT(*) as count FROM video_links")
res = cursor.fetchone()
if res: stats["video_count"] = int(cast(Dict[str, Any], res)["count"])
cursor.close()
conn.close()
seasons = get_available_seasons()
# Kullanıcı geçmişi ve izleme listesi
user_history = []
user_watchlist = []
if "user_id" in session:
user_history = db.get_user_watch_history(session["user_id"], limit=5)
user_watchlist = db.get_user_watchlist(session["user_id"])
# Jikan API'den dinamik içerik çek
top_anime = []
recommendations = []
try:
top_anime_res = requests.get(f"{JIKAN_API_BASE}/top/anime", params={"limit": 10}, timeout=5)
if top_anime_res.status_code == 200:
top_anime = top_anime_res.json().get("data", [])
recommendations_res = requests.get(f"{JIKAN_API_BASE}/recommendations/anime", params={"limit": 10}, timeout=5)
if recommendations_res.status_code == 200:
recommendations = recommendations_res.json().get("data", [])
except Exception as e:
print(f"Jikan API error: {e}")
return render_template("home.html",
stats=stats,
seasons=seasons,
top_anime=top_anime,
recommendations=recommendations,
user_history=user_history,
user_watchlist=user_watchlist)
@ui_bp.route("/login")
def login_page():
return render_template("login.html")
@ui_bp.route("/register")
def register_page():
return render_template("register.html")
@ui_bp.route("/player")
def player():
"""Anime oynatıcı sayfası."""
mal_id = request.args.get("mal_id", type=int)
ep = request.args.get("ep", 1, type=int)
if not mal_id:
return redirect("/")
# Anime bilgilerini çek (Helper kullan)
anime_raw = ensure_anime_data(mal_id)
if not anime_raw:
return "Anime bulunamadı", 404
anime = cast(Dict[str, Any], anime_raw)
# Bölümleri çek
conn = db.get_connection()
episodes = []
if conn:
cursor = conn.cursor(dictionary=True)
cursor.execute("""
SELECT e.*,
(SELECT COUNT(*) FROM video_links WHERE episode_id = e.id) as video_count
FROM episodes e
WHERE e.anime_id = %s
ORDER BY e.episode_number
""", (int(anime["id"]),))
rows = cursor.fetchall()
for row in rows:
row_dict = cast(Dict[str, Any], row)
episodes.append({
"episode_number": row_dict["episode_number"],
"title": row_dict["title"],
"has_video": (row_dict["video_count"] or 0) > 0
})
cursor.close()
conn.close()
# Eğer bölüm yoksa, en az total episode kadar placeholder oluştur
total_eps = anime.get("episodes")
if not episodes and total_eps:
episodes = [{"episode_number": i, "title": f"{i}. Bölüm", "has_video": False}
for i in range(1, int(total_eps) + 1)]
return render_template("player.html",
anime=anime,
episodes=episodes,
current_ep=ep)
@ui_bp.route("/season/<int:year>/<season>")
def season_page(year, season):
"""Sezon anime listesi sayfası."""
# Jikan'dan sezon listesini çek
anime_list_raw = fetch_all_season_anime(year, season)
# Veritabanındaki MAL ID'leri
db_mal_ids = set(db.get_all_mal_ids())
anime_list = []
in_db_count = 0
for anime in anime_list_raw:
mal_id = anime.get("mal_id")
in_db = mal_id in db_mal_ids
if in_db:
in_db_count += 1
images = anime.get("images", {}).get("jpg", {})
anime_list.append({
"mal_id": mal_id,
"title": anime.get("title", ""),
"image": images.get("large_image_url") or images.get("image_url", ""),
"score": anime.get("score"),
"type": anime.get("type"),
"in_db": in_db
})
return render_template("season.html",
year=year,
season=season,
anime_list=anime_list,
in_db_count=in_db_count)
@ui_bp.route("/search")
def search_page():
"""Arama sayfası."""
query = request.args.get("q", "")
if not query:
return redirect("/")
results = db.get_anime_by_title(query) or []
return render_template("search.html", query=query, results=results)
@ui_bp.route("/covers/<filename>")
def serve_cover(filename):
"""Cover resimlerini sun."""
from flask import send_from_directory
return send_from_directory("covers", filename)
def get_available_seasons() -> list:
"""Mevcut sezonları listele (2000'den bugüne)."""
import datetime
current_year = datetime.datetime.now().year
seasons = ["winter", "spring", "summer", "fall"]
result = []
for year in range(current_year, 1999, -1):
for season in seasons:
result.append({"year": year, "season": season})
return result
def ensure_anime_data(mal_id):
"""Anime verisi eksikse veya yoksa tamamla."""
anime = db.get_anime_by_mal_id(mal_id)
if not anime:
print(f"[Info] Anime {mal_id} DB'de yok, indiriliyor...")
from main import update_anime_by_mal_id
update_anime_by_mal_id(mal_id)
return db.get_anime_by_mal_id(mal_id)
return anime