diff --git a/server/app.py b/server/app.py index 80973f1..6fbca1d 100644 --- a/server/app.py +++ b/server/app.py @@ -648,13 +648,14 @@ def show_thumb(thumbnail_id: int) -> ResponseReturnValue: if thumbnail is None: # do send only 404 (not default thumbnail) as invalid id was requested return make_response(f"Not found", 404) + thumb_cache = thumbnail.receive() # TODO do not load data from database until send_file requires that return send_file( - io.BytesIO(thumbnail.receive_data()), - mimetype=thumbnail.mime_type, + io.BytesIO(thumb_cache.access_data()), + mimetype=thumb_cache.mime_type, etag=True, as_attachment=False, - last_modified=thumbnail.last_downloaded, + last_modified=thumb_cache.last_downloaded, max_age=24 * 60 * 60, ) diff --git a/server/entertainment_decider/models/__init__.py b/server/entertainment_decider/models/__init__.py index 942408a..931bf63 100644 --- a/server/entertainment_decider/models/__init__.py +++ b/server/entertainment_decider/models/__init__.py @@ -10,6 +10,7 @@ from .entities import ( MediaCollectionLink, MediaElement, MediaThumbnail, + MediaThumbnailCache, MediaUriMapping, Tag, Tagable, diff --git a/server/entertainment_decider/models/entities.py b/server/entertainment_decider/models/entities.py index 4f66782..1d48d8a 100644 --- a/server/entertainment_decider/models/entities.py +++ b/server/entertainment_decider/models/entities.py @@ -572,58 +572,59 @@ class MediaThumbnail(db.Entity): str, unique=True, ) - last_downloaded: datetime = orm.Optional( - datetime, - default=None, + + __cache_obj: MediaThumbnailCache = orm.Optional( + lambda: MediaThumbnailCache, nullable=True, ) + elements: Set[MediaElement] = orm.Set(lambda: MediaElement) + + @classmethod + def from_uri(cls, uri: str) -> MediaThumbnail: + return cls.get(uri=uri) or MediaThumbnail(uri=uri) + + def receive(self) -> MediaThumbnailCache: + return self.__cache_obj or MediaThumbnailCache.download(self) + + +class MediaThumbnailCache(db.Entity): + thumbnail: MediaThumbnail = orm.PrimaryKey( + lambda: MediaThumbnail, + auto=False, + ) + last_downloaded: datetime = orm.Required( + datetime, + ) last_accessed: datetime = orm.Optional( datetime, - default=None, nullable=True, ) - mime_type: str = orm.Optional( + mime_type: str = orm.Required( str, - default="", ) - data: bytes = orm.Optional( + _data: bytes = orm.Required( bytes, - default=None, - nullable=True, + column="data", lazy=True, # do not always preload huge image data ) - elements: Set[MediaElement] = orm.Set(lambda: MediaElement) - @classmethod - def from_uri(cls, uri: str) -> MediaThumbnail: - return cls.get(uri=uri) or MediaThumbnail(uri=uri) - - def access(self) -> None: - self.last_accessed = datetime.now() - - def download(self) -> bool: - res = requests.get(url=self.uri, headers=THUMBNAIL_HEADERS) + def download(cls, thumbnail: MediaThumbnail) -> MediaThumbnailCache: + res = requests.get(url=thumbnail.uri, headers=THUMBNAIL_HEADERS) mime = magic.from_buffer(res.content, mime=True) if mime not in THUMBNAIL_ALLOWED_TYPES: raise Exception(f"Couldn't download thumbnail: {thumbnail.uri}") - self.mime_type = mime - self.data = res.content - self.last_downloaded = datetime.now() - return True - - def prepare(self) -> bool: - if self.last_downloaded is not None: - return True - return self.download() - - def access_data(self) -> None: - self.prepare() - self.access() + now = datetime.now() + return cls( + thumbnail=thumbnail, + last_downloaded=now, + mime_type=mime, + _data=res.content, + ) - def receive_data(self) -> bytes: - self.access_data() - return self.data + def access_data(self) -> bytes: + self.last_accessed = datetime.now() + return self._data class MediaUriMapping(db.Entity):