implemented UI for media details (TODO add download capabilities)

This commit is contained in:
Francesco Grazioso 2024-05-04 14:40:33 +02:00
parent 6b22a90688
commit 0e93552e69
5 changed files with 183 additions and 132 deletions

View File

@ -55,6 +55,7 @@ class SearchView(viewsets.ViewSet):
try: try:
match self.type_media: match self.type_media:
case "TV": case "TV":
def stream_episodes():
self.site_version, self.domain = get_version_and_domain() self.site_version, self.domain = get_version_and_domain()
video_source = VideoSource() video_source = VideoSource()
@ -79,7 +80,11 @@ class SearchView(viewsets.ViewSet):
] ]
episodes[i_season][i_episode] = episode.__dict__ episodes[i_season][i_episode] = episode.__dict__
return Response({"episodes": episodes}) yield f'{json.dumps({"episodes": episodes})}\n\n'
response = StreamingHttpResponse(stream_episodes(), content_type='text/event-stream')
return response
case "TV_ANIME": case "TV_ANIME":
def stream_episodes(): def stream_episodes():
episodes_downloader = EpisodeDownloader(self.media_id, self.media_slug) episodes_downloader = EpisodeDownloader(self.media_id, self.media_slug)
@ -88,11 +93,13 @@ class SearchView(viewsets.ViewSet):
for i in range(1, episoded_count + 1): for i in range(1, episoded_count + 1):
episode_info = episodes_downloader.get_info_episode(index_ep=i) episode_info = episodes_downloader.get_info_episode(index_ep=i)
episode_info["episode_id"] = i episode_info["episode_id"] = i
episode_info["episode_total"] = episoded_count
print(f"Getting episode {i} of {episoded_count} info...") print(f"Getting episode {i} of {episoded_count} info...")
yield f'data: {json.dumps(episode_info)}\n\n' yield f'{json.dumps(episode_info)}\n\n'
response = StreamingHttpResponse(stream_episodes(), content_type='text/event-stream') response = StreamingHttpResponse(stream_episodes(), content_type='text/event-stream')
return response return response
except Exception as e: except Exception as e:
return Response( return Response(
{ {
@ -112,6 +119,7 @@ class DownloadView(viewsets.ViewSet):
self.type_media = request.data.get("type_media").upper() self.type_media = request.data.get("type_media").upper()
self.download_id = request.data.get("download_id") self.download_id = request.data.get("download_id")
if self.type_media in ["TV", "MOVIE"]:
self.site_version, self.domain = get_version_and_domain() self.site_version, self.domain = get_version_and_domain()
response_dict = {"error": "No media found with that search query"} response_dict = {"error": "No media found with that search query"}

View File

@ -3,71 +3,14 @@ import { RouterLink, RouterView } from 'vue-router'
</script> </script>
<template> <template>
<!--<RouterLink to="/">Home</RouterLink>-->
<RouterView /> <RouterView />
</template> </template>
<style scoped> <style>
header { #app {
line-height: 1.5; font-family: Avenir, Helvetica, Arial, sans-serif;
max-height: 100vh; -webkit-font-smoothing: antialiased;
} -moz-osx-font-smoothing: grayscale;
margin-top: 60px;
.logo {
display: block;
margin: 0 auto 2rem;
}
nav {
width: 100%;
font-size: 12px;
text-align: center;
margin-top: 2rem;
}
nav a.router-link-exact-active {
color: var(--color-text);
}
nav a.router-link-exact-active:hover {
background-color: transparent;
}
nav a {
display: inline-block;
padding: 0 1rem;
border-left: 1px solid var(--color-border);
}
nav a:first-of-type {
border: 0;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
nav {
text-align: left;
margin-left: -1rem;
font-size: 1rem;
padding: 1rem 0;
margin-top: 1rem;
}
} }
</style> </style>

View File

@ -17,15 +17,12 @@ export default function search(query: string, type: string) : Promise<MediaItemR
export async function getEpisodesInfo(mediaId: number, mediaSlug: string, mediaType: string): Promise<Response> { export async function getEpisodesInfo(mediaId: number, mediaSlug: string, mediaType: string): Promise<Response> {
const url = `${BASE_URL}/search/get_episodes_info?media_id=${mediaId}&media_slug=${mediaSlug}&type_media=${mediaType}`; const url = `${BASE_URL}/search/get_episodes_info?media_id=${mediaId}&media_slug=${mediaSlug}&type_media=${mediaType}`;
if (mediaType === 'TV_ANIME') {
return await fetch(url, { return await fetch(url, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'text/event-stream' 'Content-Type': 'text/event-stream'
} }
}); });
} else {
return Promise.resolve(new Response());
}
} }

View File

@ -24,7 +24,7 @@ export interface MediaItemResponse {
media: MediaItem[]; media: MediaItem[];
} }
export interface EpisodeAnime { export interface Episode {
id: number; id: number;
anime_id: number; anime_id: number;
user_id: number | null; user_id: number | null;
@ -38,8 +38,21 @@ export interface EpisodeAnime {
file_name: string; file_name: string;
tg_post: number; tg_post: number;
episode_id: number; episode_id: number;
episode_total: number;
name: string; // TV Show exclusive
plot: string; // TV Show exclusive
duration: number; // TV Show exclusive
season_id: number; // TV Show exclusive
created_by: any; // TV Show exclusive
updated_at: string; // TV Show exclusive
} }
export interface EpisodeAnimeResponse { export interface Season {
episodes: EpisodeAnime[]; [key: string]: {
[key: string]: Episode;
};
}
export interface SeasonResponse {
episodes: Season;
} }

View File

@ -1,21 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import type { EpisodeAnime, MediaItem } from "@/api/interfaces"; import type {Episode, MediaItem, Season, SeasonResponse} from "@/api/interfaces";
import { onMounted, ref, onUnmounted } from "vue"; import { onMounted, ref } from "vue";
import { getEpisodesInfo } from "@/api/api"; import { getEpisodesInfo } from "@/api/api";
const route = useRoute() const route = useRoute()
const item: MediaItem = JSON.parse(<string>route.params.item) const item: MediaItem = JSON.parse(<string>route.params.item)
const imageUrl: string = <string>route.params.imageUrl const imageUrl: string = <string>route.params.imageUrl
const episodes = ref<EpisodeAnime[]>([]) const animeEpisodes = ref<Episode[]>([])
const tvShowEpisodes = ref<any[]>([])
const loading = ref(false) const loading = ref(false)
onMounted(async () => { onMounted(async () => {
if (item.type !== 'TV_ANIME' && item.type !== 'TV') { if (['MOVIE', 'OVA'].includes(item.type)) {
return return
} } else {
loading.value = true loading.value = true;
const response = await getEpisodesInfo(item.id, item.slug, item.type) const response = await getEpisodesInfo(item.id, item.slug, item.type)
if (response && response.body) { if (response && response.body) {
loading.value = false; loading.value = false;
@ -25,8 +26,22 @@ onMounted(async () => {
if (done) { if (done) {
break; break;
} }
const episodesData:EpisodeAnime = JSON.parse(value.split("a:")[1].trim()); if (item.type === 'TV_ANIME') {
episodes.value.push(episodesData); const episodesData:Episode = JSON.parse(value.trim());
animeEpisodes.value.push(episodesData);
} else {
const episodesData:SeasonResponse = JSON.parse(value.trim());
for (const seasonKey in episodesData.episodes) {
const season = episodesData.episodes[seasonKey];
const episodes:Episode[] = [];
for (const episodeKey in season) {
const episode:Episode = season[episodeKey];
episodes.push(episode);
}
tvShowEpisodes.value.push(episodes);
}
}
}
} }
} }
@ -36,29 +51,57 @@ onMounted(async () => {
<template> <template>
<div class="details-container"> <div class="details-container">
<div class="details-card"> <div class="details-card">
<!--HEADER SECTION-->
<div class="details-header"> <div class="details-header">
<img :src="imageUrl" :alt="item.name" class="details-image" /> <img :src="imageUrl" :alt="item.name" class="details-image" />
<div class="details-title-container"> <div class="details-title-container">
<h1 class="details-title">{{ item.name }}</h1> <h1 class="details-title">{{ item.name }}</h1>
<h3> {{ item.score }}</h3> <h3> {{ item.score }}</h3>
<div class="details-description"> <div class="details-description">
<p>{{ item.plot }}</p> <p v-if="item.type == 'TV_ANIME'">{{ item.plot }}</p>
<p v-else-if="tvShowEpisodes.length > 0">{{ tvShowEpisodes[0][0].plot }}</p>
</div>
<h3 v-if="animeEpisodes.length > 0 && !loading">Numero episodi: {{ animeEpisodes[0].episode_total }}</h3>
<h3 v-if="tvShowEpisodes.length > 0 && !loading">Numero stagioni: {{ tvShowEpisodes.length }}</h3>
</div>
</div>
<!--SERIES SECTION-->
<div v-if="!loading && ['TV_ANIME', 'TV'].includes(item.type)" :class="item.type == 'TV_ANIME' ? 'episodes-container' : 'season-container'">
<div v-if="animeEpisodes.length == 0 && tvShowEpisodes.length == 0">
<p>Non ci sono episodi...</p>
</div>
<div v-else-if="item.type == 'TV_ANIME'" v-for="episode in animeEpisodes" :key="episode.id" class="episode-item">
<div class="episode-title">Episodio {{ episode.number }}</div>
</div>
<div v-else-if="item.type == 'TV'" v-for="(season, index) in tvShowEpisodes" class="season-item">
<div class="season-title">Stagione {{ index + 1 }}</div>
<div class="episode-container">
<div v-for="episode in season" :key="episode.id" class="episode-item">
<div class="episode-title">
Episodio {{ episode.number }} -
{{episode.name.slice(0, 40) + (episode.name.length > 39 ? '...' : '')}}
</div> </div>
</div> </div>
</div> </div>
<div class="episodes-container">
<div v-if="!loading" v-for="episode in episodes" :key="episode.id" class="episode-item">
<div class="episode-title">{{ episode.number }} - {{ episode.file_name }}</div>
</div> </div>
<div v-else> </div>
<!--LOADING SECTION-->
<div v-else-if="loading">
<p>Loading...</p> <p>Loading...</p>
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>
<style scoped> <style scoped>
h3 {
padding-top: 10px;
padding-bottom: 10px;
font-weight: bold;
}
.details-container { .details-container {
padding-top: 10px; padding-top: 10px;
justify-content: center; justify-content: center;
@ -113,15 +156,62 @@ onMounted(async () => {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.details-info {
display: flex;
gap: 1rem;
font-size: 0.9rem;
color: #999;
}
.details-description { .details-description {
padding-top: 10px;
line-height: 1.5; line-height: 1.5;
} }
.episodes-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.episode-item {
background-color: #333;
padding: 1rem;
border-radius: 0.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
cursor: pointer;
}
.season-item {
background-color: #2a2a2a;
padding: 1rem;
margin-top: 5px;
margin-bottom: 5px;
border-radius: 0.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.season-item div {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 1rem;
}
.season-title {
font-size: 1.5rem;
font-weight: bold;
padding-bottom: 15px;
}
.episode-item:hover {
transform: translateY(-5px);
}
.episode-title {
font-size: 1.2rem;
font-weight: bold;
}
@media (max-width: 768px) {
.episodes-container {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.season-item div {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
}
</style> </style>