Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
Crossoufire committed Sep 28, 2024
2 parents 1c91155 + 1f58b01 commit 67fb18e
Show file tree
Hide file tree
Showing 112 changed files with 1,475 additions and 1,009 deletions.
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
## CHANGELOG v2.1.0
---

### Under the Hood

- Add `Redis-Server` to the backend
- Add `Flask-Limiter` to the backend
- Use Redis for `Flask-Cache`
- Add `gzip` compression with `nginx` for json responses
- Replace `datetime.utcnow` (deprecated python 3.12)

### Features

- Add privacy modes (`restricted` and `public`) `private` mode soon™
- Add `authors` to the book edition (in `/details`) (`manager` only)

### Fixes

- Fix tv refresh data from TMDB
- Fix changing a season was not resetting the episodes dropdown to 1 (in `/details` and `/list`)
- Fix possible to edit history when not current user (in `/profile`)
- Fix E00 for tv shows (in `/details` and `/list`)
- Fix oAuth2 new username could have > 14 characters
- Fix no Stale-While-Revalidate for the `/list` page
- Fix browser history and edit media (`/details`)
- Fix correct % of non-rated media in `/profile` (do not take into account the `plan to X` media)

## CHANGELOG v2.0.0
---

Expand Down
4 changes: 2 additions & 2 deletions backend/api/core/errors.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from datetime import datetime
from http import HTTPStatus
import json
import traceback
Expand All @@ -10,6 +9,7 @@

from backend.api.core import token_auth, basic_auth, current_user
from backend.api.schemas.core import ApiValidationError
from backend.api.utils.functions import naive_utcnow


errors = Blueprint("errors_api", __name__)
Expand Down Expand Up @@ -51,7 +51,7 @@ def log_error(error: Exception):
body=request.get_data(as_text=True),
user_id=current_user.id if current_user else None,
user_username=current_user.username if current_user else None,
timestamp=f"{datetime.utcnow().strftime('%d-%b-%Y %H:%M:%S')} UTC",
timestamp=f"{naive_utcnow().strftime('%d-%b-%Y %H:%M:%S')} UTC",
))

current_app.logger.error(
Expand Down
22 changes: 11 additions & 11 deletions backend/api/managers/ApiManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import json
import secrets
from datetime import datetime, timedelta
from datetime import timedelta
from io import BytesIO
from pathlib import Path
from typing import Dict, List, Literal, Optional, Type
Expand All @@ -19,7 +19,7 @@
from backend.api.managers.ModelsManager import ModelsManager
from backend.api.utils.enums import MediaType, ModelTypes
from backend.api.utils.functions import (clean_html_text, get, is_latin, format_datetime, resize_and_save_image,
reorder_seas_eps, global_limiter)
reorder_seas_eps, global_limiter, naive_utcnow)


""" --- GENERAL --------------------------------------------------------------------------------------------- """
Expand Down Expand Up @@ -291,7 +291,7 @@ def _format_api_data(self, bulk: bool = False):
origin_country=get(self.api_data, "origin_country", 0),
created_by=self._format_creators(),
api_id=self.api_data["id"],
last_api_update=datetime.utcnow(),
last_api_update=naive_utcnow(),
image_cover=self._get_media_cover(),
next_episode_to_air=None,
season_to_air=None,
Expand Down Expand Up @@ -372,7 +372,7 @@ def get_changed_api_ids(self) -> List[int]:
query = media_model.query.with_entities(media_model.api_id).filter(
media_model.lock_status.is_not(True),
media_model.api_id.in_(changed_api_ids),
media_model.last_api_update < datetime.utcnow() - timedelta(seconds=86000),
media_model.last_api_update < naive_utcnow() - timedelta(seconds=86000),
).all()

return [tv_id[0] for tv_id in query]
Expand Down Expand Up @@ -498,7 +498,7 @@ def _format_api_data(self, bulk: bool = False):
api_id=self.api_data.get("id"),
director_name=None,
image_cover=self._get_media_cover(),
last_api_update=datetime.utcnow(),
last_api_update=naive_utcnow(),
)

all_crew = get(self.api_data, "credits", "crew", default=[])
Expand All @@ -520,8 +520,8 @@ def get_changed_api_ids(self) -> List[int]:

query = media_model.query.with_entities(media_model.api_id).filter(
media_model.lock_status.is_not(True),
media_model.last_api_update < datetime.utcnow() - timedelta(days=7),
or_(media_model.release_date > datetime.utcnow() - timedelta(days=90), media_model.release_date.is_(None)),
media_model.last_api_update < naive_utcnow() - timedelta(days=7),
or_(media_model.release_date > naive_utcnow() - timedelta(days=90), media_model.release_date.is_(None)),
).all()

return [movie_id[0] for movie_id in query]
Expand Down Expand Up @@ -644,7 +644,7 @@ def _format_api_data(self, bulk: bool = False):
player_perspective=get(self.api_data, "player_perspectives", 0, "name"),
game_modes=",".join([g.get("name") for g in get(self.api_data, "game_modes", default=[])]),
api_id=self.api_data["id"],
last_api_update=datetime.utcnow(),
last_api_update=naive_utcnow(),
image_cover=self._get_media_cover(),
)

Expand Down Expand Up @@ -713,8 +713,8 @@ def get_changed_api_ids(self) -> List[int]:
model = ModelsManager.get_unique_model(self.GROUP, ModelTypes.MEDIA)

query = model.query.with_entities(model.api_id).filter(
or_(model.release_date > datetime.utcnow(), model.release_date.is_(None)),
model.last_api_update < datetime.utcnow() - timedelta(days=7),
or_(model.release_date > naive_utcnow(), model.release_date.is_(None)),
model.last_api_update < naive_utcnow() - timedelta(days=7),
).all()

return [int(game_id[0]) for game_id in query]
Expand Down Expand Up @@ -787,7 +787,7 @@ def _format_api_data(self, bulk: bool = False):
language=get(self.api_data, "language"),
release_date=format_datetime(self.api_data.get("publishedDate")),
image_cover=self._get_media_cover(),
last_api_update=datetime.utcnow(),
last_api_update=naive_utcnow(),
lock_status=True,
)

Expand Down
31 changes: 20 additions & 11 deletions backend/api/managers/TasksManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from backend.api.managers.ModelsManager import ModelsManager
from backend.api.models.user import User, UserMediaUpdate, Notifications, UserMediaSettings
from backend.api.utils.enums import MediaType, ModelTypes, NotificationType, Status
from backend.api.utils.functions import naive_utcnow


class TasksManagerMeta(type):
Expand Down Expand Up @@ -56,20 +57,28 @@ def _initialize_media_models(self):
def get_subclass(cls, media_type: MediaType) -> Type[TasksManager]:
return cls.subclasses.get(media_type, cls)

@staticmethod
def change_privacy_setting(user: str | int, privacy: str):
filter_ = {"username": user} if isinstance(user, str) else {"id": user}
user = User.query.filter_by(**filter_).first()
if not user:
raise ValueError(f"User not found with criteria: {filter_}")
user.privacy = privacy
db.session.commit()

@staticmethod
def reactivate_update_modal(value: bool = True):
db.session.execute(db.update(User).values(show_update_modal=value))
db.session.commit()

@staticmethod
def activate_user_account(username: str, toggle: bool):
def activate_user_account(user: str | int, toggle: bool):
""" Toggle users accounts activation manually """

user = User.query.filter(User.username == username).first()
filter_ = {"username": user} if isinstance(user, str) else {"id": user}
user = User.query.filter_by(**filter_).first()
if not user:
print(f"User {username} not found")
return

raise ValueError(f"User not found with criteria: {filter_}")
user.active = toggle
db.session.commit()

Expand Down Expand Up @@ -197,7 +206,7 @@ def remove_all_old_covers(self):

if len(images_to_remove) > 100:
current_app.logger.error(f"Too many images to remove ({len(images_to_remove)}). Validation necessary.")
raise Exception("Too many images to remove")
return

count = 0
current_app.logger.info(f"Deleting {self.GROUP.value} covers...")
Expand Down Expand Up @@ -247,8 +256,8 @@ def add_notifications(self):
.join(self.media_list, self.media.id == self.media_list.media_id)
.filter(
self.media.release_date.is_not(None),
self.media.release_date > datetime.utcnow(),
self.media.release_date <= datetime.utcnow() + timedelta(days=self.media.RELEASE_WINDOW),
self.media.release_date > naive_utcnow(),
self.media.release_date <= naive_utcnow() + timedelta(days=self.media.RELEASE_WINDOW),
).all()
)

Expand Down Expand Up @@ -291,8 +300,8 @@ def add_notifications(self):
.join(top_eps_subq, self.media.id == top_eps_subq.c.media_id)
.filter(
self.media.next_episode_to_air.is_not(None),
self.media.next_episode_to_air > datetime.utcnow(),
self.media.next_episode_to_air <= datetime.utcnow() + timedelta(days=self.media.RELEASE_WINDOW),
self.media.next_episode_to_air > naive_utcnow(),
self.media.next_episode_to_air <= naive_utcnow() + timedelta(days=self.media.RELEASE_WINDOW),
self.media_list.status.notin_([Status.RANDOM, Status.DROPPED]),
).all()
)
Expand Down Expand Up @@ -345,7 +354,7 @@ def remove_non_list_media(self):
current_app.logger.info(f"Movies successfully deleted")

def automatic_locking(self):
locking_threshold = datetime.utcnow() - timedelta(days=self.media.LOCKING_DAYS)
locking_threshold = naive_utcnow() - timedelta(days=self.media.LOCKING_DAYS)
locked_movies = (
self.media.query.filter(
self.media.lock_status.is_not(True),
Expand Down
30 changes: 21 additions & 9 deletions backend/api/models/abstracts.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
from __future__ import annotations
from datetime import datetime

from typing import List, Dict, Optional

from flask import url_for
from sqlalchemy import func, desc

from backend.api import db
from backend.api.core import current_user
from backend.api.managers.ModelsManager import ModelsManager
from backend.api.models.mixins import UpdateMixin
from backend.api.models.user import User, followers, UserMediaUpdate
from backend.api.utils.enums import ModelTypes, Status, MediaType
from backend.api.utils.functions import safe_div
from backend.api.utils.functions import safe_div, naive_utcnow


class Media(db.Model, UpdateMixin):
Expand Down Expand Up @@ -77,18 +79,18 @@ def in_follows_lists(self) -> List[Dict]:

return data

def get_user_list_info(self, label_class: Labels) -> Optional[Dict]:
def get_user_media_info(self, label_class: Labels) -> Optional[Dict]:
media_assoc = self.list_info.filter_by(user_id=current_user.id).first()
user_data = media_assoc.to_dict() if media_assoc else None
user_media = media_assoc.to_dict() if media_assoc else None

if user_data:
user_data.update(dict(
if user_media:
user_media.update(dict(
username=current_user.username,
labels=label_class.get_user_media_labels(user_id=current_user.id, media_id=self.id),
history=UserMediaUpdate.get_history(self.id, self.GROUP)),
)

return user_data
return user_media


class MediaList(db.Model):
Expand All @@ -108,6 +110,8 @@ class MediaList(db.Model):

@classmethod
def get_media_count_per_status(cls, user_id: int) -> Dict:
plan_to_x = (Status.PLAN_TO_WATCH, Status.PLAN_TO_PLAY, Status.PLAN_TO_READ)

media_count = (
db.session.query(cls.status, func.count(cls.status))
.filter_by(user_id=user_id).group_by(cls.status)
Expand All @@ -116,6 +120,7 @@ def get_media_count_per_status(cls, user_id: int) -> Dict:

status_count = {status.value: {"count": 0, "percent": 0} for status in Status.by(cls.GROUP)}
total_media = sum(count for _, count in media_count)
total_media_no_plan_to_x = total_media - sum(c for s, c in media_count if s in plan_to_x)
no_data = (total_media == 0)

# Update <status_count> dict with actual values from <media_count> query
Expand All @@ -129,7 +134,14 @@ def get_media_count_per_status(cls, user_id: int) -> Dict:

status_list = [{"status": key, **val} for key, val in status_count.items()]

return dict(total_media=total_media, no_data=no_data, status_count=status_list)
data = dict(
total_media=total_media,
total_media_no_plan_to_x=total_media_no_plan_to_x,
no_data=no_data,
status_count=status_list
)

return data

@classmethod
def get_media_count_per_rating(cls, user: User) -> List[int]:
Expand Down Expand Up @@ -211,7 +223,7 @@ def get_coming_next(cls) -> List[Dict]:
next_media = (
db.session.query(media).join(cls, media.id == cls.media_id)
.filter(
getattr(media, media_date) > datetime.utcnow(),
getattr(media, media_date) > naive_utcnow(),
cls.user_id == current_user.id,
cls.status.notin_([Status.DROPPED, Status.RANDOM]),
).order_by(getattr(media, media_date))
Expand Down
23 changes: 13 additions & 10 deletions backend/api/models/books.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from __future__ import annotations

from typing import List, Dict, Tuple
from typing import List, Dict

from flask import abort
from sqlalchemy import func, ColumnElement
from sqlalchemy import func

from backend.api import db
from backend.api.core import current_user
Expand Down Expand Up @@ -155,14 +155,6 @@ def get_available_sorting(cls, is_feeling: bool) -> Dict:
def total_user_time_def(cls):
return func.sum(cls.TIME_PER_PAGE * cls.total)

@classmethod
def additional_search_joins(cls) -> List[Tuple]:
return [(BooksAuthors, BooksAuthors.media_id == Books.id)]

@classmethod
def additional_search_filters(cls, search: str) -> List[ColumnElement]:
return [Books.name.ilike(f"%{search}%"), BooksAuthors.name.ilike(f"%{search}%")]


class BooksGenre(Genres):
GROUP = MediaType.BOOKS
Expand Down Expand Up @@ -197,6 +189,17 @@ class BooksAuthors(db.Model):
# --- Relationships -----------------------------------------------------------
media = db.relationship("Books", back_populates="authors", lazy="select")

@classmethod
def replace_authors(cls, authors: str, media_id: int):
try:
authors = authors.split(", ")
except:
return

cls.query.filter_by(media_id=media_id).delete()
db.session.add_all([cls(media_id=media_id, name=author) for author in authors])
db.session.commit()


class BooksLabels(Labels):
GROUP = MediaType.BOOKS
Expand Down
14 changes: 2 additions & 12 deletions backend/api/models/games.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import Dict, List, Tuple
from typing import Dict, List

from flask import abort
from sqlalchemy import func, ColumnElement
Expand Down Expand Up @@ -110,7 +110,7 @@ def to_dict(self) -> Dict:
media_dict["platform"] = self.platform.value if self.platform else None
media_dict["media_name"] = self.media.name
media_dict["all_status"] = Status.by(self.GROUP)
media_dict["all_platforms"] = GamesPlatformsEnum.to_list()
media_dict["all_platforms"] = [platform.value for platform in GamesPlatformsEnum]
media_dict["rating"] = {
"type": "feeling" if is_feeling else "score",
"value": self.feeling if is_feeling else self.score
Expand Down Expand Up @@ -153,16 +153,6 @@ def get_available_sorting(cls, is_feeling: bool) -> Dict[str, ColumnElement]:
def total_user_time_def(cls):
return func.sum(cls.playtime)

@classmethod
def additional_search_joins(cls) -> List[Tuple]:
return [(GamesPlatforms, GamesPlatforms.media_id == Games.id),
(GamesCompanies, GamesCompanies.media_id == Games.id)]

@classmethod
def additional_search_filters(cls, search: str) -> List[ColumnElement]:
return [Games.name.ilike(f"%{search}%"), GamesPlatforms.name.ilike(f"%{search}%"),
GamesCompanies.name.ilike(f"%{search}%")]


class GamesGenre(Genres):
GROUP = MediaType.GAMES
Expand Down
Loading

0 comments on commit 67fb18e

Please sign in to comment.