Data structures cleanup.
This commit is contained in:
+41
-42
@@ -370,7 +370,7 @@ function showDetail(item: MediaItem) {
|
|||||||
if (item.type === 'episode') {
|
if (item.type === 'episode') {
|
||||||
// For episodes, play directly if possible, otherwise show the series
|
// For episodes, play directly if possible, otherwise show the series
|
||||||
const epData = item.data as EpisodeWithSeries;
|
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) {
|
if (playableFile) {
|
||||||
handlePlay(playableFile);
|
handlePlay(playableFile);
|
||||||
} else {
|
} else {
|
||||||
@@ -389,10 +389,9 @@ function closeDetail() {
|
|||||||
|
|
||||||
// Convert raw data to MediaItem format
|
// Convert raw data to MediaItem format
|
||||||
function movieToMediaItem(movie: Movie): MediaItem {
|
function movieToMediaItem(movie: Movie): MediaItem {
|
||||||
// Get resolution from first version if available
|
// Get resolution from first torrent if available
|
||||||
const resolution = movie.versions && movie.versions.length > 0
|
const torrents = Object.values(movie.torrents || {});
|
||||||
? movie.versions[0].resolution
|
const resolution = torrents.length > 0 ? torrents[0].resolution : null;
|
||||||
: null;
|
|
||||||
|
|
||||||
// Use first showreel image as fallback if no cover
|
// Use first showreel image as fallback if no cover
|
||||||
const coverPath = movie.cover_path ||
|
const coverPath = movie.cover_path ||
|
||||||
@@ -400,7 +399,7 @@ function movieToMediaItem(movie: Movie): MediaItem {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: movie.id,
|
id: movie.id,
|
||||||
title: movie.title,
|
title: movie.title || 'Unknown',
|
||||||
year: movie.year,
|
year: movie.year,
|
||||||
cover_path: coverPath,
|
cover_path: coverPath,
|
||||||
showreel_images: movie.showreel_images,
|
showreel_images: movie.showreel_images,
|
||||||
@@ -426,7 +425,7 @@ function seriesToMediaItem(series: Series): MediaItem {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: series.id,
|
id: series.id,
|
||||||
title: series.title,
|
title: series.title || 'Unknown',
|
||||||
year: null,
|
year: null,
|
||||||
cover_path: coverPath,
|
cover_path: coverPath,
|
||||||
showreel_images: reelImages.length > 0 ? reelImages : null,
|
showreel_images: reelImages.length > 0 ? reelImages : null,
|
||||||
@@ -465,16 +464,16 @@ function sortByRating(items: MediaItem[]): MediaItem[] {
|
|||||||
|
|
||||||
if (a.type === 'episode') {
|
if (a.type === 'episode') {
|
||||||
const epData = a.data as EpisodeWithSeries;
|
const epData = a.data as EpisodeWithSeries;
|
||||||
ratingA = epData.episode.rating ?? epData.series.rating ?? 0;
|
ratingA = epData.episode.rating ?? epData.series.info?.rating ?? 0;
|
||||||
} else {
|
} else {
|
||||||
ratingA = (a.data as Movie | Series).rating ?? 0;
|
ratingA = (a.data as Movie | Series).info?.rating ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (b.type === 'episode') {
|
if (b.type === 'episode') {
|
||||||
const epData = b.data as EpisodeWithSeries;
|
const epData = b.data as EpisodeWithSeries;
|
||||||
ratingB = epData.episode.rating ?? epData.series.rating ?? 0;
|
ratingB = epData.episode.rating ?? epData.series.info?.rating ?? 0;
|
||||||
} else {
|
} else {
|
||||||
ratingB = (b.data as Movie | Series).rating ?? 0;
|
ratingB = (b.data as Movie | Series).info?.rating ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ratingB - ratingA;
|
return ratingB - ratingA;
|
||||||
@@ -492,7 +491,7 @@ const moviesByGenre = computed(() => {
|
|||||||
// Apply search filter
|
// Apply search filter
|
||||||
const searchFilter = (m: MediaItem) => {
|
const searchFilter = (m: MediaItem) => {
|
||||||
if (!searchQuery.value) return true;
|
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)
|
// Assign movies to categories by matching priority (lowest priority number first)
|
||||||
@@ -503,7 +502,7 @@ const moviesByGenre = computed(() => {
|
|||||||
if (!searchFilter(movie)) continue;
|
if (!searchFilter(movie)) continue;
|
||||||
|
|
||||||
const movieData = movie.data as Movie;
|
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)
|
// Check if movie matches this category (has keyword and no excluded genres)
|
||||||
const hasKeyword = category.keywords.some(keyword =>
|
const hasKeyword = category.keywords.some(keyword =>
|
||||||
@@ -563,7 +562,7 @@ const seriesByGenre = computed(() => {
|
|||||||
// Apply search filter
|
// Apply search filter
|
||||||
const searchFilter = (s: MediaItem) => {
|
const searchFilter = (s: MediaItem) => {
|
||||||
if (!searchQuery.value) return true;
|
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)
|
// Assign series to categories by matching priority (lowest priority number first)
|
||||||
@@ -574,7 +573,7 @@ const seriesByGenre = computed(() => {
|
|||||||
if (!searchFilter(series)) continue;
|
if (!searchFilter(series)) continue;
|
||||||
|
|
||||||
const seriesData = series.data as Series;
|
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)
|
// Check if series matches this category (has keyword and no excluded genres)
|
||||||
const hasKeyword = category.keywords.some(keyword =>
|
const hasKeyword = category.keywords.some(keyword =>
|
||||||
@@ -784,7 +783,7 @@ function performSearch(query: string) {
|
|||||||
if (movie.year === searchYear) {
|
if (movie.year === searchYear) {
|
||||||
allScored.push({
|
allScored.push({
|
||||||
item: movieToMediaItem(movie),
|
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'
|
matchType: 'movies'
|
||||||
});
|
});
|
||||||
processedIds.add(movie.id);
|
processedIds.add(movie.id);
|
||||||
@@ -793,11 +792,11 @@ function performSearch(query: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Direct title match -> Movies category
|
// 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) {
|
if (titleScore > 0) {
|
||||||
allScored.push({
|
allScored.push({
|
||||||
item: movieToMediaItem(movie),
|
item: movieToMediaItem(movie),
|
||||||
score: titleScore + (movie.rating ?? 0) / 10,
|
score: titleScore + (movie.info?.rating ?? 0) / 10,
|
||||||
matchType: 'movies'
|
matchType: 'movies'
|
||||||
});
|
});
|
||||||
processedIds.add(movie.id);
|
processedIds.add(movie.id);
|
||||||
@@ -805,13 +804,13 @@ function performSearch(query: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cast/director match -> People category
|
// 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) {
|
if (peopleMatch.matches.length > 0) {
|
||||||
const item = movieToMediaItem(movie);
|
const item = movieToMediaItem(movie);
|
||||||
item.searchMatchInfo = { matchedPeople: formatMatchedPeople(peopleMatch.matches) };
|
item.searchMatchInfo = { matchedPeople: formatMatchedPeople(peopleMatch.matches) };
|
||||||
allScored.push({
|
allScored.push({
|
||||||
item,
|
item,
|
||||||
score: peopleMatch.score + (movie.rating ?? 0) / 10,
|
score: peopleMatch.score + (movie.info?.rating ?? 0) / 10,
|
||||||
matchType: 'people'
|
matchType: 'people'
|
||||||
});
|
});
|
||||||
processedIds.add(movie.id);
|
processedIds.add(movie.id);
|
||||||
@@ -820,16 +819,16 @@ function performSearch(query: string) {
|
|||||||
|
|
||||||
// Other metadata matches -> Other category
|
// Other metadata matches -> Other category
|
||||||
const otherScore = getBestScore(query,
|
const otherScore = getBestScore(query,
|
||||||
movie.genres?.join(' '),
|
movie.info?.genres?.join(' '),
|
||||||
movie.keywords?.join(' '),
|
movie.info?.keywords?.join(' '),
|
||||||
movie.overview,
|
movie.info?.overview,
|
||||||
movie.tagline,
|
movie.info?.tagline,
|
||||||
movie.similar?.map(s => s.title).join(' ')
|
movie.info?.similar?.map(s => s.title).join(' ')
|
||||||
);
|
);
|
||||||
if (otherScore > 0) {
|
if (otherScore > 0) {
|
||||||
allScored.push({
|
allScored.push({
|
||||||
item: movieToMediaItem(movie),
|
item: movieToMediaItem(movie),
|
||||||
score: otherScore + (movie.rating ?? 0) / 10,
|
score: otherScore + (movie.info?.rating ?? 0) / 10,
|
||||||
matchType: 'other'
|
matchType: 'other'
|
||||||
});
|
});
|
||||||
processedIds.add(movie.id);
|
processedIds.add(movie.id);
|
||||||
@@ -841,11 +840,11 @@ function performSearch(query: string) {
|
|||||||
// Year search - match series that started that year
|
// Year search - match series that started that year
|
||||||
if (isYearSearch) {
|
if (isYearSearch) {
|
||||||
// Extract year from release_date (format: "YYYY-MM-DD" or just "YYYY")
|
// 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) {
|
if (seriesYear === searchYear) {
|
||||||
allScored.push({
|
allScored.push({
|
||||||
item: seriesToMediaItem(series),
|
item: seriesToMediaItem(series),
|
||||||
score: 100 + (series.rating ?? 0) / 10,
|
score: 100 + (series.info?.rating ?? 0) / 10,
|
||||||
matchType: 'series'
|
matchType: 'series'
|
||||||
});
|
});
|
||||||
processedIds.add(series.id);
|
processedIds.add(series.id);
|
||||||
@@ -854,11 +853,11 @@ function performSearch(query: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Direct title match -> Series category
|
// 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) {
|
if (titleScore > 0) {
|
||||||
allScored.push({
|
allScored.push({
|
||||||
item: seriesToMediaItem(series),
|
item: seriesToMediaItem(series),
|
||||||
score: titleScore + (series.rating ?? 0) / 10,
|
score: titleScore + (series.info?.rating ?? 0) / 10,
|
||||||
matchType: 'series'
|
matchType: 'series'
|
||||||
});
|
});
|
||||||
processedIds.add(series.id);
|
processedIds.add(series.id);
|
||||||
@@ -869,8 +868,8 @@ function performSearch(query: string) {
|
|||||||
const matchedEpisodes: MatchedEpisode[] = [];
|
const matchedEpisodes: MatchedEpisode[] = [];
|
||||||
let episodeScore = 0;
|
let episodeScore = 0;
|
||||||
// Check if series has only one season and has ended (hide "SN" in that case)
|
// 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)) &&
|
const isEndedSingleSeason = (series.info?.number_of_seasons === 1 || (series.seasons?.length === 1)) &&
|
||||||
['Ended', 'Canceled', 'Cancelled'].includes(series.status || '');
|
['Ended', 'Canceled', 'Cancelled'].includes(series.info?.status || '');
|
||||||
|
|
||||||
for (const season of series.seasons || []) {
|
for (const season of series.seasons || []) {
|
||||||
for (const episode of season.episodes || []) {
|
for (const episode of season.episodes || []) {
|
||||||
@@ -898,7 +897,7 @@ function performSearch(query: string) {
|
|||||||
item.searchMatchInfo = { matchedEpisodes };
|
item.searchMatchInfo = { matchedEpisodes };
|
||||||
allScored.push({
|
allScored.push({
|
||||||
item,
|
item,
|
||||||
score: episodeScore + (series.rating ?? 0) / 10,
|
score: episodeScore + (series.info?.rating ?? 0) / 10,
|
||||||
matchType: 'series'
|
matchType: 'series'
|
||||||
});
|
});
|
||||||
processedIds.add(series.id);
|
processedIds.add(series.id);
|
||||||
@@ -906,13 +905,13 @@ function performSearch(query: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cast/creators match -> People category
|
// 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)) {
|
if (peopleMatch.matches.length > 0 && !processedIds.has(series.id)) {
|
||||||
const item = seriesToMediaItem(series);
|
const item = seriesToMediaItem(series);
|
||||||
item.searchMatchInfo = { matchedPeople: formatMatchedPeople(peopleMatch.matches) };
|
item.searchMatchInfo = { matchedPeople: formatMatchedPeople(peopleMatch.matches) };
|
||||||
allScored.push({
|
allScored.push({
|
||||||
item,
|
item,
|
||||||
score: peopleMatch.score + (series.rating ?? 0) / 10,
|
score: peopleMatch.score + (series.info?.rating ?? 0) / 10,
|
||||||
matchType: 'people'
|
matchType: 'people'
|
||||||
});
|
});
|
||||||
processedIds.add(series.id);
|
processedIds.add(series.id);
|
||||||
@@ -922,17 +921,17 @@ function performSearch(query: string) {
|
|||||||
// Other metadata matches -> Other category
|
// Other metadata matches -> Other category
|
||||||
if (!processedIds.has(series.id)) {
|
if (!processedIds.has(series.id)) {
|
||||||
const otherScore = getBestScore(query,
|
const otherScore = getBestScore(query,
|
||||||
series.genres?.join(' '),
|
series.info?.genres?.join(' '),
|
||||||
series.keywords?.join(' '),
|
series.info?.keywords?.join(' '),
|
||||||
series.overview,
|
series.info?.overview,
|
||||||
series.tagline,
|
series.info?.tagline,
|
||||||
series.similar?.map(s => s.title).join(' '),
|
series.info?.similar?.map(s => s.title).join(' '),
|
||||||
series.networks?.join(' ')
|
series.info?.networks?.join(' ')
|
||||||
);
|
);
|
||||||
if (otherScore > 0) {
|
if (otherScore > 0) {
|
||||||
allScored.push({
|
allScored.push({
|
||||||
item: seriesToMediaItem(series),
|
item: seriesToMediaItem(series),
|
||||||
score: otherScore + (series.rating ?? 0) / 10,
|
score: otherScore + (series.info?.rating ?? 0) / 10,
|
||||||
matchType: 'other'
|
matchType: 'other'
|
||||||
});
|
});
|
||||||
processedIds.add(series.id);
|
processedIds.add(series.id);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<img
|
<img
|
||||||
v-if="getImageUrl(item)"
|
v-if="getImageUrl(item)"
|
||||||
:src="getImageUrl(item)!"
|
:src="getImageUrl(item)!"
|
||||||
:alt="item.title"
|
:alt="item.title || 'Unknown'"
|
||||||
class="hex-clip"
|
class="hex-clip"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<img
|
<img
|
||||||
v-if="index !== 0 && getImageUrl(item)"
|
v-if="index !== 0 && getImageUrl(item)"
|
||||||
:src="getImageUrl(item)!"
|
:src="getImageUrl(item)!"
|
||||||
:alt="item.title"
|
:alt="item.title || 'Unknown'"
|
||||||
class="collage-media"
|
class="collage-media"
|
||||||
/>
|
/>
|
||||||
<video
|
<video
|
||||||
@@ -444,9 +444,9 @@ function getVideoUrl(item: MediaItem): string | undefined {
|
|||||||
|
|
||||||
function getRating(item: MediaItem): number | null {
|
function getRating(item: MediaItem): number | null {
|
||||||
if (item.type === 'movies') {
|
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 {
|
function getRatingClass(item: MediaItem): string {
|
||||||
@@ -460,15 +460,15 @@ function getRatingClass(item: MediaItem): string {
|
|||||||
function getResolution(item: MediaItem): string | null {
|
function getResolution(item: MediaItem): string | null {
|
||||||
if (item.type === 'movies') {
|
if (item.type === 'movies') {
|
||||||
const movie = item.data as Movie;
|
const movie = item.data as Movie;
|
||||||
return movie.versions?.[0]?.resolution ?? null;
|
return Object.values(movie.torrents || {})[0]?.resolution ?? null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOverview(item: MediaItem): string | null {
|
function getOverview(item: MediaItem): string | null {
|
||||||
const overview = item.type === 'movies'
|
const overview = item.type === 'movies'
|
||||||
? (item.data as Movie).overview
|
? (item.data as Movie).info?.overview
|
||||||
: (item.data as Series).overview;
|
: (item.data as Series).info?.overview;
|
||||||
if (!overview) return null;
|
if (!overview) return null;
|
||||||
return overview.length > 150 ? overview.slice(0, 150) + '...' : overview;
|
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 {
|
function getPlayableFile(item: MediaItem): string | null {
|
||||||
if (item.type === 'movies') {
|
if (item.type === 'movies') {
|
||||||
const movie = item.data as Movie;
|
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;
|
const series = item.data as Series;
|
||||||
for (const season of series.seasons || []) {
|
for (const season of series.seasons || []) {
|
||||||
for (const episode of season.episodes || []) {
|
for (const episode of season.episodes || []) {
|
||||||
for (const release of episode.releases || []) {
|
for (const torrent of Object.values(episode.torrents || {})) {
|
||||||
if (release.playable_file) return release.playable_file;
|
if (torrent.playable_file) return torrent.playable_file;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<div
|
<div
|
||||||
v-if="coverUrl"
|
v-if="coverUrl"
|
||||||
class="hero-background"
|
class="hero-background"
|
||||||
:style="{ backgroundImage: `url('${coverUrl}')` }"
|
:style="{ backgroundImage: `url('${coverUrl}')` }"
|
||||||
@@ -53,7 +53,8 @@ const coverUrl = computed(() => {
|
|||||||
const resolution = computed(() => {
|
const resolution = computed(() => {
|
||||||
if (props.item.type === 'movies') {
|
if (props.item.type === 'movies') {
|
||||||
const movie = props.item.data as Movie;
|
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;
|
return null;
|
||||||
});
|
});
|
||||||
@@ -61,16 +62,17 @@ const resolution = computed(() => {
|
|||||||
const quality = computed(() => {
|
const quality = computed(() => {
|
||||||
if (props.item.type === 'movies') {
|
if (props.item.type === 'movies') {
|
||||||
const movie = props.item.data as Movie;
|
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;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const rating = computed(() => {
|
const rating = computed(() => {
|
||||||
if (props.item.type === 'movies') {
|
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(() => {
|
const ratingClass = computed(() => {
|
||||||
@@ -82,29 +84,27 @@ const ratingClass = computed(() => {
|
|||||||
|
|
||||||
const overview = computed(() => {
|
const overview = computed(() => {
|
||||||
if (props.item.type === 'movies') {
|
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;
|
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;
|
return o ? (o.length > 200 ? o.slice(0, 200) + '...' : o) : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const playableFile = computed(() => {
|
const playableFile = computed(() => {
|
||||||
if (props.item.type === 'movies') {
|
if (props.item.type === 'movies') {
|
||||||
const movie = props.item.data as Movie;
|
const movie = props.item.data as Movie;
|
||||||
// Get the first version's playable file
|
const torrents = Object.values(movie.torrents || {});
|
||||||
if (movie.versions && movie.versions.length > 0) {
|
return torrents.length > 0 ? torrents[0].playable_file : null;
|
||||||
return movie.versions[0].playable_file;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
// For series, get first available file from episodes
|
// For series, get first available file from episodes
|
||||||
const series = props.item.data as Series;
|
const series = props.item.data as Series;
|
||||||
for (const season of series.seasons || []) {
|
for (const season of series.seasons || []) {
|
||||||
for (const episode of season.episodes || []) {
|
for (const episode of season.episodes || []) {
|
||||||
for (const release of episode.releases || []) {
|
const torrents = Object.values(episode.torrents || {});
|
||||||
if (release.playable_file) {
|
for (const torrent of torrents) {
|
||||||
return release.playable_file;
|
if (torrent.playable_file) {
|
||||||
|
return torrent.playable_file;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<img
|
<img
|
||||||
v-if="coverUrl && !imageError"
|
v-if="coverUrl && !imageError"
|
||||||
:src="coverUrl"
|
:src="coverUrl"
|
||||||
:alt="item.title"
|
:alt="item.title || 'Unknown'"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
@error="imageError = true"
|
@error="imageError = true"
|
||||||
/>
|
/>
|
||||||
@@ -90,13 +90,13 @@ const coverUrl = computed(() => {
|
|||||||
|
|
||||||
const rating = computed(() => {
|
const rating = computed(() => {
|
||||||
if (props.item.type === 'movies') {
|
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') {
|
if (props.item.type === 'episode') {
|
||||||
const epData = props.item.data as EpisodeWithSeries;
|
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(() => {
|
const ratingClass = computed(() => {
|
||||||
@@ -121,7 +121,7 @@ const subtitle = computed(() => {
|
|||||||
}
|
}
|
||||||
// For series, show creators
|
// For series, show creators
|
||||||
if (props.item.type === 'series') {
|
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 creators && creators.length > 0 ? creators.join(', ') : null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -130,7 +130,7 @@ const subtitle = computed(() => {
|
|||||||
// Director for movies
|
// Director for movies
|
||||||
const director = computed(() => {
|
const director = computed(() => {
|
||||||
if (props.item.type !== 'movies') return null;
|
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
|
// 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
|
// Cast names, excluding director if they appear in cast
|
||||||
const filteredCastNames = computed(() => {
|
const filteredCastNames = computed(() => {
|
||||||
if (props.item.type !== 'movies') return null;
|
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;
|
if (!cast || cast.length === 0) return null;
|
||||||
|
|
||||||
const directorName = director.value?.toLowerCase();
|
const directorName = director.value?.toLowerCase();
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
<button
|
<button
|
||||||
class="btn btn-small btn-secondary"
|
class="btn btn-small btn-secondary"
|
||||||
v-bind="navAttrs(2, index * 2 + 1)"
|
v-bind="navAttrs(2, index * 2 + 1)"
|
||||||
@click="handleOpenFolder(version.path)"
|
@click="handleOpenFolder(version.playable_file || '')"
|
||||||
>📁</button>
|
>📁</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch, onMounted } from 'vue';
|
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 { getCoverUrl } from '../api';
|
||||||
import SeriesFullView from './SeriesFullView.vue';
|
import SeriesFullView from './SeriesFullView.vue';
|
||||||
import { navAttrs } from '../composables/useKeyboardNavigation';
|
import { navAttrs } from '../composables/useKeyboardNavigation';
|
||||||
@@ -270,10 +270,10 @@ function getShowreelUrl(path: string): string {
|
|||||||
return getCoverUrl(path);
|
return getCoverUrl(path);
|
||||||
}
|
}
|
||||||
// Movie versions
|
// Movie versions
|
||||||
const movieVersions = computed((): MovieVersion[] => {
|
const movieVersions = computed((): Torrent[] => {
|
||||||
if (props.item.type !== 'movies') return [];
|
if (props.item.type !== 'movies') return [];
|
||||||
const movie = props.item.data as Movie;
|
const movie = props.item.data as Movie;
|
||||||
return movie.versions || [];
|
return Object.values(movie.torrents || {});
|
||||||
});
|
});
|
||||||
|
|
||||||
const headerStyle = computed(() => {
|
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)
|
// 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;
|
if (!version.playable_file) return false;
|
||||||
const filename = version.playable_file.toLowerCase();
|
const filename = version.playable_file.toLowerCase();
|
||||||
return filename.endsWith('index.bdmv') || filename.endsWith('.iso');
|
return filename.endsWith('index.bdmv') || filename.endsWith('.iso');
|
||||||
@@ -307,42 +307,42 @@ function isVersionDisc(version: MovieVersion): boolean {
|
|||||||
|
|
||||||
const movieGenres = computed(() => {
|
const movieGenres = computed(() => {
|
||||||
if (props.item.type !== 'movies') return null;
|
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(() => {
|
const movieTagline = computed(() => {
|
||||||
if (props.item.type !== 'movies') return null;
|
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(() => {
|
const movieDirector = computed(() => {
|
||||||
if (props.item.type !== 'movies') return null;
|
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(() => {
|
const movieCast = computed(() => {
|
||||||
if (props.item.type !== 'movies') return null;
|
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(() => {
|
const movieRuntime = computed(() => {
|
||||||
if (props.item.type !== 'movies') return null;
|
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(() => {
|
const movieReleaseDate = computed(() => {
|
||||||
if (props.item.type !== 'movies') return null;
|
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(() => {
|
const movieStatus = computed(() => {
|
||||||
if (props.item.type !== 'movies') return null;
|
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(() => {
|
const movieKeywords = computed(() => {
|
||||||
if (props.item.type !== 'movies') return null;
|
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 {
|
function formatRuntime(minutes: number): string {
|
||||||
@@ -354,16 +354,16 @@ function formatRuntime(minutes: number): string {
|
|||||||
|
|
||||||
const rating = computed(() => {
|
const rating = computed(() => {
|
||||||
if (props.item.type === 'movies') {
|
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(() => {
|
const overview = computed(() => {
|
||||||
if (props.item.type === 'movies') {
|
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(() => {
|
const ratingClass = computed(() => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<section class="series-hero">
|
<section class="series-hero">
|
||||||
<div class="hero-bg">
|
<div class="hero-bg">
|
||||||
<!-- Use backdrop if available, otherwise create collage from season posters -->
|
<!-- 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-else class="hero-collage">
|
||||||
<div
|
<div
|
||||||
v-for="(season, i) in seasonsWithPosters.slice(0, 5)"
|
v-for="(season, i) in seasonsWithPosters.slice(0, 5)"
|
||||||
@@ -19,12 +19,12 @@
|
|||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<h1 class="series-title">{{ series.title }}</h1>
|
<h1 class="series-title">{{ series.title }}</h1>
|
||||||
<div class="series-meta">
|
<div class="series-meta">
|
||||||
<span v-if="series.rating" class="meta-rating" :class="ratingClass">★ {{ series.rating.toFixed(1) }}</span>
|
<span v-if="series.info?.rating" class="meta-rating" :class="ratingClass">★ {{ series.info.rating.toFixed(1) }}</span>
|
||||||
<span v-if="series.number_of_seasons" class="meta-item">{{ series.number_of_seasons }} Seasons</span>
|
<span v-if="series.info?.number_of_seasons" class="meta-item">{{ series.info.number_of_seasons }} Seasons</span>
|
||||||
<span v-if="series.status" class="meta-badge">{{ series.status }}</span>
|
<span v-if="series.info?.status" class="meta-badge">{{ series.info.status }}</span>
|
||||||
<span v-if="series.genres?.length" class="meta-genres">{{ series.genres.slice(0, 3).join(' • ') }}</span>
|
<span v-if="series.info?.genres?.length" class="meta-genres">{{ series.info.genres.slice(0, 3).join(' • ') }}</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -125,24 +125,24 @@
|
|||||||
<div class="context-menu-header">
|
<div class="context-menu-header">
|
||||||
{{ contextMenu.episode.name || `Episode ${contextMenu.episode.episode_number}` }}
|
{{ contextMenu.episode.name || `Episode ${contextMenu.episode.episode_number}` }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="contextMenu.episode.releases && contextMenu.episode.releases.length > 0">
|
<div v-if="Object.values(contextMenu.episode.torrents || {}).length > 0">
|
||||||
<div
|
<div
|
||||||
v-for="(release, index) in contextMenu.episode.releases"
|
v-for="(torrent, index) in Object.values(contextMenu.episode.torrents || {})"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="context-menu-version"
|
class="context-menu-version"
|
||||||
>
|
>
|
||||||
<div class="version-label">{{ getVersionLabel(release) }}</div>
|
<div class="version-label">{{ getVersionLabel(torrent) }}</div>
|
||||||
<div class="version-actions">
|
<div class="version-actions">
|
||||||
<button
|
<button
|
||||||
class="ctx-btn ctx-btn-play"
|
class="ctx-btn ctx-btn-play"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click="handlePlayVersion(release.playable_file)"
|
@click="handlePlayVersion(torrent.playable_file)"
|
||||||
:disabled="!release.playable_file"
|
:disabled="!torrent.playable_file"
|
||||||
>▶ Play</button>
|
>▶ Play</button>
|
||||||
<button
|
<button
|
||||||
class="ctx-btn ctx-btn-folder"
|
class="ctx-btn ctx-btn-folder"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click="handleOpenFolder(release.path)"
|
@click="handleOpenFolder(torrent.playable_file || '')"
|
||||||
>📁</button>
|
>📁</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,7 +157,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, nextTick, watch } from 'vue';
|
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 { getCoverUrl } from '../api';
|
||||||
import { navAttrs } from '../composables/useKeyboardNavigation';
|
import { navAttrs } from '../composables/useKeyboardNavigation';
|
||||||
|
|
||||||
@@ -309,12 +309,12 @@ function handleOpenFolder(folderPath: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get version display label
|
// Get version display label
|
||||||
function getVersionLabel(release: EpisodeRelease): string {
|
function getVersionLabel(torrent: Torrent): string {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (release.resolution) parts.push(release.resolution);
|
if (torrent.resolution) parts.push(torrent.resolution);
|
||||||
if (release.quality) parts.push(release.quality);
|
if (torrent.quality) parts.push(torrent.quality);
|
||||||
if (release.codec) parts.push(release.codec);
|
if (torrent.codec) parts.push(torrent.codec);
|
||||||
if (release.audio) parts.push(release.audio);
|
if (torrent.audio) parts.push(torrent.audio);
|
||||||
return parts.length > 0 ? parts.join(' • ') : 'Unknown';
|
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)
|
// Backdrop URL - only use backdrop_path, fall back to collage (handled in template)
|
||||||
const backdropUrl = computed(() => {
|
const backdropUrl = computed(() => {
|
||||||
if (props.series.backdrop_path) {
|
if (props.series.info?.backdrop_path) {
|
||||||
return getCoverUrl(props.series.backdrop_path);
|
return getCoverUrl(props.series.info.backdrop_path);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
@@ -403,9 +403,9 @@ const seasonsWithPosters = computed(() => {
|
|||||||
|
|
||||||
// Rating class
|
// Rating class
|
||||||
const ratingClass = computed(() => {
|
const ratingClass = computed(() => {
|
||||||
if (!props.series.rating) return '';
|
if (!props.series.info?.rating) return '';
|
||||||
if (props.series.rating >= 7.5) return 'rating-high';
|
if (props.series.info.rating >= 7.5) return 'rating-high';
|
||||||
if (props.series.rating >= 6) return 'rating-medium';
|
if (props.series.info.rating >= 6) return 'rating-medium';
|
||||||
return 'rating-low';
|
return 'rating-low';
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -450,7 +450,7 @@ function truncate(text: string, maxLength: number): string {
|
|||||||
|
|
||||||
// Handle play
|
// Handle play
|
||||||
function handlePlay(episode: Episode) {
|
function handlePlay(episode: Episode) {
|
||||||
const playableFile = episode.releases?.[0]?.playable_file;
|
const playableFile = Object.values(episode.torrents || {})[0]?.playable_file;
|
||||||
if (playableFile) {
|
if (playableFile) {
|
||||||
emit('play', playableFile);
|
emit('play', playableFile);
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-55
@@ -1,19 +1,6 @@
|
|||||||
// Type definitions for the media browser
|
// Type definitions for the media browser
|
||||||
|
|
||||||
export interface MovieVersion {
|
export interface CastMember {
|
||||||
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 {
|
|
||||||
name: string;
|
name: string;
|
||||||
character?: string | null;
|
character?: string | null;
|
||||||
profile_path: string | null;
|
profile_path: string | null;
|
||||||
@@ -25,18 +12,11 @@ export interface SimilarMedia {
|
|||||||
poster_path: string | null;
|
poster_path: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Movie {
|
export interface Info {
|
||||||
id: string;
|
tmdb_id: number;
|
||||||
title: string;
|
title: string | null;
|
||||||
original_title: string | null;
|
original_title: string | null;
|
||||||
alternative_titles: 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;
|
rating: number | null;
|
||||||
vote_count: number | null;
|
vote_count: number | null;
|
||||||
overview: string | null;
|
overview: string | null;
|
||||||
@@ -49,13 +29,16 @@ export interface Movie {
|
|||||||
backdrop_path: string | null;
|
backdrop_path: string | null;
|
||||||
similar: SimilarMedia[] | null;
|
similar: SimilarMedia[] | null;
|
||||||
keywords: string[] | null;
|
keywords: string[] | null;
|
||||||
cast: TmdbPerson[] | null;
|
cast: CastMember[] | null;
|
||||||
director: string | 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 {
|
export interface Torrent {
|
||||||
path: string;
|
title: string | null;
|
||||||
playable_file: string | null;
|
playable_file: string | null;
|
||||||
resolution: string | null;
|
resolution: string | null;
|
||||||
quality: string | null;
|
quality: string | null;
|
||||||
@@ -63,7 +46,19 @@ export interface EpisodeRelease {
|
|||||||
audio: string | null;
|
audio: string | null;
|
||||||
encoder: string | null;
|
encoder: string | null;
|
||||||
size: number | 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 {
|
export interface Episode {
|
||||||
@@ -76,7 +71,7 @@ export interface Episode {
|
|||||||
rating: number | null;
|
rating: number | null;
|
||||||
director: string | null;
|
director: string | null;
|
||||||
reel_image: string | null;
|
reel_image: string | null;
|
||||||
releases: EpisodeRelease[];
|
torrents: { [key: string]: Torrent };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Season {
|
export interface Season {
|
||||||
@@ -91,31 +86,12 @@ export interface Season {
|
|||||||
|
|
||||||
export interface Series {
|
export interface Series {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string | null;
|
||||||
original_title: string | null;
|
info: Info | 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;
|
|
||||||
newest: number | null;
|
newest: number | null;
|
||||||
|
cover_path: string | null;
|
||||||
|
backdrop_path: string | null;
|
||||||
|
seasons: Season[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MediaStats {
|
export interface MediaStats {
|
||||||
@@ -160,7 +136,7 @@ export interface SearchMatchInfo {
|
|||||||
|
|
||||||
export interface MediaItem {
|
export interface MediaItem {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string | null;
|
||||||
year?: number | null;
|
year?: number | null;
|
||||||
cover_path: string | null;
|
cover_path: string | null;
|
||||||
showreel_images?: string[] | null;
|
showreel_images?: string[] | null;
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ Usage:
|
|||||||
hivescan /path/* --port 9000 # Custom port
|
hivescan /path/* --port 9000 # Custom port
|
||||||
|
|
||||||
Or as a library:
|
Or as a library:
|
||||||
from mediahive.hivescan.index_store import IndexStore
|
from mediahive.models.data import Movie, Series, TaskInfo
|
||||||
from mediahive.hivescan.structs import Movie, Series, TaskInfo
|
|
||||||
from mediahive.hivescan.server import app
|
from mediahive.hivescan.server import app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -21,21 +20,15 @@ from mediahive.hivescan.scanning import (
|
|||||||
from mediahive.hivescan.index_store import IndexStore
|
from mediahive.hivescan.index_store import IndexStore
|
||||||
from mediahive.hivescan.utils import DEFAULT_OUTPUT_FOLDER, find_common_root
|
from mediahive.hivescan.utils import DEFAULT_OUTPUT_FOLDER, find_common_root
|
||||||
from mediahive.hivescan.showreel import generate_showreel_images, generate_episode_reel
|
from mediahive.hivescan.showreel import generate_showreel_images, generate_episode_reel
|
||||||
from mediahive.hivescan.structs import (
|
from mediahive.models.data import (
|
||||||
CastMember,
|
|
||||||
Episode,
|
Episode,
|
||||||
EpisodeRelease,
|
|
||||||
IndexSnapshot,
|
IndexSnapshot,
|
||||||
MediaStats,
|
MediaStats,
|
||||||
Movie,
|
Movie,
|
||||||
MovieVersion,
|
|
||||||
Season,
|
Season,
|
||||||
Series,
|
Series,
|
||||||
SimilarMedia,
|
|
||||||
TaskInfo,
|
TaskInfo,
|
||||||
TMDbEpisodeInfo,
|
Torrent,
|
||||||
TMDbInfo,
|
|
||||||
TMDbSeasonInfo,
|
|
||||||
)
|
)
|
||||||
from mediahive.hivescan.tmdb_client import (
|
from mediahive.hivescan.tmdb_client import (
|
||||||
fetch_movie_info,
|
fetch_movie_info,
|
||||||
@@ -59,18 +52,17 @@ __all__ = [
|
|||||||
# Struct types
|
# Struct types
|
||||||
"CastMember",
|
"CastMember",
|
||||||
"Episode",
|
"Episode",
|
||||||
"EpisodeRelease",
|
|
||||||
"IndexSnapshot",
|
"IndexSnapshot",
|
||||||
"MediaStats",
|
"MediaStats",
|
||||||
"Movie",
|
"Movie",
|
||||||
"MovieVersion",
|
|
||||||
"Season",
|
"Season",
|
||||||
"Series",
|
"Series",
|
||||||
"SimilarMedia",
|
"SimilarMedia",
|
||||||
"TaskInfo",
|
"TaskInfo",
|
||||||
"TMDbEpisodeInfo",
|
"Torrent",
|
||||||
"TMDbInfo",
|
"EpisodeInfo",
|
||||||
"TMDbSeasonInfo",
|
"Info",
|
||||||
|
"SeasonInfo",
|
||||||
# Showreel generation
|
# Showreel generation
|
||||||
"generate_showreel_images",
|
"generate_showreel_images",
|
||||||
"generate_episode_reel",
|
"generate_episode_reel",
|
||||||
@@ -83,4 +75,3 @@ __all__ = [
|
|||||||
"DEFAULT_OUTPUT_FOLDER",
|
"DEFAULT_OUTPUT_FOLDER",
|
||||||
"find_common_root",
|
"find_common_root",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,14 @@ import msgspec
|
|||||||
from aiopathlib import AsyncPath
|
from aiopathlib import AsyncPath
|
||||||
from fastapi import WebSocket
|
from fastapi import WebSocket
|
||||||
|
|
||||||
from mediahive.hivescan.structs import (
|
from mediahive.models.data import (
|
||||||
IndexSnapshot,
|
IndexSnapshot,
|
||||||
MediaStats,
|
MediaStats,
|
||||||
Movie,
|
Movie,
|
||||||
Series,
|
Series,
|
||||||
TaskInfo,
|
TaskInfo,
|
||||||
|
)
|
||||||
|
from mediahive.models.protocol import (
|
||||||
WsInit,
|
WsInit,
|
||||||
WsInitData,
|
WsInitData,
|
||||||
WsRemove,
|
WsRemove,
|
||||||
@@ -250,4 +252,3 @@ class IndexStore:
|
|||||||
movies=movies_list,
|
movies=movies_list,
|
||||||
series=series_list,
|
series=series_list,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -10,16 +10,12 @@ from mediahive.hivescan.showreel import (
|
|||||||
get_expected_episode_reel_path,
|
get_expected_episode_reel_path,
|
||||||
get_expected_showreel_paths,
|
get_expected_showreel_paths,
|
||||||
)
|
)
|
||||||
from mediahive.hivescan.structs import (
|
from mediahive.models.data import (
|
||||||
Episode,
|
Episode,
|
||||||
EpisodeRelease,
|
|
||||||
Movie,
|
Movie,
|
||||||
MovieVersion,
|
|
||||||
Season,
|
Season,
|
||||||
Series,
|
Series,
|
||||||
TMDbEpisodeInfo,
|
Torrent,
|
||||||
TMDbInfo,
|
|
||||||
TMDbSeasonInfo,
|
|
||||||
)
|
)
|
||||||
from mediahive.hivescan.tmdb_client import (
|
from mediahive.hivescan.tmdb_client import (
|
||||||
fetch_movie_info,
|
fetch_movie_info,
|
||||||
@@ -46,18 +42,18 @@ from mediahive.hivescan.utils import (
|
|||||||
logger = logging.getLogger("hivescan.indexer")
|
logger = logging.getLogger("hivescan.indexer")
|
||||||
|
|
||||||
|
|
||||||
async def _build_version_info(
|
async def _build_torrent_info(
|
||||||
item: ParsedContent, media_root: Optional[str] = None
|
item: ParsedContent, media_root: Optional[str] = None
|
||||||
) -> MovieVersion:
|
) -> Torrent:
|
||||||
"""Build version/release info for a single torrent."""
|
"""Build torrent info for a single torrent."""
|
||||||
playable_file = await find_playable_file(item.path)
|
playable_file = await find_playable_file(item.path)
|
||||||
if item.content_hash and item.content_hash.size == 0:
|
if item.content_hash and item.content_hash.size == 0:
|
||||||
item.content_hash.size = await get_directory_size(item.content_hash.path)
|
item.content_hash.size = await get_directory_size(item.content_hash.path)
|
||||||
size = item.content_hash.size if item.content_hash else None
|
size = item.content_hash.size if item.content_hash else None
|
||||||
newest = await get_added_timestamp(item.path)
|
added_at = await get_added_timestamp(item.path)
|
||||||
|
|
||||||
return MovieVersion(
|
return Torrent(
|
||||||
torrent_title=item.title,
|
title=item.title,
|
||||||
playable_file=make_relative_path(playable_file, media_root),
|
playable_file=make_relative_path(playable_file, media_root),
|
||||||
resolution=item.resolution,
|
resolution=item.resolution,
|
||||||
quality=item.quality,
|
quality=item.quality,
|
||||||
@@ -65,7 +61,7 @@ async def _build_version_info(
|
|||||||
audio=item.audio,
|
audio=item.audio,
|
||||||
encoder=item.encoder,
|
encoder=item.encoder,
|
||||||
size=size,
|
size=size,
|
||||||
newest=newest,
|
added_at=added_at,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -144,7 +140,7 @@ async def _collect_episode_files(
|
|||||||
|
|
||||||
def _build_episodes_data(
|
def _build_episodes_data(
|
||||||
episodes_in_season: Dict[int, List[Dict]],
|
episodes_in_season: Dict[int, List[Dict]],
|
||||||
tmdb_episodes: Dict[int, TMDbEpisodeInfo],
|
tmdb_episodes: Dict[int, EpisodeInfo],
|
||||||
series_folder: Path,
|
series_folder: Path,
|
||||||
season_num: int,
|
season_num: int,
|
||||||
generate_showreels: bool,
|
generate_showreels: bool,
|
||||||
@@ -175,11 +171,11 @@ def _build_episodes_data(
|
|||||||
(best_file, series_folder, season_num, episode_num, series_title)
|
(best_file, series_folder, season_num, episode_num, series_title)
|
||||||
)
|
)
|
||||||
|
|
||||||
releases = {}
|
torrents = {}
|
||||||
for f in episode_files:
|
for f in episode_files:
|
||||||
relpath = make_relative_path(f["torrent_path"], media_root)
|
relpath = make_relative_path(f["torrent_path"], media_root)
|
||||||
releases[relpath] = EpisodeRelease(
|
torrents[relpath] = Torrent(
|
||||||
torrent_title=f["torrent_title"],
|
title=f["torrent_title"],
|
||||||
playable_file=make_relative_path(f["path"], media_root),
|
playable_file=make_relative_path(f["path"], media_root),
|
||||||
resolution=f.get("resolution"),
|
resolution=f.get("resolution"),
|
||||||
quality=f.get("quality"),
|
quality=f.get("quality"),
|
||||||
@@ -199,7 +195,7 @@ def _build_episodes_data(
|
|||||||
rating=tmdb_ep.vote_average if tmdb_ep else None,
|
rating=tmdb_ep.vote_average if tmdb_ep else None,
|
||||||
director=tmdb_ep.director if tmdb_ep else None,
|
director=tmdb_ep.director if tmdb_ep else None,
|
||||||
reel_image=reel_path,
|
reel_image=reel_path,
|
||||||
releases=releases,
|
torrents=torrents,
|
||||||
)
|
)
|
||||||
episodes_data.append(episode_data)
|
episodes_data.append(episode_data)
|
||||||
|
|
||||||
@@ -231,7 +227,7 @@ async def _build_seasons_data(
|
|||||||
|
|
||||||
# Fetch TMDb season details if we have a TMDb ID
|
# Fetch TMDb season details if we have a TMDb ID
|
||||||
tmdb_season = None
|
tmdb_season = None
|
||||||
tmdb_episodes: Dict[int, TMDbEpisodeInfo] = {}
|
tmdb_episodes: Dict[int, EpisodeInfo] = {}
|
||||||
|
|
||||||
if tmdb_id:
|
if tmdb_id:
|
||||||
cache_key = (tmdb_id, season_num)
|
cache_key = (tmdb_id, season_num)
|
||||||
@@ -395,13 +391,13 @@ async def _process_movies(
|
|||||||
tmdb_info.poster_path, display_title, year, "movie", cover_dir
|
tmdb_info.poster_path, display_title, year, "movie", cover_dir
|
||||||
)
|
)
|
||||||
|
|
||||||
versions = {}
|
torrents = {}
|
||||||
for item in items:
|
for item in items:
|
||||||
relpath = make_relative_path(str(item.path), media_root)
|
relpath = make_relative_path(str(item.path), media_root)
|
||||||
version = await _build_version_info(item, media_root)
|
torrent = await _build_torrent_info(item, media_root)
|
||||||
versions[relpath] = version
|
torrents[relpath] = torrent
|
||||||
|
|
||||||
sort_by_quality(list(versions.values()))
|
sort_by_quality(list(torrents.values()))
|
||||||
|
|
||||||
# Queue showreel generation
|
# Queue showreel generation
|
||||||
showreel_paths = []
|
showreel_paths = []
|
||||||
@@ -413,7 +409,7 @@ async def _process_movies(
|
|||||||
versions[k].size or 0,
|
versions[k].size or 0,
|
||||||
k
|
k
|
||||||
))
|
))
|
||||||
best_version = versions[best_relpath]
|
best_version = torrents[best_relpath]
|
||||||
if best_version.playable_file:
|
if best_version.playable_file:
|
||||||
abs_playable = (
|
abs_playable = (
|
||||||
str(Path(media_root) / best_version.playable_file)
|
str(Path(media_root) / best_version.playable_file)
|
||||||
@@ -435,35 +431,19 @@ async def _process_movies(
|
|||||||
tmdb_info.backdrop_path, display_title, year, "movie", cover_dir
|
tmdb_info.backdrop_path, display_title, year, "movie", cover_dir
|
||||||
)
|
)
|
||||||
|
|
||||||
version_timestamps = [v.newest for v in versions.values() if v.newest]
|
version_timestamps = [v.added_at for v in torrents.values() if v.added_at]
|
||||||
newest = max(version_timestamps) if version_timestamps else None
|
newest = max(version_timestamps) if version_timestamps else None
|
||||||
|
|
||||||
movie = Movie(
|
movie = Movie(
|
||||||
id=item_id,
|
id=item_id,
|
||||||
title=display_title,
|
title=display_title,
|
||||||
original_title=tmdb_info.original_title,
|
info=tmdb_info,
|
||||||
alternative_titles=tmdb_info.alternative_titles,
|
|
||||||
year=year,
|
year=year,
|
||||||
newest=newest,
|
newest=newest,
|
||||||
cover_path=make_relative_path(cover_path, media_root),
|
cover_path=make_relative_path(cover_path, media_root),
|
||||||
backdrop_path=make_relative_path(backdrop_path, media_root),
|
backdrop_path=make_relative_path(backdrop_path, media_root),
|
||||||
showreel_images=showreel_paths if showreel_paths else None,
|
showreel_images=showreel_paths if showreel_paths else None,
|
||||||
versions=versions,
|
torrents=torrents,
|
||||||
tmdb_id=tmdb_info.tmdb_id,
|
|
||||||
tmdb_title=tmdb_info.title,
|
|
||||||
rating=tmdb_info.rating,
|
|
||||||
vote_count=tmdb_info.vote_count,
|
|
||||||
overview=tmdb_info.overview,
|
|
||||||
genres=tmdb_info.genres,
|
|
||||||
release_date=tmdb_info.release_date,
|
|
||||||
runtime=tmdb_info.runtime,
|
|
||||||
status=tmdb_info.status,
|
|
||||||
tagline=tmdb_info.tagline,
|
|
||||||
poster_path=tmdb_info.poster_path,
|
|
||||||
similar=tmdb_info.similar,
|
|
||||||
keywords=tmdb_info.keywords,
|
|
||||||
cast=tmdb_info.cast,
|
|
||||||
director=tmdb_info.director,
|
|
||||||
)
|
)
|
||||||
yield movie, showreel_task
|
yield movie, showreel_task
|
||||||
|
|
||||||
@@ -478,24 +458,24 @@ async def _process_movies(
|
|||||||
await find_cover_image(title, year, "movie", cover_dir) if fetch_covers else None
|
await find_cover_image(title, year, "movie", cover_dir) if fetch_covers else None
|
||||||
)
|
)
|
||||||
|
|
||||||
versions = {}
|
torrents = {}
|
||||||
for item in items:
|
for item in items:
|
||||||
relpath = make_relative_path(str(item.path), media_root)
|
relpath = make_relative_path(str(item.path), media_root)
|
||||||
version = await _build_version_info(item, media_root)
|
torrent = await _build_torrent_info(item, media_root)
|
||||||
versions[relpath] = version
|
torrents[relpath] = torrent
|
||||||
|
|
||||||
sort_by_quality(list(versions.values()))
|
sort_by_quality(list(torrents.values()))
|
||||||
|
|
||||||
showreel_paths = []
|
showreel_paths = []
|
||||||
showreel_task = None
|
showreel_task = None
|
||||||
if generate_showreels and versions:
|
if generate_showreels and torrents:
|
||||||
# Find the best version for showreel (highest quality)
|
# Find the best version for showreel (highest quality)
|
||||||
best_relpath = max(versions.keys(), key=lambda k: (
|
best_relpath = max(torrents.keys(), key=lambda k: (
|
||||||
RESOLUTION_PRIORITY.get(versions[k].resolution or "", 0),
|
RESOLUTION_PRIORITY.get(torrents[k].resolution or "", 0),
|
||||||
versions[k].size or 0,
|
torrents[k].size or 0,
|
||||||
k
|
k
|
||||||
))
|
))
|
||||||
best_version = versions[best_relpath]
|
best_version = torrents[best_relpath]
|
||||||
if best_version.playable_file and not best_version.playable_file.endswith(".bdmv"):
|
if best_version.playable_file and not best_version.playable_file.endswith(".bdmv"):
|
||||||
abs_playable = (
|
abs_playable = (
|
||||||
str(Path(media_root) / best_version.playable_file)
|
str(Path(media_root) / best_version.playable_file)
|
||||||
@@ -511,7 +491,7 @@ async def _process_movies(
|
|||||||
)
|
)
|
||||||
showreel_task = (abs_playable, media_folder, title)
|
showreel_task = (abs_playable, media_folder, title)
|
||||||
|
|
||||||
version_timestamps = [v.newest for v in versions.values() if v.newest]
|
version_timestamps = [v.added_at for v in torrents.values() if v.added_at]
|
||||||
newest = max(version_timestamps) if version_timestamps else None
|
newest = max(version_timestamps) if version_timestamps else None
|
||||||
|
|
||||||
movie = Movie(
|
movie = Movie(
|
||||||
@@ -521,7 +501,7 @@ async def _process_movies(
|
|||||||
newest=newest,
|
newest=newest,
|
||||||
cover_path=make_relative_path(cover_path, media_root),
|
cover_path=make_relative_path(cover_path, media_root),
|
||||||
showreel_images=showreel_paths if showreel_paths else None,
|
showreel_images=showreel_paths if showreel_paths else None,
|
||||||
versions=versions,
|
torrents=torrents,
|
||||||
)
|
)
|
||||||
yield movie, showreel_task
|
yield movie, showreel_task
|
||||||
|
|
||||||
@@ -672,29 +652,12 @@ async def _process_series(
|
|||||||
series = Series(
|
series = Series(
|
||||||
id=series_id,
|
id=series_id,
|
||||||
title=display_title,
|
title=display_title,
|
||||||
original_title=tmdb_info.original_title,
|
info=tmdb_info,
|
||||||
alternative_titles=different_titles if different_titles else None,
|
alternative_titles=different_titles if different_titles else None,
|
||||||
newest=newest,
|
newest=newest,
|
||||||
cover_path=make_relative_path(cover_path, media_root),
|
cover_path=make_relative_path(cover_path, media_root),
|
||||||
backdrop_path=make_relative_path(backdrop_path, media_root),
|
backdrop_path=make_relative_path(backdrop_path, media_root),
|
||||||
seasons=seasons_data,
|
seasons=seasons_data,
|
||||||
tmdb_id=tmdb_info.tmdb_id,
|
|
||||||
tmdb_title=tmdb_info.title,
|
|
||||||
rating=tmdb_info.rating,
|
|
||||||
vote_count=tmdb_info.vote_count,
|
|
||||||
overview=tmdb_info.overview,
|
|
||||||
genres=tmdb_info.genres,
|
|
||||||
release_date=tmdb_info.release_date,
|
|
||||||
status=tmdb_info.status,
|
|
||||||
tagline=tmdb_info.tagline,
|
|
||||||
poster_path=tmdb_info.poster_path,
|
|
||||||
similar=tmdb_info.similar,
|
|
||||||
keywords=tmdb_info.keywords,
|
|
||||||
cast=tmdb_info.cast,
|
|
||||||
creators=tmdb_info.creators,
|
|
||||||
number_of_seasons=tmdb_info.number_of_seasons,
|
|
||||||
number_of_episodes=tmdb_info.number_of_episodes,
|
|
||||||
networks=tmdb_info.networks,
|
|
||||||
)
|
)
|
||||||
yield series, ep_reel_tasks
|
yield series, ep_reel_tasks
|
||||||
|
|
||||||
@@ -739,4 +702,3 @@ async def _process_series(
|
|||||||
seasons=seasons_data,
|
seasons=seasons_data,
|
||||||
)
|
)
|
||||||
yield series, ep_reel_tasks
|
yield series, ep_reel_tasks
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ import msgspec
|
|||||||
from aiopathlib import AsyncPath
|
from aiopathlib import AsyncPath
|
||||||
from mediahive.hivescan.index_store import IndexStore
|
from mediahive.hivescan.index_store import IndexStore
|
||||||
from mediahive.hivescan.indexer import _process_movies, _process_series
|
from mediahive.hivescan.indexer import _process_movies, _process_series
|
||||||
from mediahive.hivescan.structs import MsgspecResponse, ScanRequest, StatusResponse, TaskInfo
|
from mediahive.models.data import TaskInfo
|
||||||
|
from mediahive.models.protocol import MsgspecResponse, ScanRequest, StatusResponse
|
||||||
from mediahive.hivescan.models import ContentType, ParsedContent
|
from mediahive.hivescan.models import ContentType, ParsedContent
|
||||||
from mediahive.hivescan.parsing import parse_download
|
from mediahive.hivescan.parsing import parse_download
|
||||||
from mediahive.hivescan.scanning import categorize_downloads
|
from mediahive.hivescan.scanning import categorize_downloads
|
||||||
@@ -494,4 +495,3 @@ def run(host: str = "0.0.0.0", port: int = 8421):
|
|||||||
datefmt="%H:%M:%S",
|
datefmt="%H:%M:%S",
|
||||||
)
|
)
|
||||||
uvicorn.run(app, host=host, port=port, log_level="info")
|
uvicorn.run(app, host=host, port=port, log_level="info")
|
||||||
|
|
||||||
|
|||||||
@@ -1,337 +0,0 @@
|
|||||||
"""
|
|
||||||
Typed structures for the hivescan/mediahive API, index state, and WebSocket protocol.
|
|
||||||
|
|
||||||
All API and state types are msgspec.Structs for fast serialization.
|
|
||||||
Internal scanning types (ContentHash, ParsedContent) remain in models.py.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import msgspec
|
|
||||||
from fastapi.responses import Response
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Sub-types (shared by TMDb results and index items)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class CastMember(msgspec.Struct):
|
|
||||||
"""Actor/crew member."""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
character: str | None = None
|
|
||||||
profile_path: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class SimilarMedia(msgspec.Struct):
|
|
||||||
"""Pointer to a similar movie/series on TMDb."""
|
|
||||||
|
|
||||||
id: int
|
|
||||||
title: str
|
|
||||||
poster_path: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# TMDb result types (returned by tmdb_client, consumed by indexer)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class TMDbEpisodeInfo(msgspec.Struct):
|
|
||||||
"""Episode metadata from TMDb."""
|
|
||||||
|
|
||||||
episode_number: int
|
|
||||||
season_number: int
|
|
||||||
name: str | None = None
|
|
||||||
overview: str | None = None
|
|
||||||
air_date: str | None = None
|
|
||||||
runtime: int | None = None
|
|
||||||
still_path: str | None = None
|
|
||||||
vote_average: float | None = None
|
|
||||||
vote_count: int | None = None
|
|
||||||
director: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class TMDbSeasonInfo(msgspec.Struct):
|
|
||||||
"""Season metadata from TMDb."""
|
|
||||||
|
|
||||||
season_number: int
|
|
||||||
name: str | None = None
|
|
||||||
overview: str | None = None
|
|
||||||
air_date: str | None = None
|
|
||||||
poster_path: str | None = None
|
|
||||||
episode_count: int | None = None
|
|
||||||
episodes: list[TMDbEpisodeInfo] | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class TMDbInfo(msgspec.Struct):
|
|
||||||
"""Full metadata result from TMDb (movies or series)."""
|
|
||||||
|
|
||||||
tmdb_id: int
|
|
||||||
title: str | None = None
|
|
||||||
original_title: str | None = None
|
|
||||||
alternative_titles: list[str] | None = None
|
|
||||||
rating: float | None = None
|
|
||||||
vote_count: int | None = None
|
|
||||||
overview: str | None = None
|
|
||||||
genres: list[str] | None = None
|
|
||||||
release_date: str | None = None
|
|
||||||
runtime: int | None = None
|
|
||||||
status: str | None = None
|
|
||||||
tagline: str | None = None
|
|
||||||
poster_path: str | None = None
|
|
||||||
backdrop_path: str | None = None
|
|
||||||
similar: list[SimilarMedia] | None = None
|
|
||||||
keywords: list[str] | None = None
|
|
||||||
cast: list[CastMember] | None = None
|
|
||||||
director: str | None = None
|
|
||||||
creators: list[str] | None = None
|
|
||||||
number_of_seasons: int | None = None
|
|
||||||
number_of_episodes: int | None = None
|
|
||||||
networks: list[str] | None = None
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Index item types (the state stored in IndexStore, sent over WS/API)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class MovieVersion(msgspec.Struct):
|
|
||||||
"""One release/torrent of a movie, keyed by relative torrent path."""
|
|
||||||
|
|
||||||
torrent_title: str | None = None
|
|
||||||
playable_file: str | None = None
|
|
||||||
resolution: str | None = None
|
|
||||||
quality: str | None = None
|
|
||||||
codec: str | None = None
|
|
||||||
audio: str | None = None
|
|
||||||
encoder: str | None = None
|
|
||||||
size: int | None = None
|
|
||||||
newest: int | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class EpisodeRelease(msgspec.Struct):
|
|
||||||
"""One release/torrent file of an episode, keyed by relative torrent path."""
|
|
||||||
|
|
||||||
torrent_title: str | None = None
|
|
||||||
playable_file: str | None = None
|
|
||||||
resolution: str | None = None
|
|
||||||
quality: str | None = None
|
|
||||||
codec: str | None = None
|
|
||||||
audio: str | None = None
|
|
||||||
encoder: str | None = None
|
|
||||||
size: int | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class Episode(msgspec.Struct):
|
|
||||||
"""Episode within a season."""
|
|
||||||
|
|
||||||
episode_number: int
|
|
||||||
name: str | None = None
|
|
||||||
overview: str | None = None
|
|
||||||
air_date: str | None = None
|
|
||||||
runtime: int | None = None
|
|
||||||
still_path: str | None = None
|
|
||||||
rating: float | None = None
|
|
||||||
director: str | None = None
|
|
||||||
reel_image: str | None = None
|
|
||||||
releases: dict[str, EpisodeRelease] = {}
|
|
||||||
|
|
||||||
|
|
||||||
class Season(msgspec.Struct):
|
|
||||||
"""Season within a series."""
|
|
||||||
|
|
||||||
season_number: int
|
|
||||||
name: str | None = None
|
|
||||||
overview: str | None = None
|
|
||||||
air_date: str | None = None
|
|
||||||
poster_path: str | None = None
|
|
||||||
episode_count: int | None = None
|
|
||||||
episodes: list[Episode] = []
|
|
||||||
|
|
||||||
|
|
||||||
class Movie(msgspec.Struct):
|
|
||||||
"""A movie in the index (one or more versions/releases)."""
|
|
||||||
|
|
||||||
id: str
|
|
||||||
title: str
|
|
||||||
original_title: str | None = None
|
|
||||||
alternative_titles: list[str] | None = None
|
|
||||||
year: int | None = None
|
|
||||||
newest: int | None = None
|
|
||||||
cover_path: str | None = None
|
|
||||||
backdrop_path: str | None = None
|
|
||||||
showreel_images: list[str] | None = None
|
|
||||||
versions: dict[str, MovieVersion] = {}
|
|
||||||
tmdb_id: int | None = None
|
|
||||||
tmdb_title: str | None = None
|
|
||||||
rating: float | None = None
|
|
||||||
vote_count: int | None = None
|
|
||||||
overview: str | None = None
|
|
||||||
genres: list[str] | None = None
|
|
||||||
release_date: str | None = None
|
|
||||||
runtime: int | None = None
|
|
||||||
status: str | None = None
|
|
||||||
tagline: str | None = None
|
|
||||||
poster_path: str | None = None
|
|
||||||
similar: list[SimilarMedia] | None = None
|
|
||||||
keywords: list[str] | None = None
|
|
||||||
cast: list[CastMember] | None = None
|
|
||||||
director: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class Series(msgspec.Struct):
|
|
||||||
"""A TV series in the index."""
|
|
||||||
|
|
||||||
id: str
|
|
||||||
title: str
|
|
||||||
original_title: str | None = None
|
|
||||||
alternative_titles: list[str] | None = None
|
|
||||||
newest: int | None = None
|
|
||||||
cover_path: str | None = None
|
|
||||||
backdrop_path: str | None = None
|
|
||||||
seasons: list[Season] = []
|
|
||||||
tmdb_id: int | None = None
|
|
||||||
tmdb_title: str | None = None
|
|
||||||
rating: float | None = None
|
|
||||||
vote_count: int | None = None
|
|
||||||
overview: str | None = None
|
|
||||||
genres: list[str] | None = None
|
|
||||||
release_date: str | None = None
|
|
||||||
status: str | None = None
|
|
||||||
tagline: str | None = None
|
|
||||||
poster_path: str | None = None
|
|
||||||
similar: list[SimilarMedia] | None = None
|
|
||||||
keywords: list[str] | None = None
|
|
||||||
cast: list[CastMember] | None = None
|
|
||||||
creators: list[str] | None = None
|
|
||||||
number_of_seasons: int | None = None
|
|
||||||
number_of_episodes: int | None = None
|
|
||||||
networks: list[str] | None = None
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Snapshot (disk format for index.json)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class MediaStats(msgspec.Struct):
|
|
||||||
"""Aggregate counts for the index snapshot."""
|
|
||||||
|
|
||||||
total_movies: int = 0
|
|
||||||
total_movie_versions: int = 0
|
|
||||||
total_series: int = 0
|
|
||||||
total_series_episodes: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
class IndexSnapshot(msgspec.Struct):
|
|
||||||
"""On-disk recovery snapshot of the full index."""
|
|
||||||
|
|
||||||
version: int = 6
|
|
||||||
generated_at: str = ""
|
|
||||||
media_root: str | None = None
|
|
||||||
stats: MediaStats = msgspec.UNSET # type: ignore[assignment]
|
|
||||||
movies: list[Movie] = []
|
|
||||||
series: list[Series] = []
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
if self.stats is msgspec.UNSET:
|
|
||||||
self.stats = MediaStats()
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# WebSocket message types
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class WsInitData(msgspec.Struct):
|
|
||||||
"""Payload of the init message."""
|
|
||||||
|
|
||||||
movies: list[Movie]
|
|
||||||
series: list[Series]
|
|
||||||
|
|
||||||
|
|
||||||
class WsInit(msgspec.Struct, tag="init"):
|
|
||||||
"""Full index sent on WS connect."""
|
|
||||||
|
|
||||||
data: WsInitData
|
|
||||||
|
|
||||||
|
|
||||||
class WsUpsert(msgspec.Struct, tag="upsert"):
|
|
||||||
"""Single item inserted or updated."""
|
|
||||||
|
|
||||||
kind: str
|
|
||||||
item: Movie | Series
|
|
||||||
|
|
||||||
|
|
||||||
class WsRemove(msgspec.Struct, tag="remove"):
|
|
||||||
"""Single item removed."""
|
|
||||||
|
|
||||||
kind: str
|
|
||||||
id: str
|
|
||||||
|
|
||||||
|
|
||||||
class TaskInfo(msgspec.Struct):
|
|
||||||
"""Progress info for a background task (scan, showreel, etc.)."""
|
|
||||||
|
|
||||||
id: str
|
|
||||||
status: str
|
|
||||||
progress: float = 0.0
|
|
||||||
detail: str = ""
|
|
||||||
|
|
||||||
|
|
||||||
class WsTask(msgspec.Struct, tag="task"):
|
|
||||||
"""Task progress broadcast."""
|
|
||||||
|
|
||||||
data: TaskInfo
|
|
||||||
|
|
||||||
|
|
||||||
# Union of all outbound WS messages (for documentation / future decoding)
|
|
||||||
WsMessage = WsInit | WsUpsert | WsRemove | WsTask
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# API request / response types
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class ScanRequest(msgspec.Struct):
|
|
||||||
"""POST /api/scan body."""
|
|
||||||
|
|
||||||
paths: list[str] | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class StatusResponse(msgspec.Struct):
|
|
||||||
"""GET /api/status response."""
|
|
||||||
|
|
||||||
scanning: bool = False
|
|
||||||
movies: int = 0
|
|
||||||
series: int = 0
|
|
||||||
showreel_queue: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
class PlayMediaRequest(msgspec.Struct):
|
|
||||||
"""POST /api/play body (mediahive server)."""
|
|
||||||
|
|
||||||
file_path: str = ""
|
|
||||||
|
|
||||||
|
|
||||||
class OpenFolderRequest(msgspec.Struct):
|
|
||||||
"""POST /api/open-folder body (mediahive server)."""
|
|
||||||
|
|
||||||
folder_path: str = ""
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# FastAPI response helper
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class MsgspecResponse(Response):
|
|
||||||
"""FastAPI response that serializes content with msgspec.json."""
|
|
||||||
|
|
||||||
media_type = "application/json; charset=utf-8"
|
|
||||||
|
|
||||||
def render(self, content: object) -> bytes:
|
|
||||||
return msgspec.json.encode(content)
|
|
||||||
|
|
||||||
@@ -15,12 +15,10 @@ from typing import Dict, Optional
|
|||||||
import httpx
|
import httpx
|
||||||
from aiopathlib import AsyncPath
|
from aiopathlib import AsyncPath
|
||||||
|
|
||||||
from mediahive.hivescan.structs import (
|
from mediahive.models.tmdb import (
|
||||||
CastMember,
|
EpisodeInfo,
|
||||||
SimilarMedia,
|
Info,
|
||||||
TMDbEpisodeInfo,
|
SeasonInfo,
|
||||||
TMDbInfo,
|
|
||||||
TMDbSeasonInfo,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# TMDb API configuration
|
# TMDb API configuration
|
||||||
@@ -101,7 +99,7 @@ async def _save_to_cache(cache_path: Path, data: Optional[Dict]):
|
|||||||
pass # Cache write failures are not critical
|
pass # Cache write failures are not critical
|
||||||
|
|
||||||
|
|
||||||
# TMDbEpisodeInfo, TMDbSeasonInfo, TMDbInfo imported from mediahive.hivescan.structs
|
# EpisodeInfo, SeasonInfo, Info imported from mediahive.models.tmdb
|
||||||
|
|
||||||
|
|
||||||
async def tmdb_api_request(
|
async def tmdb_api_request(
|
||||||
@@ -192,7 +190,7 @@ async def fetch_season_details(
|
|||||||
director = crew_member.get("name")
|
director = crew_member.get("name")
|
||||||
break
|
break
|
||||||
|
|
||||||
episode = TMDbEpisodeInfo(
|
episode = EpisodeInfo(
|
||||||
episode_number=ep_data.get("episode_number", 0),
|
episode_number=ep_data.get("episode_number", 0),
|
||||||
season_number=ep_data.get("season_number", season_number),
|
season_number=ep_data.get("season_number", season_number),
|
||||||
name=ep_data.get("name"),
|
name=ep_data.get("name"),
|
||||||
@@ -562,4 +560,3 @@ async def fetch_series_info(title: str) -> Optional[TMDbInfo]:
|
|||||||
number_of_episodes=details.get("number_of_episodes"),
|
number_of_episodes=details.get("number_of_episodes"),
|
||||||
networks=networks if networks else None,
|
networks=networks if networks else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
"""
|
||||||
|
Data structures for mediahive and hivescan.
|
||||||
|
|
||||||
|
All types are msgspec.Structs for fast serialization.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import msgspec
|
||||||
|
|
||||||
|
from .tmdb import CastMember, EpisodeInfo, Info, SimilarMedia
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Index item types (the state stored in IndexStore, sent over WS/API)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class Torrent(msgspec.Struct):
|
||||||
|
"""A torrent file, either for a movie or an episode."""
|
||||||
|
|
||||||
|
title: str | None = None
|
||||||
|
playable_file: str | None = None
|
||||||
|
resolution: str | None = None
|
||||||
|
quality: str | None = None
|
||||||
|
codec: str | None = None
|
||||||
|
audio: str | None = None
|
||||||
|
encoder: str | None = None
|
||||||
|
size: int | None = None
|
||||||
|
added_at: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Episode(msgspec.Struct):
|
||||||
|
"""Episode within a season."""
|
||||||
|
|
||||||
|
episode_number: int
|
||||||
|
name: str | None = None
|
||||||
|
overview: str | None = None
|
||||||
|
air_date: str | None = None
|
||||||
|
runtime: int | None = None
|
||||||
|
still_path: str | None = None
|
||||||
|
rating: float | None = None
|
||||||
|
director: str | None = None
|
||||||
|
reel_image: str | None = None
|
||||||
|
torrents: dict[str, Torrent] = {}
|
||||||
|
|
||||||
|
|
||||||
|
class Season(msgspec.Struct):
|
||||||
|
"""Season within a series."""
|
||||||
|
|
||||||
|
season_number: int
|
||||||
|
name: str | None = None
|
||||||
|
overview: str | None = None
|
||||||
|
air_date: str | None = None
|
||||||
|
poster_path: str | None = None
|
||||||
|
episode_count: int | None = None
|
||||||
|
episodes: list[Episode] = []
|
||||||
|
|
||||||
|
|
||||||
|
class Movie(msgspec.Struct):
|
||||||
|
"""A movie in the index (one or more versions/releases)."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
title: str | None = None
|
||||||
|
info: Info | None = None
|
||||||
|
year: int | None = None
|
||||||
|
newest: int | None = None
|
||||||
|
cover_path: str | None = None
|
||||||
|
backdrop_path: str | None = None
|
||||||
|
showreel_images: list[str] | None = None
|
||||||
|
torrents: dict[str, Torrent] = {}
|
||||||
|
|
||||||
|
|
||||||
|
class Series(msgspec.Struct):
|
||||||
|
"""A TV series in the index."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
title: str | None = None
|
||||||
|
info: Info | None = None
|
||||||
|
newest: int | None = None
|
||||||
|
cover_path: str | None = None
|
||||||
|
backdrop_path: str | None = None
|
||||||
|
seasons: list[Season] = []
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Snapshot (disk format for index.json)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class MediaStats(msgspec.Struct):
|
||||||
|
"""Aggregate counts for the index snapshot."""
|
||||||
|
|
||||||
|
total_movies: int = 0
|
||||||
|
total_movie_versions: int = 0
|
||||||
|
total_series: int = 0
|
||||||
|
total_series_episodes: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class IndexSnapshot(msgspec.Struct):
|
||||||
|
"""On-disk recovery snapshot of the full index."""
|
||||||
|
|
||||||
|
version: int = 6
|
||||||
|
generated_at: str = ""
|
||||||
|
media_root: str | None = None
|
||||||
|
stats: MediaStats = msgspec.UNSET # type: ignore[assignment]
|
||||||
|
movies: list[Movie] = []
|
||||||
|
series: list[Series] = []
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.stats is msgspec.UNSET:
|
||||||
|
self.stats = MediaStats()
|
||||||
|
|
||||||
|
|
||||||
|
class TaskInfo(msgspec.Struct):
|
||||||
|
"""Progress info for a background task (scan, showreel, etc.)."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
status: str
|
||||||
|
progress: float = 0.0
|
||||||
|
detail: str = ""
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"""
|
||||||
|
Protocol structures for API and WebSocket communication.
|
||||||
|
|
||||||
|
All types are msgspec.Structs for fast serialization.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import msgspec
|
||||||
|
from fastapi.responses import Response
|
||||||
|
|
||||||
|
from .data import Movie, Series, TaskInfo
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# WebSocket message types
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class WsInitData(msgspec.Struct):
|
||||||
|
"""Payload of the init message."""
|
||||||
|
|
||||||
|
movies: list[Movie]
|
||||||
|
series: list[Series]
|
||||||
|
|
||||||
|
|
||||||
|
class WsInit(msgspec.Struct, tag="init"):
|
||||||
|
"""Full index sent on WS connect."""
|
||||||
|
|
||||||
|
data: WsInitData
|
||||||
|
|
||||||
|
|
||||||
|
class WsUpsert(msgspec.Struct, tag="upsert"):
|
||||||
|
"""Single item inserted or updated."""
|
||||||
|
|
||||||
|
kind: str
|
||||||
|
item: Movie | Series
|
||||||
|
|
||||||
|
|
||||||
|
class WsRemove(msgspec.Struct, tag="remove"):
|
||||||
|
"""Single item removed."""
|
||||||
|
|
||||||
|
kind: str
|
||||||
|
id: str
|
||||||
|
|
||||||
|
|
||||||
|
class WsTask(msgspec.Struct, tag="task"):
|
||||||
|
"""Task progress broadcast."""
|
||||||
|
|
||||||
|
data: TaskInfo
|
||||||
|
|
||||||
|
|
||||||
|
# Union of all outbound WS messages (for documentation / future decoding)
|
||||||
|
WsMessage = WsInit | WsUpsert | WsRemove | WsTask
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# API request / response types
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class ScanRequest(msgspec.Struct):
|
||||||
|
"""POST /api/scan body."""
|
||||||
|
|
||||||
|
paths: list[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class StatusResponse(msgspec.Struct):
|
||||||
|
"""GET /api/status response."""
|
||||||
|
|
||||||
|
scanning: bool = False
|
||||||
|
movies: int = 0
|
||||||
|
series: int = 0
|
||||||
|
showreel_queue: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class PlayMediaRequest(msgspec.Struct):
|
||||||
|
"""POST /api/play body (mediahive server)."""
|
||||||
|
|
||||||
|
file_path: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class OpenFolderRequest(msgspec.Struct):
|
||||||
|
"""POST /api/open-folder body (mediahive server)."""
|
||||||
|
|
||||||
|
folder_path: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# FastAPI response helper
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class MsgspecResponse(Response):
|
||||||
|
"""FastAPI response that serializes content with msgspec.json."""
|
||||||
|
|
||||||
|
media_type = "application/json; charset=utf-8"
|
||||||
|
|
||||||
|
def render(self, content: object) -> bytes:
|
||||||
|
return msgspec.json.encode(content)</content>
|
||||||
|
<parameter name="filePath">c:\mediahive\mediahive\models\protocol.py
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
"""
|
||||||
|
TMDb data structures.
|
||||||
|
|
||||||
|
All types are msgspec.Structs for fast serialization.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import msgspec
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Sub-types (shared by TMDb results and index items)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class CastMember(msgspec.Struct):
|
||||||
|
"""Actor/crew member."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
character: str | None = None
|
||||||
|
profile_path: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class SimilarMedia(msgspec.Struct):
|
||||||
|
"""Pointer to a similar movie/series on TMDb."""
|
||||||
|
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
poster_path: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# TMDb result types
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class EpisodeInfo(msgspec.Struct):
|
||||||
|
"""Episode metadata from TMDb."""
|
||||||
|
|
||||||
|
episode_number: int
|
||||||
|
season_number: int
|
||||||
|
name: str | None = None
|
||||||
|
overview: str | None = None
|
||||||
|
air_date: str | None = None
|
||||||
|
runtime: int | None = None
|
||||||
|
still_path: str | None = None
|
||||||
|
vote_average: float | None = None
|
||||||
|
vote_count: int | None = None
|
||||||
|
director: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class SeasonInfo(msgspec.Struct):
|
||||||
|
"""Season metadata from TMDb."""
|
||||||
|
|
||||||
|
season_number: int
|
||||||
|
name: str | None = None
|
||||||
|
overview: str | None = None
|
||||||
|
air_date: str | None = None
|
||||||
|
poster_path: str | None = None
|
||||||
|
episode_count: int | None = None
|
||||||
|
episodes: list[EpisodeInfo] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Info(msgspec.Struct):
|
||||||
|
"""Full metadata result from TMDb (movies or series)."""
|
||||||
|
|
||||||
|
tmdb_id: int
|
||||||
|
title: str | None = None
|
||||||
|
original_title: str | None = None
|
||||||
|
alternative_titles: list[str] | None = None
|
||||||
|
rating: float | None = None
|
||||||
|
vote_count: int | None = None
|
||||||
|
overview: str | None = None
|
||||||
|
genres: list[str] | None = None
|
||||||
|
release_date: str | None = None
|
||||||
|
runtime: int | None = None
|
||||||
|
status: str | None = None
|
||||||
|
tagline: str | None = None
|
||||||
|
poster_path: str | None = None
|
||||||
|
backdrop_path: str | None = None
|
||||||
|
similar: list[SimilarMedia] | None = None
|
||||||
|
keywords: list[str] | None = None
|
||||||
|
cast: list[CastMember] | None = None
|
||||||
|
director: str | None = None
|
||||||
|
creators: list[str] | None = None
|
||||||
|
number_of_seasons: int | None = None
|
||||||
|
number_of_episodes: int | None = None
|
||||||
|
networks: list[str] | None = None</content>
|
||||||
|
<parameter name="filePath">c:\mediahive\mediahive\models\tmdb.py
|
||||||
+1
-2
@@ -18,7 +18,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
from fastapi.responses import FileResponse, StreamingResponse
|
from fastapi.responses import FileResponse, StreamingResponse
|
||||||
from fastapi_vue import Frontend
|
from fastapi_vue import Frontend
|
||||||
|
|
||||||
from mediahive.hivescan.structs import PlayMediaRequest, OpenFolderRequest
|
from mediahive.models.protocol import PlayMediaRequest, OpenFolderRequest
|
||||||
|
|
||||||
from mediahive.__main__ import DEVMODE
|
from mediahive.__main__ import DEVMODE
|
||||||
|
|
||||||
@@ -262,4 +262,3 @@ async def serve_media_file(file_path: str):
|
|||||||
|
|
||||||
# Serve the Vue frontend (needs to be last if SPA catch-all is used)
|
# Serve the Vue frontend (needs to be last if SPA catch-all is used)
|
||||||
frontend.route(app, "/")
|
frontend.route(app, "/")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user