diff --git a/src/components/plex/PlexLibraryStats.vue b/src/components/plex/PlexLibraryStats.vue new file mode 100644 index 0000000..8a78e38 --- /dev/null +++ b/src/components/plex/PlexLibraryStats.vue @@ -0,0 +1,178 @@ + + + + + diff --git a/src/components/plex/PlexServerInfo.vue b/src/components/plex/PlexServerInfo.vue new file mode 100644 index 0000000..050fea8 --- /dev/null +++ b/src/components/plex/PlexServerInfo.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/src/composables/usePlexLibraries.ts b/src/composables/usePlexLibraries.ts new file mode 100644 index 0000000..99c229f --- /dev/null +++ b/src/composables/usePlexLibraries.ts @@ -0,0 +1,120 @@ +import { ref } from "vue"; +import { usePlexApi } from "./usePlexApi"; +import { + processLibraryItem, + calculateGenreStats, + calculateDuration +} from "@/utils/plexHelpers"; + +export function usePlexLibraries() { + const { plexServerUrl, fetchLibrarySections, fetchLibraryDetails } = + usePlexApi(); + + const loading = ref(false); + const libraryStats = ref({ + movies: 0, + shows: 0, + music: 0, + watchtime: 0 + }); + + const libraryDetails = ref({ + movies: { + total: 0, + recentlyAdded: [], + genres: [], + totalDuration: "0 hours" + }, + shows: { + total: 0, + recentlyAdded: [], + genres: [], + totalEpisodes: 0, + totalDuration: "0 hours" + }, + music: { total: 0, recentlyAdded: [], genres: [], totalTracks: 0 } + }); + + async function loadLibraries(authToken: string) { + loading.value = true; + + // Reset stats + libraryStats.value = { movies: 0, shows: 0, music: 0, watchtime: 0 }; + + try { + const sections = await fetchLibrarySections(authToken); + + for (const section of sections) { + const type = section.type; + const key = section.key; + + if (type === "movie") { + await processLibrarySection(authToken, key, "movies"); + } else if (type === "show") { + await processLibrarySection(authToken, key, "shows"); + } else if (type === "artist") { + await processLibrarySection(authToken, key, "music"); + } + } + } catch (error) { + console.error("[PlexLibraries] Error loading libraries:", error); + throw error; + } finally { + loading.value = false; + } + } + + async function processLibrarySection( + authToken: string, + sectionKey: string, + libraryType: string + ) { + try { + const data = await fetchLibraryDetails(authToken, sectionKey); + if (!data) return; + + const totalCount = data.all.MediaContainer?.size || 0; + + // Update stats + if (libraryType === "movies") { + libraryStats.value.movies += totalCount; + } else if (libraryType === "shows") { + libraryStats.value.shows += totalCount; + } else if (libraryType === "music") { + libraryStats.value.music += totalCount; + } + + // Process recently added items + const recentItems = data.recentMetadata + .slice(0, 5) + .map((item: any) => + processLibraryItem(item, libraryType, authToken, plexServerUrl.value) + ); + + // Calculate stats + const genres = calculateGenreStats(data.metadata); + const durations = calculateDuration(data.metadata, libraryType); + + // Update library details + libraryDetails.value[libraryType] = { + total: totalCount, + recentlyAdded: recentItems, + genres, + totalDuration: durations.totalDuration, + ...(libraryType === "shows" && { + totalEpisodes: durations.totalEpisodes + }), + ...(libraryType === "music" && { totalTracks: durations.totalTracks }) + }; + } catch (error) { + console.error(`[PlexLibraries] Error processing ${libraryType}:`, error); + } + } + + return { + loading, + libraryStats, + libraryDetails, + loadLibraries + }; +} diff --git a/src/utils/plexHelpers.ts b/src/utils/plexHelpers.ts new file mode 100644 index 0000000..be5eed0 --- /dev/null +++ b/src/utils/plexHelpers.ts @@ -0,0 +1,126 @@ +export function getLibraryIcon(type: string): string { + const icons: Record = { + movies: "🎬", + shows: "📺", + music: "🎵" + }; + return icons[type] || "📁"; +} + +export function getLibraryTitle(type: string): string { + const titles: Record = { + movies: "Movies", + shows: "TV Shows", + music: "Music" + }; + return titles[type] || type; +} + +export function formatDate(dateString: string): string { + try { + const date = new Date(dateString); + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric" + }); + } catch { + return dateString; + } +} + +export function formatMemberSince(dateString: string): string { + try { + const date = new Date(dateString); + const now = new Date(); + const years = now.getFullYear() - date.getFullYear(); + + if (years === 0) return "New Member"; + if (years === 1) return "1 Year"; + return `${years} Years`; + } catch { + return "Member"; + } +} + +export function processLibraryItem( + item: any, + libraryType: string, + authToken: string, + serverUrl: string +) { + // Get poster/thumbnail URL + let posterUrl = null; + if (item.thumb) { + posterUrl = `${serverUrl}${item.thumb}?X-Plex-Token=${authToken}`; + } else if (item.grandparentThumb) { + posterUrl = `${serverUrl}${item.grandparentThumb}?X-Plex-Token=${authToken}`; + } + + const baseItem = { + title: item.title, + year: item.year || item.parentYear || new Date().getFullYear(), + poster: posterUrl, + fallbackIcon: getLibraryIcon(libraryType), + rating: item.rating ? Math.round(item.rating * 10) / 10 : null + }; + + if (libraryType === "shows") { + return { + ...baseItem, + episodes: item.leafCount || 0 + }; + } else if (libraryType === "music") { + return { + ...baseItem, + artist: item.parentTitle || "Unknown Artist", + tracks: item.leafCount || 0 + }; + } + + return baseItem; +} + +export function calculateGenreStats(metadata: any[]) { + const genreMap = new Map(); + + metadata.forEach((item: any) => { + if (item.Genre) { + item.Genre.forEach((genre: any) => { + genreMap.set(genre.tag, (genreMap.get(genre.tag) || 0) + 1); + }); + } + }); + + return Array.from(genreMap.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5) + .map(([name, count]) => ({ name, count })); +} + +export function calculateDuration(metadata: any[], libraryType: string) { + let totalDuration = 0; + let totalEpisodes = 0; + let totalTracks = 0; + + metadata.forEach((item: any) => { + if (item.duration) { + totalDuration += item.duration; + } + + if (libraryType === "shows" && item.leafCount) { + totalEpisodes += item.leafCount; + } else if (libraryType === "music" && item.leafCount) { + totalTracks += item.leafCount; + } + }); + + const hours = Math.round(totalDuration / (1000 * 60 * 60)); + const formattedDuration = hours.toLocaleString() + " hours"; + + return { + totalDuration: formattedDuration, + totalEpisodes, + totalTracks + }; +}