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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +66 -49
app.py CHANGED
@@ -1,10 +1,10 @@
1
  import os
2
  import json
3
  import uuid
 
4
  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, Query
9
  from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
10
  from fastapi.staticfiles import StaticFiles
@@ -41,13 +41,12 @@ class Token(BaseModel):
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,26 +57,29 @@ class UserCreate(UserBase):
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):
65
  profile_picture_url: Optional[str] = None
66
  watch_history_detailed: Dict[str, Any] = Field(default_factory=dict)
 
67
 
 
 
 
 
68
 
69
  # --- Database Helper Functions (using JSON file) ---
70
  def load_users() -> Dict[str, Dict]:
71
  if not os.path.exists(USERS_DB_FILE):
72
  return {}
73
- with open(USERS_DB_FILE, "r") as f:
74
- try:
75
  return json.load(f)
76
- except json.JSONDecodeError:
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()
@@ -130,7 +132,6 @@ async def get_current_user(token: str = Depends(oauth2_scheme)) -> UserInDB:
130
  # --- FastAPI App Initialization ---
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=["*"],
@@ -139,12 +140,9 @@ app.add_middleware(
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:
@@ -154,6 +152,9 @@ def structure_watch_history(history_list: List[Dict]) -> Dict:
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] = {
159
  "show_id": show_id,
@@ -165,7 +166,6 @@ def structure_watch_history(history_list: List[Dict]) -> Dict:
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
 
@@ -174,12 +174,9 @@ def structure_watch_history(history_list: List[Dict]) -> Dict:
174
 
175
  @app.post("/token", response_model=Token, tags=["Authentication"])
176
  async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
177
- """
178
- Standard OAuth2 login. Takes username and password from a form.
179
- """
180
  users_db = load_users()
181
  user_data = users_db.get(form_data.username)
182
- if not user_data or not verify_password(form_data.password, user_data["hashed_password"]):
183
  raise HTTPException(
184
  status_code=status.HTTP_401_UNAUTHORIZED,
185
  detail="Incorrect username or password",
@@ -194,9 +191,6 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(
194
 
195
  @app.post("/signup", status_code=status.HTTP_201_CREATED, tags=["Authentication"])
196
  async def signup_user(user: UserCreate):
197
- """
198
- Creates a new user account.
199
- """
200
  users_db = load_users()
201
  if user.username in users_db:
202
  raise HTTPException(
@@ -204,9 +198,13 @@ async def signup_user(user: UserCreate):
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."}
@@ -214,13 +212,12 @@ async def signup_user(user: UserCreate):
214
 
215
  @app.get("/users/me", response_model=UserPublic, tags=["User"])
216
  async def read_users_me(current_user: UserInDB = Depends(get_current_user)):
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,
225
  watch_history_detailed=detailed_history
226
  )
@@ -232,10 +229,10 @@ async def upload_profile_picture(
232
  file: UploadFile = File(...),
233
  current_user: UserInDB = Depends(get_current_user)
234
  ):
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
 
@@ -251,53 +248,73 @@ async def upload_profile_picture(
251
  detailed_history = structure_watch_history(current_user.watch_history)
252
  return UserPublic(
253
  username=current_user.username,
 
254
  profile_picture_url=current_user.profile_picture_url,
255
  watch_history_detailed=detailed_history
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)
281
- for item in user_data["watch_history"]
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")
 
1
  import os
2
  import json
3
  import uuid
4
+ import secrets
5
  from datetime import datetime, timedelta
6
  from typing import List, Dict, Optional, Any
7
 
 
8
  from fastapi import FastAPI, Depends, HTTPException, status, UploadFile, File, Query
9
  from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
10
  from fastapi.staticfiles import StaticFiles
 
41
  class TokenData(BaseModel):
42
  username: Optional[str] = None
43
 
 
44
  class WatchHistoryEntry(BaseModel):
45
  show_id: str
46
  show_title: str
47
  season_number: int
48
  episode_number: int
49
+ watch_timestamp: datetime
50
 
51
  class UserBase(BaseModel):
52
  username: str
 
57
  class UserInDB(UserBase):
58
  hashed_password: str
59
  profile_picture_url: Optional[str] = None
 
60
  watch_history: List[Dict[str, Any]] = Field(default_factory=list)
61
 
62
  class UserPublic(UserBase):
63
  profile_picture_url: Optional[str] = None
64
  watch_history_detailed: Dict[str, Any] = Field(default_factory=dict)
65
+ email: Optional[str] = None # Added for display on settings page
66
 
67
+ # NEW: Model for the password change request
68
+ class PasswordChange(BaseModel):
69
+ current_password: str
70
+ new_password: str
71
 
72
  # --- Database Helper Functions (using JSON file) ---
73
  def load_users() -> Dict[str, Dict]:
74
  if not os.path.exists(USERS_DB_FILE):
75
  return {}
76
+ try:
77
+ with open(USERS_DB_FILE, "r") as f:
78
  return json.load(f)
79
+ except (json.JSONDecodeError, FileNotFoundError):
80
+ return {}
81
 
82
  def save_users(users_db: Dict[str, Dict]):
 
83
  def json_serializer(obj):
84
  if isinstance(obj, datetime):
85
  return obj.isoformat()
 
132
  # --- FastAPI App Initialization ---
133
  app = FastAPI(title="Media Auth API")
134
 
 
135
  app.add_middleware(
136
  CORSMiddleware,
137
  allow_origins=["*"],
 
140
  allow_headers=["*"],
141
  )
142
 
143
+ # --- Helper function to structure watch history ---
 
144
  def structure_watch_history(history_list: List[Dict]) -> Dict:
 
145
  structured = {}
 
146
  sorted_history = sorted(history_list, key=lambda x: x.get("watch_timestamp", ""), reverse=True)
147
 
148
  for item in sorted_history:
 
152
  episode_num = item.get("episode_number")
153
  timestamp = item.get("watch_timestamp")
154
 
155
+ if not all([show_id, season_num is not None, episode_num is not None, timestamp]):
156
+ continue
157
+
158
  if show_id not in structured:
159
  structured[show_id] = {
160
  "show_id": show_id,
 
166
  "season_number": season_num,
167
  "episodes": {}
168
  }
 
169
  structured[show_id]["seasons"][season_num]["episodes"][episode_num] = timestamp
170
  return structured
171
 
 
174
 
175
  @app.post("/token", response_model=Token, tags=["Authentication"])
176
  async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
 
 
 
177
  users_db = load_users()
178
  user_data = users_db.get(form_data.username)
179
+ if not user_data or not verify_password(form_data.password, user_data.get("hashed_password")):
180
  raise HTTPException(
181
  status_code=status.HTTP_401_UNAUTHORIZED,
182
  detail="Incorrect username or password",
 
191
 
192
  @app.post("/signup", status_code=status.HTTP_201_CREATED, tags=["Authentication"])
193
  async def signup_user(user: UserCreate):
 
 
 
194
  users_db = load_users()
195
  if user.username in users_db:
196
  raise HTTPException(
 
198
  detail="Username already registered",
199
  )
200
  hashed_password = get_password_hash(user.password)
201
+ # Using UserInDB model ensures all default fields like watch_history are present
202
+ new_user = UserInDB(
203
+ username=user.username,
204
+ hashed_password=hashed_password,
205
+ profile_picture_url=None,
206
+ watch_history=[]
207
+ )
208
  users_db[user.username] = new_user.dict()
209
  save_users(users_db)
210
  return {"message": "User created successfully. Please login."}
 
212
 
213
  @app.get("/users/me", response_model=UserPublic, tags=["User"])
214
  async def read_users_me(current_user: UserInDB = Depends(get_current_user)):
 
 
 
215
  detailed_history = structure_watch_history(current_user.watch_history)
216
 
217
+ # We add the email field for the UI, which is the same as the username
218
  user_public_data = UserPublic(
219
  username=current_user.username,
220
+ email=current_user.username,
221
  profile_picture_url=current_user.profile_picture_url,
222
  watch_history_detailed=detailed_history
223
  )
 
229
  file: UploadFile = File(...),
230
  current_user: UserInDB = Depends(get_current_user)
231
  ):
232
+ file_extension = os.path.splitext(file.filename)[1].lower()
233
+ if file_extension not in ['.png', '.jpg', '.jpeg', '.gif', '.webp']:
234
+ raise HTTPException(status_code=400, detail="Invalid file type.")
235
+
236
  unique_filename = f"{uuid.uuid4()}{file_extension}"
237
  file_path = os.path.join(UPLOAD_DIR, unique_filename)
238
 
 
248
  detailed_history = structure_watch_history(current_user.watch_history)
249
  return UserPublic(
250
  username=current_user.username,
251
+ email=current_user.username,
252
  profile_picture_url=current_user.profile_picture_url,
253
  watch_history_detailed=detailed_history
254
  )
255
 
256
 
 
257
  @app.get("/users/me/watch-history", status_code=status.HTTP_200_OK, tags=["User"])
258
  async def update_watch_history(
259
+ show_id: str = Query(...),
260
+ show_title: str = Query(...),
261
+ season_number: int = Query(..., ge=0),
262
+ episode_number: int = Query(..., ge=1),
263
  current_user: UserInDB = Depends(get_current_user)
264
  ):
 
 
 
 
265
  users_db = load_users()
266
  user_data = users_db[current_user.username]
 
 
267
  episode_id = f"{show_id}_{season_number}_{episode_number}"
268
 
 
269
  is_already_watched = any(
270
  (f"{item.get('show_id')}_{item.get('season_number')}_{item.get('episode_number')}" == episode_id)
271
+ for item in user_data.get("watch_history", [])
272
  )
273
 
274
  if not is_already_watched:
 
275
  new_entry = WatchHistoryEntry(
276
  show_id=show_id,
277
  show_title=show_title,
278
  season_number=season_number,
279
  episode_number=episode_number,
280
+ watch_timestamp=datetime.utcnow()
281
  )
282
+ user_data.setdefault("watch_history", []).append(new_entry.dict())
 
283
  save_users(users_db)
284
  return {"message": "Watch history updated."}
285
 
286
  return {"message": "Episode already in watch history."}
287
 
288
 
289
+ # NEW: Endpoint for changing the user's password
290
+ @app.post("/users/me/password", status_code=status.HTTP_200_OK, tags=["User"])
291
+ async def change_user_password(
292
+ password_data: PasswordChange,
293
+ current_user: UserInDB = Depends(get_current_user)
294
+ ):
295
+ """
296
+ Allows an authenticated user to change their password.
297
+ """
298
+ users_db = load_users()
299
+ user_data = users_db[current_user.username]
300
+
301
+ # 1. Verify the current password is correct
302
+ if not verify_password(password_data.current_password, user_data["hashed_password"]):
303
+ raise HTTPException(
304
+ status_code=status.HTTP_400_BAD_REQUEST,
305
+ detail="Incorrect current password",
306
+ )
307
+
308
+ # 2. Hash the new password
309
+ new_hashed_password = get_password_hash(password_data.new_password)
310
+
311
+ # 3. Update the password in the database
312
+ user_data["hashed_password"] = new_hashed_password
313
+ save_users(users_db)
314
+
315
+ return {"message": "Password updated successfully"}
316
+
317
+
318
  # --- Static File Serving ---
319
  app.mount("/uploads", StaticFiles(directory=UPLOAD_DIR), name="uploads")
320
  app.mount("/", StaticFiles(directory="static", html=True), name="static")