aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--detail/__init__.py0
-rw-r--r--detail/admin.py3
-rw-r--r--detail/apps.py6
-rw-r--r--detail/migrations/__init__.py0
-rw-r--r--detail/models.py3
-rw-r--r--detail/tests.py3
-rw-r--r--detail/urls.py8
-rw-r--r--detail/views.py41
-rw-r--r--static/css/main.css269
-rw-r--r--templates/detail/detail.html276
-rw-r--r--templates/home/index.html2
-rw-r--r--templates/partials/datacard_render.html2
-rw-r--r--templates/partials/datacardcompact_render.html2
-rw-r--r--templates/partials/datacardwide_render.html2
-rw-r--r--watch/utils.py16
-rw-r--r--watch/views.py7
-rw-r--r--yugen/settings.py4
-rw-r--r--yugen/urls.py1
18 files changed, 643 insertions, 2 deletions
diff --git a/detail/__init__.py b/detail/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/detail/__init__.py
diff --git a/detail/admin.py b/detail/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/detail/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/detail/apps.py b/detail/apps.py
new file mode 100644
index 0000000..c7f8bff
--- /dev/null
+++ b/detail/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class DetailConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "detail"
diff --git a/detail/migrations/__init__.py b/detail/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/detail/migrations/__init__.py
diff --git a/detail/models.py b/detail/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/detail/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/detail/tests.py b/detail/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/detail/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/detail/urls.py b/detail/urls.py
new file mode 100644
index 0000000..ca85152
--- /dev/null
+++ b/detail/urls.py
@@ -0,0 +1,8 @@
+from django.urls import path
+
+from . import views
+
+app_name = "detail"
+urlpatterns = [
+ path('<int:anime_id>/', views.detail, name='detail'),
+]
diff --git a/detail/views.py b/detail/views.py
new file mode 100644
index 0000000..da67f3f
--- /dev/null
+++ b/detail/views.py
@@ -0,0 +1,41 @@
+import json
+import os
+from django.shortcuts import render
+import requests
+
+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))
+
+ if anime_episodes is not None:
+ anime_episodes_metadata = get_all_episode_metadata(anime_data)
+ # attach metadata to episodes
+ for i, episode in enumerate(anime_episodes["episodes"]):
+ episode["metadata"] = anime_episodes_metadata[i]
+
+ context = {
+ "anime": anime_data,
+ "episodes": anime_episodes,
+ }
+
+ return render(request, "detail/detail.html", context) \ No newline at end of file
diff --git a/static/css/main.css b/static/css/main.css
index a0ea458..28015bf 100644
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -669,6 +669,46 @@ video {
top: 100%;
}
+.top-48 {
+ top: 12rem;
+}
+
+.top-16 {
+ top: 4rem;
+}
+
+.top-32 {
+ top: 8rem;
+}
+
+.top-36 {
+ top: 9rem;
+}
+
+.left-8 {
+ left: 2rem;
+}
+
+.left-\[18rem\] {
+ left: 18rem;
+}
+
+.top-96 {
+ top: 24rem;
+}
+
+.top-72 {
+ top: 18rem;
+}
+
+.-top-64 {
+ top: -16rem;
+}
+
+.top-64 {
+ top: 16rem;
+}
+
.z-10 {
z-index: 10;
}
@@ -701,6 +741,11 @@ video {
margin-bottom: 2rem;
}
+.my-2 {
+ margin-top: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
.mb-2 {
margin-bottom: 0.5rem;
}
@@ -741,6 +786,22 @@ video {
margin-top: 2rem;
}
+.mt-12 {
+ margin-top: 3rem;
+}
+
+.mt-16 {
+ margin-top: 4rem;
+}
+
+.mt-20 {
+ margin-top: 5rem;
+}
+
+.mb-8 {
+ margin-bottom: 2rem;
+}
+
.block {
display: block;
}
@@ -757,6 +818,10 @@ video {
display: inline-flex;
}
+.grid {
+ display: grid;
+}
+
.hidden {
display: none;
}
@@ -829,6 +894,14 @@ video {
height: 100%;
}
+.h-32 {
+ height: 8rem;
+}
+
+.h-28 {
+ height: 7rem;
+}
+
.max-h-24 {
max-height: 6rem;
}
@@ -898,10 +971,46 @@ video {
width: max-content;
}
+.w-1\/6 {
+ width: 16.666667%;
+}
+
+.w-1\/12 {
+ width: 8.333333%;
+}
+
+.w-2\/12 {
+ width: 16.666667%;
+}
+
+.w-10\/12 {
+ width: 83.333333%;
+}
+
+.w-4\/12 {
+ width: 33.333333%;
+}
+
+.w-8\/12 {
+ width: 66.666667%;
+}
+
+.w-\[100vw\] {
+ width: 100vw;
+}
+
.min-w-32 {
min-width: 8rem;
}
+.min-w-\[100vw\] {
+ min-width: 100vw;
+}
+
+.min-w-\[80vw\] {
+ min-width: 80vw;
+}
+
.max-w-7xl {
max-width: 80rem;
}
@@ -927,10 +1036,22 @@ video {
max-width: max-content;
}
+.max-w-\[100vw\] {
+ max-width: 100vw;
+}
+
+.max-w-\[80vw\] {
+ max-width: 80vw;
+}
+
.flex-1 {
flex: 1 1 0%;
}
+.flex-shrink-0 {
+ flex-shrink: 0;
+}
+
.origin-left {
transform-origin: left;
}
@@ -987,6 +1108,10 @@ video {
list-style-position: inside;
}
+.grid-cols-1 {
+ grid-template-columns: repeat(1, minmax(0, 1fr));
+}
+
.flex-row {
flex-direction: row;
}
@@ -1061,6 +1186,10 @@ video {
overflow: hidden;
}
+.overflow-x-auto {
+ overflow-x: auto;
+}
+
.overflow-y-auto {
overflow-y: auto;
}
@@ -1390,6 +1519,11 @@ video {
background-color: rgb(161 98 7 / var(--tw-bg-opacity));
}
+.bg-gray-800 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(31 41 55 / var(--tw-bg-opacity));
+}
+
.bg-opacity-10 {
--tw-bg-opacity: 0.1;
}
@@ -1402,6 +1536,10 @@ video {
--tw-bg-opacity: 0.4;
}
+.bg-opacity-30 {
+ --tw-bg-opacity: 0.3;
+}
+
.bg-cover {
background-size: cover;
}
@@ -1486,6 +1624,11 @@ video {
padding-bottom: 2rem;
}
+.px-8 {
+ padding-left: 2rem;
+ padding-right: 2rem;
+}
+
.pb-4 {
padding-bottom: 1rem;
}
@@ -1510,6 +1653,10 @@ video {
padding-top: 140%;
}
+.pb-2 {
+ padding-bottom: 0.5rem;
+}
+
.text-center {
text-align: center;
}
@@ -2160,6 +2307,10 @@ main {
--tw-bg-opacity: 0.3;
}
+.hover\:bg-opacity-50:hover {
+ --tw-bg-opacity: 0.5;
+}
+
.hover\:text-purple-600:hover {
--tw-text-opacity: 1;
color: rgb(147 51 234 / var(--tw-text-opacity));
@@ -2236,6 +2387,10 @@ main {
}
@media (min-width: 640px) {
+ .sm\:top-96 {
+ top: 24rem;
+ }
+
.sm\:size-3 {
width: 0.75rem;
height: 0.75rem;
@@ -2246,10 +2401,26 @@ main {
height: 1rem;
}
+ .sm\:max-w-full {
+ max-width: 100%;
+ }
+
+ .sm\:grid-cols-2 {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
.sm\:gap-2 {
gap: 0.5rem;
}
+ .sm\:overflow-scroll {
+ overflow: scroll;
+ }
+
+ .sm\:overflow-x-scroll {
+ overflow-x: scroll;
+ }
+
.sm\:p-2 {
padding: 0.5rem;
}
@@ -2259,6 +2430,16 @@ main {
padding-right: 1.5rem;
}
+ .sm\:px-4 {
+ padding-left: 1rem;
+ padding-right: 1rem;
+ }
+
+ .sm\:py-2 {
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ }
+
.sm\:text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
@@ -2281,6 +2462,43 @@ main {
}
@media (min-width: 1024px) {
+ .lg\:top-48 {
+ top: 12rem;
+ }
+
+ .lg\:top-56 {
+ top: 14rem;
+ }
+
+ .lg\:bottom-12 {
+ bottom: 3rem;
+ }
+
+ .lg\:bottom-0 {
+ bottom: 0px;
+ }
+
+ .lg\:top-96 {
+ top: 24rem;
+ }
+
+ .lg\:top-64 {
+ top: 16rem;
+ }
+
+ .lg\:left-\[18rem\] {
+ left: 18rem;
+ }
+
+ .lg\:top-0 {
+ top: 0px;
+ }
+
+ .lg\:my-4 {
+ margin-top: 1rem;
+ margin-bottom: 1rem;
+ }
+
.lg\:block {
display: block;
}
@@ -2297,6 +2515,10 @@ main {
height: 39vw;
}
+ .lg\:h-96 {
+ height: 24rem;
+ }
+
.lg\:max-h-\[761px\] {
max-height: 761px;
}
@@ -2321,10 +2543,40 @@ main {
width: 15rem;
}
+ .lg\:w-10\/12 {
+ width: 83.333333%;
+ }
+
+ .lg\:w-2\/12 {
+ width: 16.666667%;
+ }
+
+ .lg\:w-full {
+ width: 100%;
+ }
+
+ .lg\:w-fit {
+ width: -moz-fit-content;
+ width: fit-content;
+ }
+
+ .lg\:max-w-fit {
+ max-width: -moz-fit-content;
+ max-width: fit-content;
+ }
+
+ .lg\:grid-cols-3 {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+
.lg\:flex-row {
flex-direction: row;
}
+ .lg\:flex-col {
+ flex-direction: column;
+ }
+
.lg\:flex-nowrap {
flex-wrap: nowrap;
}
@@ -2341,6 +2593,10 @@ main {
justify-content: flex-end;
}
+ .lg\:gap-4 {
+ gap: 1rem;
+ }
+
.lg\:bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
@@ -2363,6 +2619,15 @@ main {
padding-left: 2rem;
padding-right: 2rem;
}
+
+ .lg\:pb-0 {
+ padding-bottom: 0px;
+ }
+
+ .lg\:text-4xl {
+ font-size: 2.25rem;
+ line-height: 2.5rem;
+ }
}
@media (min-width: 1280px) {
@@ -2386,6 +2651,10 @@ main {
width: 24rem;
}
+ .xl\:grid-cols-4 {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ }
+
.xl\:flex-row {
flex-direction: row;
}
diff --git a/templates/detail/detail.html b/templates/detail/detail.html
new file mode 100644
index 0000000..360fd2c
--- /dev/null
+++ b/templates/detail/detail.html
@@ -0,0 +1,276 @@
+{% extends "partials/base.html" %}
+{% load custom_filters %}
+
+{% 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>
+ <div class="w-4/12 lg:w-2/12 absolute px-2 lg:px-8 top-16 lg:top-48">
+ <img src="{{ anime.image }}" alt="{{ anime.title.english }}" class="rounded-lg" class="w-full h-full rounded-lg object-cover"/>
+ <a href="{% url "watch:watch" anime.id %}?forward=detail" class="bg-{{ user.preferences.accent_colour }}-600 hover:bg-{{ user.preferences.accent_colour }}-700 text-white text-xs sm:text-sm font-bold py-1 px-2 sm:py-2 sm:px-4 rounded-lg my-2 flex flex-row items-center justify-center gap-1">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4">
+ <path fill-rule="evenodd" d="M4.5 5.653c0-1.427 1.529-2.33 2.779-1.643l11.54 6.347c1.295.712 1.295 2.573 0 3.286L7.28 19.99c-1.25.687-2.779-.217-2.779-1.643V5.653Z" clip-rule="evenodd" />
+ </svg>
+ <span>Watch Now</span>
+ </a>
+ <div class="flex flex-row gap-2 my-2">
+ <a href="https://anilist.co/anime/{{ anime.id }}" target="_blank" class="text-xs font-bold bg-{{ user.preferences.accent_colour }}-400 bg-opacity-30 flex-1 justify-center flex hover:bg-opacity-50 rounded px-2 py-1">
+ <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="M24 17.53v2.421c0 .71-.391 1.101-1.1 1.101h-5l-.057-.165L11.84 3.736c.106-.502.46-.788 1.053-.788h2.422c.71 0 1.1.391 1.1 1.1v12.38H22.9c.71 0 1.1.392 1.1 1.101zM11.034 2.947l6.337 18.104h-4.918l-1.052-3.131H6.019l-1.077 3.131H0L6.361 2.948h4.673zm-.66 10.96-1.69-5.014-1.541 5.015h3.23z"></path></svg>
+ </a>
+ <a href="https://myanimelist.net/anime/{{ anime.malId }}" target="_blank" class="ext-xs font-bold bg-{{ user.preferences.accent_colour }}-400 bg-opacity-30 flex-1 justify-center flex hover:bg-opacity-50 rounded px-2 py-1">
+ <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">
+ <span class="font-bold">Format: </span>{{ anime.type }}
+ </div>
+ <div class="whitespace-nowrap">
+ <span class="font-bold">Episodes: </span>{{ anime.totalEpisodes }}
+ </div>
+ <div class="whitespace-nowrap">
+ <span class="font-bold">Year: </span>{{ anime.releaseDate }}
+ </div>
+ <div class="whitespace-nowrap">
+ <span class="font-bold">Duration: </span>{{ anime.duration }} mins
+ </div>
+ <div class="whitespace-nowrap">
+ <span class="font-bold">Status: </span>{{ anime.status }}
+ </div>
+ <div class="whitespace-nowrap capitalize">
+ <span class="font-bold">Season: </span>{{ anime.season }}
+ </div>
+ <div class="whitespace-nowrap">
+ <span class="font-bold">Rating: </span>{{ anime.rating }} / 100
+ </div>
+ <div class="whitespace-nowrap">
+ <span class="font-bold">Popularity: </span>{{ anime.popularity }}
+ </div>
+ <div class="whitespace-nowrap">
+ <span class="font-bold">Country: </span>{{ anime.countryOfOrigin }}
+ </div>
+ <div class="whitespace-nowrap">
+ <span class="font-bold">Studios: </span>
+ {% for studio in anime.studios %}
+ <span>{{ studio }}</span>{% if not forloop.last %}, {% endif %}
+ {% endfor %}
+ </div>
+ </div>
+ </div>
+ <div class="w-8/12 lg:w-10/12 absolute top-36 lg:top-48 right-0">
+ <div class="inline-flex gap-1 sm:gap-2 flex-wrap">
+ <span {% if anime.color %}style="background-color: {{ anime.color }}3a;"{% endif %} class="text-[10px] sm:text-xs font-bold bg-white bg-opacity-10 p-1 rounded flex items-center gap-1">
+ <svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" class="size-3 sm:size-4">
+ <path d="M3.604 7.197l7.138 -3.109a.96 .96 0 0 1 1.27 .527l4.924 11.902a1 1 0 0 1 -.514 1.304l-7.137 3.109a.96 .96 0 0 1 -1.271 -.527l-4.924 -11.903a1 1 0 0 1 .514 -1.304z"></path>
+ <path d="M15 4h1a1 1 0 0 1 1 1v3.5"></path>
+ <path d="M20 6c.264 .112 .52 .217 .768 .315a1 1 0 0 1 .53 1.311l-2.298 5.374"></path>
+ </svg>
+ {{ anime.type }}
+ </span>
+ <span {% if anime.color %}style="background-color: {{ anime.color }}3a;"{% endif %} class="text-[10px] sm:text-xs font-bold bg-white bg-opacity-10 p-1 rounded flex items-center gap-1">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-3 sm:size-4">
+ <path fill-rule="evenodd" d="M6.75 2.25A.75.75 0 0 1 7.5 3v1.5h9V3A.75.75 0 0 1 18 3v1.5h.75a3 3 0 0 1 3 3v11.25a3 3 0 0 1-3 3H5.25a3 3 0 0 1-3-3V7.5a3 3 0 0 1 3-3H6V3a.75.75 0 0 1 .75-.75Zm13.5 9a1.5 1.5 0 0 0-1.5-1.5H5.25a1.5 1.5 0 0 0-1.5 1.5v7.5a1.5 1.5 0 0 0 1.5 1.5h13.5a1.5 1.5 0 0 0 1.5-1.5v-7.5Z" clip-rule="evenodd" />
+ </svg>
+ {{ anime.releaseDate }}
+ </span>
+ <span {% if anime.color %}style="background-color: {{ anime.color }}3a;"{% endif %} class="text-[10px] sm:text-xs font-bold bg-white bg-opacity-10 p-1 rounded flex items-center gap-1">
+ {% if anime.status == "Ongoing" %}
+ <span class="text-green-500">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-2 sm:size-3">
+ <circle cx="12" cy="12" r="12" />
+ </svg>
+ </span>
+ {% elif anime.status == "Not yet aired" %}
+ <span class="text-yellow-500">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-2 sm:size-3">
+ <circle cx="12" cy="12" r="12" />
+ </svg>
+ </span>
+ {% else %}
+ <span class="text-blue-500">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-2 sm:size-3">
+ <circle cx="12" cy="12" r="12" />
+ </svg>
+ </span>
+ {% endif %}
+ {{ anime.status }}
+ </span>
+ <span {% if anime.color %}style="background-color: {{ anime.color }}3a;"{% endif %} class="text-[10px] sm:text-xs font-bold bg-white bg-opacity-10 p-1 rounded flex items-center gap-1">
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-3 sm:size-4">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z"/>
+ </svg>
+ {{ anime.rating }}
+ </span>
+ <span class="text-xs sm:text-sm font-bold flex gap-2 flex-row flex-wrap items-center">
+ {% for genre in anime.genres %}
+ {% if genre == "Action" %}
+ <span class="text-xs font-bold bg-green-100 bg-opacity-20 text-green-300 py-1 px-2 rounded-full">
+ {% elif genre == "Adventure" %}
+ <span class="text-xs font-bold bg-pink-100 bg-opacity-20 text-pink-300 py-1 px-2 rounded-full">
+ {% elif genre == "Cars" %}
+ <span class="text-xs font-bold bg-orange-100 bg-opacity-20 text-orange-300 py-1 px-2 rounded-full">
+ {% elif genre == "Comedy" %}
+ <span class="text-xs font-bold bg-purple-100 bg-opacity-20 text-purple-300 py-1 px-2 rounded-full">
+ {% elif genre == "Drama" %}
+ <span class="text-xs font-bold bg-blue-100 bg-opacity-20 text-blue-300 py-1 px-2 rounded-full">
+ {% elif genre == "Fantasy" %}
+ <span class="text-xs font-bold bg-yellow-100 bg-opacity-20 text-yellow-300 py-1 px-2 rounded-full">
+ {% elif genre == "Horror" %}
+ <span class="text-xs font-bold bg-red-100 bg-opacity-20 text-red-300 py-1 px-2 rounded-full">
+ {% elif genre == "Mahou Shoujo" %}
+ <span class="text-xs font-bold bg-teal-100 bg-opacity-20 text-teal-300 py-1 px-2 rounded-full">
+ {% elif genre == "Mecha" %}
+ <span class="text-xs font-bold bg-indigo-100 bg-opacity-20 text-indigo-300 py-1 px-2 rounded-full">
+ {% elif genre == "Music" %}
+ <span class="text-xs font-bold bg-pink-100 bg-opacity-20 text-pink-300 py-1 px-2 rounded-full">
+ {% elif genre == "Mystery" %}
+ <span class="text-xs font-bold bg-purple-100 bg-opacity-20 text-purple-300 py-1 px-2 rounded-full">
+ {% elif genre == "Psychological" %}
+ <span class="text-xs font-bold bg-blue-100 bg-opacity-20 text-blue-300 py-1 px-2 rounded-full">
+ {% elif genre == "Romance" %}
+ <span class="text-xs font-bold bg-yellow-100 bg-opacity-20 text-yellow-300 py-1 px-2 rounded-full">
+ {% elif genre == "Sci-Fi" %}
+ <span class="text-xs font-bold bg-red-100 bg-opacity-20 text-red-300 py-1 px-2 rounded-full">
+ {% elif genre == "Slice of Life" %}
+ <span class="text-xs font-bold bg-teal-100 bg-opacity-20 text-teal-300 py-1 px-2 rounded-full">
+ {% elif genre == "Sports" %}
+ <span class="text-xs font-bold bg-indigo-100 bg-opacity-20 text-indigo-300 py-1 px-2 rounded-full">
+ {% elif genre == "Supernatural" %}
+ <span class="text-xs font-bold bg-green-100 bg-opacity-20 text-green-300 py-1 px-2 rounded-full">
+ {% elif genre == "Thriller" %}
+ <span class="text-xs font-bold bg-orange-100 bg-opacity-20 text-orange-300 py-1 px-2 rounded-full">
+ {% else %}
+ <span class="text-xs font-bold bg-white bg-opacity-20 text-white py-1 px-2 rounded-full">
+ {% endif %}
+ {{ genre }}
+ </span>
+ {% endfor %}
+ </span>
+ </div>
+ <h2 class="text-2xl lg:text-4xl font-bold text-transparent bg-clip-text my-2" style="background: linear-gradient(-45deg, {% if anime.color %}{{ anime.color }}{% else %}white{% endif %}, white); -webkit-background-clip: text; background-clip: text;">
+ {% if user.preferences.title_language == "english" and anime.title.english %}
+ {{ anime.title.english }}
+ {% elif user.preferences.title_language == "native" and anime.title.native %}
+ {{ anime.title.native }}
+ {% else %}
+ {{ anime.title.romaji }}
+ {% endif %}
+ </h2>
+ <p class="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>
+ </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="{{ episode.metadata.image }}" alt="{{ episode.title }}" class="w-48 h-28 object-cover rounded-lg"/>
+ </div>
+ <div class="flex flex-col gap-2">
+ <h2 class="font-bold">{{ episode.metadata.title }}</h2>
+ <p class="text-sm">{{ episode.metadata.description }}</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>
+ {% 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>
+{% endblock content %}
+{% block scripts %}
+<script>
+ const categorySwitch = document.querySelectorAll(".category-switch");
+ categorySwitch.forEach((button) => {
+ button.addEventListener("click", () => {
+ categorySwitch.forEach((button) => {
+ button.classList.remove("active-category");
+ button.classList.remove("bg-{{ user.preferences.accent_colour }}-600");
+ });
+ button.classList.add("active-category");
+ button.classList.add("bg-{{ user.preferences.accent_colour }}-600");
+
+ const target = button.getAttribute("data-target");
+ const sections = [document.getElementById("characters"), document.getElementById("episodes"), document.getElementById("trailer")];
+ sections.forEach((section) => {
+ section.classList.add("hidden");
+ section.classList.remove("flex");
+ });
+ document.getElementById(target).classList.remove("hidden");
+ document.getElementById(target).classList.add("flex");
+ });
+ });
+</script>
+{% endblock scripts %} \ No newline at end of file
diff --git a/templates/home/index.html b/templates/home/index.html
index 423441e..9f770fa 100644
--- a/templates/home/index.html
+++ b/templates/home/index.html
@@ -57,7 +57,7 @@
</div>
</div>
<div class="flex flex-row w-full lg:w-1/2 gap-2 lg:justify-end items-end">
- <a href="#future_anime_details" class="bg-white bg-opacity-10 text-white text-sm font-bold py-3 px-6 rounded-full flex items-center gap-2">
+ <a href="{% url "detail:detail" anime.id %}" class="bg-white bg-opacity-10 text-white text-sm font-bold py-3 px-6 rounded-full flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"/>
</svg>
diff --git a/templates/partials/datacard_render.html b/templates/partials/datacard_render.html
index a2b40ee..00fae91 100644
--- a/templates/partials/datacard_render.html
+++ b/templates/partials/datacard_render.html
@@ -1,4 +1,5 @@
{% for anime in data %}
+{% if not anime.status == "Not yet aired"%}
<a href="{% url "watch:watch" anime.id %}" class="w-1/2 md:w-1/3 lg:w-1/4 xl:w-1/5 2xl:w-1/6 text-gray-500 p-1 sm:p-2 mb-4 hover:text-white flex flex-col">
<div class="relative pt-[140%]">
<img src="{{ anime.image }}" alt="{{ anime.title.english }}" class="absolute top-0 left-0 w-full h-full rounded-lg object-cover"/>
@@ -58,4 +59,5 @@
</div>
</div>
</a>
+{% endif %}
{% endfor %} \ No newline at end of file
diff --git a/templates/partials/datacardcompact_render.html b/templates/partials/datacardcompact_render.html
index 461c450..4aecc1c 100644
--- a/templates/partials/datacardcompact_render.html
+++ b/templates/partials/datacardcompact_render.html
@@ -1,4 +1,5 @@
{% for anime in data %}
+{% if not anime.status == "Not yet aired"%}
<div class="w-full max-w-full px-2 mb-2">
<a href="{% url 'watch:watch' anime.id %}">
<div class="flex flex-row bg-neutral-950 rounded hover:bg-{{ user.preferences.accent_colour }}-600 hover:bg-opacity-30 p-2 gap-4 items-center">
@@ -122,4 +123,5 @@
</div>
</a>
</div>
+{% endif %}
{% endfor %} \ No newline at end of file
diff --git a/templates/partials/datacardwide_render.html b/templates/partials/datacardwide_render.html
index 93886dd..827270b 100644
--- a/templates/partials/datacardwide_render.html
+++ b/templates/partials/datacardwide_render.html
@@ -1,5 +1,6 @@
{% load custom_filters %}
{% for anime in data %}
+{% if not anime.status == "Not yet aired"%}
<div 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 flex-col gap-2 min-w-32">
@@ -134,4 +135,5 @@
</a>
</div>
</div>
+{% endif %}
{% endfor %} \ No newline at end of file
diff --git a/watch/utils.py b/watch/utils.py
index c6b5180..af3b74f 100644
--- a/watch/utils.py
+++ b/watch/utils.py
@@ -18,7 +18,6 @@ r = redis.Redis(
# print("Redis cache flushed")
def get_episode_metadata(anime_data, episode):
- # Special Cases:
special_case = False
if anime_data["title"]["english"] == "Attack on Titan Final Season THE FINAL CHAPTERS Special 1" and episode == 2:
episode = 1
@@ -34,6 +33,21 @@ def get_episode_metadata(anime_data, episode):
return current_episode_metadata
+def get_all_episode_metadata(anime_data):
+ episode_metadata = get_from_redis_cache(f"anime_{anime_data['id']}_episode_metadata")
+ if episode_metadata:
+ episode_metadata = json.loads(episode_metadata)
+ else:
+ episode_metadata = get_anime_episodes(anime_data)
+ store_in_redis_cache(f"anime_{anime_data['id']}_episode_metadata", json.dumps(episode_metadata))
+
+ # Special cases
+ if anime_data["title"]["english"] == "Attack on Titan Final Season THE FINAL CHAPTERS Special 1":
+ episode_metadata.insert(0, episode_metadata[0])
+
+ return episode_metadata
+
+
def update_anime_user_history(user, anime_id, episode, time_watched):
# per episode history
history, created = UserHistory.objects.get_or_create(user=user, anime_id=anime_id, episode=episode)
diff --git a/watch/views.py b/watch/views.py
index 7d3b981..e5ccb06 100644
--- a/watch/views.py
+++ b/watch/views.py
@@ -11,6 +11,10 @@ import json
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:
+ return redirect("detail:detail", anime_id=anime_id)
+
anime_history = get_anime_user_history(request.user, anime_id)
watched_episodes = [h.episode for h in anime_history]
@@ -60,6 +64,9 @@ def watch(request, anime_id, episode=None):
anime_data = response.json()
store_in_redis_cache(f"anime_{anime_id}_anime_data", json.dumps(anime_data))
+ if anime_data["status"] == "Not yet aired":
+ return redirect("detail:detail", anime_id=anime_id)
+
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:
diff --git a/yugen/settings.py b/yugen/settings.py
index c8dedbc..07052b9 100644
--- a/yugen/settings.py
+++ b/yugen/settings.py
@@ -32,7 +32,11 @@ DEBUG = True if os.environ.get("DEBUG") == "True" else False
ALLOWED_HOSTS = ["127.0.0.1", "localhost", ".vercel.app", ".rize.moe"]
AUTH_USER_MODEL = "authentication.User"
+X_FRAME_OPTIONS = "SAMEORIGIN"
+CORS_ALLOWED_ORIGINS = [
+ "https://www.youtube.com",
+]
# Application definition
INSTALLED_APPS = [
diff --git a/yugen/urls.py b/yugen/urls.py
index b89fcff..db1aa79 100644
--- a/yugen/urls.py
+++ b/yugen/urls.py
@@ -22,6 +22,7 @@ from django.conf.urls.static import static
urlpatterns = [
path("", include("homepage.urls", namespace="home")),
path("watch/", include("watch.urls", namespace="watch")),
+ path("detail/", include("detail.urls", namespace="detail")),
path("profile/", include("user_profile.urls", namespace="profile")),
path("auth/", include("authentication.urls", namespace="auth")),
path("admin/", admin.site.urls),