Skip to content

Commit

Permalink
Merge pull request #188 from CollinHeist/develop
Browse files Browse the repository at this point in the history
Get source images from untrusted Plex servers, multiple translations per-series, minor changes/fixes
  • Loading branch information
CollinHeist authored Jun 15, 2022
2 parents 033260f + dc542b3 commit f2ca07a
Show file tree
Hide file tree
Showing 9 changed files with 97 additions and 61 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,5 @@ RUN pip3 install --no-cache-dir --upgrade pipenv; \
RUN rm -f Pipfile Pipfile.lock requirements.txt

# Entrypoint
CMD ["python3", "main.py", "--run"]
CMD ["python3", "main.py", "--run", "--no-color"]
ENTRYPOINT ["bash", "./start.sh"]
10 changes: 7 additions & 3 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,13 @@ def run():
RemoteFile.LOADED.truncate()

# Create Manager, run, and write missing report
tcm = Manager()
tcm.run()
tcm.report_missing(args.missing)
try:
tcm = Manager()
tcm.run()
tcm.report_missing(args.missing)
except PermissionError as error:
log.critical(f'Invalid permissions - {error}')
exit(1)

# Run immediately if specified
if args.run:
Expand Down
4 changes: 2 additions & 2 deletions modules/Manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ def create_season_posters(self) -> None:
"""Create season posters for all shows known to this Manager."""

# For each show in the Manager, create its posters
for show in (pbar := tqdm(self.shows, desc='Creating season posters',
**TQDM_KWARGS)):
for show in (pbar := tqdm(self.shows + self.archives,
desc='Creating season posters',**TQDM_KWARGS)):
show.create_season_posters()


Expand Down
13 changes: 9 additions & 4 deletions modules/PlexInterface.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from pathlib import Path

from plexapi.server import PlexServer, NotFound
from plexapi.server import PlexServer, NotFound, Unauthorized
from tenacity import retry, stop_after_attempt, wait_fixed, wait_exponential
from tinydb import TinyDB, where
from tqdm import tqdm
from yaml import safe_load

from modules.Debug import log, TQDM_KWARGS

Expand Down Expand Up @@ -34,7 +33,12 @@ def __init__(self, url: str, x_plex_token: str=None) -> None:
"""

# Create PlexServer object with these arguments
self.__server = PlexServer(url, x_plex_token)
try:
self.__token = x_plex_token
self.__server = PlexServer(url, x_plex_token)
except Unauthorized:
log.critical(f'Invalid Plex Token "{x_plex_token}"')
exit(1)

# Create/read loaded card database
self.__db = TinyDB(self.LOADED_DB)
Expand Down Expand Up @@ -319,7 +323,8 @@ def get_source_image(self, library_name: str, series_info: 'SeriesInfo',
episode=episode_info.episode_number
)

return f'{self.__server._baseurl}{plex_episode.thumb}'
return (f'{self.__server._baseurl}{plex_episode.thumb}'
f'?X-Plex-Token={self.__token}')
except NotFound:
# Episode DNE in Plex, return
return None
Expand Down
5 changes: 3 additions & 2 deletions modules/SeasonPosterSet.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ def __init__(self, episode_map: 'EpisodeMap', source_directory: Path,

# If posters aren't enabled, skip rest of parsing
poster_config = {} if poster_config is None else poster_config
if not poster_config.get('create', True):
if (self.__media_directory is None
or not poster_config.get('create', True)):
return None

# Read the font specification
Expand Down Expand Up @@ -85,7 +86,7 @@ def __read_font(self, font_config: dict) -> None:
self.valid = False

if (color := font_config.get('color')) != None:
if (not isinstance(value, str)
if (not isinstance(color, str)
or not bool(match('^#[a-fA-F0-9]{6}$', color))):
log.error(f'Font color "{color}" is invalid, specify as '
f'"#xxxxxx"')
Expand Down
92 changes: 56 additions & 36 deletions modules/Show.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from copy import copy
from pathlib import Path

from tqdm import tqdm
Expand Down Expand Up @@ -95,7 +96,7 @@ def __init__(self, name: str, yaml_dict: dict, library_map: dict,
self.unwatched_style = self.preferences.global_unwatched_style
self.hide_seasons = False
self.__episode_map = EpisodeMap()
self.title_language = {}
self.title_languages = {}
self.extras = {}

# Set object attributes based off YAML and update validity
Expand Down Expand Up @@ -142,14 +143,23 @@ def __repr__(self) -> str:
return f'<Show "{self.series_info}" with {len(self.episodes)} Episodes>'


def __copy__(self) -> 'Show':
def _copy_with_modified_media_directory(self,
media_directory: Path) -> 'Show':
"""
Copy this Show object into a new (identical) Show.
Recreate this Show object with a modified media directory.
:param media_directory: Media directory the returned Show object
will utilize.
:returns: A newly constructed Show object.
"""

return Show(self.series_info.name, self._base_yaml, self.__library_map,
# Modify base yaml to have overriden media_directory attribute
modified_base = copy(self._base_yaml)
modified_base['media_directory'] = str(media_directory.resolve())

# Recreate Show object with modified YAML
return Show(self.series_info.name, modified_base, self.__library_map,
self.font._Font__font_map, self.source_directory.parent)


Expand Down Expand Up @@ -245,13 +255,19 @@ def __parse_yaml(self):
if (value := self._get('seasons', 'hide', type_=bool)) is not None:
self.hide_seasons = value

if (self._is_specified('translation', 'language')
and (key := self._get('translation', 'key',type_=str)) is not None):
if key in ('title', 'abs_number'):
log.error(f'Cannot add translations under the key "{key}" in '
f'series {self}')
if (value := self._get('translation')) is not None:
if isinstance(value, dict) and value.keys() == {'language', 'key'}:
# Single translation
self.title_languages = [value]
elif isinstance(value, list):
# List of translations
if all(isinstance(t, dict) and t.keys() == {'language', 'key'}
for t in value):
self.title_languages = value
else:
log.error(f'Invalid language translations in series {self}')
else:
self.title_language = self._get('translation')
log.error(f'Invalid language translations in series {self}')

# Construct EpisodeMap on seasons/episode ranges specification
self.__episode_map = EpisodeMap(
Expand Down Expand Up @@ -431,42 +447,44 @@ def add_translations(self, tmdb_interface: 'TMDbInterface') -> None:
episode titles.
"""

# If no title language was specified, or TMDb syncing isn't enabled,skip
if self.title_language == {} or not self.tmdb_sync:
# If no translations were specified, or TMDb syncing isn't enabled, skip
if len(self.title_languages) == 0 or not self.tmdb_sync:
return None

# Go through every episode and look for translations
modified = False
for _, episode in (pbar := tqdm(self.episodes.items(), **TQDM_KWARGS)):
# If the key already exists, skip this episode
if self.title_language['key'] in episode.extra_characteristics:
continue
# Get each translation for this series
for translation in self.title_languages:
# If the key already exists, skip this episode
if translation['key'] in episode.extra_characteristics:
continue

# Update progress bar
pbar.set_description(f'Checking {episode}')
# Update progress bar
pbar.set_description(f'Checking {episode}')

# Query TMDb for the title of this episode in the requested language
language_title = tmdb_interface.get_episode_title(
self.series_info,
episode.episode_info,
self.title_language['language'],
)
# Query TMDb for the title of this episode in this language
language_title = tmdb_interface.get_episode_title(
self.series_info,
episode.episode_info,
translation['language'],
)

# If episode wasn't found, or the original title was returned, skip!
if (language_title is None
or language_title == episode.episode_info.title.full_title):
continue
# If episode wasn't found, or original title was returned, skip
if (language_title is None
or language_title == episode.episode_info.title.full_title):
continue

# Adding translated title, log it
log.debug(f'Adding "{language_title}" to '
f'"{self.title_language["key"]}" of {self}')
# Modify data file entry with new title
modified = True
self.file_interface.add_data_to_entry(
episode.episode_info,
**{translation['key']: language_title},
)

# Modify data file entry with new title
modified = True
self.file_interface.add_data_to_entry(
episode.episode_info,
**{self.title_language['key']: language_title},
)
# Adding translated title, log it
log.debug(f'Added "{language_title}" to '
f'"{translation["key"]}" for {self}')

# If any translations were added, re-read source
if modified:
Expand Down Expand Up @@ -660,6 +678,8 @@ def select_source_images(self, plex_interface: PlexInterface=None,
# If URL was returned by either interface, download
if image_url is not None:
WebInterface.download_image(image_url, episode.source)
log.debug(f'Downloaded {episode.source.name} for {self} '
f'from {source_interface.title()}')
break

# Query TMDb for the backdrop if one does not exist and is needed
Expand Down
15 changes: 9 additions & 6 deletions modules/ShowArchive.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,20 +65,23 @@ def __init__(self, archive_directory: 'Path', base_show: 'Show') -> None:

# Go through each valid profile
for profile_attributes in valid_profiles:
# Create show object for this profile
new_show = copy(base_show)

# Update media directory
# Get directory name for this profile
profile_directory = self.PROFILE_DIRECTORY_MAP[
f'{profile_attributes["seasons"]}-{profile_attributes["font"]}'
]

# For non-standard card classes, modify archive directory name
# For non-standard card classes, modify profile directory name
if base_show.card_class.ARCHIVE_NAME != 'standard':
profile_directory += f' - {base_show.card_class.ARCHIVE_NAME}'

# Get modified media directory within the archive directory
temp_path = archive_directory / base_show.series_info.legal_path
new_show.media_directory = temp_path / profile_directory
new_media_directory = temp_path / profile_directory

# Create modified Show object for this profile
new_show = base_show._copy_with_modified_media_directory(
new_media_directory
)

# Convert this new show's profile
new_show.profile.convert_profile(
Expand Down
6 changes: 3 additions & 3 deletions modules/StandardTitleCard.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,8 +381,8 @@ def _combine_titled_image_series_count_text(self, titled_image: Path,
@staticmethod
def is_custom_font(font: 'Font') -> bool:
"""
Determines whether the given font characteristics constitute a default
or custom font.
Determine whether the given font characteristics constitute a default or
custom font.
:param font: The Font being evaluated.
Expand All @@ -403,7 +403,7 @@ def is_custom_font(font: 'Font') -> bool:
def is_custom_season_titles(custom_episode_map: bool,
episode_text_format: str) -> bool:
"""
Determines whether the given attributes constitute custom or generic
Determine whether the given attributes constitute custom or generic
season titles.
:param custom_episode_map: Whether the EpisodeMap was
Expand Down
11 changes: 7 additions & 4 deletions modules/Title.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,14 @@ def split(self, max_line_width: int, max_line_count: int,
for _ in range(max_line_count+2-1):
# Start splitting from the last line added
top, bottom = all_lines.pop(), ''
while ((len(top) > max_line_width or len(bottom) in range(1, 6))
while ((len(top) > max_line_width
or len(bottom) in range(1, 6))
and ' ' in top):
# Look to split on special characters
special_split = False
for char in self.SPLIT_CHARACTERS:
if f'{char} ' in top[:max_line_width]:
# Split only if present after first third of next line
if f'{char} ' in top[max_line_width//2:max_line_width]:
top, bottom_add = top.rsplit(f'{char} ', 1)
top += char
bottom = f'{bottom_add} {bottom}'
Expand Down Expand Up @@ -172,12 +174,13 @@ def split(self, max_line_width: int, max_line_count: int,
# For bottom heavy splitting, start on bottom and move text UP
for _ in range(max_line_count+2-1):
top, bottom = '', all_lines.pop()
while ((len(bottom) > max_line_width or len(top) in range(1, 6))
while ((len(bottom) > max_line_width
or len(top) in range(1, 6))
and ' ' in bottom):
# Look to split on special characters
special_split = False
for char in self.SPLIT_CHARACTERS:
if f'{char} ' in bottom[:min(max_line_width, len(bottom)//2)]:
if f'{char} ' in bottom[:min(max_line_width,len(bottom)//2)]:
top_add, bottom = bottom.split(f'{char} ', 1)
top = f'{top} {top_add}{char}'
special_split = True
Expand Down

0 comments on commit f2ca07a

Please sign in to comment.