Skip to content

Commit b82ae6d

Browse files
Release v1.8.0 (#104)
2 parents 827774e + 011a5d4 commit b82ae6d

39 files changed

+2214
-540
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,9 @@ logs/
4848
*.ipynb
4949
.vercel
5050
migration.py
51+
52+
53+
.cursor
54+
.github
55+
.pytest_cache
56+
.ruff_cache

app/api/endpoints/catalogs.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ async def get_catalog(response: Response, type: str, id: str, token: str, extra:
1515
1616
This endpoint delegates all logic to CatalogService facade.
1717
"""
18+
if type not in ("movie", "series"):
19+
raise HTTPException(status_code=400, detail="Invalid content type. Must be 'movie' or 'series'.")
20+
21+
if len(token) > 30: # normal stremio tokens are 24 length. But we are using this just to be safe.
22+
raise HTTPException(status_code=400, detail="Invalid token.")
23+
1824
try:
1925
# Delegate to catalog service facade
2026
recommendations, headers = await catalog_service.get_catalog(token, type, id)

app/api/endpoints/tokens.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from datetime import datetime, timezone
2+
from typing import Literal
23

34
from fastapi import APIRouter, HTTPException, Request
45
from loguru import logger
@@ -23,6 +24,11 @@ class TokenRequest(BaseModel):
2324
poster_rating: PosterRatingConfig | None = Field(default=None, description="Poster rating provider configuration")
2425
excluded_movie_genres: list[str] = Field(default_factory=list, description="List of movie genre IDs to exclude")
2526
excluded_series_genres: list[str] = Field(default_factory=list, description="List of series genre IDs to exclude")
27+
popularity: Literal["mainstream", "balanced", "gems", "all"] = Field(
28+
default="balanced", description="Popularity for TMDB API"
29+
)
30+
year_min: int = Field(default=2010, description="Minimum release year for TMDB API")
31+
year_max: int = Field(default=2025, description="Maximum release year for TMDB API")
2632

2733

2834
class TokenResponse(BaseModel):
@@ -85,6 +91,9 @@ async def create_token(payload: TokenRequest, request: Request) -> TokenResponse
8591
poster_rating=poster_rating,
8692
excluded_movie_genres=payload.excluded_movie_genres,
8793
excluded_series_genres=payload.excluded_series_genres,
94+
year_min=payload.year_min,
95+
year_max=payload.year_max,
96+
popularity=payload.popularity,
8897
)
8998

9099
# 4. Prepare payload to store

app/core/app.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ async def lifespan(app: FastAPI):
3030
"""
3131
Manage application lifespan events (startup/shutdown).
3232
"""
33+
# Startup checks
34+
if settings.TOKEN_SALT == "change-me" and settings.APP_ENV == "production":
35+
logger.warning(
36+
"Security Warning: TOKEN_SALT is set to default 'change-me' in production environment! "
37+
"Please set the TOKEN_SALT environment variable."
38+
)
39+
3340
yield
3441
try:
3542
await redis_service.close()

app/core/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,15 @@ class Settings(BaseSettings):
3131
TOKEN_TTL_SECONDS: int = 0 # 0 = never expire
3232
ANNOUNCEMENT_HTML: str = ""
3333
AUTO_UPDATE_CATALOGS: bool = True
34-
CATALOG_REFRESH_INTERVAL_SECONDS: int = 43200 # 12 hours
34+
CATALOG_REFRESH_INTERVAL_SECONDS: int = 86400 # 24 hours
3535
APP_ENV: Literal["development", "production", "vercel"] = "production"
3636
HOST_NAME: str = "https://1ccea4301587-watchly.baby-beamup.club"
3737

3838
RECOMMENDATION_SOURCE_ITEMS_LIMIT: int = 10
3939
LIBRARY_ITEMS_LIMIT: int = 20
4040

4141
CATALOG_CACHE_TTL: int = 43200 # 12 hours
42+
CATALOG_STALE_TTL: int = 604800 # 7 days (soft expiration fallback)
4243

4344
# AI
4445
DEFAULT_GEMINI_MODEL: str = "gemma-3-27b-it"

app/core/constants.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
DEFAULT_MIN_ITEMS: int = 8
33
DEFAULT_CATALOG_LIMIT: int = 20
44

5+
MAX_CATALOG_ITEMS: int = 100
6+
57
DEFAULT_CONCURRENCY_LIMIT: int = 30
68

79
DEFAULT_MINIMUM_RATING_FOR_THEME_BASED_MOVIE: float = 7.2
@@ -16,3 +18,27 @@
1618

1719

1820
DISCOVER_ONLY_EXTRA: list[dict] = [{"name": "genre", "isRequired": True, "options": ["All"], "optionsLimit": 1}]
21+
22+
23+
DISCOVERY_SETTINGS: dict = {
24+
"mainstream": {
25+
"popularity.gte": 30,
26+
"vote_average.gte": 6.2,
27+
"vote_count.gte": 500,
28+
},
29+
"balanced": {
30+
"popularity.lte": 30,
31+
"vote_average.gte": 6.7,
32+
"vote_count.gte": 250,
33+
},
34+
"gems": {
35+
"popularity.lte": 15,
36+
"vote_average.gte": 7.2,
37+
"vote_count.gte": 100,
38+
},
39+
"all": {
40+
"popularity.gte": 0,
41+
"vote_average.gte": 5.0,
42+
"vote_count.gte": 100,
43+
},
44+
}

app/core/settings.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ class UserSettings(BaseModel):
3030
poster_rating: PosterRatingConfig | None = Field(default=None, description="Poster rating provider configuration")
3131
excluded_movie_genres: list[str] = Field(default_factory=list)
3232
excluded_series_genres: list[str] = Field(default_factory=list)
33+
year_min: int = Field(default=1970, description="Minimum release year")
34+
year_max: int = Field(default=2026, description="Maximum release year")
35+
popularity: Literal["mainstream", "balanced", "gems", "all"] = Field(
36+
default="balanced", description="Popularity preference"
37+
)
3338

3439

3540
# Catalog descriptions for frontend

app/core/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.7.0"
1+
__version__ = "1.7.1-rc.1"

app/models/taste_profile.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,21 @@ class TasteProfile(BaseModel):
2323
country_scores: dict[str, float] = Field(default_factory=dict, description="Country code → accumulated score")
2424
director_scores: dict[int, float] = Field(default_factory=dict, description="Director ID → accumulated score")
2525
cast_scores: dict[int, float] = Field(default_factory=dict, description="Actor ID → accumulated score")
26+
runtime_bucket_scores: dict[str, float] = Field(
27+
default_factory=dict,
28+
description="Runtime bucket (short/medium/long) → accumulated score",
29+
)
2630

2731
# Metadata
32+
average_episodes: float | None = Field(
33+
default=None, description="Weighted average episodes per series (series only)"
34+
)
2835
last_updated: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
2936
content_type: str | None = Field(default=None, description="movie or series")
37+
processed_items: set[str] = Field(
38+
default_factory=set,
39+
description="Set of processed item IDs to prevent double counting",
40+
)
3041

3142
class Config:
3243
"""Pydantic configuration."""
@@ -91,4 +102,5 @@ def normalize_dict(scores: dict[Any, float]) -> dict[Any, float]:
91102
"directors": normalize_dict(self.director_scores),
92103
"cast": normalize_dict(self.cast_scores),
93104
"creators": normalize_dict({**self.director_scores, **self.cast_scores}),
105+
"runtime_buckets": normalize_dict(self.runtime_bucket_scores),
94106
}

app/services/catalog.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ async def _generate_for_type(media_type: str, genres: list[int]):
134134
library_items, media_type, None, None
135135
)
136136
if not profile:
137+
logger.warning(f"Failed to build profile for {media_type}")
137138
return media_type, []
138139

139140
try:

0 commit comments

Comments
 (0)