mirror of
https://github.com/FuzzyGrim/Yamtrack.git
synced 2026-06-28 06:45:58 +00:00
add get manga metadata from mangaupdates
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 }}" />
|
||||
|
||||
Reference in New Issue
Block a user