Skip to content

Commit 51900ee

Browse files
refactor: rearchitect app to make systems modular (#66)
1 parent f6c22a0 commit 51900ee

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2907
-3265
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,39 @@
11
name: Build and Push Docker Image
2-
32
env:
43
DOCKER_BUILDKIT: 1
54
COMPOSE_DOCKER_CLI_BUILD: 1
6-
75
on:
86
push:
97
branches:
108
- main
119
paths:
1210
- 'app/core/version.py'
1311
- 'pyproject.toml'
14-
1512
concurrency:
1613
group: ${{ github.head_ref || github.run_id }}
1714
cancel-in-progress: true
18-
1915
jobs:
2016
build-and-push:
2117
runs-on: ubuntu-latest
2218
permissions:
2319
id-token: write
2420
contents: write
25-
2621
steps:
2722
- name: Checkout code
2823
uses: actions/checkout@v5
29-
3024
- uses: docker/login-action@v3
3125
with:
3226
registry: ghcr.io
3327
username: ${{ github.actor }}
3428
password: ${{ secrets.CR_TOKEN }}
35-
3629
- name: Set up QEMU
3730
uses: docker/setup-qemu-action@v3
38-
3931
- name: Set up Docker Buildx
4032
uses: docker/setup-buildx-action@v3
41-
4233
- name: Set up Python
4334
uses: actions/setup-python@v6
4435
with:
4536
python-version: '3.11'
46-
4737
- name: Read version from version.py
4838
id: get-version
4939
run: |
@@ -55,14 +45,12 @@ jobs:
5545
fi
5646
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
5747
echo "Read version: ${VERSION}"
58-
5948
- name: Set Docker image tag
6049
id: set-tag
6150
run: |
6251
VERSION="${{ steps.get-version.outputs.VERSION }}"
6352
echo "IMAGE_TAG=${VERSION}" >> $GITHUB_OUTPUT
6453
echo "Building Docker image with version: ${VERSION}"
65-
6654
- name: Build and Push Docker image
6755
working-directory: "./"
6856
run: |
@@ -74,7 +62,6 @@ jobs:
7462
# Also tag as latest
7563
docker tag ghcr.io/${REPO_NAME}:${IMAGE_TAG} ghcr.io/${REPO_NAME}:latest
7664
docker push ghcr.io/${REPO_NAME}:latest
77-
7865
- name: Create and Push Git Tag
7966
run: |
8067
VERSION="${{ steps.get-version.outputs.VERSION }}"

.github/workflows/linter.yml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,23 @@
11
name: Linter
2-
32
# Enable Buildkit and let compose use it to speed up image building
43
env:
54
DOCKER_BUILDKIT: 1
65
COMPOSE_DOCKER_CLI_BUILD: 1
7-
86
on:
97
pull_request:
108
push:
11-
129
concurrency:
1310
group: ${{ github.head_ref || github.run_id }}
1411
cancel-in-progress: true
15-
1612
jobs:
1713
linter:
1814
runs-on: ubuntu-latest
1915
steps:
2016
- name: Checkout Code Repository
2117
uses: actions/checkout@v5
22-
2318
- name: Set up Python
2419
uses: actions/setup-python@v6
2520
with:
2621
python-version: '3.11'
27-
2822
- name: Run pre-commit
2923
uses: pre-commit/[email protected]

.github/workflows/release.yml

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
name: Create GitHub Release
2-
32
on:
43
workflow_run:
54
workflows: ["Build and Push Docker Image"]
@@ -9,37 +8,30 @@ on:
98
- main
109
push:
1110
tags:
12-
- '*' # Also trigger on manual tag pushes
13-
14-
11+
- '*' # Also trigger on manual tag pushes
1512
jobs:
1613
release:
1714
runs-on: ubuntu-latest
1815
# Only run if the triggering workflow succeeded
1916
if: ${{ github.event_name == 'push' || github.event.workflow_run.conclusion == 'success' }}
20-
2117
permissions:
2218
packages: write
2319
contents: write
24-
2520
steps:
2621
- name: Checkout repository
2722
uses: actions/checkout@v5
2823
with:
29-
fetch-depth: 0 # Fetch all history for all tags and branches
30-
fetch-tags: true # Fetch all tags
24+
fetch-depth: 0 # Fetch all history for all tags and branches
25+
fetch-tags: true # Fetch all tags
3126
ref: ${{ github.event.workflow_run.head_branch || github.ref }}
32-
3327
- name: Set up Python
3428
uses: actions/setup-python@v6
3529
with:
3630
python-version: '3.11'
37-
3831
- name: Install dependencies
3932
run: |
4033
python -m pip install --upgrade pip
4134
pip install openai pydantic
42-
4335
- name: Get current tag
4436
id: get-tag
4537
run: |
@@ -63,13 +55,11 @@ jobs:
6355
echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_OUTPUT
6456
echo "Current tag from push: ${TAG_NAME}"
6557
fi
66-
6758
- name: Checkout tag commit
6859
run: |
6960
TAG_NAME="${{ steps.get-tag.outputs.TAG_NAME }}"
7061
git checkout ${TAG_NAME} || git checkout -b temp-${TAG_NAME} ${TAG_NAME}
7162
echo "Checked out tag: ${TAG_NAME}"
72-
7363
- name: Run Python script to generate release notes
7464
id: generate_release_notes
7565
env:
@@ -80,12 +70,10 @@ jobs:
8070
echo "Running generate_release_notes.py"
8171
python scripts/generate_release_notes.py
8272
echo "Script completed"
83-
8473
- name: Debug Outputs
8574
run: |
8675
echo "Version: ${{ steps.generate_release_notes.outputs.version }}"
8776
echo "Release Notes: ${{ steps.generate_release_notes.outputs.release_notes }}"
88-
8977
- name: Create GitHub Release
9078
uses: actions/create-release@v1
9179
env:

.pre-commit-config.yaml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
default_stages: [pre-commit]
22
exclude: '^misc/|^data/|^docs/'
3-
43
repos:
54
- repo: https://github.com/pre-commit/pre-commit-hooks
65
rev: v4.6.0
@@ -16,33 +15,27 @@ repos:
1615
- id: check-case-conflict
1716
- id: check-docstring-first
1817
- id: detect-private-key
19-
2018
- repo: https://github.com/asottile/pyupgrade
2119
rev: v3.15.2
2220
hooks:
2321
- id: pyupgrade
2422
args: [--py311-plus]
25-
2623
- repo: https://github.com/psf/black
2724
rev: 24.4.0
2825
hooks:
2926
- id: black
30-
3127
- repo: https://github.com/PyCQA/isort
3228
rev: 5.13.2
3329
hooks:
3430
- id: isort
35-
3631
- repo: https://github.com/PyCQA/flake8
3732
rev: 7.0.0
3833
hooks:
3934
- id: flake8
40-
4135
- repo: https://github.com/google/yamlfmt
4236
rev: v0.11.0
4337
hooks:
4438
- id: yamlfmt
45-
4639
# sets up .pre-commit-ci.yaml to ensure pre-commit dependencies stay up to date
4740
ci:
4841
autoupdate_schedule: weekly

app/api/endpoints/catalogs.py

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
from app.core.security import redact_token
88
from app.core.settings import UserSettings, get_default_settings
99
from app.services.catalog_updater import refresh_catalogs_for_credentials
10-
from app.services.recommendation_service import RecommendationService
11-
from app.services.stremio_service import StremioService
10+
from app.services.recommendation.engine import RecommendationEngine
11+
from app.services.stremio.service import StremioBundle
1212
from app.services.token_store import token_store
1313

1414
MAX_RESULTS = 50
@@ -79,27 +79,50 @@ async def get_catalog(type: str, id: str, response: Response, token: str):
7979
credentials = await token_store.get_user_data(token)
8080
if not credentials:
8181
raise HTTPException(status_code=401, detail="Invalid or expired token. Please reconfigure the addon.")
82+
83+
bundle = StremioBundle()
8284
try:
83-
# Extract settings from credentials
85+
# 1. Resolve Auth Key (with potential fallback to login)
86+
auth_key = credentials.get("authKey")
87+
email = credentials.get("email")
88+
password = credentials.get("password")
89+
90+
is_valid = False
91+
if auth_key:
92+
try:
93+
await bundle.auth.get_user_info(auth_key)
94+
is_valid = True
95+
except Exception:
96+
pass
97+
98+
if not is_valid and email and password:
99+
try:
100+
auth_key = await bundle.auth.login(email, password)
101+
credentials["authKey"] = auth_key
102+
await token_store.update_user_data(token, credentials)
103+
except Exception as e:
104+
logger.error(f"Failed to refresh auth key during catalog fetch: {e}")
105+
106+
if not auth_key:
107+
raise HTTPException(status_code=401, detail="Stremio session expired. Please reconfigure.")
108+
109+
# 2. Extract settings from credentials
84110
settings_dict = credentials.get("settings", {})
85111
user_settings = UserSettings(**settings_dict) if settings_dict else get_default_settings()
86112
language = user_settings.language if user_settings else "en-US"
87113

88-
# Create a single service; get_auth_key() will validate/refresh as needed
89-
stremio_service = StremioService(
90-
username=credentials.get("email", ""),
91-
password=credentials.get("password", ""),
92-
auth_key=credentials.get("authKey"),
93-
)
94-
# Fetch library once per request and reuse across recommendation paths
95-
library_items = await stremio_service.get_library_items()
96-
recommendation_service = RecommendationService(
97-
stremio_service=stremio_service,
114+
# 3. Fetch library once per request and reuse across recommendation paths
115+
library_items = await bundle.library.get_library_items(auth_key)
116+
engine = RecommendationEngine(
117+
stremio_service=bundle,
98118
language=language,
99119
user_settings=user_settings,
100120
token=token,
101121
library_data=library_items,
102122
)
123+
# Custom attribute for modularized exclusion logic if needed
124+
# In this refactor, RecommendationFiltering.get_exclusion_sets(stremio_service, library_data)
125+
# is called inside engine, and we pass bundle as stremio_service.
103126

104127
# Resolve per-catalog limits (min/max)
105128
def _get_limits() -> tuple[int, int]:
@@ -129,13 +152,10 @@ def _get_limits() -> tuple[int, int]:
129152

130153
# Handle item-based recommendations
131154
if id.startswith("tt"):
132-
try:
133-
recommendation_service.per_item_limit = max_items
134-
except Exception:
135-
pass
136-
recommendations = await recommendation_service.get_recommendations_for_item(item_id=id)
155+
engine.per_item_limit = max_items
156+
recommendations = await engine.get_recommendations_for_item(item_id=id, media_type=type)
137157
if len(recommendations) < min_items:
138-
recommendations = await recommendation_service.pad_to_min(type, recommendations, min_items)
158+
recommendations = await engine.pad_to_min(type, recommendations, min_items)
139159
logger.info(f"Found {len(recommendations)} recommendations for {id}")
140160

141161
elif any(
@@ -148,29 +168,26 @@ def _get_limits() -> tuple[int, int]:
148168
):
149169
# Extract actual item ID (tt... or tmdb:...)
150170
item_id = re.sub(r"^watchly\.(item|loved|watched)\.", "", id)
151-
try:
152-
recommendation_service.per_item_limit = max_items
153-
except Exception:
154-
pass
155-
recommendations = await recommendation_service.get_recommendations_for_item(item_id=item_id)
171+
engine.per_item_limit = max_items
172+
recommendations = await engine.get_recommendations_for_item(item_id=item_id, media_type=type)
156173
if len(recommendations) < min_items:
157-
recommendations = await recommendation_service.pad_to_min(type, recommendations, min_items)
174+
recommendations = await engine.pad_to_min(type, recommendations, min_items)
158175
logger.info(f"Found {len(recommendations)} recommendations for item {item_id}")
159176

160177
elif id.startswith("watchly.theme."):
161-
recommendations = await recommendation_service.get_recommendations_for_theme(
178+
recommendations = await engine.get_recommendations_for_theme(
162179
theme_id=id, content_type=type, limit=max_items
163180
)
164181
if len(recommendations) < min_items:
165-
recommendations = await recommendation_service.pad_to_min(type, recommendations, min_items)
182+
recommendations = await engine.pad_to_min(type, recommendations, min_items)
166183
logger.info(f"Found {len(recommendations)} recommendations for theme {id}")
167184

168185
else:
169-
recommendations = await recommendation_service.get_recommendations(
186+
recommendations = await engine.get_recommendations(
170187
content_type=type, source_items_limit=SOURCE_ITEMS_LIMIT, max_results=max_items
171188
)
172189
if len(recommendations) < min_items:
173-
recommendations = await recommendation_service.pad_to_min(type, recommendations, min_items)
190+
recommendations = await engine.pad_to_min(type, recommendations, min_items)
174191
logger.info(f"Found {len(recommendations)} recommendations for {type}")
175192

176193
logger.info(f"Returning {len(recommendations)} items for {type}")
@@ -184,6 +201,8 @@ def _get_limits() -> tuple[int, int]:
184201
except Exception as e:
185202
logger.exception(f"[{redact_token(token)}] Error fetching catalog for {type}/{id}: {e}")
186203
raise HTTPException(status_code=500, detail=str(e))
204+
finally:
205+
await bundle.close()
187206

188207

189208
@router.get("/{token}/catalog/update")

0 commit comments

Comments
 (0)