add get manga metadata from mangaupdates

This commit is contained in:
FuzzyGrim
2024-09-22 23:43:00 +02:00
parent 20758695db
commit 75e496ae89
11 changed files with 159 additions and 24 deletions

View File

@@ -44,7 +44,8 @@ Alternatively, if you need a PostgreSQL database, you can use the `docker-compos
| TMDB_NSFW | Bool | Default to false, set to true to include adult content in tv and movie searches |
| TMDB_LANG | String | TMDB metadata language, uses a Language code in ISO 639-1 e.g "en", for more specific results a country code in ISO 3166-1 can be added e.g "en-US" |
| MAL_API | String | MyAnimeList API key, for anime and manga, a default key is provided |
| MAL_NSFW | Bool | Default to false, set to true to include adult content in anime and manga searches |
| MAL_NSFW | Bool | Default to false, set to true to include adult content in anime and manga searches from MyAnimeList |
| MU_NSFW | Bool | Default to false, set to true to include adult content in manga searches from MangaUpdates |
| IGDB_ID | String | IGDB API key for games, a default key is provided but it's recommended to get your own as it has a low rate limit. |
| IGDB_SECRET | String | IGDB API secret for games, a default value is provided but it's recommended to get your own as it has a low rate limit. |
| IGDB_NSFW | Bool | Default to false, set to true to include adult content in game searches |

View File

@@ -184,6 +184,7 @@ class Media(models.Model):
max_progress = services.get_media_metadata(
self.item.media_type,
self.item.media_id,
self.item.source,
)["max_progress"]
if max_progress:
@@ -202,6 +203,7 @@ class Media(models.Model):
max_progress = services.get_media_metadata(
self.item.media_type,
self.item.media_id,
self.item.source,
)["max_progress"]
if max_progress:
@@ -230,6 +232,7 @@ class Media(models.Model):
media_metadata = services.get_media_metadata(
self.item.media_type,
self.item.media_id,
self.item.source,
)
response = {"item": self.item}
max_progress = media_metadata["max_progress"]
@@ -513,6 +516,7 @@ class Season(Media):
media_metadata = services.get_media_metadata(
self.item.media_type,
self.item.media_id,
self.item.source,
self.item.season_number,
)
response = {"item": self.item}

View File

@@ -1,3 +1,8 @@
import asyncio
import re
import aiohttp
import requests
from django.conf import settings
from django.core.cache import cache
@@ -48,6 +53,120 @@ def search(query):
return data
def manga(media_id):
"""Get metadata for a manga from MangaUpdates."""
return asyncio.run(async_manga(media_id))
async def async_manga(media_id):
"""Asynchronous implementation of manga metadata retrieval."""
data = cache.get(f"mangaupdates_manga_{media_id}")
if data is None:
url = f"{base_url}/series/{media_id}"
response = services.api_request("MANGAUPDATES", "GET", url)
num_chapters = response["latest_chapter"]
# Run related_manga and recommendations concurrently
related_task = asyncio.create_task(
get_related_series(response["related_series"]),
)
recommendations_task = asyncio.create_task(
get_recommendations(response["recommendations"]),
)
data = {
"media_id": media_id,
"source": "mangaupdates",
"media_type": "manga",
"title": response["title"],
"image": get_image_url(response),
"synopsis": response["description"],
"max_progress": num_chapters,
"details": {
"format": response["type"],
"authors": get_authors(response["authors"]),
"year": response["year"],
"status": get_status(response["status"]),
"number_of_chapters": num_chapters,
"genres": get_genres(response["genres"]),
},
"related": {
"related_manga": await related_task,
"recommendations": await recommendations_task,
},
}
cache.set(f"mangaupdates_manga_{media_id}", data)
return data
def get_image_url(response):
"""Get the image URL for a media item."""
return response["image"]["url"]["original"]
# when no image, value from response is null
url = response["image"]["url"]["original"]
return url if url else settings.IMG_NONE
def get_genres(genres):
"""Return the genres for the media."""
return ", ".join(item["genre"] for item in genres)
def get_authors(authors):
"""Get the authors for a media item."""
return ", ".join(item["name"] for item in authors)
def get_status(status):
"""Return the status of the media."""
# e.g berserk 51239621230 needs parsing
pattern = r"(\d+\s+Volumes\s+\([^)]+\))"
match = re.search(pattern, status)
if match:
return match.group(1)
return status
async def get_related_series(related):
"""Return list of related media for the selected media asynchronously."""
async with aiohttp.ClientSession() as session:
tasks = [
fetch_series_data(
session,
f"{base_url}/series/{item['related_series_id']}",
item,
)
for item in related
if item["related_series_name"]
]
results = await asyncio.gather(*tasks)
return [item for item in results if item is not None]
async def get_recommendations(recommendations):
"""Return list of recommended media for the selected media asynchronously."""
async with aiohttp.ClientSession() as session:
tasks = [
fetch_series_data(session, f"{base_url}/series/{item['series_id']}", item)
for item in recommendations
if item["series_name"]
]
results = await asyncio.gather(*tasks)
return [item for item in results if item is not None]
async def fetch_series_data(session, url, item):
"""Fetch series data asynchronously."""
async with session.get(url) as response:
if response.status == requests.codes.ok:
data = await response.json()
image = get_image_url(data)
return {
"media_id": item.get("related_series_id") or item.get("series_id"),
"title": item.get("related_series_name") or item.get("series_name"),
"image": image,
}
return None

View File

@@ -8,7 +8,7 @@ from pyrate_limiter import RedisBucket
from redis import ConnectionPool
from requests_ratelimiter import LimiterSession
from app.providers import igdb, mal, tmdb
from app.providers import igdb, mal, mangaupdates, tmdb
logger = logging.getLogger(__name__)
@@ -121,13 +121,16 @@ def request_error_handling(error, *args):
raise error # re-raise the error if it's not handled
def get_media_metadata(media_type, media_id, season_number=None):
def get_media_metadata(media_type, media_id, source, season_number=None):
"""Return the metadata for the selected media."""
match media_type:
case "anime":
media_metadata = mal.anime(media_id)
case "manga":
media_metadata = mal.manga(media_id)
if source == "mangaupdates":
media_metadata = mangaupdates.manga(media_id)
else:
media_metadata = mal.manga(media_id)
case "tv":
media_metadata = tmdb.tv(media_id)
case "season":

View File

@@ -114,7 +114,8 @@ def media_search(request):
@require_GET
def media_details(request, media_type, media_id, title): # noqa: ARG001 title for URL
"""Return the details page for a media item."""
media_metadata = services.get_media_metadata(media_type, media_id)
source = request.GET.get("source")
media_metadata = services.get_media_metadata(media_type, media_id, source)
context = {"media": media_metadata}
return render(request, "app/media_details.html", context)
@@ -289,18 +290,18 @@ def episode_handler(request):
@require_GET
def history(request):
"""Return the history page for a media item."""
media_id = request.GET["media_id"]
source = request.GET["source"]
media_type = request.GET["media_type"]
season_number = request.GET.get("season_number")
episode_number = request.GET.get("episode_number")
item = Item.objects.get(
media_id=media_id,
source=source,
item, _ = Item.objects.get_or_create(
media_id=request.GET["media_id"],
source=request.GET["source"],
media_type=media_type,
season_number=season_number,
episode_number=episode_number,
season_number=request.GET.get("season_number"),
episode_number=request.GET.get("episode_number"),
defaults={
"title": request.GET["title"],
"image": request.GET["image"],
},
)
media = database.get_media(media_type, item, request.user)

View File

@@ -72,7 +72,11 @@ def process_item(item, events_bulk):
metadata = tmdb.season(item.media_id, item.season_number)
reloaded = process_season(item, metadata, events_bulk)
else:
metadata = services.get_media_metadata(item.media_type, item.media_id)
metadata = services.get_media_metadata(
item.media_type,
item.media_id,
item.source,
)
reloaded = process_other(item, metadata, events_bulk)
return reloaded

View File

@@ -27,7 +27,7 @@ def importer(file, user, status):
# if movie or tv show (not episode)
if media_type == "movie" or (media_type == "tv" and episode_number == ""):
media_metadata = services.get_media_metadata(media_type, media_id)
media_metadata = services.get_media_metadata(media_type, media_id, "tmdb")
item, _ = models.Item.objects.get_or_create(
media_id=media_metadata["media_id"],

View File

@@ -1,7 +1,9 @@
"""Contains views for importing and exporting media data from various sources."""
import logging
from datetime import datetime
from django.conf import settings
from django.contrib import messages
from django.http import HttpResponse
from django.shortcuts import redirect
@@ -92,10 +94,11 @@ def import_yamtrack(request):
@require_GET
def export_csv(request):
"""View for exporting all media data to a CSV file."""
today = datetime.now(tz=settings.TZ).strftime("%Y-%m-%d")
# Create the HttpResponse object with the appropriate CSV header.
response = HttpResponse(
content_type="text/csv",
headers={"Content-Disposition": 'attachment; filename="yamtrack.csv"'},
headers={"Content-Disposition": f'attachment; filename="yamtrack_{today}.csv"'},
)
response = exports.db_to_csv(response, request.user)

View File

@@ -17,7 +17,7 @@
{% endif %}
<h1 class="title">{{ media.title }}</h1>
<p>{{ media.synopsis }}</p>
<p>{{ media.synopsis|safe }}</p>
<div class="additional-data d-flex flex-wrap">
{% for key, value in media.details.items %}
@@ -31,7 +31,7 @@
<div class="mt-sm-auto mt-3 d-flex gap-2">
{% include "app/components/open_modal.html" with modal_type="track" source=media.source image=media.image %}
{% include "app/components/open_modal.html" with modal_type="lists" source=media.source image=media.image %}
{% include "app/components/open_modal.html" with modal_type="history" source=media.source %}
{% include "app/components/open_modal.html" with modal_type="history" source=media.source image=media.image %}
</div>
</div>
</div>

View File

@@ -15,10 +15,10 @@
<div class="grid mb-4">
{% for related in related_items %}
<div class="card">
<a href="{% if name == "seasons" %}{% url 'season_details' media_id=media.media_id title=media.title|slug season_number=related.season_number %}{% else %}{% url 'media_details' media_type=media.media_type media_id=related.media_id title=related.title|slug %}{% endif %}">
<a href="{% if name == "seasons" %}{% url 'season_details' media_id=media.media_id title=media.title|slug season_number=related.season_number %}{% else %}{% url 'media_details' media_type=media.media_type media_id=related.media_id title=related.title|slug %}{% endif %}{% if request.GET.source %}?source={{ request.GET.source }}{% endif %}">
<img src="{{ related.image }}"
class="card-img {% if related.image == IMG_NONE %}image-not-found{% else %}poster{% endif %}"
alt="{{ related.title }}" />
class="card-img {% if related.image == IMG_NONE %}image-not-found{% else %}poster{% endif %}"
alt="{{ related.title }}" />
</a>
<div class="card-img-overlay">

View File

@@ -31,7 +31,7 @@
<div class="grid">
{% for media in query_list %}
<div class="card">
<a href="{% url 'media_details' media_type=media.media_type media_id=media.media_id title=media.title|slug %}">
<a href="{% url 'media_details' media_type=media.media_type media_id=media.media_id title=media.title|slug %}?source={{ media.source }}">
<img src="{{ media.image }}"
class="card-img {% if media.image == IMG_NONE %}image-not-found{% else %}poster{% endif %}"
alt="{{ media.title }}" />