Data structures cleanup.
This commit is contained in:
+41
-42
@@ -370,7 +370,7 @@ function showDetail(item: MediaItem) {
|
||||
if (item.type === 'episode') {
|
||||
// For episodes, play directly if possible, otherwise show the series
|
||||
const epData = item.data as EpisodeWithSeries;
|
||||
const playableFile = epData.episode.releases?.[0]?.playable_file;
|
||||
const playableFile = Object.values(epData.episode.torrents || {})[0]?.playable_file;
|
||||
if (playableFile) {
|
||||
handlePlay(playableFile);
|
||||
} else {
|
||||
@@ -389,10 +389,9 @@ function closeDetail() {
|
||||
|
||||
// Convert raw data to MediaItem format
|
||||
function movieToMediaItem(movie: Movie): MediaItem {
|
||||
// Get resolution from first version if available
|
||||
const resolution = movie.versions && movie.versions.length > 0
|
||||
? movie.versions[0].resolution
|
||||
: null;
|
||||
// Get resolution from first torrent if available
|
||||
const torrents = Object.values(movie.torrents || {});
|
||||
const resolution = torrents.length > 0 ? torrents[0].resolution : null;
|
||||
|
||||
// Use first showreel image as fallback if no cover
|
||||
const coverPath = movie.cover_path ||
|
||||
@@ -400,7 +399,7 @@ function movieToMediaItem(movie: Movie): MediaItem {
|
||||
|
||||
return {
|
||||
id: movie.id,
|
||||
title: movie.title,
|
||||
title: movie.title || 'Unknown',
|
||||
year: movie.year,
|
||||
cover_path: coverPath,
|
||||
showreel_images: movie.showreel_images,
|
||||
@@ -426,7 +425,7 @@ function seriesToMediaItem(series: Series): MediaItem {
|
||||
|
||||
return {
|
||||
id: series.id,
|
||||
title: series.title,
|
||||
title: series.title || 'Unknown',
|
||||
year: null,
|
||||
cover_path: coverPath,
|
||||
showreel_images: reelImages.length > 0 ? reelImages : null,
|
||||
@@ -465,16 +464,16 @@ function sortByRating(items: MediaItem[]): MediaItem[] {
|
||||
|
||||
if (a.type === 'episode') {
|
||||
const epData = a.data as EpisodeWithSeries;
|
||||
ratingA = epData.episode.rating ?? epData.series.rating ?? 0;
|
||||
ratingA = epData.episode.rating ?? epData.series.info?.rating ?? 0;
|
||||
} else {
|
||||
ratingA = (a.data as Movie | Series).rating ?? 0;
|
||||
ratingA = (a.data as Movie | Series).info?.rating ?? 0;
|
||||
}
|
||||
|
||||
if (b.type === 'episode') {
|
||||
const epData = b.data as EpisodeWithSeries;
|
||||
ratingB = epData.episode.rating ?? epData.series.rating ?? 0;
|
||||
ratingB = epData.episode.rating ?? epData.series.info?.rating ?? 0;
|
||||
} else {
|
||||
ratingB = (b.data as Movie | Series).rating ?? 0;
|
||||
ratingB = (b.data as Movie | Series).info?.rating ?? 0;
|
||||
}
|
||||
|
||||
return ratingB - ratingA;
|
||||
@@ -492,7 +491,7 @@ const moviesByGenre = computed(() => {
|
||||
// Apply search filter
|
||||
const searchFilter = (m: MediaItem) => {
|
||||
if (!searchQuery.value) return true;
|
||||
return m.title.toLowerCase().includes(searchQuery.value.toLowerCase());
|
||||
return (m.title || '').toLowerCase().includes(searchQuery.value.toLowerCase());
|
||||
};
|
||||
|
||||
// Assign movies to categories by matching priority (lowest priority number first)
|
||||
@@ -503,7 +502,7 @@ const moviesByGenre = computed(() => {
|
||||
if (!searchFilter(movie)) continue;
|
||||
|
||||
const movieData = movie.data as Movie;
|
||||
const genres = movieData.genres || [];
|
||||
const genres = movieData.info?.genres || [];
|
||||
|
||||
// Check if movie matches this category (has keyword and no excluded genres)
|
||||
const hasKeyword = category.keywords.some(keyword =>
|
||||
@@ -563,7 +562,7 @@ const seriesByGenre = computed(() => {
|
||||
// Apply search filter
|
||||
const searchFilter = (s: MediaItem) => {
|
||||
if (!searchQuery.value) return true;
|
||||
return s.title.toLowerCase().includes(searchQuery.value.toLowerCase());
|
||||
return (s.title || '').toLowerCase().includes(searchQuery.value.toLowerCase());
|
||||
};
|
||||
|
||||
// Assign series to categories by matching priority (lowest priority number first)
|
||||
@@ -574,7 +573,7 @@ const seriesByGenre = computed(() => {
|
||||
if (!searchFilter(series)) continue;
|
||||
|
||||
const seriesData = series.data as Series;
|
||||
const genres = seriesData.genres || [];
|
||||
const genres = seriesData.info?.genres || [];
|
||||
|
||||
// Check if series matches this category (has keyword and no excluded genres)
|
||||
const hasKeyword = category.keywords.some(keyword =>
|
||||
@@ -784,7 +783,7 @@ function performSearch(query: string) {
|
||||
if (movie.year === searchYear) {
|
||||
allScored.push({
|
||||
item: movieToMediaItem(movie),
|
||||
score: 100 + (movie.rating ?? 0) / 10, // High base score, ranked by rating
|
||||
score: 100 + (movie.info?.rating ?? 0) / 10, // High base score, ranked by rating
|
||||
matchType: 'movies'
|
||||
});
|
||||
processedIds.add(movie.id);
|
||||
@@ -793,11 +792,11 @@ function performSearch(query: string) {
|
||||
}
|
||||
|
||||
// Direct title match -> Movies category
|
||||
const titleScore = getBestScore(query, movie.title, movie.original_title);
|
||||
const titleScore = getBestScore(query, movie.title, movie.info?.original_title);
|
||||
if (titleScore > 0) {
|
||||
allScored.push({
|
||||
item: movieToMediaItem(movie),
|
||||
score: titleScore + (movie.rating ?? 0) / 10,
|
||||
score: titleScore + (movie.info?.rating ?? 0) / 10,
|
||||
matchType: 'movies'
|
||||
});
|
||||
processedIds.add(movie.id);
|
||||
@@ -805,13 +804,13 @@ function performSearch(query: string) {
|
||||
}
|
||||
|
||||
// Cast/director match -> People category
|
||||
const peopleMatch = matchesPeople(query, movie.cast, movie.director);
|
||||
const peopleMatch = matchesPeople(query, movie.info?.cast, movie.info?.director);
|
||||
if (peopleMatch.matches.length > 0) {
|
||||
const item = movieToMediaItem(movie);
|
||||
item.searchMatchInfo = { matchedPeople: formatMatchedPeople(peopleMatch.matches) };
|
||||
allScored.push({
|
||||
item,
|
||||
score: peopleMatch.score + (movie.rating ?? 0) / 10,
|
||||
score: peopleMatch.score + (movie.info?.rating ?? 0) / 10,
|
||||
matchType: 'people'
|
||||
});
|
||||
processedIds.add(movie.id);
|
||||
@@ -820,16 +819,16 @@ function performSearch(query: string) {
|
||||
|
||||
// Other metadata matches -> Other category
|
||||
const otherScore = getBestScore(query,
|
||||
movie.genres?.join(' '),
|
||||
movie.keywords?.join(' '),
|
||||
movie.overview,
|
||||
movie.tagline,
|
||||
movie.similar?.map(s => s.title).join(' ')
|
||||
movie.info?.genres?.join(' '),
|
||||
movie.info?.keywords?.join(' '),
|
||||
movie.info?.overview,
|
||||
movie.info?.tagline,
|
||||
movie.info?.similar?.map(s => s.title).join(' ')
|
||||
);
|
||||
if (otherScore > 0) {
|
||||
allScored.push({
|
||||
item: movieToMediaItem(movie),
|
||||
score: otherScore + (movie.rating ?? 0) / 10,
|
||||
score: otherScore + (movie.info?.rating ?? 0) / 10,
|
||||
matchType: 'other'
|
||||
});
|
||||
processedIds.add(movie.id);
|
||||
@@ -841,11 +840,11 @@ function performSearch(query: string) {
|
||||
// Year search - match series that started that year
|
||||
if (isYearSearch) {
|
||||
// Extract year from release_date (format: "YYYY-MM-DD" or just "YYYY")
|
||||
const seriesYear = series.release_date ? parseInt(series.release_date.substring(0, 4), 10) : null;
|
||||
const seriesYear = series.info?.release_date ? parseInt(series.info.release_date.substring(0, 4), 10) : null;
|
||||
if (seriesYear === searchYear) {
|
||||
allScored.push({
|
||||
item: seriesToMediaItem(series),
|
||||
score: 100 + (series.rating ?? 0) / 10,
|
||||
score: 100 + (series.info?.rating ?? 0) / 10,
|
||||
matchType: 'series'
|
||||
});
|
||||
processedIds.add(series.id);
|
||||
@@ -854,11 +853,11 @@ function performSearch(query: string) {
|
||||
}
|
||||
|
||||
// Direct title match -> Series category
|
||||
const titleScore = getBestScore(query, series.title, series.original_title);
|
||||
const titleScore = getBestScore(query, series.title, series.info?.original_title);
|
||||
if (titleScore > 0) {
|
||||
allScored.push({
|
||||
item: seriesToMediaItem(series),
|
||||
score: titleScore + (series.rating ?? 0) / 10,
|
||||
score: titleScore + (series.info?.rating ?? 0) / 10,
|
||||
matchType: 'series'
|
||||
});
|
||||
processedIds.add(series.id);
|
||||
@@ -869,8 +868,8 @@ function performSearch(query: string) {
|
||||
const matchedEpisodes: MatchedEpisode[] = [];
|
||||
let episodeScore = 0;
|
||||
// Check if series has only one season and has ended (hide "SN" in that case)
|
||||
const isEndedSingleSeason = (series.number_of_seasons === 1 || (series.seasons?.length === 1)) &&
|
||||
['Ended', 'Canceled', 'Cancelled'].includes(series.status || '');
|
||||
const isEndedSingleSeason = (series.info?.number_of_seasons === 1 || (series.seasons?.length === 1)) &&
|
||||
['Ended', 'Canceled', 'Cancelled'].includes(series.info?.status || '');
|
||||
|
||||
for (const season of series.seasons || []) {
|
||||
for (const episode of season.episodes || []) {
|
||||
@@ -898,7 +897,7 @@ function performSearch(query: string) {
|
||||
item.searchMatchInfo = { matchedEpisodes };
|
||||
allScored.push({
|
||||
item,
|
||||
score: episodeScore + (series.rating ?? 0) / 10,
|
||||
score: episodeScore + (series.info?.rating ?? 0) / 10,
|
||||
matchType: 'series'
|
||||
});
|
||||
processedIds.add(series.id);
|
||||
@@ -906,13 +905,13 @@ function performSearch(query: string) {
|
||||
}
|
||||
|
||||
// Cast/creators match -> People category
|
||||
const peopleMatch = matchesPeople(query, series.cast, null, series.creators);
|
||||
const peopleMatch = matchesPeople(query, series.info?.cast, null, series.info?.creators);
|
||||
if (peopleMatch.matches.length > 0 && !processedIds.has(series.id)) {
|
||||
const item = seriesToMediaItem(series);
|
||||
item.searchMatchInfo = { matchedPeople: formatMatchedPeople(peopleMatch.matches) };
|
||||
allScored.push({
|
||||
item,
|
||||
score: peopleMatch.score + (series.rating ?? 0) / 10,
|
||||
score: peopleMatch.score + (series.info?.rating ?? 0) / 10,
|
||||
matchType: 'people'
|
||||
});
|
||||
processedIds.add(series.id);
|
||||
@@ -922,17 +921,17 @@ function performSearch(query: string) {
|
||||
// Other metadata matches -> Other category
|
||||
if (!processedIds.has(series.id)) {
|
||||
const otherScore = getBestScore(query,
|
||||
series.genres?.join(' '),
|
||||
series.keywords?.join(' '),
|
||||
series.overview,
|
||||
series.tagline,
|
||||
series.similar?.map(s => s.title).join(' '),
|
||||
series.networks?.join(' ')
|
||||
series.info?.genres?.join(' '),
|
||||
series.info?.keywords?.join(' '),
|
||||
series.info?.overview,
|
||||
series.info?.tagline,
|
||||
series.info?.similar?.map(s => s.title).join(' '),
|
||||
series.info?.networks?.join(' ')
|
||||
);
|
||||
if (otherScore > 0) {
|
||||
allScored.push({
|
||||
item: seriesToMediaItem(series),
|
||||
score: otherScore + (series.rating ?? 0) / 10,
|
||||
score: otherScore + (series.info?.rating ?? 0) / 10,
|
||||
matchType: 'other'
|
||||
});
|
||||
processedIds.add(series.id);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<img
|
||||
v-if="getImageUrl(item)"
|
||||
:src="getImageUrl(item)!"
|
||||
:alt="item.title"
|
||||
:alt="item.title || 'Unknown'"
|
||||
class="hex-clip"
|
||||
/>
|
||||
</div>
|
||||
@@ -28,7 +28,7 @@
|
||||
<img
|
||||
v-if="index !== 0 && getImageUrl(item)"
|
||||
:src="getImageUrl(item)!"
|
||||
:alt="item.title"
|
||||
:alt="item.title || 'Unknown'"
|
||||
class="collage-media"
|
||||
/>
|
||||
<video
|
||||
@@ -444,9 +444,9 @@ function getVideoUrl(item: MediaItem): string | undefined {
|
||||
|
||||
function getRating(item: MediaItem): number | null {
|
||||
if (item.type === 'movies') {
|
||||
return (item.data as Movie).rating ?? null;
|
||||
return (item.data as Movie).info?.rating ?? null;
|
||||
}
|
||||
return (item.data as Series).rating ?? null;
|
||||
return (item.data as Series).info?.rating ?? null;
|
||||
}
|
||||
|
||||
function getRatingClass(item: MediaItem): string {
|
||||
@@ -460,15 +460,15 @@ function getRatingClass(item: MediaItem): string {
|
||||
function getResolution(item: MediaItem): string | null {
|
||||
if (item.type === 'movies') {
|
||||
const movie = item.data as Movie;
|
||||
return movie.versions?.[0]?.resolution ?? null;
|
||||
return Object.values(movie.torrents || {})[0]?.resolution ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getOverview(item: MediaItem): string | null {
|
||||
const overview = item.type === 'movies'
|
||||
? (item.data as Movie).overview
|
||||
: (item.data as Series).overview;
|
||||
? (item.data as Movie).info?.overview
|
||||
: (item.data as Series).info?.overview;
|
||||
if (!overview) return null;
|
||||
return overview.length > 150 ? overview.slice(0, 150) + '...' : overview;
|
||||
}
|
||||
@@ -476,13 +476,13 @@ function getOverview(item: MediaItem): string | null {
|
||||
function getPlayableFile(item: MediaItem): string | null {
|
||||
if (item.type === 'movies') {
|
||||
const movie = item.data as Movie;
|
||||
return movie.versions?.[0]?.playable_file ?? null;
|
||||
return Object.values(movie.torrents || {})[0]?.playable_file ?? null;
|
||||
}
|
||||
const series = item.data as Series;
|
||||
for (const season of series.seasons || []) {
|
||||
for (const episode of season.episodes || []) {
|
||||
for (const release of episode.releases || []) {
|
||||
if (release.playable_file) return release.playable_file;
|
||||
for (const torrent of Object.values(episode.torrents || {})) {
|
||||
if (torrent.playable_file) return torrent.playable_file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<section class="hero">
|
||||
<div
|
||||
<div
|
||||
v-if="coverUrl"
|
||||
class="hero-background"
|
||||
:style="{ backgroundImage: `url('${coverUrl}')` }"
|
||||
@@ -53,7 +53,8 @@ const coverUrl = computed(() => {
|
||||
const resolution = computed(() => {
|
||||
if (props.item.type === 'movies') {
|
||||
const movie = props.item.data as Movie;
|
||||
return movie.versions && movie.versions.length > 0 ? movie.versions[0].resolution : null;
|
||||
const torrents = Object.values(movie.torrents || {});
|
||||
return torrents.length > 0 ? torrents[0].resolution : null;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
@@ -61,16 +62,17 @@ const resolution = computed(() => {
|
||||
const quality = computed(() => {
|
||||
if (props.item.type === 'movies') {
|
||||
const movie = props.item.data as Movie;
|
||||
return movie.versions && movie.versions.length > 0 ? movie.versions[0].quality : null;
|
||||
const torrents = Object.values(movie.torrents || {});
|
||||
return torrents.length > 0 ? torrents[0].quality : null;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const rating = computed(() => {
|
||||
if (props.item.type === 'movies') {
|
||||
return (props.item.data as Movie).rating;
|
||||
return (props.item.data as Movie).info?.rating;
|
||||
}
|
||||
return (props.item.data as Series).rating;
|
||||
return (props.item.data as Series).info?.rating;
|
||||
});
|
||||
|
||||
const ratingClass = computed(() => {
|
||||
@@ -82,29 +84,27 @@ const ratingClass = computed(() => {
|
||||
|
||||
const overview = computed(() => {
|
||||
if (props.item.type === 'movies') {
|
||||
const o = (props.item.data as Movie).overview;
|
||||
const o = (props.item.data as Movie).info?.overview;
|
||||
return o ? (o.length > 200 ? o.slice(0, 200) + '...' : o) : null;
|
||||
}
|
||||
const o = (props.item.data as Series).overview;
|
||||
const o = (props.item.data as Series).info?.overview;
|
||||
return o ? (o.length > 200 ? o.slice(0, 200) + '...' : o) : null;
|
||||
});
|
||||
|
||||
const playableFile = computed(() => {
|
||||
if (props.item.type === 'movies') {
|
||||
const movie = props.item.data as Movie;
|
||||
// Get the first version's playable file
|
||||
if (movie.versions && movie.versions.length > 0) {
|
||||
return movie.versions[0].playable_file;
|
||||
}
|
||||
return null;
|
||||
const torrents = Object.values(movie.torrents || {});
|
||||
return torrents.length > 0 ? torrents[0].playable_file : null;
|
||||
}
|
||||
// For series, get first available file from episodes
|
||||
const series = props.item.data as Series;
|
||||
for (const season of series.seasons || []) {
|
||||
for (const episode of season.episodes || []) {
|
||||
for (const release of episode.releases || []) {
|
||||
if (release.playable_file) {
|
||||
return release.playable_file;
|
||||
const torrents = Object.values(episode.torrents || {});
|
||||
for (const torrent of torrents) {
|
||||
if (torrent.playable_file) {
|
||||
return torrent.playable_file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<img
|
||||
v-if="coverUrl && !imageError"
|
||||
:src="coverUrl"
|
||||
:alt="item.title"
|
||||
:alt="item.title || 'Unknown'"
|
||||
loading="lazy"
|
||||
@error="imageError = true"
|
||||
/>
|
||||
@@ -90,13 +90,13 @@ const coverUrl = computed(() => {
|
||||
|
||||
const rating = computed(() => {
|
||||
if (props.item.type === 'movies') {
|
||||
return (props.item.data as Movie).rating;
|
||||
return (props.item.data as Movie).info?.rating;
|
||||
}
|
||||
if (props.item.type === 'episode') {
|
||||
const epData = props.item.data as EpisodeWithSeries;
|
||||
return epData.episode.rating ?? epData.series.rating;
|
||||
return epData.episode.rating ?? epData.series.info?.rating;
|
||||
}
|
||||
return (props.item.data as Series).rating;
|
||||
return (props.item.data as Series).info?.rating;
|
||||
});
|
||||
|
||||
const ratingClass = computed(() => {
|
||||
@@ -121,7 +121,7 @@ const subtitle = computed(() => {
|
||||
}
|
||||
// For series, show creators
|
||||
if (props.item.type === 'series') {
|
||||
const creators = (props.item.data as Series).creators;
|
||||
const creators = (props.item.data as Series).info?.creators;
|
||||
return creators && creators.length > 0 ? creators.join(', ') : null;
|
||||
}
|
||||
return null;
|
||||
@@ -130,7 +130,7 @@ const subtitle = computed(() => {
|
||||
// Director for movies
|
||||
const director = computed(() => {
|
||||
if (props.item.type !== 'movies') return null;
|
||||
return (props.item.data as Movie).director;
|
||||
return (props.item.data as Movie).info?.director;
|
||||
});
|
||||
|
||||
// Check if we have director and/or cast to display
|
||||
@@ -142,7 +142,7 @@ const directorAndCast = computed(() => {
|
||||
// Cast names, excluding director if they appear in cast
|
||||
const filteredCastNames = computed(() => {
|
||||
if (props.item.type !== 'movies') return null;
|
||||
const cast = (props.item.data as Movie).cast;
|
||||
const cast = (props.item.data as Movie).info?.cast;
|
||||
if (!cast || cast.length === 0) return null;
|
||||
|
||||
const directorName = director.value?.toLowerCase();
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
<button
|
||||
class="btn btn-small btn-secondary"
|
||||
v-bind="navAttrs(2, index * 2 + 1)"
|
||||
@click="handleOpenFolder(version.path)"
|
||||
@click="handleOpenFolder(version.playable_file || '')"
|
||||
>📁</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -140,7 +140,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, onMounted } from 'vue';
|
||||
import type { MediaItem, Movie, MovieVersion, Series } from '../types';
|
||||
import type { MediaItem, Movie, Series, Torrent } from '../types';
|
||||
import { getCoverUrl } from '../api';
|
||||
import SeriesFullView from './SeriesFullView.vue';
|
||||
import { navAttrs } from '../composables/useKeyboardNavigation';
|
||||
@@ -270,10 +270,10 @@ function getShowreelUrl(path: string): string {
|
||||
return getCoverUrl(path);
|
||||
}
|
||||
// Movie versions
|
||||
const movieVersions = computed((): MovieVersion[] => {
|
||||
const movieVersions = computed((): Torrent[] => {
|
||||
if (props.item.type !== 'movies') return [];
|
||||
const movie = props.item.data as Movie;
|
||||
return movie.versions || [];
|
||||
return Object.values(movie.torrents || {});
|
||||
});
|
||||
|
||||
const headerStyle = computed(() => {
|
||||
@@ -299,7 +299,7 @@ const backdropStyle = computed(() => {
|
||||
});
|
||||
|
||||
// Check if a specific version is a disc format (Blu-ray disc has index.bdmv)
|
||||
function isVersionDisc(version: MovieVersion): boolean {
|
||||
function isVersionDisc(version: Torrent): boolean {
|
||||
if (!version.playable_file) return false;
|
||||
const filename = version.playable_file.toLowerCase();
|
||||
return filename.endsWith('index.bdmv') || filename.endsWith('.iso');
|
||||
@@ -307,42 +307,42 @@ function isVersionDisc(version: MovieVersion): boolean {
|
||||
|
||||
const movieGenres = computed(() => {
|
||||
if (props.item.type !== 'movies') return null;
|
||||
return (props.item.data as Movie).genres;
|
||||
return (props.item.data as Movie).info?.genres;
|
||||
});
|
||||
|
||||
const movieTagline = computed(() => {
|
||||
if (props.item.type !== 'movies') return null;
|
||||
return (props.item.data as Movie).tagline;
|
||||
return (props.item.data as Movie).info?.tagline;
|
||||
});
|
||||
|
||||
const movieDirector = computed(() => {
|
||||
if (props.item.type !== 'movies') return null;
|
||||
return (props.item.data as Movie).director;
|
||||
return (props.item.data as Movie).info?.director;
|
||||
});
|
||||
|
||||
const movieCast = computed(() => {
|
||||
if (props.item.type !== 'movies') return null;
|
||||
return (props.item.data as Movie).cast;
|
||||
return (props.item.data as Movie).info?.cast;
|
||||
});
|
||||
|
||||
const movieRuntime = computed(() => {
|
||||
if (props.item.type !== 'movies') return null;
|
||||
return (props.item.data as Movie).runtime;
|
||||
return (props.item.data as Movie).info?.runtime;
|
||||
});
|
||||
|
||||
const movieReleaseDate = computed(() => {
|
||||
if (props.item.type !== 'movies') return null;
|
||||
return (props.item.data as Movie).release_date;
|
||||
return (props.item.data as Movie).info?.release_date;
|
||||
});
|
||||
|
||||
const movieStatus = computed(() => {
|
||||
if (props.item.type !== 'movies') return null;
|
||||
return (props.item.data as Movie).status;
|
||||
return (props.item.data as Movie).info?.status;
|
||||
});
|
||||
|
||||
const movieKeywords = computed(() => {
|
||||
if (props.item.type !== 'movies') return null;
|
||||
return (props.item.data as Movie).keywords;
|
||||
return (props.item.data as Movie).info?.keywords;
|
||||
});
|
||||
|
||||
function formatRuntime(minutes: number): string {
|
||||
@@ -354,16 +354,16 @@ function formatRuntime(minutes: number): string {
|
||||
|
||||
const rating = computed(() => {
|
||||
if (props.item.type === 'movies') {
|
||||
return (props.item.data as Movie).rating;
|
||||
return (props.item.data as Movie).info?.rating;
|
||||
}
|
||||
return (props.item.data as Series).rating;
|
||||
return (props.item.data as Series).info?.rating;
|
||||
});
|
||||
|
||||
const overview = computed(() => {
|
||||
if (props.item.type === 'movies') {
|
||||
return (props.item.data as Movie).overview;
|
||||
return (props.item.data as Movie).info?.overview;
|
||||
}
|
||||
return (props.item.data as Series).overview;
|
||||
return (props.item.data as Series).info?.overview;
|
||||
});
|
||||
|
||||
const ratingClass = computed(() => {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<section class="series-hero">
|
||||
<div class="hero-bg">
|
||||
<!-- Use backdrop if available, otherwise create collage from season posters -->
|
||||
<img v-if="backdropUrl" :src="backdropUrl" class="hero-img" :alt="series.title" />
|
||||
<img v-if="backdropUrl" :src="backdropUrl" class="hero-img" :alt="series.title || 'Unknown'" />
|
||||
<div v-else class="hero-collage">
|
||||
<div
|
||||
v-for="(season, i) in seasonsWithPosters.slice(0, 5)"
|
||||
@@ -19,12 +19,12 @@
|
||||
<div class="hero-content">
|
||||
<h1 class="series-title">{{ series.title }}</h1>
|
||||
<div class="series-meta">
|
||||
<span v-if="series.rating" class="meta-rating" :class="ratingClass">★ {{ series.rating.toFixed(1) }}</span>
|
||||
<span v-if="series.number_of_seasons" class="meta-item">{{ series.number_of_seasons }} Seasons</span>
|
||||
<span v-if="series.status" class="meta-badge">{{ series.status }}</span>
|
||||
<span v-if="series.genres?.length" class="meta-genres">{{ series.genres.slice(0, 3).join(' • ') }}</span>
|
||||
<span v-if="series.info?.rating" class="meta-rating" :class="ratingClass">★ {{ series.info.rating.toFixed(1) }}</span>
|
||||
<span v-if="series.info?.number_of_seasons" class="meta-item">{{ series.info.number_of_seasons }} Seasons</span>
|
||||
<span v-if="series.info?.status" class="meta-badge">{{ series.info.status }}</span>
|
||||
<span v-if="series.info?.genres?.length" class="meta-genres">{{ series.info.genres.slice(0, 3).join(' • ') }}</span>
|
||||
</div>
|
||||
<p v-if="series.overview" class="series-overview">{{ series.overview }}</p>
|
||||
<p v-if="series.info?.overview" class="series-overview">{{ series.info.overview }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -125,24 +125,24 @@
|
||||
<div class="context-menu-header">
|
||||
{{ contextMenu.episode.name || `Episode ${contextMenu.episode.episode_number}` }}
|
||||
</div>
|
||||
<div v-if="contextMenu.episode.releases && contextMenu.episode.releases.length > 0">
|
||||
<div v-if="Object.values(contextMenu.episode.torrents || {}).length > 0">
|
||||
<div
|
||||
v-for="(release, index) in contextMenu.episode.releases"
|
||||
v-for="(torrent, index) in Object.values(contextMenu.episode.torrents || {})"
|
||||
:key="index"
|
||||
class="context-menu-version"
|
||||
>
|
||||
<div class="version-label">{{ getVersionLabel(release) }}</div>
|
||||
<div class="version-label">{{ getVersionLabel(torrent) }}</div>
|
||||
<div class="version-actions">
|
||||
<button
|
||||
class="ctx-btn ctx-btn-play"
|
||||
tabindex="0"
|
||||
@click="handlePlayVersion(release.playable_file)"
|
||||
:disabled="!release.playable_file"
|
||||
@click="handlePlayVersion(torrent.playable_file)"
|
||||
:disabled="!torrent.playable_file"
|
||||
>▶ Play</button>
|
||||
<button
|
||||
class="ctx-btn ctx-btn-folder"
|
||||
tabindex="0"
|
||||
@click="handleOpenFolder(release.path)"
|
||||
@click="handleOpenFolder(torrent.playable_file || '')"
|
||||
>📁</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -157,7 +157,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, nextTick, watch } from 'vue';
|
||||
import type { Series, Season, Episode, EpisodeRelease } from '../types';
|
||||
import type { Series, Season, Episode, Torrent } from '../types';
|
||||
import { getCoverUrl } from '../api';
|
||||
import { navAttrs } from '../composables/useKeyboardNavigation';
|
||||
|
||||
@@ -309,12 +309,12 @@ function handleOpenFolder(folderPath: string) {
|
||||
}
|
||||
|
||||
// Get version display label
|
||||
function getVersionLabel(release: EpisodeRelease): string {
|
||||
function getVersionLabel(torrent: Torrent): string {
|
||||
const parts: string[] = [];
|
||||
if (release.resolution) parts.push(release.resolution);
|
||||
if (release.quality) parts.push(release.quality);
|
||||
if (release.codec) parts.push(release.codec);
|
||||
if (release.audio) parts.push(release.audio);
|
||||
if (torrent.resolution) parts.push(torrent.resolution);
|
||||
if (torrent.quality) parts.push(torrent.quality);
|
||||
if (torrent.codec) parts.push(torrent.codec);
|
||||
if (torrent.audio) parts.push(torrent.audio);
|
||||
return parts.length > 0 ? parts.join(' • ') : 'Unknown';
|
||||
}
|
||||
|
||||
@@ -390,8 +390,8 @@ function handleEpisodeHover(key: string, isEntering: boolean) {
|
||||
|
||||
// Backdrop URL - only use backdrop_path, fall back to collage (handled in template)
|
||||
const backdropUrl = computed(() => {
|
||||
if (props.series.backdrop_path) {
|
||||
return getCoverUrl(props.series.backdrop_path);
|
||||
if (props.series.info?.backdrop_path) {
|
||||
return getCoverUrl(props.series.info.backdrop_path);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
@@ -403,9 +403,9 @@ const seasonsWithPosters = computed(() => {
|
||||
|
||||
// Rating class
|
||||
const ratingClass = computed(() => {
|
||||
if (!props.series.rating) return '';
|
||||
if (props.series.rating >= 7.5) return 'rating-high';
|
||||
if (props.series.rating >= 6) return 'rating-medium';
|
||||
if (!props.series.info?.rating) return '';
|
||||
if (props.series.info.rating >= 7.5) return 'rating-high';
|
||||
if (props.series.info.rating >= 6) return 'rating-medium';
|
||||
return 'rating-low';
|
||||
});
|
||||
|
||||
@@ -450,7 +450,7 @@ function truncate(text: string, maxLength: number): string {
|
||||
|
||||
// Handle play
|
||||
function handlePlay(episode: Episode) {
|
||||
const playableFile = episode.releases?.[0]?.playable_file;
|
||||
const playableFile = Object.values(episode.torrents || {})[0]?.playable_file;
|
||||
if (playableFile) {
|
||||
emit('play', playableFile);
|
||||
}
|
||||
|
||||
+31
-55
@@ -1,19 +1,6 @@
|
||||
// Type definitions for the media browser
|
||||
|
||||
export interface MovieVersion {
|
||||
path: string;
|
||||
playable_file: string | null;
|
||||
resolution: string | null;
|
||||
quality: string | null;
|
||||
codec: string | null;
|
||||
audio: string | null;
|
||||
encoder: string | null;
|
||||
size: number | null;
|
||||
in_rtorrent: boolean | null;
|
||||
torrent_path: string | null;
|
||||
}
|
||||
|
||||
export interface TmdbPerson {
|
||||
export interface CastMember {
|
||||
name: string;
|
||||
character?: string | null;
|
||||
profile_path: string | null;
|
||||
@@ -25,18 +12,11 @@ export interface SimilarMedia {
|
||||
poster_path: string | null;
|
||||
}
|
||||
|
||||
export interface Movie {
|
||||
id: string;
|
||||
title: string;
|
||||
export interface Info {
|
||||
tmdb_id: number;
|
||||
title: string | null;
|
||||
original_title: string | null;
|
||||
alternative_titles: string[] | null;
|
||||
torrent_title: string | null;
|
||||
year: number | null;
|
||||
cover_path: string | null;
|
||||
showreel_images: string[] | null;
|
||||
versions: MovieVersion[];
|
||||
tmdb_id: number | null;
|
||||
tmdb_title: string | null;
|
||||
rating: number | null;
|
||||
vote_count: number | null;
|
||||
overview: string | null;
|
||||
@@ -49,13 +29,16 @@ export interface Movie {
|
||||
backdrop_path: string | null;
|
||||
similar: SimilarMedia[] | null;
|
||||
keywords: string[] | null;
|
||||
cast: TmdbPerson[] | null;
|
||||
cast: CastMember[] | null;
|
||||
director: string | null;
|
||||
newest: number | null;
|
||||
creators: string[] | null;
|
||||
number_of_seasons: number | null;
|
||||
number_of_episodes: number | null;
|
||||
networks: string[] | null;
|
||||
}
|
||||
|
||||
export interface EpisodeRelease {
|
||||
path: string;
|
||||
export interface Torrent {
|
||||
title: string | null;
|
||||
playable_file: string | null;
|
||||
resolution: string | null;
|
||||
quality: string | null;
|
||||
@@ -63,7 +46,19 @@ export interface EpisodeRelease {
|
||||
audio: string | null;
|
||||
encoder: string | null;
|
||||
size: number | null;
|
||||
in_rtorrent: boolean | null;
|
||||
added_at: number | null;
|
||||
}
|
||||
|
||||
export interface Movie {
|
||||
id: string;
|
||||
title: string | null;
|
||||
info: Info | null;
|
||||
year: number | null;
|
||||
newest: number | null;
|
||||
cover_path: string | null;
|
||||
backdrop_path: string | null;
|
||||
showreel_images: string[] | null;
|
||||
torrents: { [key: string]: Torrent };
|
||||
}
|
||||
|
||||
export interface Episode {
|
||||
@@ -76,7 +71,7 @@ export interface Episode {
|
||||
rating: number | null;
|
||||
director: string | null;
|
||||
reel_image: string | null;
|
||||
releases: EpisodeRelease[];
|
||||
torrents: { [key: string]: Torrent };
|
||||
}
|
||||
|
||||
export interface Season {
|
||||
@@ -91,31 +86,12 @@ export interface Season {
|
||||
|
||||
export interface Series {
|
||||
id: string;
|
||||
title: string;
|
||||
original_title: string | null;
|
||||
alternative_titles: string[] | null;
|
||||
torrent_title: string | null;
|
||||
cover_path: string | null;
|
||||
seasons: Season[];
|
||||
tmdb_id: number | null;
|
||||
tmdb_title: string | null;
|
||||
rating: number | null;
|
||||
vote_count: number | null;
|
||||
overview: string | null;
|
||||
genres: string[] | null;
|
||||
release_date: string | null;
|
||||
status: string | null;
|
||||
tagline: string | null;
|
||||
poster_path: string | null;
|
||||
backdrop_path: string | null;
|
||||
similar: SimilarMedia[] | null;
|
||||
keywords: string[] | null;
|
||||
cast: TmdbPerson[] | null;
|
||||
creators: string[] | null;
|
||||
number_of_seasons: number | null;
|
||||
number_of_episodes: number | null;
|
||||
networks: string[] | null;
|
||||
title: string | null;
|
||||
info: Info | null;
|
||||
newest: number | null;
|
||||
cover_path: string | null;
|
||||
backdrop_path: string | null;
|
||||
seasons: Season[];
|
||||
}
|
||||
|
||||
export interface MediaStats {
|
||||
@@ -160,7 +136,7 @@ export interface SearchMatchInfo {
|
||||
|
||||
export interface MediaItem {
|
||||
id: string;
|
||||
title: string;
|
||||
title: string | null;
|
||||
year?: number | null;
|
||||
cover_path: string | null;
|
||||
showreel_images?: string[] | null;
|
||||
|
||||
Reference in New Issue
Block a user