Phase 3: HTMX & Alpine.js integration, router refactoring, and UI modernization
CI / Test (Python 3.11) (push) Has been cancelled
CI / Test (Python 3.12) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Type Check (push) Has been cancelled
CI / Summary (push) Has been cancelled

- Modernized the frontend with HTMX for server-driven UI and Alpine.js for client state.
- Refactored anime, player, and recommendation logic into modular routers.
- Updated README.md to reflect the latest project state and technologies (v2.4).
- Added Plyr.io for an improved streaming experience.
- Improved project structure with componentized templates.
- Added Playwright and Vitest configuration for frontend testing.
This commit is contained in:
root
2026-03-26 10:34:26 +00:00
parent a684237725
commit 9f85908ff3
31 changed files with 3413 additions and 2201 deletions
+36 -15
View File
@@ -2,49 +2,78 @@
Recommendations and releases routes for Ohm Stream Downloader API.
"""
import hashlib
from datetime import datetime
from typing import Optional
from fastapi import APIRouter
from fastapi import APIRouter, Request, Query, HTTPException
from fastapi.templating import Jinja2Templates
from app.recommendation_engine import RecommendationEngine
router = APIRouter(prefix="/api", tags=["recommendations"])
templates = Jinja2Templates(directory="templates")
# Add custom filters to Jinja2
def hash_filter(s):
return hashlib.md5(s.encode()).hexdigest()[:10]
templates.env.filters["hash"] = hash_filter
@router.get("/recommendations")
async def get_recommendations(limit: int = 15):
async def get_recommendations(
request: Request,
limit: int = 15,
html: bool = Query(False),
):
"""Get personalized anime recommendations based on download history"""
engine = RecommendationEngine(download_dir="downloads")
try:
recommendations = await engine.get_personalized_recommendations(limit=limit)
if html or request.headers.get("HX-Request"):
return templates.TemplateResponse(
"components/recommendations_list.html",
{"request": request, "recommendations": recommendations}
)
return {"recommendations": recommendations, "count": len(recommendations)}
except Exception as e:
from fastapi import HTTPException
import logging
logging.getLogger(__name__).error(f"Recommendations error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
finally:
await engine.close()
@router.get("/releases/latest")
async def get_latest_releases(limit: int = 20):
async def get_latest_releases(
request: Request,
limit: int = 20,
html: bool = Query(False),
):
"""Get latest anime releases"""
from app.recommendations import get_latest_releases_with_info
try:
releases = await get_latest_releases_with_info(limit=limit)
if html or request.headers.get("HX-Request"):
return templates.TemplateResponse(
"components/releases_list.html",
{"request": request, "releases": releases}
)
return {
"releases": releases,
"count": len(releases),
"updated": datetime.now().isoformat(),
}
except Exception as e:
from fastapi import HTTPException
import logging
logging.getLogger(__name__).error(f"Latest releases error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@@ -68,8 +97,6 @@ async def get_seasonal_anime(
"season": season or "current",
}
except Exception as e:
from fastapi import HTTPException
raise HTTPException(status_code=500, detail=str(e))
finally:
await fetcher.close()
@@ -87,8 +114,6 @@ async def get_scheduled_anime(day: Optional[str] = None):
return {"anime": anime, "count": len(anime), "day": day or "today"}
except Exception as e:
from fastapi import HTTPException
raise HTTPException(status_code=500, detail=str(e))
finally:
await fetcher.close()
@@ -109,8 +134,6 @@ async def get_top_anime(
return {"anime": anime, "count": len(anime)}
except Exception as e:
from fastapi import HTTPException
raise HTTPException(status_code=500, detail=str(e))
finally:
await fetcher.close()
@@ -126,8 +149,6 @@ async def get_download_statistics():
return stats
except Exception as e:
from fastapi import HTTPException
raise HTTPException(status_code=500, detail=str(e))
finally:
await engine.close()