aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2024-09-03 12:20:35 -0400
committerBobby <[email protected]>2024-09-03 12:20:35 -0400
commit51a27b6f400625cb0137e1394de1056ea5fb682b (patch)
tree593b782d99784c690705eb397831a9a681d406a3
parenta9e71a492486a25ec4273716080e0ca744a36646 (diff)
downloadyugen-51a27b6f400625cb0137e1394de1056ea5fb682b.tar.xz
yugen-51a27b6f400625cb0137e1394de1056ea5fb682b.zip
optimizations
-rw-r--r--detail/views.py89
-rw-r--r--homepage/views.py208
-rw-r--r--static/css/input.css3
-rw-r--r--static/css/main.css81
-rw-r--r--templates/detail/detail.html207
-rw-r--r--watch/utils.py4
-rw-r--r--watch/views.py269
7 files changed, 493 insertions, 368 deletions
diff --git a/detail/views.py b/detail/views.py
index 9f7c280..4b247e6 100644
--- a/detail/views.py
+++ b/detail/views.py
@@ -2,41 +2,74 @@ import json
import os
from django.shortcuts import render
import requests
-
+from functools import lru_cache
from watch.utils import get_all_episode_metadata, get_from_redis_cache, store_in_redis_cache
def detail(request, anime_id):
- anime_data = None
- anime_episodes = None
- anime_episodes_metadata = None
- try:
- anime_data = json.loads(get_from_redis_cache(f"anime_{anime_id}_anime_data"))
- except:
- base_url = f"{os.getenv("CONSUMET_URL")}/meta/anilist/info/{anime_id}?provider=zoro"
- response = requests.get(base_url)
- anime_data = response.json()
- store_in_redis_cache(f"anime_{anime_id}_anime_data", json.dumps(anime_data))
-
- try:
- anime_episodes = json.loads(get_from_redis_cache(f"anime_{anime_id}_anime_episodes"))
- except:
- z_anime_id = anime_data["episodes"][0]["id"].split("$")[0] if len(anime_data["episodes"]) > 0 else None
- if z_anime_id is not None:
- base_url = f"{os.getenv("ZORO_URL")}/anime/episodes/{z_anime_id}"
- response = requests.get(base_url)
- anime_episodes = response.json()
- store_in_redis_cache(f"anime_{anime_id}_anime_episodes", json.dumps(anime_episodes))
+ anime_data = get_anime_data(anime_id)
+ if not anime_data:
+ return render(request, "detail/error.html", {"error": "Anime not found"}, status=404)
- if anime_episodes is not None:
- anime_episodes_metadata = get_all_episode_metadata(anime_data)
- # attach metadata to episodes
- if anime_episodes_metadata:
- for i, episode in enumerate(anime_episodes["episodes"]):
- episode["metadata"] = anime_episodes_metadata[i]
+ anime_episodes = get_anime_episodes(anime_id)
+
+ if anime_episodes:
+ attach_episode_metadata(anime_data, anime_episodes)
context = {
"anime": anime_data,
"episodes": anime_episodes,
}
- return render(request, "detail/detail.html", context) \ No newline at end of file
+ return render(request, "detail/detail.html", context)
+
+@lru_cache(maxsize=100)
+def get_anime_data(anime_id):
+ cache_key = f"anime_{anime_id}_anime_data"
+ anime_data = get_from_redis_cache(cache_key)
+
+ if not anime_data:
+ base_url = f"{os.getenv('CONSUMET_URL')}/meta/anilist/info/{anime_id}?provider=zoro"
+ try:
+ response = requests.get(base_url, timeout=10)
+ response.raise_for_status()
+ anime_data = response.json()
+ store_in_redis_cache(cache_key, json.dumps(anime_data), 86400) # Cache for 24 hours
+ except requests.RequestException as e:
+ print(f"Error fetching anime data for ID {anime_id}: {e}")
+ return None
+ else:
+ anime_data = json.loads(anime_data)
+
+ return anime_data
+
+@lru_cache(maxsize=100)
+def get_anime_episodes(anime_id):
+ cache_key = f"anime_{anime_id}_anime_episodes"
+ anime_episodes = get_from_redis_cache(cache_key)
+
+ if not anime_episodes:
+ anime_data = get_anime_data(anime_id)
+ if not anime_data or not anime_data.get("episodes"):
+ return None
+
+ z_anime_id = anime_data["episodes"][0]["id"].split("$")[0]
+ base_url = f"{os.getenv('ZORO_URL')}/anime/episodes/{z_anime_id}"
+ try:
+ response = requests.get(base_url, timeout=10)
+ response.raise_for_status()
+ anime_episodes = response.json()
+ store_in_redis_cache(cache_key, json.dumps(anime_episodes), 86400) # Cache for 24 hours
+ except requests.RequestException as e:
+ print(f"Error fetching anime episodes for ID {anime_id}: {e}")
+ return None
+ else:
+ anime_episodes = json.loads(anime_episodes)
+
+ return anime_episodes
+
+def attach_episode_metadata(anime_data, anime_episodes):
+ anime_episodes_metadata = get_all_episode_metadata(anime_data)
+ if anime_episodes_metadata:
+ for i, episode in enumerate(anime_episodes.get("episodes", [])):
+ if i < len(anime_episodes_metadata):
+ episode["metadata"] = anime_episodes_metadata[i] \ No newline at end of file
diff --git a/homepage/views.py b/homepage/views.py
index 9e0c6cc..15d8811 100644
--- a/homepage/views.py
+++ b/homepage/views.py
@@ -13,109 +13,139 @@ from homepage.utils import (
get_upcoming_anime,
get_next_season,
)
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from functools import lru_cache
-def index(request):
- homepage_data_cached = get_from_redis_cache("homepage_data")
-
- if not homepage_data_cached:
- trending_anime = get_trending_anime()
- popular_anime = get_popular_anime()
- top_anime = get_top_anime()
- top_airing_anime = get_top_airing_anime()
- upcoming_anime = get_upcoming_anime()
- next_season = get_next_season()
-
+@lru_cache(maxsize=1)
+def get_homepage_data():
+ homepage_data = get_from_redis_cache("homepage_data")
+
+ if not homepage_data:
homepage_data = {
- "trending_anime": trending_anime,
- "popular_anime": popular_anime,
- "top_anime": top_anime,
- "top_airing_anime": top_airing_anime,
- "upcoming_anime": upcoming_anime,
- "next_season": next_season
+ "trending_anime": get_trending_anime(),
+ "popular_anime": get_popular_anime(),
+ "top_anime": get_top_anime(),
+ "top_airing_anime": get_top_airing_anime(),
+ "upcoming_anime": get_upcoming_anime(),
+ "next_season": get_next_season()
}
-
- store_in_redis_cache("homepage_data", json.dumps(homepage_data))
+ store_in_redis_cache("homepage_data", json.dumps(homepage_data), 3600) # Cache for 1 hour
else:
- homepage_data = json.loads(homepage_data_cached)
- trending_anime = homepage_data["trending_anime"]
- popular_anime = homepage_data["popular_anime"]
- top_anime = homepage_data["top_anime"]
- top_airing_anime = homepage_data["top_airing_anime"]
- upcoming_anime = homepage_data["upcoming_anime"]
- next_season = homepage_data["next_season"]
-
- if request.user.preferences.show_history_on_home:
- user_history_data = gather_watch_history(request, limit=10)
+ homepage_data = json.loads(homepage_data)
+
+ return homepage_data
+def index(request):
+ homepage_data = get_homepage_data()
+
context = {
- "trending_anime": trending_anime["results"],
- "popular_anime": popular_anime["results"],
- "top_anime": top_anime["results"],
- "top_airing_anime": top_airing_anime["results"],
- "upcoming_anime": upcoming_anime["results"],
- "next_season": next_season,
- "user_history_data": user_history_data if request.user.preferences.show_history_on_home else None
+ "trending_anime": homepage_data["trending_anime"]["results"],
+ "popular_anime": homepage_data["popular_anime"]["results"],
+ "top_anime": homepage_data["top_anime"]["results"],
+ "top_airing_anime": homepage_data["top_airing_anime"]["results"],
+ "upcoming_anime": homepage_data["upcoming_anime"]["results"],
+ "next_season": homepage_data["next_season"],
}
+ if request.user.preferences.show_history_on_home:
+ context["user_history_data"] = gather_watch_history(request)
+
return render(request, "home/index.html", context)
-def gather_watch_history(request, limit=None):
+def gather_watch_history(request, limit=10):
user = request.user
+ latest_history = get_user_watch_history(user.id, limit)
+ anime_ids = [entry['anime_id'] for entry in latest_history]
+ anime_data_map = get_bulk_anime_data(anime_ids)
+
+ return [
+ {
+ "anime_id": entry['anime_id'],
+ "title": anime_data_map[entry['anime_id']]["title"],
+ "cover": anime_data_map[entry['anime_id']]["cover"],
+ "episode": entry['episode'] if entry['last_watched'] else 1,
+ "metadata": get_episode_metadata(anime_data_map[entry['anime_id']], entry['episode'] if entry['last_watched'] else 1)
+ }
+ for entry in latest_history
+ ]
+
+@lru_cache(maxsize=100)
+def get_user_watch_history(user_id, limit):
+ query = """
+ WITH ranked_history AS (
+ SELECT
+ id,
+ anime_id,
+ episode,
+ last_watched,
+ last_updated,
+ ROW_NUMBER() OVER (PARTITION BY anime_id ORDER BY last_updated DESC) AS rn
+ FROM user_profile_userhistory
+ WHERE user_id = %s
+ )
+ SELECT id, anime_id, episode, last_watched, last_updated
+ FROM ranked_history
+ WHERE rn = 1
+ ORDER BY last_updated DESC
+ LIMIT %s
+ """
with connection.cursor() as cursor:
- query = """
- WITH ranked_history AS (
- SELECT
- id,
- anime_id,
- episode,
- last_watched,
- last_updated,
- ROW_NUMBER() OVER (PARTITION BY anime_id ORDER BY last_updated DESC) AS rn
- FROM user_profile_userhistory
- WHERE user_id = %s
- )
- SELECT id, anime_id, episode, last_watched, last_updated
- FROM ranked_history
- WHERE rn = 1
- ORDER BY last_updated DESC
- """
- if limit:
- query += " LIMIT %s"
- cursor.execute(query, [user.id, limit])
- else:
- cursor.execute(query, [user.id])
-
+ cursor.execute(query, [user_id, limit])
columns = [col[0] for col in cursor.description]
- latest_history = [dict(zip(columns, row)) for row in cursor.fetchall()]
-
- user_history_data = []
-
- for history_entry in latest_history:
- anime_id = history_entry['anime_id']
- last_watched = history_entry['episode'] if history_entry['last_watched'] else 1
-
- anime_data = None
- try:
- anime_data = json.loads(get_from_redis_cache(f"anime_{anime_id}_anime_data"))
- except:
- base_url = f"{os.getenv('CONSUMET_URL')}/meta/anilist/info/{anime_id}?provider=zoro"
- response = requests.get(base_url)
- anime_data = response.json()
- store_in_redis_cache(f"anime_{anime_id}_anime_data", json.dumps(anime_data))
-
- # attach metadata to episodes
- episode_metadata = get_episode_metadata(anime_data, last_watched)
-
- user_history_data.append({
- "anime_id": anime_id,
- "title": anime_data["title"],
- "cover": anime_data["cover"],
- "episode": last_watched,
- "metadata": episode_metadata
- })
-
- return user_history_data
+ return [dict(zip(columns, row)) for row in cursor.fetchall()]
+
+def get_bulk_anime_data(anime_ids):
+ anime_data_map = {}
+ missing_ids = []
+
+ # Check cache first (including potential caches from other functions)
+ for anime_id in anime_ids:
+ cached_data = get_cached_anime_data(anime_id)
+ if cached_data:
+ anime_data_map[anime_id] = cached_data
+ else:
+ missing_ids.append(anime_id)
+
+ # Fetch missing data in parallel
+ if missing_ids:
+ with ThreadPoolExecutor(max_workers=min(10, len(missing_ids))) as executor:
+ future_to_id = {executor.submit(fetch_anime_data, anime_id): anime_id for anime_id in missing_ids}
+ for future in as_completed(future_to_id):
+ anime_id = future_to_id[future]
+ try:
+ data = future.result()
+ anime_data_map[anime_id] = data
+ store_in_redis_cache(f"anime_{anime_id}_anime_data", json.dumps(data), 86400) # Cache for 24 hours
+ except Exception as exc:
+ print(f"Anime ID {anime_id} generated an exception: {exc}")
+
+ return anime_data_map
+
+def get_cached_anime_data(anime_id):
+ cache_keys = [
+ f"anime_{anime_id}_anime_data",
+ f"anime_{anime_id}_details", # Example of a cache key that might be used by another function
+ f"anime_info_{anime_id}" # Another example
+ ]
+
+ for key in cache_keys:
+ cached_data = get_from_redis_cache(key)
+ if cached_data:
+ return json.loads(cached_data)
+
+ return None
+
+@lru_cache(maxsize=1000)
+def fetch_anime_data(anime_id):
+ base_url = f"{os.getenv('CONSUMET_URL')}/meta/anilist/info/{anime_id}?provider=zoro"
+ try:
+ response = requests.get(base_url, timeout=10)
+ response.raise_for_status()
+ return response.json()
+ except requests.RequestException as e:
+ print(f"Error fetching data for anime ID {anime_id}: {e}")
+ return None
def search_json(request):
diff --git a/static/css/input.css b/static/css/input.css
index 4804d89..3c6acc2 100644
--- a/static/css/input.css
+++ b/static/css/input.css
@@ -41,6 +41,7 @@ main {
.peer:checked + div .toggle-dot {
transform: translateX(100%);
}
+
.toggle-dot {
transition: transform 0.1s ease-in-out;
}
@@ -49,4 +50,4 @@ main {
height: 12rem !important; /* equivalent to h-48 */
width: auto !important;
aspect-ratio: 16 / 9 !important;
-} \ No newline at end of file
+}
diff --git a/static/css/main.css b/static/css/main.css
index f554b0e..ee44acc 100644
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -673,10 +673,6 @@ video {
top: 9rem;
}
-.top-64 {
- top: 16rem;
-}
-
.top-full {
top: 100%;
}
@@ -778,6 +774,10 @@ video {
display: inline-flex;
}
+.grid {
+ display: grid;
+}
+
.hidden {
display: none;
}
@@ -939,6 +939,10 @@ video {
min-width: 8rem;
}
+.max-w-4xl {
+ max-width: 56rem;
+}
+
.max-w-7xl {
max-width: 80rem;
}
@@ -1028,6 +1032,10 @@ video {
list-style-position: inside;
}
+.grid-cols-1 {
+ grid-template-columns: repeat(1, minmax(0, 1fr));
+}
+
.flex-row {
flex-direction: row;
}
@@ -1451,25 +1459,6 @@ video {
--tw-bg-opacity: 0.4;
}
-.bg-gradient-to-t {
- background-image: linear-gradient(to top, var(--tw-gradient-stops));
-}
-
-.from-black {
- --tw-gradient-from: #000 var(--tw-gradient-from-position);
- --tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);
- --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
-}
-
-.via-transparent {
- --tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);
- --tw-gradient-stops: var(--tw-gradient-from), transparent var(--tw-gradient-via-position), var(--tw-gradient-to);
-}
-
-.to-transparent {
- --tw-gradient-to: transparent var(--tw-gradient-to-position);
-}
-
.bg-cover {
background-size: cover;
}
@@ -1554,10 +1543,6 @@ video {
padding-bottom: 2rem;
}
-.pb-2 {
- padding-bottom: 0.5rem;
-}
-
.pb-4 {
padding-bottom: 1rem;
}
@@ -1927,10 +1912,6 @@ video {
opacity: 0.25;
}
-.opacity-70 {
- opacity: 0.7;
-}
-
.shadow-lg {
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
@@ -2337,6 +2318,10 @@ main {
}
@media (min-width: 640px) {
+ .sm\:col-span-2 {
+ grid-column: span 2 / span 2;
+ }
+
.sm\:size-3 {
width: 0.75rem;
height: 0.75rem;
@@ -2347,6 +2332,10 @@ main {
height: 1rem;
}
+ .sm\:grid-cols-2 {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
.sm\:gap-2 {
gap: 0.5rem;
}
@@ -2392,18 +2381,14 @@ main {
}
@media (min-width: 1024px) {
- .lg\:left-\[18rem\] {
- left: 18rem;
- }
-
- .lg\:top-0 {
- top: 0px;
- }
-
.lg\:top-48 {
top: 12rem;
}
+ .lg\:col-span-3 {
+ grid-column: span 3 / span 3;
+ }
+
.lg\:block {
display: block;
}
@@ -2428,6 +2413,10 @@ main {
max-height: 761px;
}
+ .lg\:max-h-24 {
+ max-height: 6rem;
+ }
+
.lg\:w-1\/2 {
width: 50%;
}
@@ -2456,12 +2445,12 @@ main {
width: 15rem;
}
- .lg\:flex-row {
- flex-direction: row;
+ .lg\:grid-cols-3 {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
}
- .lg\:flex-col {
- flex-direction: column;
+ .lg\:flex-row {
+ flex-direction: row;
}
.lg\:flex-nowrap {
@@ -2503,10 +2492,6 @@ main {
padding-right: 2rem;
}
- .lg\:pb-0 {
- padding-bottom: 0px;
- }
-
.lg\:text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
@@ -2543,4 +2528,4 @@ main {
.\32xl\:w-1\/6 {
width: 16.666667%;
}
-} \ No newline at end of file
+}
diff --git a/templates/detail/detail.html b/templates/detail/detail.html
index de6235d..ce84ee5 100644
--- a/templates/detail/detail.html
+++ b/templates/detail/detail.html
@@ -1,6 +1,16 @@
{% extends "partials/base.html" %}
{% load custom_filters %}
-
+{% block css %}
+<style>
+ @media (max-width: 640px) {
+ .detail-section {
+ top: 8rem;
+ left: -8.5rem;
+ width: 100vw;
+ }
+ }
+</style>
+{% endblock css %}
{% block content %}
<div style="background-image: url('{{ anime.cover }}')" class="relative bg-center bg-cover h-32 lg:h-96 my-4 rounded-lg" >
<div class="absolute inset-0" style="background: linear-gradient(45deg, rgb(8, 8, 8) 15%, transparent 60%), linear-gradient(0deg, rgb(8, 8, 8) 0%, transparent 60%);"></div>
@@ -20,35 +30,35 @@
<svg stroke="currentColor" fill="currentColor" stroke-width="0" role="img" viewBox="0 0 24 24" height="1.5rem" width="1.5rem" xmlns="http://www.w3.org/2000/svg"><path d="M8.273 7.247v8.423l-2.103-.003v-5.216l-2.03 2.404-1.989-2.458-.02 5.285H.001L0 7.247h2.203l1.865 2.545 2.015-2.546 2.19.001zm8.628 2.069l.025 6.335h-2.365l-.008-2.871h-2.8c.07.499.21 1.266.417 1.779.155.381.298.751.583 1.128l-1.705 1.125c-.349-.636-.622-1.337-.878-2.082a9.296 9.296 0 0 1-.507-2.179c-.085-.75-.097-1.471.107-2.212a3.908 3.908 0 0 1 1.161-1.866c.313-.293.749-.5 1.1-.687.351-.187.743-.264 1.107-.359a7.405 7.405 0 0 1 1.191-.183c.398-.034 1.107-.066 2.39-.028l.545 1.749H14.51c-.593.008-.878.001-1.341.209a2.236 2.236 0 0 0-1.278 1.92l2.663.033.038-1.81h2.309zm3.992-2.099v6.627l3.107.032-.43 1.775h-4.807V7.187l2.13.03z"></path></svg>
</a>
</div>
- <div class="flex flex-row overflow-x-auto lg:flex-col gap-4 my-4 pb-2 lg:pb-0">
- <div class="whitespace-nowrap">
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 my-4 max-w-4xl mx-auto">
+ <div>
<span class="font-bold">Format: </span>{{ anime.type }}
</div>
- <div class="whitespace-nowrap">
+ <div>
<span class="font-bold">Episodes: </span>{{ anime.totalEpisodes }}
</div>
- <div class="whitespace-nowrap">
+ <div>
<span class="font-bold">Year: </span>{{ anime.releaseDate }}
</div>
- <div class="whitespace-nowrap">
+ <div>
<span class="font-bold">Duration: </span>{{ anime.duration }} mins
</div>
- <div class="whitespace-nowrap">
+ <div>
<span class="font-bold">Status: </span>{{ anime.status }}
</div>
- <div class="whitespace-nowrap capitalize">
+ <div class="capitalize">
<span class="font-bold">Season: </span>{{ anime.season }}
</div>
- <div class="whitespace-nowrap">
+ <div>
<span class="font-bold">Rating: </span>{{ anime.rating }} / 100
</div>
- <div class="whitespace-nowrap">
+ <div>
<span class="font-bold">Popularity: </span>{{ anime.popularity }}
</div>
- <div class="whitespace-nowrap">
+ <div>
<span class="font-bold">Country: </span>{{ anime.countryOfOrigin }}
</div>
- <div class="whitespace-nowrap">
+ <div class="sm:col-span-2 lg:col-span-3">
<span class="font-bold">Studios: </span>
{% for studio in anime.studios %}
<span>{{ studio }}</span>{% if not forloop.last %}, {% endif %}
@@ -155,100 +165,101 @@
{{ anime.title.romaji }}
{% endif %}
</h2>
- <p class="max-h-24 overflow-auto text-sm text-white mb-4 no-scrollbar">
+ <p class="h-96 lg:max-h-24 overflow-auto text-sm text-white mb-4 no-scrollbar">
{{ anime.description|strip_html }}
</p>
- </div>
-</div>
-<section class="w-full lg:w-10/12 flex flex-col items-center justify-center p-2 relative top-64 lg:top-0 lg:left-[18rem]">
- <section class="inline-flex w-max flex-row gap-4 rounded-full mb-8 bg-white bg-opacity-10 mx-auto">
- <button class="flex flex-row items-center focus:outline-none gap-2 text-sm font-bold py-2 px-4 rounded-full category-switch" data-target="characters">
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4">
- <path fill-rule="evenodd" d="M18.685 19.097A9.723 9.723 0 0 0 21.75 12c0-5.385-4.365-9.75-9.75-9.75S2.25 6.615 2.25 12a9.723 9.723 0 0 0 3.065 7.097A9.716 9.716 0 0 0 12 21.75a9.716 9.716 0 0 0 6.685-2.653Zm-12.54-1.285A7.486 7.486 0 0 1 12 15a7.486 7.486 0 0 1 5.855 2.812A8.224 8.224 0 0 1 12 20.25a8.224 8.224 0 0 1-5.855-2.438ZM15.75 9a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" clip-rule="evenodd" />
- </svg>
- <span>Characters</span>
- </button>
- <button class="flex flex-row items-center focus:outline-none gap-2 text-sm font-bold py-2 px-4 rounded-full category-switch bg-{{ user.preferences.accent_colour }}-600" data-target="episodes">
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4">
- <path fill-rule="evenodd" d="M1.5 5.625c0-1.036.84-1.875 1.875-1.875h17.25c1.035 0 1.875.84 1.875 1.875v12.75c0 1.035-.84 1.875-1.875 1.875H3.375A1.875 1.875 0 0 1 1.5 18.375V5.625Zm1.5 0v1.5c0 .207.168.375.375.375h1.5a.375.375 0 0 0 .375-.375v-1.5a.375.375 0 0 0-.375-.375h-1.5A.375.375 0 0 0 3 5.625Zm16.125-.375a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h1.5A.375.375 0 0 0 21 7.125v-1.5a.375.375 0 0 0-.375-.375h-1.5ZM21 9.375A.375.375 0 0 0 20.625 9h-1.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h1.5a.375.375 0 0 0 .375-.375v-1.5Zm0 3.75a.375.375 0 0 0-.375-.375h-1.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h1.5a.375.375 0 0 0 .375-.375v-1.5Zm0 3.75a.375.375 0 0 0-.375-.375h-1.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h1.5a.375.375 0 0 0 .375-.375v-1.5ZM4.875 18.75a.375.375 0 0 0 .375-.375v-1.5a.375.375 0 0 0-.375-.375h-1.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h1.5ZM3.375 15h1.5a.375.375 0 0 0 .375-.375v-1.5a.375.375 0 0 0-.375-.375h-1.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375Zm0-3.75h1.5a.375.375 0 0 0 .375-.375v-1.5A.375.375 0 0 0 4.875 9h-1.5A.375.375 0 0 0 3 9.375v1.5c0 .207.168.375.375.375Zm4.125 0a.75.75 0 0 0 0 1.5h9a.75.75 0 0 0 0-1.5h-9Z" clip-rule="evenodd" />
- </svg>
- <span>Episodes</span>
- </button>
- <button class="flex flex-row items-center focus:outline-none gap-2 text-sm font-bold py-2 px-4 rounded-full category-switch" data-target="trailer">
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4">
- <path d="M4.5 4.5a3 3 0 0 0-3 3v9a3 3 0 0 0 3 3h8.25a3 3 0 0 0 3-3v-9a3 3 0 0 0-3-3H4.5ZM19.94 18.75l-2.69-2.69V7.94l2.69-2.69c.944-.945 2.56-.276 2.56 1.06v11.38c0 1.336-1.616 2.005-2.56 1.06Z" />
- </svg>
- <span>Trailer</span>
- </button>
- </section>
- <section id="characters" class="w-full hidden">
- <div class="flex flex-wrap">
- {% for character in anime.characters %}
- <div class="w-full lg:w-1/2 px-4 py-2 flex justify-between">
- <div class="flex flex-row gap-2 items-center">
- <img src="{{ character.image }}" alt="{{ character.name }}" class="rounded-full w-16 h-16 object-cover"/>
- <div class="flex flex-col gap-2">
- <span class="font-bold">
- {% if user.preferences.character_name_language == "romaji" %}
- {{ character.name.full }}
- {% else %}
- {{ character.name.native }}
- {% endif %}
- </span>
- <span class="capitalize">{{ character.role }}</span>
- </div>
- </div>
- <div class="flex flex-col items-end">
- {% for voice_actor in character.voiceActors|slice:":1" %}
- <div class="flex flex-row gap-2 items-center mb-2">
- <div class="flex flex-col gap-2 text-right">
- <span class="font-bold">
- {% if user.preferences.character_name_language == "romaji" %}
- {{ voice_actor.name.full }}
- {% else %}
- {{ voice_actor.name.native }}
- {% endif %}
- </span>
- <span class="capitalize">{{ voice_actor.language }}</span>
+ <section class="w-full flex flex-col items-center justify-center p-2 relative detail-section">
+ <section class="inline-flex w-max flex-row gap-4 rounded-full mb-8 bg-white bg-opacity-10 mx-auto">
+ <button class="flex flex-row items-center focus:outline-none gap-2 text-sm font-bold py-2 px-4 rounded-full category-switch" data-target="characters">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4">
+ <path fill-rule="evenodd" d="M18.685 19.097A9.723 9.723 0 0 0 21.75 12c0-5.385-4.365-9.75-9.75-9.75S2.25 6.615 2.25 12a9.723 9.723 0 0 0 3.065 7.097A9.716 9.716 0 0 0 12 21.75a9.716 9.716 0 0 0 6.685-2.653Zm-12.54-1.285A7.486 7.486 0 0 1 12 15a7.486 7.486 0 0 1 5.855 2.812A8.224 8.224 0 0 1 12 20.25a8.224 8.224 0 0 1-5.855-2.438ZM15.75 9a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" clip-rule="evenodd" />
+ </svg>
+ <span>Characters</span>
+ </button>
+ <button class="flex flex-row items-center focus:outline-none gap-2 text-sm font-bold py-2 px-4 rounded-full category-switch bg-{{ user.preferences.accent_colour }}-600" data-target="episodes">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4">
+ <path fill-rule="evenodd" d="M1.5 5.625c0-1.036.84-1.875 1.875-1.875h17.25c1.035 0 1.875.84 1.875 1.875v12.75c0 1.035-.84 1.875-1.875 1.875H3.375A1.875 1.875 0 0 1 1.5 18.375V5.625Zm1.5 0v1.5c0 .207.168.375.375.375h1.5a.375.375 0 0 0 .375-.375v-1.5a.375.375 0 0 0-.375-.375h-1.5A.375.375 0 0 0 3 5.625Zm16.125-.375a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h1.5A.375.375 0 0 0 21 7.125v-1.5a.375.375 0 0 0-.375-.375h-1.5ZM21 9.375A.375.375 0 0 0 20.625 9h-1.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h1.5a.375.375 0 0 0 .375-.375v-1.5Zm0 3.75a.375.375 0 0 0-.375-.375h-1.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h1.5a.375.375 0 0 0 .375-.375v-1.5Zm0 3.75a.375.375 0 0 0-.375-.375h-1.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h1.5a.375.375 0 0 0 .375-.375v-1.5ZM4.875 18.75a.375.375 0 0 0 .375-.375v-1.5a.375.375 0 0 0-.375-.375h-1.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h1.5ZM3.375 15h1.5a.375.375 0 0 0 .375-.375v-1.5a.375.375 0 0 0-.375-.375h-1.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375Zm0-3.75h1.5a.375.375 0 0 0 .375-.375v-1.5A.375.375 0 0 0 4.875 9h-1.5A.375.375 0 0 0 3 9.375v1.5c0 .207.168.375.375.375Zm4.125 0a.75.75 0 0 0 0 1.5h9a.75.75 0 0 0 0-1.5h-9Z" clip-rule="evenodd" />
+ </svg>
+ <span>Episodes</span>
+ </button>
+ <button class="flex flex-row items-center focus:outline-none gap-2 text-sm font-bold py-2 px-4 rounded-full category-switch" data-target="trailer">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4">
+ <path d="M4.5 4.5a3 3 0 0 0-3 3v9a3 3 0 0 0 3 3h8.25a3 3 0 0 0 3-3v-9a3 3 0 0 0-3-3H4.5ZM19.94 18.75l-2.69-2.69V7.94l2.69-2.69c.944-.945 2.56-.276 2.56 1.06v11.38c0 1.336-1.616 2.005-2.56 1.06Z" />
+ </svg>
+ <span>Trailer</span>
+ </button>
+ </section>
+ <section id="characters" class="w-full hidden">
+ <div class="flex flex-wrap">
+ {% for character in anime.characters %}
+ <div class="w-full lg:w-1/2 px-4 py-2 flex justify-between">
+ <div class="flex flex-row gap-2 items-center">
+ <img src="{{ character.image }}" alt="{{ character.name }}" class="rounded-full w-16 h-16 object-cover"/>
+ <div class="flex flex-col gap-2">
+ <span class="font-bold">
+ {% if user.preferences.character_name_language == "romaji" %}
+ {{ character.name.full }}
+ {% else %}
+ {{ character.name.native }}
+ {% endif %}
+ </span>
+ <span class="capitalize">{{ character.role }}</span>
+ </div>
+ </div>
+ <div class="flex flex-col items-end">
+ {% for voice_actor in character.voiceActors|slice:":1" %}
+ <div class="flex flex-row gap-2 items-center mb-2">
+ <div class="flex flex-col gap-2 text-right">
+ <span class="font-bold">
+ {% if user.preferences.character_name_language == "romaji" %}
+ {{ voice_actor.name.full }}
+ {% else %}
+ {{ voice_actor.name.native }}
+ {% endif %}
+ </span>
+ <span class="capitalize">{{ voice_actor.language }}</span>
+ </div>
+ <img src="{{ voice_actor.image }}" alt="{{ voice_actor.name }}" class="rounded-full w-16 h-16 object-cover"/>
+ </div>
+ {% endfor %}
</div>
- <img src="{{ voice_actor.image }}" alt="{{ voice_actor.name }}" class="rounded-full w-16 h-16 object-cover"/>
</div>
{% endfor %}
</div>
- </div>
- {% endfor %}
- </div>
- </section>
- <section id="episodes" class="w-full flex-wrap flex justify-start">
- {% for episode in episodes.episodes %}
- <a href="{% url "watch:watch_episode" anime.id episode.number %}" class="w-full lg:w-1/2 px-2 mb-2">
- <div class="flex flex-row w-full bg-neutral-950 rounded hover:bg-{{ user.preferences.accent_colour }}-600 hover:bg-opacity-30 p-2 gap-4">
- <div class="flex-shrink-0">
- <img src="{% if episode.metadata.image %}{{ episode.metadata.image }}{% else %}{{ anime.cover}}{% endif %}" alt="{{ episode.title }}" class="w-48 h-28 object-cover rounded-lg"/>
+ </section>
+ <section id="episodes" class="w-full flex-wrap flex justify-start">
+ {% for episode in episodes.episodes %}
+ <a href="{% url "watch:watch_episode" anime.id episode.number %}" class="w-full lg:w-1/2 px-2 mb-2">
+ <div class="flex flex-col lg:flex-row w-full bg-neutral-950 rounded hover:bg-{{ user.preferences.accent_colour }}-600 hover:bg-opacity-30 p-2 gap-4">
+ <div class="flex-shrink-0">
+ <img src="{% if episode.metadata.image %}{{ episode.metadata.image }}{% else %}{{ anime.cover}}{% endif %}" alt="{{ episode.title }}" class="w-48 h-28 object-cover rounded-lg"/>
+ </div>
+ <div class="flex flex-col gap-2">
+ <h2 class="font-bold">{% if episode.metadata.title %}{{ episode.metadata.title }}{% else %}{{ episode.title }}{% endif %}</h2>
+ <p class="text-sm">{{ episode.metadata.description|truncatewords:50 }}</p>
+ </div>
+ </div>
+ </a>
+ {% endfor %}
+ {% if not episodes %}
+ <div class="w-full h-96 flex items-center justify-center">
+ <span>No Episodes Available</span>
</div>
- <div class="flex flex-col gap-2">
- <h2 class="font-bold">{% if episode.metadata.title %}{{ episode.metadata.title }}{% else %}{{ episode.title }}{% endif %}</h2>
- <p class="text-sm">{{ episode.metadata.description|truncatewords:50 }}</p>
+ {% endif %}
+ </section>
+ <section id="trailer" class="w-full hidden">
+ {% if anime.trailer and anime.trailer.site == "youtube" %}
+ <iframe src="https://www.youtube.com/embed/{{ anime.trailer.id }}" title="{{ anime.title.english }} Trailer" class="w-full aspect-video" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" credentialless allowfullscreen></iframe>
+ {% else %}
+ <div class="w-full h-96 flex items-center justify-center">
+ <span>No Trailer Available</span>
</div>
- </div>
- </a>
- {% endfor %}
- {% if not episodes %}
- <div class="w-full h-96 flex items-center justify-center">
- <span>No Episodes Available</span>
- </div>
- {% endif %}
- </section>
- <section id="trailer" class="w-full hidden">
- {% if anime.trailer and anime.trailer.site == "youtube" %}
- <iframe src="https://www.youtube.com/embed/{{ anime.trailer.id }}" title="{{ anime.title.english }} Trailer" class="w-full aspect-video" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" credentialless allowfullscreen></iframe>
- {% else %}
- <div class="w-full h-96 flex items-center justify-center">
- <span>No Trailer Available</span>
- </div>
- {% endif %}
- </section>
-</section>
+ {% endif %}
+ </section>
+ </section>
+ </div>
+</div>
+
{% endblock content %}
{% block scripts %}
<script>
diff --git a/watch/utils.py b/watch/utils.py
index 609ff1a..150213d 100644
--- a/watch/utils.py
+++ b/watch/utils.py
@@ -74,10 +74,10 @@ def get_anime_user_history(user, anime_id):
return history
-def store_in_redis_cache(anime_id, data):
+def store_in_redis_cache(anime_id, data, cache_time=60*60*12):
try:
print("Storing in cache=>", anime_id)
- r.set(anime_id, data, ex=60*60*12) # 1 hour
+ r.set(anime_id, data, ex=cache_time) # 1 hour
except Exception as e:
print(e)
pass
diff --git a/watch/views.py b/watch/views.py
index e5ccb06..32452a1 100644
--- a/watch/views.py
+++ b/watch/views.py
@@ -6,141 +6,206 @@ from authentication.utils import get_single_anime_mal
from watch.utils import get_episode_metadata, update_anime_user_history, get_anime_user_history, get_from_redis_cache, store_in_redis_cache
import requests
import json
-
+from functools import lru_cache
dotenv.load_dotenv()
def watch(request, anime_id, episode=None):
- forward_detail = request.GET.get("forward") == "detail"
- if not episode and request.user.preferences.default_watch_page == "detail" and not forward_detail:
+ user = request.user
+ preferences = user.preferences
+
+ if not episode and preferences.default_watch_page == "detail" and request.GET.get("forward") != "detail":
return redirect("detail:detail", anime_id=anime_id)
- anime_history = get_anime_user_history(request.user, anime_id)
-
+ anime_history = get_anime_user_history(user, anime_id)
watched_episodes = [h.episode for h in anime_history]
- current_watched_time = [h.time_watched for h in anime_history if h.episode == episode]
- current_watched_time = current_watched_time[0] if current_watched_time else 0
-
+
if not episode or episode < 1:
- episode = [h.episode for h in anime_history if h.last_watched]
- episode = episode[0] if episode else 1
+ episode = next((h.episode for h in anime_history if h.last_watched), 1)
return redirect("watch:watch_episode", anime_id=anime_id, episode=episode)
- mode = request.GET.get("mode", request.user.preferences.default_language)
-
- anime_data_cached = get_from_redis_cache(f"anime_{anime_id}_anime_data")
- anime_selected_cached = get_from_redis_cache(f"anime_{anime_id}_anime_selected")
- anime_episodes_cached = get_from_redis_cache(f"anime_{anime_id}_anime_episodes")
+ mode = request.GET.get("mode", preferences.default_language)
+
+ anime_data, anime_selected, anime_episodes = get_anime_combined_data(anime_id)
- anime_data = None
- anime_selected = None
- anime_episodes = None
- episode_data = None
- current_episode_data = None
- current_episode_name = None
- current_episode_metadata = None
+ if not anime_data or anime_data.get("status") == "Not yet aired" or not anime_selected or not anime_episodes:
+ return redirect("detail:detail", anime_id=anime_id)
- if anime_data_cached:
- try:
- anime_data = json.loads(anime_data_cached)
- except:
- anime_data = None
+ mode = process_mode(mode, anime_selected, episode)
+ episode_data, current_episode_data = process_episode_data(anime_episodes, episode, mode, preferences)
+
+ if not episode_data:
+ return redirect("watch:watch_episode", anime_id=anime_id, episode=anime_episodes["totalEpisodes"])
- if anime_selected_cached:
- try:
- anime_selected = json.loads(anime_selected_cached)
- except:
- anime_selected = None
+ current_watched_time = next((h.time_watched for h in anime_history if h.episode == episode), 0)
+ update_anime_user_history(user, anime_id, episode, current_watched_time)
- if anime_episodes_cached:
- try:
- anime_episodes = json.loads(anime_episodes_cached)
- except:
- anime_episodes = None
+ context = build_context(anime_data, anime_selected, anime_episodes, episode_data, current_episode_data,
+ episode, anime_id, anime_history, watched_episodes, mode)
- if not anime_data_cached:
- base_url = f"{os.getenv("CONSUMET_URL")}/meta/anilist/info/{anime_id}?provider=zoro"
- response = requests.get(base_url)
- anime_data = response.json()
- store_in_redis_cache(f"anime_{anime_id}_anime_data", json.dumps(anime_data))
+ if user.mal_access_token and "malId" in anime_data:
+ context["mal_data"] = get_mal_data(user.mal_access_token, anime_data["malId"])
- if anime_data["status"] == "Not yet aired":
- return redirect("detail:detail", anime_id=anime_id)
+ return render(request, "watch/watch.html", context)
- if not anime_selected_cached:
- z_anime_id = anime_data["episodes"][0]["id"].split("$")[0] if len(anime_data["episodes"]) > 0 else None
- if z_anime_id is not None:
- base_url = f"{os.getenv("ZORO_URL")}/anime/info?id={z_anime_id}"
- response = requests.get(base_url)
- anime_selected = response.json()
- store_in_redis_cache(f"anime_{anime_id}_anime_selected", json.dumps(anime_selected))
-
- if not anime_episodes_cached and anime_selected is not None:
- base_url = f"{os.getenv("ZORO_URL")}/anime/episodes/{anime_selected["anime"]["info"]["id"]}"
- response = requests.get(base_url)
- anime_episodes = response.json()
- store_in_redis_cache(f"anime_{anime_id}_anime_episodes", json.dumps(anime_episodes))
-
- if anime_selected is not None:
- if not anime_selected["anime"]["info"]["stats"]["episodes"][mode] or anime_selected["anime"]["info"]["stats"]["episodes"][mode] < episode:
- mode = "sub"
-
- if anime_episodes is not None:
- if episode > anime_episodes["totalEpisodes"]:
- return redirect("watch:watch_episode", anime_id=anime_id, episode=anime_episodes["totalEpisodes"])
-
- episode_d = [e for e in anime_episodes["episodes"] if e["number"] == episode][0]
-
- base_url = f"{os.getenv("ZORO_URL")}/anime/episode-srcs?id={episode_d["episodeId"]}?server&category={mode}"
- response = requests.get(base_url)
- episode_data = response.json()
-
- if "message" in episode_data and episode_data["message"] == "Couldn't find server. Try another server":
- base_url = f"{os.getenv("ZORO_URL")}/anime/episode-srcs?id={episode_d["episodeId"]}?server=hd-2&category={mode}"
- response = requests.get(base_url)
- episode_data = response.json()
-
- # if no captions are present and the mode is dub, and ingrain_sub_subtitles_in_dub is true, then fetch the sub track
- if "tracks" in episode_data and not any(t["kind"] == "captions" for t in episode_data["tracks"]) and mode == "dub" and request.user.preferences.ingrain_sub_subtitles_in_dub:
- base_url = f"{os.getenv("ZORO_URL")}/anime/episode-srcs?id={episode_d["episodeId"]}?server&category=sub"
- response = requests.get(base_url).json()
- captions = [t for t in response["tracks"] if t["kind"] == "captions"]
- if captions:
- episode_data["tracks"].extend(captions)
-
- current_episode_name = [e["title"] for e in anime_episodes["episodes"] if e["number"] == episode][0]
+@lru_cache(maxsize=100)
+def get_anime_combined_data(anime_id):
+ cache_keys = [
+ f"anime_{anime_id}_combined_data",
+ f"anime_{anime_id}_data",
+ f"anime_{anime_id}_selected",
+ f"anime_{anime_id}_episodes"
+ ]
- update_anime_user_history(request.user, anime_id, episode, current_watched_time)
-
- current_episode_data = [e for e in anime_episodes["episodes"] if e["number"] == episode][0]
+ cached_data = get_multiple_from_cache(cache_keys)
+
+ if cached_data.get(cache_keys[0]):
+ return json.loads(cached_data[cache_keys[0]])
+
+ anime_data = cached_data.get(cache_keys[1]) or fetch_anime_data(anime_id)
+ if not anime_data:
+ return None, None, None
+
+ anime_selected = cached_data.get(cache_keys[2]) or fetch_anime_selected(anime_data)
+ anime_episodes = cached_data.get(cache_keys[3]) or fetch_anime_episodes(anime_selected) if anime_selected else None
+
+ combined_data = [anime_data, anime_selected, anime_episodes]
+ store_in_redis_cache(cache_keys[0], json.dumps(combined_data), 3600) # Cache for 1 hour
+
+ return combined_data
+
+def get_multiple_from_cache(keys):
+ return {key: json.loads(value) if value else None
+ for key, value in zip(keys, map(get_from_redis_cache, keys))}
+
+def fetch_anime_data(anime_id):
+ url = f"{os.getenv('CONSUMET_URL')}/meta/anilist/info/{anime_id}?provider=zoro"
+ try:
+ response = requests.get(url, timeout=10)
+ response.raise_for_status()
+ data = response.json()
+ store_in_redis_cache(f"anime_{anime_id}_data", json.dumps(data), 86400) # Cache for 24 hours
+ return data
+ except requests.RequestException as e:
+ print(f"Error fetching anime data for ID {anime_id}: {e}")
+ return None
+
+def fetch_anime_selected(anime_data):
+ if anime_data and anime_data.get("episodes"):
+ z_anime_id = anime_data["episodes"][0]["id"].split("$")[0]
+ url = f"{os.getenv('ZORO_URL')}/anime/info?id={z_anime_id}"
+ try:
+ response = requests.get(url, timeout=10)
+ response.raise_for_status()
+ data = response.json()
+ store_in_redis_cache(f"anime_{anime_data['id']}_selected", json.dumps(data), 86400) # Cache for 24 hours
+ return data
+ except requests.RequestException as e:
+ print(f"Error fetching anime selected data: {e}")
+ return None
+
+def fetch_anime_episodes(anime_selected):
+ if anime_selected and anime_selected.get("anime", {}).get("info", {}).get("id"):
+ url = f"{os.getenv('ZORO_URL')}/anime/episodes/{anime_selected['anime']['info']['id']}"
+ try:
+ response = requests.get(url, timeout=10)
+ response.raise_for_status()
+ data = response.json()
+ store_in_redis_cache(f"anime_{anime_selected['anime']['info']['id']}_episodes", json.dumps(data), 86400) # Cache for 24 hours
+ return data
+ except requests.RequestException as e:
+ print(f"Error fetching anime episodes: {e}")
+ return None
+
+def process_mode(mode, anime_selected, episode):
+ if not anime_selected.get("anime", {}).get("info", {}).get("stats", {}).get("episodes", {}).get(mode) or \
+ anime_selected["anime"]["info"]["stats"]["episodes"].get(mode, 0) < episode:
+ return "sub"
+ return mode
+
+def process_episode_data(anime_episodes, episode, mode, preferences):
+ if not anime_episodes or episode > anime_episodes.get("totalEpisodes", 0):
+ return None, None
+
+ episode_d = next((e for e in anime_episodes.get("episodes", []) if e["number"] == episode), None)
+ if not episode_d:
+ return None, None
+
+ cache_key = f"episode_data_{episode_d['episodeId']}_{mode}"
+ episode_data = get_from_redis_cache(cache_key)
+
+ if not episode_data:
+ episode_data = fetch_episode_data(episode_d["episodeId"], mode)
+ if episode_data:
+ store_in_redis_cache(cache_key, json.dumps(episode_data), 3600) # Cache for 1 hour
+ else:
+ episode_data = json.loads(episode_data)
+
+ if preferences.ingrain_sub_subtitles_in_dub and mode == "dub" and \
+ not any(t.get("kind") == "captions" for t in episode_data.get("tracks", [])):
+ sub_data = fetch_episode_data(episode_d["episodeId"], "sub")
+ if sub_data:
+ episode_data.setdefault("tracks", []).extend([t for t in sub_data.get("tracks", []) if t.get("kind") == "captions"])
+
+ current_episode_data = {
+ "title": episode_d.get("title", ""),
+ "number": episode_d.get("number", 0),
+ }
- current_episode_metadata = get_episode_metadata(anime_data, episode)
+ return episode_data, current_episode_data
- context = {
+def fetch_episode_data(episode_id, mode):
+ url = f"{os.getenv('ZORO_URL')}/anime/episode-srcs?id={episode_id}?server&category={mode}"
+ try:
+ response = requests.get(url, timeout=10)
+ response.raise_for_status()
+ data = response.json()
+
+ if data.get("message") == "Couldn't find server. Try another server":
+ url = f"{os.getenv('ZORO_URL')}/anime/episode-srcs?id={episode_id}?server=hd-2&category={mode}"
+ response = requests.get(url, timeout=10)
+ response.raise_for_status()
+ data = response.json()
+
+ return data
+ except requests.RequestException as e:
+ print(f"Error fetching episode data for ID {episode_id}: {e}")
+ return None
+
+def build_context(anime_data, anime_selected, anime_episodes, episode_data, current_episode_data,
+ episode, anime_id, anime_history, watched_episodes, mode):
+ return {
"anime_data": anime_data,
"anime_selected": anime_selected,
"anime_episodes": anime_episodes,
"episode_data": episode_data,
"current_episode": episode,
"current_episode_data": current_episode_data,
- "stream_url": episode_data["sources"][0]["url"] if episode_data and "sources" in episode_data else None,
+ "stream_url": episode_data.get("sources", [{}])[0].get("url") if episode_data else None,
"anime_id": anime_id,
- "current_episode_name": current_episode_name,
+ "current_episode_name": current_episode_data.get("title") if current_episode_data else None,
"anime_history": anime_history,
"watched_episodes": watched_episodes,
- "current_watched_time": current_watched_time,
- "current_episode_metadata": current_episode_metadata,
+ "current_watched_time": next((h.time_watched for h in anime_history if h.episode == episode), 0),
+ "current_episode_metadata": get_episode_metadata(anime_data, episode),
"mode": mode,
}
- if request.user.mal_access_token and anime_data and "malId" in anime_data:
- mal_data = get_single_anime_mal(request.user.mal_access_token, anime_data["malId"])
+@lru_cache(maxsize=100)
+def get_mal_data(mal_access_token, mal_id):
+ cache_key = f"mal_data_{mal_id}"
+ mal_data = get_from_redis_cache(cache_key)
+
+ if not mal_data:
+ mal_data = get_single_anime_mal(mal_access_token, mal_id)
if mal_data:
- mal_data["average_episode_duration"] = mal_data["average_episode_duration"] // 60 + 1
- context["mal_data"] = mal_data
- context["mal_episode_range"] = range(1, mal_data["num_episodes"] + 1)
-
- return render(request, "watch/watch.html", context)
+ mal_data["average_episode_duration"] = mal_data.get("average_episode_duration", 0) // 60 + 1
+ mal_data["episode_range"] = list(range(1, mal_data.get("num_episodes", 1) + 1))
+ store_in_redis_cache(cache_key, json.dumps(mal_data), 86400) # Cache for 24 hours
+ else:
+ mal_data = json.loads(mal_data)
+
+ return mal_data
def update_episode_watch_time(request):
if request.method != "POST":