Skip to content

Commit

Permalink
[133] Implement manual override card loading for Plex
Browse files Browse the repository at this point in the history
From the UI, a Card can be selected to manually load that Card into any specific connection/library/episode. Currently only implemented for Plex
  • Loading branch information
CollinHeist committed Oct 30, 2024
1 parent 534628c commit c9db54d
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 43 deletions.
10 changes: 7 additions & 3 deletions app/internal/series.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from pathlib import Path
from time import sleep
from typing import Optional, Union
from typing import Any, Optional, Union

from fastapi import BackgroundTasks, HTTPException
from PIL import Image, UnidentifiedImageError
Expand Down Expand Up @@ -201,7 +201,7 @@ def download_series_poster(
for library in series.libraries:
if (interface := get_interface(library['interface_id'])):
try:
poster = interface.get_series_poster(
poster = interface.get_series_poster( # type: ignore
library['name'], series_info, log=log
)
except Exception:
Expand Down Expand Up @@ -694,6 +694,7 @@ def load_title_card(
interface_id: int,
interface: Union[EmbyInterface, JellyfinInterface, PlexInterface],
*,
uid: Optional[Union[int, str]] = None,
log: Logger = log,
) -> bool:
"""
Expand All @@ -706,6 +707,8 @@ def load_title_card(
db: Database to look for and add Loaded records from/to.
media_server: Which media server to load Title Cards into.
interface: Interface to load Title Cards into.
uid: Optional unique ID for for loading to a specific item in
the indicated Interface.
log: Logger for all log messages.
Returns:
Expand Down Expand Up @@ -733,10 +736,11 @@ def load_title_card(
db.query(Loaded).filter_by(**loaded_query).delete()

# Load Card
target = (card.episode, card) if uid is None else (card.episode, card, uid)
loaded_assets = interface.load_title_cards(
library_name,
card.episode.series.as_series_info,
[(card.episode, card)],
[target], # type: ignore
log=log,
)

Expand Down
12 changes: 10 additions & 2 deletions app/routers/cards.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional
from typing import Any, Optional, Union
from fastapi import APIRouter, Body, Depends, HTTPException, Query, Request
from fastapi_pagination.ext.sqlalchemy import paginate
from sqlalchemy import not_
Expand Down Expand Up @@ -498,12 +498,19 @@ def reload_card(
request: Request,
interface_id: Optional[int] = Query(default=None),
library_name: Optional[str] = Query(default=None),
uid: Optional[Union[int, str]] = Query(default=None),
db: Session = Depends(get_database),
) -> None:
"""
Reload the Title Card. This is a "force" reload.
- card_id: ID of the Card to load.
- interface_id: Optional ID of the Connection to load into.
- library_name: Optional name of the specific library to load Cards
into.
- uid: Optional unique ID of an episode in the associated Interface
and library to force load the given Card into. For PlexInterfaces,
this is the RatingKey, for Emby and Jellyfin this is the item ID.
"""

# Interface ID and library name must be provided together or not at all
Expand All @@ -524,10 +531,11 @@ def reload_card(
library_name,
interface_id,
get_interface(interface_id), # type: ignore
uid=uid,
log=request.state.log,
)
else:
# Load Cards for all libraries
else:
for library in card.episode.series.libraries:
load_title_card(
card,
Expand Down
111 changes: 106 additions & 5 deletions app/templates/js/series.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

{% if False %}
import {
AvailableFont, AvailableTemplate, Blueprint, Episode, EpisodeOverviewPage,
EpisodePage, ExternalSourceImage, LogEntryPage, MediaServerLibrary,
RemoteBlueprint, RemoteBlueprintSet, Series, Statistic, StyleOption,
SourceImagePage, TitleCardPage, UpdateEpisode,
AnyConnection, AvailableFont, AvailableTemplate, Blueprint, Episode,
EpisodeOverviewPage, EpisodePage, ExternalSourceImage, LogEntryPage,
MediaServerLibrary, RemoteBlueprint, RemoteBlueprintSet, RemoteEpisodeData,
Series, Statistic, StyleOption, SourceImagePage, TitleCardPage, UpdateEpisode,
} from './.types.js';
{% endif %}

Expand Down Expand Up @@ -255,6 +255,11 @@ async function initializeSeriesConfig() {
$.ajax({
type: 'GET',
url: '/api/available/libraries/all',
/**
* Libraries queried.
* @param {MediaServerLibrary[]} libraries List of all libraries which are
* available for Card assignment.
*/
success: libraries => {
// Start library value list with those selected by the Series
let values = series_libraries.map(library => {
Expand Down Expand Up @@ -286,6 +291,8 @@ async function initializeSeriesConfig() {
};
}),
});

// Populate the
},
error: response => showErrorToast({title: 'Error Querying Libraries', response}),
});
Expand Down Expand Up @@ -1156,6 +1163,7 @@ function getCardData(
}
preview.querySelector('.popup [data-action="delete"]').onclick = () => deleteCard(card.id);
preview.querySelector('.popup [data-action="reload"]').onclick = () => loadEpisodeCards(card.episode_id);
preview.querySelector('.popup [data-action="manually-reload"]').onclick = () => openManualCardLoadModal(card.id);

// If library unique mode is enabled, add the library name to the text (if present)
if (global_library_unique_cards) {
Expand Down Expand Up @@ -1216,7 +1224,7 @@ function getCardData(

async function initAll() {
// Initialize
initializeSeriesConfig();
await initializeSeriesConfig();
getEpisodeData();
initStyles();
getCardData(undefined, undefined, undefined, true);
Expand Down Expand Up @@ -2764,3 +2772,96 @@ function deleteBackdrop(seasonNumber) {
error: response => showErrorToast({title: 'Error Deleting File', response}),
});
}

/** @type {Object.<number, Object.<string, Object[]>>} Cache of previously queried Episode data */
let connectionEpisodeData = {};

/**
*
* @param {number} cardId ID of the Card to manually load.
* loaded.
*/
function openManualCardLoadModal(cardId) {
// Show modal
$('#loadCardsModal').modal({blurring: true, closeIcon: true}).modal('show');

$('.dropdown[data-value="load-episode-library"]').dropdown({
onChange: function(value, text, $selectedItem) {
// Get parameters
const libraryName = value;
const libraryDropdown = $selectedItem.closest('.dropdown[data-value="load-episode-library"]');
const connectionId = libraryDropdown.data('connectionId');

// Mark dropdown as loading
libraryDropdown.toggleClass('blue slow elastic loading', true);

function _populateDropdown(items) {
// Remove loading indication
libraryDropdown.toggleClass('blue slow elastic loading', false);

// Add items to dropdown
$(`.field[data-value="episodes"][data-connection-id="${connectionId}"] div.dropdown`).dropdown({
values: items,
placeholder: items.length === 0 ? 'No Episodes found' : 'Select an Episode',
onChange: function(value, text, $selectedItem) {
// Mark dropdown as loading
$selectedItem.closest('div.dropdown').toggleClass('green elastic loading', true);

// Generate query parameters
const params = new URLSearchParams({
interface_id: connectionId,
library_name: libraryName,
uid: value,
});

// Submit API request
$.ajax({
type: 'PUT',
url: `/api/cards/card/${cardId}/load?${params.toString()}`,
success: () => {
showInfoToast('Card Loaded Successfully')
},
error: response => showErrorToast({title: 'Error Loading Title Card', response}),
complete: () => $selectedItem.closest('div.dropdown').toggleClass('green elastic loading', false),
});
},
});
}

// Use cached episode data if this connection+library has already been queried
if ((connectionId in connectionEpisodeData) && (libraryName in connectionEpisodeData[connectionId])) {
_populateDropdown(connectionEpisodeData[connectionId][libraryName]);
} else {
// Query episodes for this library and episode
$.ajax({
type: 'GET',
url: `/api/episodes/series/{{ series.id }}/connection/${connectionId}?library_name=${libraryName}`,
/**
* Episode data queried.
* @param {RemoteEpisodeData[]} episodes List of all available episode
* data on the specified Connection.
*/
success: episodes => {
// Enable episodes dropdown
$(`.field[data-value="episodes"][data-connection-id="${connectionId}"]`).toggleClass('disabled', false);

const items = episodes.map(episode => {
return {
name: episode.title,
value: episode.uid,
description: `Season ${episode.season_number} Episode ${episode.episode_number}`,
descriptionVertical: true,
selected: false,
};
});

// Populate cache of episode data
connectionEpisodeData[connectionId] = connectionEpisodeData[connectionId] || {};
connectionEpisodeData[connectionId][libraryName] = items;
_populateDropdown(items);
},
});
}
},
});
}
10 changes: 7 additions & 3 deletions modules/EmbyInterface2.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,10 @@ def update_watched_statuses(self,
def load_title_cards(self,
library_name: str,
series_info: SeriesInfo,
episode_and_cards: list[tuple['Episode', 'Card']],
episode_and_cards: Union[
list[tuple['Episode', 'Card']],
list[tuple['Episode', 'Card', str]]
],
*,
log: Logger = log,
) -> list[tuple['Episode', 'Card']]:
Expand All @@ -708,7 +711,8 @@ def load_title_cards(self,
library_name: Name of the library containing the series.
series_info: SeriesInfo whose cards are being loaded.
episode_and_cards: List of tuple of Episode and their
corresponding Card objects to load.
corresponding Card objects to load. Each tuple may
optionally include a UID to force load that Card into.
log: Logger for all log messages.
Returns:
Expand All @@ -729,7 +733,7 @@ def load_title_cards(self,
emby_id = emby_info.emby_id[self._interface_id, library_name]

# Iterate through all the given episodes/cards, upload to match
for episode, card in episode_and_cards:
for episode, card, *_ in episode_and_cards:
if episode.as_episode_info == emby_info:
# Shrink image if necessary, skip if cannot be compressed
if (image := self.compress_image(card.card_file, log=log)) is None:
Expand Down
14 changes: 9 additions & 5 deletions modules/JellyfinInterface2.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class JellyfinInterface(MediaServer, EpisodeDataSource, SyncInterface, Interface
cards can be loaded into).
"""

INTERFACE_TYPE = 'Jellyfin'
INTERFACE_TYPE: str = 'Jellyfin'

"""Series ID's that can be set by Jellyfin"""
SERIES_IDS = ('imdb_id', 'jellyfin_id', 'tmdb_id', 'tvdb_id')
Expand Down Expand Up @@ -710,7 +710,10 @@ def update_watched_statuses(self,
def load_title_cards(self,
library_name: str,
series_info: SeriesInfo,
episode_and_cards: list[tuple['Episode', 'Card']],
episode_and_cards: Union[
list[tuple['Episode', 'Card']],
list[tuple['Episode', 'Card', str]]
],
*,
log: Logger = log,
) -> list[tuple['Episode', 'Card']]:
Expand All @@ -721,7 +724,8 @@ def load_title_cards(self,
library_name: Name of the library containing the series.
series_info: SeriesInfo whose cards are being loaded.
episode_and_cards: List of tuple of Episode and their
corresponding Card objects to load.
corresponding Card objects to load. Each tuple may
optionally include a UID to force load that Card into.
log: Logger for all log messages.
Returns:
Expand All @@ -736,7 +740,7 @@ def load_title_cards(self,

# Load each episode and card
loaded = []
for episode, card in episode_and_cards:
for episode, card, *_ in episode_and_cards:
# Find episode, skip if not found
episode_id = self.__get_episode_id(
library_name, series_id, episode.as_episode_info
Expand All @@ -752,7 +756,7 @@ def load_title_cards(self,
card_base64 = b64encode(image.read_bytes())
try:
self.session.session.post(
url=f'{self.url}/Items/{episode_id}/Images/Primary', # Change Primary to Backdrop
url=f'{self.url}/Items/{episode_id}/Images/Primary',
headers={'Content-Type': 'image/jpeg'},
params=self.__params,
data=card_base64,
Expand Down
7 changes: 5 additions & 2 deletions modules/MediaServer2.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod
from typing import Optional, TypeVar, Union
from typing import Any, Optional, TypeVar, Union
from pathlib import Path

from PIL import Image
Expand Down Expand Up @@ -106,7 +106,10 @@ def update_watched_statuses(self,
def load_title_cards(self,
library_name: str,
series_info: SeriesInfo,
episode_and_cards: list[tuple[_Episode, _Card]],
episode_and_cards: Union[
list[tuple[_Episode, _Card]],
list[tuple[_Episode, _Card, Any]]
],
*,
log: Logger = log,
) -> list[tuple[_Episode, _Card]]:
Expand Down
Loading

0 comments on commit c9db54d

Please sign in to comment.