Arkm20 commited on
Commit
661788f
·
verified ·
1 Parent(s): f0c04e9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +47 -85
app.py CHANGED
@@ -5,7 +5,7 @@ from datetime import datetime, timedelta
5
  from typing import List, Dict, Optional, Any
6
 
7
  import secrets
8
- from fastapi import FastAPI, Depends, HTTPException, status, UploadFile, File, Form
9
  from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
10
  from fastapi.staticfiles import StaticFiles
11
  from fastapi.responses import FileResponse, RedirectResponse
@@ -41,15 +41,13 @@ class Token(BaseModel):
41
  class TokenData(BaseModel):
42
  username: Optional[str] = None
43
 
44
- class EpisodeUpdate(BaseModel):
45
- """Data required to add an episode to the watch history."""
46
  show_id: str
 
47
  season_number: int
48
  episode_number: int
49
-
50
- class WatchedEpisode(EpisodeUpdate):
51
- """Represents a single episode in the user's watch history, including the timestamp."""
52
- watch_timestamp: datetime
53
 
54
  class UserBase(BaseModel):
55
  username: str
@@ -60,6 +58,7 @@ class UserCreate(UserBase):
60
  class UserInDB(UserBase):
61
  hashed_password: str
62
  profile_picture_url: Optional[str] = None
 
63
  watch_history: List[Dict[str, Any]] = Field(default_factory=list)
64
 
65
  class UserPublic(UserBase):
@@ -78,8 +77,14 @@ def load_users() -> Dict[str, Dict]:
78
  return {}
79
 
80
  def save_users(users_db: Dict[str, Dict]):
 
 
 
 
 
 
81
  with open(USERS_DB_FILE, "w") as f:
82
- json.dump(users_db, f, indent=4)
83
 
84
  # --- Password & Token Functions ---
85
  def verify_password(plain_password, hashed_password):
@@ -126,24 +131,28 @@ async def get_current_user(token: str = Depends(oauth2_scheme)) -> UserInDB:
126
  app = FastAPI(title="Media Auth API")
127
 
128
  # CORS middleware to allow the frontend to communicate with the API
129
- # This is crucial for web apps.
130
  app.add_middleware(
131
  CORSMiddleware,
132
- allow_origins=["*"], # Allows all origins
133
  allow_credentials=True,
134
- allow_methods=["*"], # Allows all methods
135
- allow_headers=["*"], # Allows all headers
136
  )
137
 
138
  # --- Helper function to structure watch history for the frontend ---
 
139
  def structure_watch_history(history_list: List[Dict]) -> Dict:
140
  """Transforms a flat list of watched episodes into a nested dictionary."""
141
  structured = {}
142
- for item in history_list:
 
 
 
143
  show_id = item.get("show_id")
144
  show_title = item.get("show_title", "Unknown Show")
145
  season_num = item.get("season_number")
146
  episode_num = item.get("episode_number")
 
147
 
148
  if show_id not in structured:
149
  structured[show_id] = {
@@ -156,7 +165,8 @@ def structure_watch_history(history_list: List[Dict]) -> Dict:
156
  "season_number": season_num,
157
  "episodes": {}
158
  }
159
- structured[show_id]["seasons"][season_num]["episodes"][episode_num] = True
 
160
  return structured
161
 
162
 
@@ -194,7 +204,9 @@ async def signup_user(user: UserCreate):
194
  detail="Username already registered",
195
  )
196
  hashed_password = get_password_hash(user.password)
 
197
  new_user = UserInDB(username=user.username, hashed_password=hashed_password)
 
198
  users_db[user.username] = new_user.dict()
199
  save_users(users_db)
200
  return {"message": "User created successfully. Please login."}
@@ -205,10 +217,8 @@ async def read_users_me(current_user: UserInDB = Depends(get_current_user)):
205
  """
206
  Fetch the profile of the currently authenticated user.
207
  """
208
- # The frontend expects a detailed, nested history object. We transform it here.
209
  detailed_history = structure_watch_history(current_user.watch_history)
210
 
211
- # We create a UserPublic object to avoid sending the hashed_password
212
  user_public_data = UserPublic(
213
  username=current_user.username,
214
  profile_picture_url=current_user.profile_picture_url,
@@ -225,23 +235,18 @@ async def upload_profile_picture(
225
  """
226
  Upload or update the user's profile picture.
227
  """
228
- # Generate a unique filename to prevent overwrites and add extension
229
  file_extension = os.path.splitext(file.filename)[1]
230
  unique_filename = f"{uuid.uuid4()}{file_extension}"
231
  file_path = os.path.join(UPLOAD_DIR, unique_filename)
232
 
233
- # Save the file
234
  with open(file_path, "wb") as buffer:
235
  buffer.write(await file.read())
236
 
237
- # Update user data with the URL to the new picture
238
- # The URL must match the static file mount path
239
  profile_picture_url = f"/uploads/{unique_filename}"
240
  users_db = load_users()
241
  users_db[current_user.username]["profile_picture_url"] = profile_picture_url
242
  save_users(users_db)
243
 
244
- # Return the updated user profile
245
  current_user.profile_picture_url = profile_picture_url
246
  detailed_history = structure_watch_history(current_user.watch_history)
247
  return UserPublic(
@@ -251,66 +256,25 @@ async def upload_profile_picture(
251
  )
252
 
253
 
254
- import status
255
- from fastapi import Depends, APIRouter
256
- from pydantic import BaseModel
257
- from datetime import datetime, timezone
258
- from typing import List
259
-
260
- # Assume these are defined elsewhere in your application
261
- # from .dependencies import get_current_user, load_users, save_users
262
- # from .models import UserInDB
263
-
264
- # --- Pydantic Models ---
265
- # It's good practice to define your data models clearly.
266
-
267
-
268
-
269
-
270
-
271
-
272
- @app.get("/users/me/watch-history", response_model=List[WatchedEpisode])
273
- async def get_watch_history(
274
- current_user: UserInDB = Depends(get_current_user)
275
- ):
276
- """
277
- Retrieves the current user's watch history, sorted by most recently watched.
278
- """
279
- users_db = load_users()
280
- user_data = users_db.get(current_user.username, {})
281
-
282
- # Get the watch history, defaulting to an empty list if it doesn't exist
283
- watch_history = user_data.get("watch_history", [])
284
-
285
- # Optional: Sort the history so the most recently watched items are first
286
- # The `key` lambda function handles cases where an old item might not have a timestamp.
287
- watch_history.sort(
288
- key=lambda item: item.get("watch_timestamp", "1970-01-01T00:00:00Z"),
289
- reverse=True
290
- )
291
-
292
- return watch_history
293
-
294
-
295
- @app.post("/users/me/watch-history", status_code=status.HTTP_200_OK)
296
  async def update_watch_history(
297
- episode_data: EpisodeUpdate,
 
 
 
298
  current_user: UserInDB = Depends(get_current_user)
299
  ):
300
  """
301
- Adds a new episode with a timestamp to the user's watch history.
302
- If the episode already exists, its timestamp is not updated.
303
  """
304
  users_db = load_users()
305
  user_data = users_db[current_user.username]
306
-
307
- # Ensure watch_history list exists
308
- if "watch_history" not in user_data:
309
- user_data["watch_history"] = []
310
 
311
  # Create a unique identifier for the episode to prevent duplicates
312
- episode_id = f"{episode_data.show_id}_{episode_data.season_number}_{episode_data.episode_number}"
313
-
314
  # Check if this exact episode is already in the history
315
  is_already_watched = any(
316
  (f"{item.get('show_id')}_{item.get('season_number')}_{item.get('episode_number')}" == episode_id)
@@ -318,29 +282,27 @@ async def update_watch_history(
318
  )
319
 
320
  if not is_already_watched:
321
- # Convert the Pydantic model to a dictionary to add the new field
322
- new_episode_record = episode_data.dict()
323
-
324
- # Add the UTC timestamp in standard ISO format
325
- new_episode_record["watch_timestamp"] = datetime.now(timezone.utc).isoformat()
326
-
327
- user_data["watch_history"].append(new_episode_record)
 
 
 
328
  save_users(users_db)
329
  return {"message": "Watch history updated."}
330
-
331
  return {"message": "Episode already in watch history."}
332
 
333
 
334
  # --- Static File Serving ---
335
- # This serves the uploaded profile pictures from the persistent /data/uploads directory
336
  app.mount("/uploads", StaticFiles(directory=UPLOAD_DIR), name="uploads")
337
-
338
- # This serves the main login/settings pages from the /static directory
339
- # 'html=True' makes it so that visiting "/" serves "index.html" (we'll name login.html as index.html)
340
  app.mount("/", StaticFiles(directory="static", html=True), name="static")
341
 
342
 
343
- # Redirect root to login page for convenience
344
  @app.get("/", include_in_schema=False)
345
  def root():
346
  return RedirectResponse(url="/login.html")
 
5
  from typing import List, Dict, Optional, Any
6
 
7
  import secrets
8
+ from fastapi import FastAPI, Depends, HTTPException, status, UploadFile, File, Query
9
  from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
10
  from fastapi.staticfiles import StaticFiles
11
  from fastapi.responses import FileResponse, RedirectResponse
 
41
  class TokenData(BaseModel):
42
  username: Optional[str] = None
43
 
44
+ # NEW: Model for a single watch history record, including the timestamp
45
+ class WatchHistoryEntry(BaseModel):
46
  show_id: str
47
+ show_title: str
48
  season_number: int
49
  episode_number: int
50
+ watch_timestamp: datetime # Will be stored as an ISO 8601 string in JSON
 
 
 
51
 
52
  class UserBase(BaseModel):
53
  username: str
 
58
  class UserInDB(UserBase):
59
  hashed_password: str
60
  profile_picture_url: Optional[str] = None
61
+ # This reflects the raw data stored in the JSON file
62
  watch_history: List[Dict[str, Any]] = Field(default_factory=list)
63
 
64
  class UserPublic(UserBase):
 
77
  return {}
78
 
79
  def save_users(users_db: Dict[str, Dict]):
80
+ # Use a custom default function to handle datetime objects during JSON serialization
81
+ def json_serializer(obj):
82
+ if isinstance(obj, datetime):
83
+ return obj.isoformat()
84
+ raise TypeError(f"Type {type(obj)} not serializable")
85
+
86
  with open(USERS_DB_FILE, "w") as f:
87
+ json.dump(users_db, f, indent=4, default=json_serializer)
88
 
89
  # --- Password & Token Functions ---
90
  def verify_password(plain_password, hashed_password):
 
131
  app = FastAPI(title="Media Auth API")
132
 
133
  # CORS middleware to allow the frontend to communicate with the API
 
134
  app.add_middleware(
135
  CORSMiddleware,
136
+ allow_origins=["*"],
137
  allow_credentials=True,
138
+ allow_methods=["*"],
139
+ allow_headers=["*"],
140
  )
141
 
142
  # --- Helper function to structure watch history for the frontend ---
143
+ # MODIFIED: Now includes the watch timestamp for each episode.
144
  def structure_watch_history(history_list: List[Dict]) -> Dict:
145
  """Transforms a flat list of watched episodes into a nested dictionary."""
146
  structured = {}
147
+ # Sort by timestamp to ensure the latest watch time is used if duplicates exist
148
+ sorted_history = sorted(history_list, key=lambda x: x.get("watch_timestamp", ""), reverse=True)
149
+
150
+ for item in sorted_history:
151
  show_id = item.get("show_id")
152
  show_title = item.get("show_title", "Unknown Show")
153
  season_num = item.get("season_number")
154
  episode_num = item.get("episode_number")
155
+ timestamp = item.get("watch_timestamp")
156
 
157
  if show_id not in structured:
158
  structured[show_id] = {
 
165
  "season_number": season_num,
166
  "episodes": {}
167
  }
168
+ # Store the timestamp instead of just 'True'
169
+ structured[show_id]["seasons"][season_num]["episodes"][episode_num] = timestamp
170
  return structured
171
 
172
 
 
204
  detail="Username already registered",
205
  )
206
  hashed_password = get_password_hash(user.password)
207
+ # Note: watch_history defaults to an empty list
208
  new_user = UserInDB(username=user.username, hashed_password=hashed_password)
209
+ # Pydantic's .dict() serializes the model to a dictionary suitable for JSON
210
  users_db[user.username] = new_user.dict()
211
  save_users(users_db)
212
  return {"message": "User created successfully. Please login."}
 
217
  """
218
  Fetch the profile of the currently authenticated user.
219
  """
 
220
  detailed_history = structure_watch_history(current_user.watch_history)
221
 
 
222
  user_public_data = UserPublic(
223
  username=current_user.username,
224
  profile_picture_url=current_user.profile_picture_url,
 
235
  """
236
  Upload or update the user's profile picture.
237
  """
 
238
  file_extension = os.path.splitext(file.filename)[1]
239
  unique_filename = f"{uuid.uuid4()}{file_extension}"
240
  file_path = os.path.join(UPLOAD_DIR, unique_filename)
241
 
 
242
  with open(file_path, "wb") as buffer:
243
  buffer.write(await file.read())
244
 
 
 
245
  profile_picture_url = f"/uploads/{unique_filename}"
246
  users_db = load_users()
247
  users_db[current_user.username]["profile_picture_url"] = profile_picture_url
248
  save_users(users_db)
249
 
 
250
  current_user.profile_picture_url = profile_picture_url
251
  detailed_history = structure_watch_history(current_user.watch_history)
252
  return UserPublic(
 
256
  )
257
 
258
 
259
+ # MODIFIED: Changed from POST to GET, accepts query parameters, and adds a timestamp.
260
+ @app.get("/users/me/watch-history", status_code=status.HTTP_200_OK, tags=["User"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  async def update_watch_history(
262
+ show_id: str = Query(..., description="The unique ID of the show (e.g., from TMDB)"),
263
+ show_title: str = Query(..., description="The title of the show"),
264
+ season_number: int = Query(..., ge=0, description="The season number"),
265
+ episode_number: int = Query(..., ge=1, description="The episode number"),
266
  current_user: UserInDB = Depends(get_current_user)
267
  ):
268
  """
269
+ Adds a new episode to the user's watch history via GET query parameters.
270
+ Includes a server-generated timestamp for when the episode was marked as watched.
271
  """
272
  users_db = load_users()
273
  user_data = users_db[current_user.username]
 
 
 
 
274
 
275
  # Create a unique identifier for the episode to prevent duplicates
276
+ episode_id = f"{show_id}_{season_number}_{episode_number}"
277
+
278
  # Check if this exact episode is already in the history
279
  is_already_watched = any(
280
  (f"{item.get('show_id')}_{item.get('season_number')}_{item.get('episode_number')}" == episode_id)
 
282
  )
283
 
284
  if not is_already_watched:
285
+ # Create a new entry using our Pydantic model for validation and structure
286
+ new_entry = WatchHistoryEntry(
287
+ show_id=show_id,
288
+ show_title=show_title,
289
+ season_number=season_number,
290
+ episode_number=episode_number,
291
+ watch_timestamp=datetime.utcnow() # Add server-side timestamp
292
+ )
293
+ # .dict() converts the model (including the datetime object) to a JSON-serializable dict
294
+ user_data["watch_history"].append(new_entry.dict())
295
  save_users(users_db)
296
  return {"message": "Watch history updated."}
297
+
298
  return {"message": "Episode already in watch history."}
299
 
300
 
301
  # --- Static File Serving ---
 
302
  app.mount("/uploads", StaticFiles(directory=UPLOAD_DIR), name="uploads")
 
 
 
303
  app.mount("/", StaticFiles(directory="static", html=True), name="static")
304
 
305
 
 
306
  @app.get("/", include_in_schema=False)
307
  def root():
308
  return RedirectResponse(url="/login.html")