From 6427cf3732b7e0feec4d7806d9672a4b63a9e9fb Mon Sep 17 00:00:00 2001 From: FuzzyGrim Date: Wed, 27 Sep 2023 22:57:48 +0200 Subject: [PATCH] change save images locally to use external cdns --- .dockerignore | 2 - .gitignore | 2 - docker-compose.postgres.yml | 2 - docker-compose.yml | 3 +- entrypoint.sh | 1 - ruff.toml | 2 +- src/app/context_processors.py | 6 +- ..._anime_image_alter_manga_image_and_more.py | 37 ++++++++++ src/app/models.py | 2 +- src/app/tests/test_media.py | 5 +- src/app/utils/form_handlers.py | 7 +- src/app/utils/helpers.py | 74 ------------------- src/app/utils/metadata.py | 36 ++++----- src/app/utils/search.py | 8 +- src/config/settings.py | 2 + src/config/uwsgi.ini | 2 - src/integrations/tests/test_imports.py | 18 ++--- src/integrations/utils/imports/anilist.py | 17 +---- src/integrations/utils/imports/mal.py | 18 ++--- src/integrations/utils/imports/tmdb.py | 17 +---- src/integrations/utils/imports/yamtrack.py | 42 ----------- src/media/none.svg | 12 --- src/static/css/_structure.scss | 30 ++++---- src/templates/app/home.html | 14 ++-- src/templates/app/media_details.html | 21 +++--- src/templates/app/media_grid.html | 4 +- src/templates/app/media_table.html | 2 +- src/templates/app/search.html | 3 +- src/templates/app/season_details.html | 10 +-- 29 files changed, 132 insertions(+), 267 deletions(-) create mode 100644 src/app/migrations/0002_alter_anime_image_alter_manga_image_and_more.py delete mode 100644 src/media/none.svg diff --git a/.dockerignore b/.dockerignore index eec515cc..beefd284 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,5 +6,3 @@ venv src/static/css/main.css src/static/css/main.css.map src/db/db.sqlite3 -src/media/* -!src/media/none.svg \ No newline at end of file diff --git a/.gitignore b/.gitignore index eec515cc..beefd284 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,3 @@ venv src/static/css/main.css src/static/css/main.css.map src/db/db.sqlite3 -src/media/* -!src/media/none.svg \ No newline at end of file diff --git a/docker-compose.postgres.yml b/docker-compose.postgres.yml index 1990260a..6cec774b 100644 --- a/docker-compose.postgres.yml +++ b/docker-compose.postgres.yml @@ -21,8 +21,6 @@ services: - DB_USER=yamtrack - DB_PASS=yamtrack - DB_PORT=5432 - volumes: - - media:/yamtrack/media ports: - "8000:8000" diff --git a/docker-compose.yml b/docker-compose.yml index d501b6d6..37f7a106 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: "3" services: yamtrack: container_name: yamtrack - image: ghcr.io/fuzzygrim/yamtrack + build: . restart: unless-stopped environment: - TMDB_API=TMDB_API_KEY @@ -16,7 +16,6 @@ services: # - REGISTRATION=False volumes: - ./db:/yamtrack/db - - media:/yamtrack/media ports: - "8000:8000" diff --git a/entrypoint.sh b/entrypoint.sh index abc8c29a..92a932a5 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -10,6 +10,5 @@ PGID=${PGID:-911} groupmod -o -g "$PGID" abc usermod -o -u "$PUID" abc chown -R abc:abc db -chown -R abc:abc media exec gosu abc:abc uwsgi --ini /yamtrack/config/uwsgi.ini \ No newline at end of file diff --git a/ruff.toml b/ruff.toml index 5c597cd3..830e70d3 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,7 +1,7 @@ select = ["ALL"] # Never enforce `E501` (line length violations). This is handled by Black. -ignore = ["E501"] +ignore = ["E501", "D100", "D104", "D202", "D203", "D213", "PT009", "PTH123", "PT027", "RUF012"] # Allow autofix for all enabled rules (when `--fix`) is provided. fixable = ["ALL"] diff --git a/src/app/context_processors.py b/src/app/context_processors.py index 29a8686d..c330f9d2 100644 --- a/src/app/context_processors.py +++ b/src/app/context_processors.py @@ -1,9 +1,13 @@ # https://docs.djangoproject.com/en/4.2/ref/templates/api/#writing-your-own-context-processors +from config import settings from decouple import config from django.http import HttpRequest def export_vars(request: HttpRequest) -> dict: # noqa: ARG001 """Export variables to templates.""" - return {"REGISTRATION": config("REGISTRATION", default=True, cast=bool)} + return { + "REGISTRATION": config("REGISTRATION", default=True, cast=bool), + "IMG_NONE": settings.IMG_NONE, + } diff --git a/src/app/migrations/0002_alter_anime_image_alter_manga_image_and_more.py b/src/app/migrations/0002_alter_anime_image_alter_manga_image_and_more.py new file mode 100644 index 00000000..6b5f7214 --- /dev/null +++ b/src/app/migrations/0002_alter_anime_image_alter_manga_image_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2 on 2023-09-27 20:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("app", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="anime", + name="image", + field=models.URLField(), + ), + migrations.AlterField( + model_name="manga", + name="image", + field=models.URLField(), + ), + migrations.AlterField( + model_name="movie", + name="image", + field=models.URLField(), + ), + migrations.AlterField( + model_name="season", + name="image", + field=models.URLField(), + ), + migrations.AlterField( + model_name="tv", + name="image", + field=models.URLField(), + ), + ] diff --git a/src/app/models.py b/src/app/models.py index 06d8da03..5230d5bf 100644 --- a/src/app/models.py +++ b/src/app/models.py @@ -13,7 +13,7 @@ class Media(models.Model): media_id = models.PositiveIntegerField() title = models.CharField(max_length=255) - image = models.ImageField() + image = models.URLField() score = models.DecimalField( null=True, blank=True, diff --git a/src/app/tests/test_media.py b/src/app/tests/test_media.py index 4c8136cc..bcd1724d 100644 --- a/src/app/tests/test_media.py +++ b/src/app/tests/test_media.py @@ -4,6 +4,7 @@ import shutil from datetime import date from unittest.mock import patch +from config import settings from django.test import TestCase, override_settings from django.urls import reverse from users.models import User @@ -344,7 +345,7 @@ class DetailsMedia(TestCase): # anime without picture, synopsis, duration and genres response = metadata.anime_manga("anime", "0") self.assertEqual(response["title"], "Unknown Example") - self.assertEqual(response["image"], "none.svg") + self.assertEqual(response["image"], settings.IMG_NONE) self.assertEqual(response["synopsis"], "No synopsis available.") self.assertEqual(response["runtime"], "Unknown") self.assertEqual(response["genres"], [{"name": "Unknown"}]) @@ -377,7 +378,7 @@ class DetailsMedia(TestCase): response = metadata.movie("0") self.assertEqual(response["title"], "Unknown Movie") - self.assertEqual(response["image"], "none.svg") + self.assertEqual(response["image"], settings.IMG_NONE) self.assertEqual(response["start_date"], "Unknown") self.assertEqual(response["synopsis"], "No synopsis available.") self.assertEqual(response["runtime"], "Unknown") diff --git a/src/app/utils/form_handlers.py b/src/app/utils/form_handlers.py index 640d7070..9379d7f7 100644 --- a/src/app/utils/form_handlers.py +++ b/src/app/utils/form_handlers.py @@ -70,12 +70,10 @@ def media_save( instance = model.objects.get(**search_params) except model.DoesNotExist: # If the model instance doesn't exist, create a new one - if media_metadata["image"] != "none.svg": - image = helpers.download_image(media_metadata["image"], media_type) default_params = { "user": request.user, "title": media_metadata["title"], - "image": image, + "image": media_metadata["image"], } if season_number is not None: default_params["season_number"] = season_number @@ -136,11 +134,10 @@ def episode_form_handler( season_number=season_number, ) except Season.DoesNotExist: - image = helpers.download_image(season_metadata["image"], "season") related_season = Season.objects.create( media_id=media_id, title=season_metadata["title"], - image=image, + image=season_metadata["image"], score=None, status="Watching", notes="", diff --git a/src/app/utils/helpers.py b/src/app/utils/helpers.py index 3fc6e39c..00aface6 100644 --- a/src/app/utils/helpers.py +++ b/src/app/utils/helpers.py @@ -1,12 +1,5 @@ -import asyncio import logging -import pathlib -import aiofiles -import aiohttp -import requests -import requests_cache -from django.conf import settings from django.http import HttpRequest from app.forms import AnimeForm, MangaForm, MovieForm, SeasonForm, TVForm @@ -15,62 +8,6 @@ from app.models import TV, Anime, Manga, Movie, Season logger = logging.getLogger(__name__) -def download_image(url: str, media_type: str) -> str: - """Download an image from the given URL and saves it to the media directory. - - Returns the filename of the downloaded image. - """ - - filename = get_image_filename(url, media_type) - - location = f"{settings.MEDIA_ROOT}/{filename}" - - # download image if it doesn't exist - if not pathlib.Path(location).is_file(): - with requests_cache.disabled(): # don't cache images - r = requests.get(url, timeout=10) - with open(location, "wb") as f: - f.write(r.content) - - return filename - - -async def images_downloader(images_to_download: list, media_type: str) -> None: - """Create tasks to download images asynchronously.""" - - async with aiohttp.ClientSession() as session: - tasks = [ - download_image_async(session, url, media_type) for url in images_to_download - ] - await asyncio.gather(*tasks) - - -async def download_image_async( - session: aiohttp.ClientSession, - url: str, - media_type: str, -) -> None: - """Download images asynchronously using aiohttp.""" - - filename = get_image_filename(url, media_type) - location = f"{settings.MEDIA_ROOT}/{filename}" - - # download image if it doesn't exist - if not pathlib.Path(location).is_file(): - with requests_cache.disabled(): # don't cache images - async with session.get(url) as resp: - f = await aiofiles.open(location, mode="wb") - await f.write(await resp.read()) - await f.close() - - -def get_image_filename(url: str, media_type: str) -> str: - """Return filename of image based on the media type and last element of URL.""" - # rsplit is used to split the url at the last / and taking the last element - # https://api-cdn.myanimelist.net/images/anime/12/76049.jpg -> 76049.jpg - return f"{media_type}-{url.rsplit('/', 1)[-1]}" - - def get_client_ip(request: HttpRequest) -> str: """Return the client's IP address. @@ -102,10 +39,6 @@ def media_type_mapper(media_type: str) -> dict: ("start_date", "Start Date"), ("end_date", "End Date"), ], - "img_url": { - "mal": "https://api-cdn.myanimelist.net/images/manga/{media_id}/{image_file}", - "anilist": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/{image_file}", - }, }, "anime": { "model": Anime, @@ -118,10 +51,6 @@ def media_type_mapper(media_type: str) -> dict: ("start_date", "Start Date"), ("end_date", "End Date"), ], - "img_url": { - "mal": "https://api-cdn.myanimelist.net/images/anime/{media_id}/{image_file}", - "anilist": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/{image_file}", - }, }, "movie": { "model": Movie, @@ -132,7 +61,6 @@ def media_type_mapper(media_type: str) -> dict: ("title", "Title"), ("end_date", "End Date"), ], - "img_url": "https://image.tmdb.org/t/p/w500/{image_file}", }, "tv": { "model": TV, @@ -142,7 +70,6 @@ def media_type_mapper(media_type: str) -> dict: ("-score", "Score"), ("title", "Title"), ], - "img_url": "https://image.tmdb.org/t/p/w500/{image_file}", }, "season": { "model": Season, @@ -152,7 +79,6 @@ def media_type_mapper(media_type: str) -> dict: ("-score", "Score"), ("title", "Title"), ], - "img_url": "https://image.tmdb.org/t/p/w500/{image_file}", }, } return media_mapping[media_type] diff --git a/src/app/utils/metadata.py b/src/app/utils/metadata.py index 0e9d247e..8e9b873b 100644 --- a/src/app/utils/metadata.py +++ b/src/app/utils/metadata.py @@ -1,4 +1,5 @@ import requests +from config import settings from decouple import config TMDB_API = config("TMDB_API", default=None) @@ -23,7 +24,9 @@ def anime_manga(media_type: str, media_id: str) -> dict: url = f"https://api.myanimelist.net/v2/{media_type}/{media_id}?fields=title,main_picture,media_type,start_date,synopsis,status,genres,num_episodes,num_chapters,average_episode_duration,related_anime,related_manga,recommendations" response = requests.get( - url, headers={"X-MAL-CLIENT-ID": MAL_API}, timeout=5 + url, + headers={"X-MAL-CLIENT-ID": MAL_API}, + timeout=5, ).json() if response["media_type"] == "tv": @@ -35,7 +38,7 @@ def anime_manga(media_type: str, media_id: str) -> dict: if "main_picture" in response: response["image"] = response["main_picture"]["large"] else: - response["image"] = "none.svg" + response["image"] = settings.IMG_NONE # Map status to human-readable values status_map = { @@ -79,12 +82,11 @@ def anime_manga(media_type: str, media_id: str) -> dict: if "main_picture" in item["node"]: item["node"]["image"] = item["node"]["main_picture"]["large"] else: - item["node"]["image"] = "none.svg" + item["node"]["image"] = settings.IMG_NONE item.update(item["node"]) response[key] = items - return response @@ -103,11 +105,9 @@ def tv(media_id: str) -> dict: # tmdb will either not return the key or return an empty value/string if response["poster_path"]: - response[ - "image" - ] = f"https://image.tmdb.org/t/p/w500{response['poster_path']}" + response["image"] = f"https://image.tmdb.org/t/p/w500{response['poster_path']}" else: - response["image"] = "none.svg" + response["image"] = settings.IMG_NONE # tv shows have name instead of title response["title"] = response["name"] @@ -142,11 +142,9 @@ def tv(media_id: str) -> dict: items = response.get(key) for item in items: if item["poster_path"]: - item[ - "image" - ] = f"https://image.tmdb.org/t/p/w500{item['poster_path']}" + item["image"] = f"https://image.tmdb.org/t/p/w500{item['poster_path']}" else: - item["image"] = "none.svg" + item["image"] = settings.IMG_NONE item["title"] = item["name"] @@ -171,11 +169,9 @@ def movie(media_id: str) -> dict: # tmdb will either not return the key or return an empty value/string if response["poster_path"]: - response[ - "image" - ] = f"https://image.tmdb.org/t/p/w500{response['poster_path']}" + response["image"] = f"https://image.tmdb.org/t/p/w500{response['poster_path']}" else: - response["image"] = "none.svg" + response["image"] = settings.IMG_NONE if "release_date" in response and response["release_date"] != "": response["start_date"] = response["release_date"] @@ -206,7 +202,7 @@ def movie(media_id: str) -> dict: "image" ] = f"https://image.tmdb.org/t/p/w500{recommendation['poster_path']}" else: - recommendation["image"] = "none.svg" + recommendation["image"] = settings.IMG_NONE return response @@ -218,11 +214,9 @@ def season(tv_id: str, season_number: int) -> dict: response = requests.get(url, timeout=5).json() if response["poster_path"]: - response[ - "image" - ] = f"https://image.tmdb.org/t/p/w500{response['poster_path']}" + response["image"] = f"https://image.tmdb.org/t/p/w500{response['poster_path']}" else: - response["image"] = "none.svg" + response["image"] = settings.IMG_NONE if response["air_date"] != "": response["start_date"] = response["air_date"] diff --git a/src/app/utils/search.py b/src/app/utils/search.py index 766e7955..36eca422 100644 --- a/src/app/utils/search.py +++ b/src/app/utils/search.py @@ -1,6 +1,7 @@ import logging import requests +from config import settings from decouple import config logger = logging.getLogger(__name__) @@ -30,7 +31,7 @@ def tmdb(media_type: str, query: str) -> list: if media["poster_path"]: media["image"] = f"https://image.tmdb.org/t/p/w500{media['poster_path']}" else: - media["image"] = "none.svg" + media["image"] = settings.IMG_NONE return response @@ -43,7 +44,8 @@ def mal(media_type: str, query: str) -> list: if "error" in response: if response["error"] == "forbidden": logger.error( - "MAL: %s, probably no API key provided", response["error"].title(), + "MAL: %s, probably no API key provided", + response["error"].title(), ) elif response["error"] == "bad_request": if response["message"] == "invalid q": @@ -66,7 +68,7 @@ def mal(media_type: str, query: str) -> list: if "main_picture" in media["node"]: media["image"] = media["node"]["main_picture"]["large"] else: - media["image"] = "none.svg" + media["image"] = settings.IMG_NONE # remove node layer media.update(media.pop("node")) return response diff --git a/src/config/settings.py b/src/config/settings.py index 5ef703fd..c1bf303b 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -224,3 +224,5 @@ SASS_OUTPUT_STYLE = "compact" DEBUG_TOOLBAR_CONFIG = { "SKIP_TEMPLATE_PREFIXES": ("app/components/"), } + +IMG_NONE = "https://www.themoviedb.org/assets/2/v4/glyphicons/basic/glyphicons-basic-38-picture-grey-c2ebdbb057f2a7614185931650f8cee23fa137b93812ccb132b9df511df1cfac.svg" \ No newline at end of file diff --git a/src/config/uwsgi.ini b/src/config/uwsgi.ini index c8b14f49..22be757c 100644 --- a/src/config/uwsgi.ini +++ b/src/config/uwsgi.ini @@ -14,9 +14,7 @@ disable-logging=true static-map = /favicon.ico=/yamtrack/static/favicon.ico static-map = /static=/yamtrack/static -static-map = /media=/yamtrack/media static-expires = /yamtrack/static/* 7776000 -static-expires = /yamtrack/media/* 7776000 offload-threads = %k max-fd=65535 diff --git a/src/integrations/tests/test_imports.py b/src/integrations/tests/test_imports.py index 4912b57b..2753fe99 100644 --- a/src/integrations/tests/test_imports.py +++ b/src/integrations/tests/test_imports.py @@ -3,6 +3,7 @@ import os from unittest.mock import MagicMock, patch from app.models import TV, Anime, Episode, Manga, Movie, Season +from config import settings from django.test import TestCase from users.models import User @@ -20,8 +21,7 @@ class ImportMAL(TestCase): self.user = User.objects.create_user(**self.credentials) @patch("requests.get") - @patch("app.utils.helpers.images_downloader", return_value=None) - def test_import_animelist(self: "ImportMAL", mock_asyncio, mock_request) -> None: + def test_import_animelist(self: "ImportMAL", mock_request) -> None: """Basic test importing anime and manga from MyAnimeList.""" with open(mock_path + "/import_mal_anime.json") as file: @@ -40,7 +40,7 @@ class ImportMAL(TestCase): self.assertEqual(Manga.objects.filter(user=self.user).count(), 2) self.assertEqual( Anime.objects.get(user=self.user, title="Ama Gli Animali").image - == "none.svg", + == settings.IMG_NONE, True, ) self.assertEqual( @@ -68,8 +68,7 @@ class ImportTMDB(TestCase): self.credentials = {"username": "test", "password": "12345"} self.user = User.objects.create_user(**self.credentials) - @patch("app.utils.helpers.images_downloader", return_value=None) - def test_tmdb_import_ratings(self: "ImportTMDB", mock_asyncio) -> None: + def test_tmdb_import_ratings(self: "ImportTMDB") -> None: """Test importing ratings from TMDB.""" with open(mock_path + "/import_tmdb_ratings.csv", "rb") as file: @@ -77,8 +76,7 @@ class ImportTMDB(TestCase): self.assertEqual(Movie.objects.filter(user=self.user).count(), 2) self.assertEqual(TV.objects.filter(user=self.user).count(), 1) - @patch("app.utils.helpers.images_downloader", return_value=None) - def test_tmdb_import_watchlist(self: "ImportTMDB", mock_asyncio) -> None: + def test_tmdb_import_watchlist(self: "ImportTMDB") -> None: """Test importing watchlist from TMDB.""" with open(mock_path + "/import_tmdb_watchlist.csv", "rb") as file: @@ -98,8 +96,7 @@ class ImportAniList(TestCase): self.user = User.objects.create_user(**self.credentials) @patch("requests.post") - @patch("app.utils.helpers.images_downloader", return_value=None) - def test_import_anilist(self: "ImportAniList", mock_asyncio, mock_request) -> None: + def test_import_anilist(self: "ImportAniList", mock_request) -> None: """Basic test importing anime and manga from AniList.""" with open(mock_path + "/import_anilist.json") as file: @@ -134,8 +131,7 @@ class ImportYamtrack(TestCase): self.credentials = {"username": "test", "password": "12345"} self.user = User.objects.create_user(**self.credentials) - @patch("app.utils.helpers.images_downloader", return_value=None) - def test_import_yamtrack(self: "ImportYamtrack", mock_asyncio) -> None: + def test_import_yamtrack(self: "ImportYamtrack") -> None: """Basic test importing media from Yamtrack.""" with open(mock_path + "/import_yamtrack.csv", "rb") as file: diff --git a/src/integrations/utils/imports/anilist.py b/src/integrations/utils/imports/anilist.py index 6a2f5d3a..c60842db 100644 --- a/src/integrations/utils/imports/anilist.py +++ b/src/integrations/utils/imports/anilist.py @@ -1,6 +1,5 @@ from __future__ import annotations -import asyncio import datetime import logging from typing import TYPE_CHECKING @@ -87,7 +86,7 @@ def anilist_data(username: str, user: User) -> str: variables = {"userName": username} url = "https://graphql.anilist.co" - with requests_cache.disabled(): # don't cache request as it can change frequently + with requests_cache.disabled(): # don't cache request as it can change frequently response = requests.post( url, json={"query": query, "variables": variables}, @@ -110,8 +109,6 @@ def add_media_list(query: dict, warning_message: str, user: User) -> str: bulk_media = {"anime": [], "manga": []} for media_type in query["data"]: - bulk_image = [] - logger.info("Importing %ss from Anilist", media_type) media_mapping = helpers.media_type_mapper(media_type) @@ -126,18 +123,10 @@ def add_media_list(query: dict, warning_message: str, user: User) -> str: else: status = content["status"].capitalize() - image_url = content["media"]["coverImage"]["large"] - bulk_image.append(image_url) - - image_filename = helpers.get_image_filename( - image_url, - media_type, - ) - instance = media_mapping["model"]( user=user, title=content["media"]["title"]["userPreferred"], - image=image_filename, + image=content["media"]["coverImage"]["large"], ) form = media_mapping["form"]( data={ @@ -158,8 +147,6 @@ def add_media_list(query: dict, warning_message: str, user: User) -> str: else: warning_message += f"\n {content['media']['title']['userPreferred']} ({media_type.capitalize()}): {form.errors.as_text()}" - asyncio.run(helpers.images_downloader(bulk_image, media_type)) - Anime.objects.bulk_create(bulk_media["anime"], ignore_conflicts=True) logger.info("Imported %s animes", len(bulk_media["anime"])) diff --git a/src/integrations/utils/imports/mal.py b/src/integrations/utils/imports/mal.py index f373f059..e2b45d55 100644 --- a/src/integrations/utils/imports/mal.py +++ b/src/integrations/utils/imports/mal.py @@ -1,10 +1,10 @@ -import asyncio import logging import requests import requests_cache from app.models import Anime, Manga from app.utils import helpers +from config import settings from decouple import config from users.models import User @@ -38,7 +38,7 @@ def get_whole_response(url: str, header: dict) -> dict: Continues to fetch data from the next URL until there is no more data to fetch. """ - with requests_cache.disabled(): # don't cache request as it can change frequently + with requests_cache.disabled(): # don't cache request as it can change frequently response = requests.get(url, headers=header, timeout=5) data = response.json() @@ -65,7 +65,6 @@ def add_media_list(response: dict, media_type: str, user: User) -> list: logger.info("Importing %ss from MyAnimeList", media_type) bulk_media = [] - bulk_images = [] media_mapping = helpers.media_type_mapper(media_type) for content in response["data"]: @@ -76,18 +75,15 @@ def add_media_list(response: dict, media_type: str, user: User) -> list: else: progress = content["list_status"]["num_chapters_read"] - if "main_picture" in content["node"]: + try: image_url = content["node"]["main_picture"]["large"] - bulk_images.append(image_url) - - image_filename = helpers.get_image_filename(image_url, media_type) - else: - image_filename = "none.svg" + except KeyError: + image_url = settings.IMG_NONE instance = media_mapping["model"]( user=user, title=content["node"]["title"], - image=image_filename, + image=image_url, ) form = media_mapping["form"]( @@ -113,8 +109,6 @@ def add_media_list(response: dict, media_type: str, user: User) -> list: ) logger.error(error_message) - asyncio.run(helpers.images_downloader(bulk_images, media_type)) - return bulk_media diff --git a/src/integrations/utils/imports/tmdb.py b/src/integrations/utils/imports/tmdb.py index e1c19b00..2180ccb1 100644 --- a/src/integrations/utils/imports/tmdb.py +++ b/src/integrations/utils/imports/tmdb.py @@ -40,11 +40,6 @@ def tmdb_data(file: InMemoryUploadedFile, user: User, status: str) -> None: "season": [], } - bulk_images = { - "movie": [], - "tv": [], - "season": [], - } for row in reader: media_type = row["Type"] @@ -58,9 +53,11 @@ def tmdb_data(file: InMemoryUploadedFile, user: User, status: str) -> None: # if tv show watchlist, add first season as planning if media_type == "tv" and status == "Planning": media_type = "season" - # get title from tv metadata as it's not available in season metadata + + # get title and id from tv metadata as it's not in season metadata tv_title = media_metadata["title"] media_id = media_metadata["id"] + media_metadata = metadata.season(media_id, season_number=1) media_metadata["media_type"] = "season" media_metadata["title"] = tv_title @@ -76,12 +73,6 @@ def tmdb_data(file: InMemoryUploadedFile, user: User, status: str) -> None: error_message = f"Error importing {media_metadata['title']}: {form.errors.as_data()}" logger.error(error_message) - if media_metadata["image"] != "none.svg": - bulk_images[media_type].append(media_metadata["image"]) - - # download images - for media_type, images in bulk_images.items(): - asyncio.run(helpers.images_downloader(images, media_type)) # bulk create tv, seasons and movie for media_type, medias in bulk_media.items(): @@ -103,7 +94,7 @@ def create_instance( instance = media_mapping["model"]( user=user, title=media_metadata["title"], - image=helpers.get_image_filename(media_metadata["image"], media_type), + image=media_metadata["image"], ) # if tv watchlist, create first season diff --git a/src/integrations/utils/imports/yamtrack.py b/src/integrations/utils/imports/yamtrack.py index 6fd2b043..dddfa0ba 100644 --- a/src/integrations/utils/imports/yamtrack.py +++ b/src/integrations/utils/imports/yamtrack.py @@ -1,4 +1,3 @@ -import asyncio import logging from csv import DictReader @@ -33,14 +32,6 @@ def yamtrack_data(file: InMemoryUploadedFile, user: User) -> None: "season": [], } - bulk_images = { - "anime": [], - "manga": [], - "movie": [], - "tv": [], - "season": [], - } - episodes = [] for row in reader: @@ -61,13 +52,6 @@ def yamtrack_data(file: InMemoryUploadedFile, user: User) -> None: media_mapping = helpers.media_type_mapper(media_type) add_bulk_media(row, media_mapping, user, bulk_media) - if row["image"] != "none.svg": - add_bulk_image(row, media_mapping, bulk_images) - - # download images - for media_type, images in bulk_images.items(): - asyncio.run(helpers.images_downloader(images, media_type)) - # bulk create tv, season, movie, anime and manga for media_type, medias in bulk_media.items(): model_type = helpers.media_type_mapper(media_type)["model"] @@ -121,29 +105,3 @@ def add_bulk_media( bulk_media[media_type].append(form.instance) else: logger.error("Error importing %s: %s", row["title"], form.errors.as_data()) - - -def add_bulk_image(row: dict, media_mapping: dict, bulk_image: dict) -> None: - """Add image to list for bulk download.""" - media_type = row["media_type"] - img_url_format = media_mapping["img_url"] - img_filename = row["image"].split("-", 1)[-1] - - if media_type in ("anime", "manga"): - # check if anilist or mal - # mal -> anime-11111l.jpg (all digits except last letter "l") - # anilist -> anime-dv332fds.jpg - if row["image"][5:-5].isdigit() and row["image"][-5] == "l": - bulk_image[media_type].append( - img_url_format["mal"].format( - media_id=row["media_id"], - image_file=img_filename, - ), - ) - else: - bulk_image[media_type].append( - img_url_format["anilist"].format(image_file=img_filename), - ) - else: - # tv-f496cm9enuEsZkSPzCwnTESEK5s.jpg -> https://image.tmdb.org/t/p/w500/f496cm9enuEsZkSPzCwnTESEK5s.jpg - bulk_image[media_type].append(img_url_format.format(image_file=img_filename)) diff --git a/src/media/none.svg b/src/media/none.svg deleted file mode 100644 index f83ba5b8..00000000 --- a/src/media/none.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/src/static/css/_structure.scss b/src/static/css/_structure.scss index 5738b93f..c9573ad5 100644 --- a/src/static/css/_structure.scss +++ b/src/static/css/_structure.scss @@ -28,8 +28,8 @@ .grid { display: grid; - grid-gap: .5rem; - grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr)); + grid-gap: 0.5rem; + grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr)); .card { width: 100%; @@ -37,6 +37,7 @@ .poster { width: 100%; + aspect-ratio: 2/3; object-fit: cover; cursor: pointer; } @@ -91,7 +92,6 @@ } } - .table-responsive { background-color: $primary; } @@ -99,6 +99,11 @@ .table { --bs-table-bg: $primary; + // mainly to fill td for image not found + td img { + background-color: rgb(44, 48, 50); + } + th { vertical-align: middle; } @@ -107,7 +112,7 @@ width: 1%; // set width of image td to the minimum } - .media-title{ + .media-title { text-align: left; max-width: 30dvw; // auto adjust width of title margin-left: 0; @@ -131,12 +136,12 @@ border-bottom-right-radius: 0; border-bottom-left-radius: 0; } - + .middle-input { margin-bottom: -1px; border-radius: 0; } - + .last-input { margin-bottom: 10px; border-top-left-radius: 0; @@ -148,10 +153,8 @@ ul { margin-bottom: 0; } - } - .profile-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(20em, 1fr)); @@ -168,7 +171,6 @@ } } - .details-top { .poster { border-radius: 10px; @@ -178,8 +180,9 @@ } .image-not-found { - object-fit: contain; + border-radius: 10px; width: 14rem; + object-fit: contain; aspect-ratio: 2/3; background-color: rgb(44, 48, 50); } @@ -187,7 +190,7 @@ .details-data { margin-left: 1rem; // link to main details from season details - a { + a { color: var(--bs-body-color); } @@ -204,7 +207,6 @@ } } - .episode { .check-div { flex-basis: 1em; @@ -246,10 +248,10 @@ } } -.no-content{ +.no-content { display: flex; flex-direction: column; height: 75%; justify-content: center; - align-items: center; + align-items: center; } diff --git a/src/templates/app/home.html b/src/templates/app/home.html index 2fa896e4..a2af486a 100644 --- a/src/templates/app/home.html +++ b/src/templates/app/home.html @@ -22,14 +22,14 @@
{% for media in watching_list %}
- - {% else %} - href="{% url 'media_details' media_type=media_type media_id=media.media_id title=media.title|slugify %}"> - {% endif %} + {{ media }} diff --git a/src/templates/app/media_details.html b/src/templates/app/media_details.html index 6598b9d4..d415e4d8 100644 --- a/src/templates/app/media_details.html +++ b/src/templates/app/media_details.html @@ -7,14 +7,11 @@
- {% if media.image != "none.svg" %} - {{ media.title }} - {% else %} - {{ media.title }} - {% endif %} + {{ media.title }}
+
{{ media.title }}
@@ -62,8 +59,9 @@ {% for season in seasons %}
- {{ season }} + {{ season.title }}
{{ season.title }}
@@ -82,8 +80,9 @@ {% for related in related_data.data %}
- {{ related.title }} + {{ related.title }}
{{ related.title }}
diff --git a/src/templates/app/media_grid.html b/src/templates/app/media_grid.html index 1d48dfe4..30015f8b 100644 --- a/src/templates/app/media_grid.html +++ b/src/templates/app/media_grid.html @@ -44,8 +44,8 @@ {% else %} "{% url 'season_details' media_id=media.media_id title=media.title|slugify season_number=media.season_number %}" {% endif %}> - {{ media }} diff --git a/src/templates/app/media_table.html b/src/templates/app/media_table.html index 7f7595cf..8fe37475 100644 --- a/src/templates/app/media_table.html +++ b/src/templates/app/media_table.html @@ -48,7 +48,7 @@ - {{ media.title }}
diff --git a/src/templates/app/season_details.html b/src/templates/app/season_details.html index e09b158c..4f904679 100644 --- a/src/templates/app/season_details.html +++ b/src/templates/app/season_details.html @@ -7,13 +7,9 @@
- {% if season.image != "none.svg" %} - {{ season.name }} - {% else %} - season.name - {% endif %} + {{ season.name }}