2021-09-21 03:57:16 +00:00
import logging , os , plexapi , requests
2021-05-27 17:40:35 +00:00
from modules import builder , util
2021-09-21 03:57:16 +00:00
from modules . library import Library
2021-06-30 15:02:55 +00:00
from modules . util import Failed , ImageData
2021-12-19 00:48:11 +00:00
from PIL import Image
2021-05-02 04:22:48 +00:00
from plexapi import utils
2022-01-19 15:41:08 +00:00
from plexapi . audio import Artist
2021-01-20 21:37:59 +00:00
from plexapi . exceptions import BadRequest , NotFound , Unauthorized
2021-06-16 18:20:48 +00:00
from plexapi . collection import Collection
2021-12-17 14:24:46 +00:00
from plexapi . playlist import Playlist
2021-01-20 21:37:59 +00:00
from plexapi . server import PlexServer
2021-12-19 00:48:11 +00:00
from plexapi . video import Movie , Show
2021-01-20 21:37:59 +00:00
from retrying import retry
2021-05-02 04:22:48 +00:00
from urllib import parse
2021-06-22 20:28:12 +00:00
from xml . etree . ElementTree import ParseError
2021-01-20 21:37:59 +00:00
logger = logging . getLogger ( " Plex Meta Manager " )
2021-05-14 19:48:33 +00:00
builders = [ " plex_all " , " plex_collectionless " , " plex_search " ]
2021-03-30 05:50:53 +00:00
search_translation = {
2021-08-23 13:30:40 +00:00
" episode_title " : " episode.title " ,
" network " : " show.network " ,
2021-04-08 03:45:37 +00:00
" critic_rating " : " rating " ,
2021-08-23 13:30:40 +00:00
" audience_rating " : " audienceRating " ,
2021-05-05 18:01:41 +00:00
" user_rating " : " userRating " ,
2021-08-23 13:30:40 +00:00
" episode_user_rating " : " episode.userRating " ,
" content_rating " : " contentRating " ,
" episode_year " : " episode.year " ,
" release " : " originallyAvailableAt " ,
" episode_unmatched " : " episode.unmatched " ,
" episode_duplicate " : " episode.duplicate " ,
" added " : " addedAt " ,
2021-05-05 18:01:41 +00:00
" episode_added " : " episode.addedAt " ,
2021-05-27 17:40:35 +00:00
" episode_air_date " : " episode.originallyAvailableAt " ,
2021-08-23 13:30:40 +00:00
" plays " : " viewCount " ,
2021-08-22 21:43:26 +00:00
" episode_plays " : " episode.viewCount " ,
2021-08-23 13:30:40 +00:00
" last_played " : " lastViewedAt " ,
" episode_last_played " : " episode.lastViewedAt " ,
" unplayed " : " unwatched " ,
2021-08-22 21:43:26 +00:00
" episode_unplayed " : " episode.unwatched " ,
2021-08-23 13:30:40 +00:00
" subtitle_language " : " subtitleLanguage " ,
" audio_language " : " audioLanguage " ,
" progress " : " inProgress " ,
2021-08-22 21:43:26 +00:00
" episode_progress " : " episode.inProgress " ,
2021-12-29 15:51:22 +00:00
" unplayed_episodes " : " show.unwatchedLeaves " ,
" artist_title " : " artist.title " ,
" artist_user_rating " : " artist.userRating " ,
" artist_genre " : " artist.genre " ,
" artist_collection " : " artist.collection " ,
" artist_country " : " artist.country " ,
" artist_mood " : " artist.mood " ,
" artist_style " : " artist.style " ,
" artist_added " : " artist.addedAt " ,
" artist_last_played " : " artist.lastViewedAt " ,
" artist_unmatched " : " artist.unmatched " ,
" album_title " : " album.title " ,
" album_year " : " album.year " ,
" album_decade " : " album.decade " ,
" album_genre " : " album.genre " ,
" album_plays " : " album.viewCount " ,
" album_last_played " : " album.lastViewedAt " ,
" album_user_rating " : " album.userRating " ,
" album_critic_rating " : " album.rating " ,
" album_record_label " : " album.studio " ,
" album_mood " : " album.mood " ,
" album_style " : " album.style " ,
" album_format " : " album.format " ,
" album_type " : " album.subformat " ,
" album_collection " : " album.collection " ,
" album_added " : " album.addedAt " ,
" album_released " : " album.originallyAvailableAt " ,
" album_unmatched " : " album.unmatched " ,
" album_source " : " album.source " ,
" album_label " : " album.label " ,
" track_mood " : " track.mood " ,
" track_title " : " track.title " ,
" track_plays " : " track.viewCount " ,
" track_last_played " : " track.lastViewedAt " ,
" track_skips " : " track.skipCount " ,
" track_last_skipped " : " track.lastSkippedAt " ,
" track_user_rating " : " track.userRating " ,
" track_last_rated " : " track.lastRatedAt " ,
" track_added " : " track.addedAt " ,
" track_trash " : " track.trash " ,
" track_source " : " track.source "
2021-05-05 18:01:41 +00:00
}
2021-07-26 18:52:32 +00:00
show_translation = {
2021-08-23 13:30:40 +00:00
" title " : " show.title " ,
" studio " : " show.studio " ,
" rating " : " show.rating " ,
" audienceRating " : " show.audienceRating " ,
" userRating " : " show.userRating " ,
" contentRating " : " show.contentRating " ,
" year " : " show.year " ,
" originallyAvailableAt " : " show.originallyAvailableAt " ,
" unmatched " : " show.unmatched " ,
" genre " : " show.genre " ,
" collection " : " show.collection " ,
" actor " : " show.actor " ,
" addedAt " : " show.addedAt " ,
2021-08-22 21:43:26 +00:00
" viewCount " : " show.viewCount " ,
2021-08-23 13:30:40 +00:00
" lastViewedAt " : " show.lastViewedAt " ,
" resolution " : " episode.resolution " ,
2021-07-26 18:52:32 +00:00
" hdr " : " episode.hdr " ,
" subtitleLanguage " : " episode.subtitleLanguage " ,
2021-08-23 13:30:40 +00:00
" audioLanguage " : " episode.audioLanguage " ,
" trash " : " episode.trash " ,
" label " : " show.label " ,
2021-07-26 18:52:32 +00:00
}
2021-05-05 18:01:41 +00:00
modifier_translation = {
2021-09-13 02:42:33 +00:00
" " : " " , " .not " : " ! " , " .is " : " % 3D " , " .isnot " : " ! % 3D " , " .gt " : " %3E %3E " , " .gte " : " %3E " , " .lt " : " % 3C % 3C " , " .lte " : " % 3C " ,
2021-07-23 19:44:21 +00:00
" .before " : " % 3C % 3C " , " .after " : " %3E %3E " , " .begins " : " % 3C " , " .ends " : " %3E "
2021-03-30 05:50:53 +00:00
}
episode_sorting_options = { " default " : " -1 " , " oldest " : " 0 " , " newest " : " 1 " }
2021-12-29 15:51:22 +00:00
album_sorting_options = { " default " : - 1 , " newest " : 0 , " oldest " : 1 , " name " : 2 }
2021-03-30 05:50:53 +00:00
keep_episodes_options = { " all " : 0 , " 5_latest " : 5 , " 3_latest " : 3 , " latest " : 1 , " past_3 " : - 3 , " past_7 " : - 7 , " past_30 " : - 30 }
delete_episodes_options = { " never " : 0 , " day " : 1 , " week " : 7 , " refresh " : 100 }
season_display_options = { " default " : - 1 , " show " : 0 , " hide " : 1 }
2021-05-05 17:34:09 +00:00
episode_ordering_options = { " default " : None , " tmdb_aired " : " tmdbAiring " , " tvdb_aired " : " aired " , " tvdb_dvd " : " dvd " , " tvdb_absolute " : " absolute " }
2021-03-30 05:50:53 +00:00
plex_languages = [ " default " , " ar-SA " , " ca-ES " , " cs-CZ " , " da-DK " , " de-DE " , " el-GR " , " en-AU " , " en-CA " , " en-GB " , " en-US " ,
" es-ES " , " es-MX " , " et-EE " , " fa-IR " , " fi-FI " , " fr-CA " , " fr-FR " , " he-IL " , " hi-IN " , " hu-HU " , " id-ID " ,
" it-IT " , " ja-JP " , " ko-KR " , " lt-LT " , " lv-LV " , " nb-NO " , " nl-NL " , " pl-PL " , " pt-BR " , " pt-PT " , " ro-RO " ,
" ru-RU " , " sk-SK " , " sv-SE " , " th-TH " , " tr-TR " , " uk-UA " , " vi-VN " , " zh-CN " , " zh-HK " , " zh-TW " ]
metadata_language_options = { lang . lower ( ) : lang for lang in plex_languages }
metadata_language_options [ " default " ] = None
2021-04-05 22:07:44 +00:00
use_original_title_options = { " default " : - 1 , " no " : 0 , " yes " : 1 }
2021-07-29 02:26:39 +00:00
collection_order_options = [ " release " , " alpha " , " custom " ]
2021-12-29 15:51:22 +00:00
collection_level_show_options = [ " episode " , " season " ]
collection_level_music_options = [ " album " , " track " ]
collection_level_options = collection_level_show_options + collection_level_music_options
2021-05-02 04:22:48 +00:00
collection_mode_keys = { - 1 : " default " , 0 : " hide " , 1 : " hideItems " , 2 : " showItems " }
collection_order_keys = { 0 : " release " , 1 : " alpha " , 2 : " custom " }
2021-04-21 12:49:27 +00:00
item_advance_keys = {
2021-12-29 15:51:22 +00:00
" item_album_sorting " : ( " albumSort " , album_sorting_options ) ,
2021-04-21 12:49:27 +00:00
" item_episode_sorting " : ( " episodeSort " , episode_sorting_options ) ,
" item_keep_episodes " : ( " autoDeletionItemPolicyUnwatchedLibrary " , keep_episodes_options ) ,
" item_delete_episodes " : ( " autoDeletionItemPolicyWatchedLibrary " , delete_episodes_options ) ,
" item_season_display " : ( " flattenSeasons " , season_display_options ) ,
" item_episode_ordering " : ( " showOrdering " , episode_ordering_options ) ,
" item_metadata_language " : ( " languageOverride " , metadata_language_options ) ,
" item_use_original_title " : ( " useOriginalTitle " , use_original_title_options )
}
new_plex_agents = [ " tv.plex.agents.movie " , " tv.plex.agents.series " ]
2021-12-29 15:51:22 +00:00
music_searches = [
" artist_title " , " artist_title.not " , " artist_title.is " , " artist_title.isnot " , " artist_title.begins " , " artist_title.ends " ,
" artist_user_rating.gt " , " artist_user_rating.gte " , " artist_user_rating.lt " , " artist_user_rating.lte " ,
" artist_genre " , " artist_genre.not " ,
" artist_collection " , " artist_collection.not " ,
" artist_country " , " artist_country.not " ,
" artist_mood " , " artist_mood.not " ,
" artist_style " , " artist_style.not " ,
" artist_added " , " artist_added.not " , " artist_added.before " , " artist_added.after " ,
" artist_last_played " , " artist_last_played.not " , " artist_last_played.before " , " artist_last_played.after " ,
" artist_unmatched " ,
" album_title " , " album_title.not " , " album_title.is " , " album_title.isnot " , " album_title.begins " , " album_title.ends " ,
" album_year.gt " , " album_year.gte " , " album_year.lt " , " album_year.lte " ,
" album_decade " ,
" album_genre " , " album_genre.not " ,
" album_plays.gt " , " album_plays.gte " , " album_plays.lt " , " album_plays.lte " ,
" album_last_played " , " album_last_played.not " , " album_last_played.before " , " album_last_played.after " ,
" album_user_rating.gt " , " album_user_rating.gte " , " album_user_rating.lt " , " album_user_rating.lte " ,
" album_critic_rating.gt " , " album_critic_rating.gte " , " album_critic_rating.lt " , " album_critic_rating.lte " ,
" album_record_label " , " album_record_label.not " , " album_record_label.is " , " album_record_label.isnot " , " album_record_label.begins " , " album_record_label.ends " ,
" album_mood " , " album_mood.not " ,
" album_style " , " album_style.not " ,
" album_format " , " album_format.not " ,
" album_type " , " album_type.not " ,
" album_collection " , " album_collection.not " ,
" album_added " , " album_added.not " , " album_added.before " , " album_added.after " ,
" album_released " , " album_released.not " , " album_released.before " , " album_released.after " ,
" album_unmatched " ,
" album_source " , " album_source.not " ,
" album_label " , " album_label.not " ,
" track_mood " , " track_mood.not " ,
" track_title " , " track_title.not " , " track_title.is " , " track_title.isnot " , " track_title.begins " , " track_title.ends " ,
" track_plays.gt " , " track_plays.gte " , " track_plays.lt " , " track_plays.lte " ,
" track_last_played " , " track_last_played.not " , " track_last_played.before " , " track_last_played.after " ,
" track_skips.gt " , " track_skips.gte " , " track_skips.lt " , " track_skips.lte " ,
" track_last_skipped " , " track_last_skipped.not " , " track_last_skipped.before " , " track_last_skipped.after " ,
" track_user_rating.gt " , " track_user_rating.gte " , " track_user_rating.lt " , " track_user_rating.lte " ,
" track_last_rated " , " track_last_rated.not " , " track_last_rated.before " , " track_last_rated.after " ,
" track_added " , " track_added.not " , " track_added.before " , " track_added.after " ,
" track_trash " ,
" track_source " , " track_source.not "
]
2021-03-30 05:50:53 +00:00
searches = [
2021-09-13 02:42:33 +00:00
" title " , " title.not " , " title.is " , " title.isnot " , " title.begins " , " title.ends " ,
" studio " , " studio.not " , " studio.is " , " studio.isnot " , " studio.begins " , " studio.ends " ,
2021-05-29 02:44:01 +00:00
" actor " , " actor.not " ,
" audio_language " , " audio_language.not " ,
" collection " , " collection.not " ,
" content_rating " , " content_rating.not " ,
" country " , " country.not " ,
" director " , " director.not " ,
" genre " , " genre.not " ,
" label " , " label.not " ,
" network " , " network.not " ,
" producer " , " producer.not " ,
" subtitle_language " , " subtitle_language.not " ,
" writer " , " writer.not " ,
2021-05-27 20:09:39 +00:00
" decade " , " resolution " , " hdr " , " unmatched " , " duplicate " , " unplayed " , " progress " , " trash " ,
" last_played " , " last_played.not " , " last_played.before " , " last_played.after " ,
2021-05-05 18:01:41 +00:00
" added " , " added.not " , " added.before " , " added.after " ,
2021-05-27 20:09:39 +00:00
" release " , " release.not " , " release.before " , " release.after " ,
2021-05-05 18:01:41 +00:00
" duration.gt " , " duration.gte " , " duration.lt " , " duration.lte " ,
2021-05-27 17:40:35 +00:00
" plays.gt " , " plays.gte " , " plays.lt " , " plays.lte " ,
2021-05-05 18:01:41 +00:00
" user_rating.gt " , " user_rating.gte " , " user_rating.lt " , " user_rating.lte " ,
" critic_rating.gt " , " critic_rating.gte " , " critic_rating.lt " , " critic_rating.lte " ,
2021-05-08 00:40:07 +00:00
" audience_rating.gt " , " audience_rating.gte " , " audience_rating.lt " , " audience_rating.lte " ,
2021-05-27 17:40:35 +00:00
" year " , " year.not " , " year.gt " , " year.gte " , " year.lt " , " year.lte " ,
2021-05-27 20:09:39 +00:00
" unplayed_episodes " , " episode_unplayed " , " episode_duplicate " , " episode_progress " , " episode_unmatched " ,
2021-09-13 02:42:33 +00:00
" episode_title " , " episode_title.not " , " episode_title.is " , " episode_title.isnot " , " episode_title.begins " , " episode_title.ends " ,
2021-05-27 17:40:35 +00:00
" episode_added " , " episode_added.not " , " episode_added.before " , " episode_added.after " ,
2021-05-27 20:09:39 +00:00
" episode_air_date " , " episode_air_date.not " , " episode_air_date.before " , " episode_air_date.after " ,
" episode_last_played " , " episode_last_played.not " , " episode_last_played.before " , " episode_last_played.after " ,
2021-05-27 17:40:35 +00:00
" episode_plays.gt " , " episode_plays.gte " , " episode_plays.lt " , " episode_plays.lte " ,
" episode_user_rating.gt " , " episode_user_rating.gte " , " episode_user_rating.lt " , " episode_user_rating.lte " ,
" episode_year " , " episode_year.not " , " episode_year.gt " , " episode_year.gte " , " episode_year.lt " , " episode_year.lte "
2021-12-29 15:51:22 +00:00
] + music_searches
2021-05-30 02:19:38 +00:00
and_searches = [
" title.and " , " studio.and " , " actor.and " , " audio_language.and " , " collection.and " ,
" content_rating.and " , " country.and " , " director.and " , " genre.and " , " label.and " ,
" network.and " , " producer.and " , " subtitle_language.and " , " writer.and "
]
or_searches = [
" title " , " studio " , " actor " , " audio_language " , " collection " , " content_rating " ,
" country " , " director " , " genre " , " label " , " network " , " producer " , " subtitle_language " ,
" writer " , " decade " , " resolution " , " year " , " episode_title " , " episode_year "
]
2021-03-30 05:50:53 +00:00
movie_only_searches = [
2021-07-23 19:44:21 +00:00
" country " , " country.not " , " director " , " director.not " , " producer " , " producer.not " , " writer " , " writer.not " ,
2021-08-23 13:30:40 +00:00
" decade " , " duplicate " , " unplayed " , " progress " ,
2021-08-22 21:43:26 +00:00
" duration.gt " , " duration.gte " , " duration.lt " , " duration.lte "
2021-03-30 05:50:53 +00:00
]
2021-04-01 15:34:02 +00:00
show_only_searches = [
2021-05-29 02:44:01 +00:00
" network " , " network.not " ,
2021-09-13 02:42:33 +00:00
" episode_title " , " episode_title.not " , " episode_title.is " , " episode_title.isnot " , " episode_title.begins " , " episode_title.ends " ,
2021-05-27 17:40:35 +00:00
" episode_added " , " episode_added.not " , " episode_added.before " , " episode_added.after " ,
" episode_air_date " , " episode_air_date.not " ,
" episode_air_date.before " , " episode_air_date.after " ,
2021-08-23 13:30:40 +00:00
" episode_last_played " , " episode_last_played.not " , " episode_last_played.before " , " episode_last_played.after " ,
2021-05-27 17:40:35 +00:00
" episode_plays.gt " , " episode_plays.gte " , " episode_plays.lt " , " episode_plays.lte " ,
" episode_user_rating.gt " , " episode_user_rating.gte " , " episode_user_rating.lt " , " episode_user_rating.lte " ,
2021-08-23 13:30:40 +00:00
" episode_year " , " episode_year.not " , " episode_year.gt " , " episode_year.gte " , " episode_year.lt " , " episode_year.lte " ,
" unplayed_episodes " , " episode_unplayed " , " episode_duplicate " , " episode_progress " , " episode_unmatched " ,
2021-04-01 15:34:02 +00:00
]
2021-12-29 15:51:22 +00:00
string_attributes = [ " title " , " studio " , " episode_title " , " artist_title " , " album_title " , " album_record_label " , " track_title " ]
float_attributes = [
2022-01-22 06:46:52 +00:00
" user_rating " , " episode_user_rating " , " critic_rating " , " audience_rating " , " duration " ,
2021-12-29 15:51:22 +00:00
" artist_user_rating " , " album_user_rating " , " album_critic_rating " , " track_user_rating "
]
2021-05-28 23:18:28 +00:00
boolean_attributes = [
2021-12-29 15:51:22 +00:00
" hdr " , " unmatched " , " duplicate " , " unplayed " , " progress " , " trash " , " unplayed_episodes " , " episode_unplayed " ,
" episode_duplicate " , " episode_progress " , " episode_unmatched " , " artist_unmatched " , " album_unmatched " , " track_trash "
2021-05-27 20:09:39 +00:00
]
2021-05-28 23:18:28 +00:00
tmdb_attributes = [ " actor " , " director " , " producer " , " writer " ]
2021-12-29 15:51:22 +00:00
date_attributes = [
" added " , " episode_added " , " release " , " episode_air_date " , " last_played " , " episode_last_played " ,
" first_episode_aired " , " last_episode_aired " , " artist_added " , " artist_last_played " , " album_last_played " ,
" album_added " , " album_released " , " track_last_played " , " track_last_skipped " , " track_last_rated " , " track_added "
]
year_attributes = [ " decade " , " year " , " episode_year " , " album_year " , " album_decade " ]
2022-01-22 06:46:52 +00:00
number_attributes = [ " plays " , " episode_plays " , " tmdb_vote_count " , " album_plays " , " track_plays " , " track_skips " ] + year_attributes
2021-07-23 19:44:21 +00:00
search_display = { " added " : " Date Added " , " release " : " Release Date " , " hdr " : " HDR " , " progress " : " In Progress " , " episode_progress " : " Episode In Progress " }
2021-12-29 15:51:22 +00:00
tag_attributes = [
" actor " , " audio_language " , " collection " , " content_rating " , " country " , " director " , " genre " , " label " , " network " ,
2021-12-30 18:59:54 +00:00
" producer " , " resolution " , " studio " , " subtitle_language " , " writer " , " artist_genre " , " artist_collection " ,
2021-12-29 15:51:22 +00:00
" artist_country " , " artist_mood " , " artist_style " , " album_genre " , " album_mood " , " album_style " , " album_format " ,
" album_type " , " album_collection " , " album_source " , " album_label " , " track_mood " , " track_source "
2021-05-05 18:01:41 +00:00
]
2021-05-29 02:33:55 +00:00
movie_sorts = {
2021-05-05 18:01:41 +00:00
" title.asc " : " titleSort " , " title.desc " : " titleSort % 3Adesc " ,
" year.asc " : " year " , " year.desc " : " year % 3Adesc " ,
" originally_available.asc " : " originallyAvailableAt " , " originally_available.desc " : " originallyAvailableAt % 3Adesc " ,
2021-05-27 17:40:35 +00:00
" release.asc " : " originallyAvailableAt " , " release.desc " : " originallyAvailableAt % 3Adesc " ,
2021-05-05 18:01:41 +00:00
" critic_rating.asc " : " rating " , " critic_rating.desc " : " rating % 3Adesc " ,
" audience_rating.asc " : " audienceRating " , " audience_rating.desc " : " audienceRating % 3Adesc " ,
" user_rating.asc " : " userRating " , " user_rating.desc " : " userRating % 3Adesc " ,
" content_rating.asc " : " contentRating " , " content_rating.desc " : " contentRating % 3Adesc " ,
" duration.asc " : " duration " , " duration.desc " : " duration % 3Adesc " ,
2021-12-29 15:51:22 +00:00
" progress.asc " : " viewOffset " , " progress.desc " : " viewOffset % 3Adesc " ,
2021-05-05 18:01:41 +00:00
" plays.asc " : " viewCount " , " plays.desc " : " viewCount % 3Adesc " ,
" added.asc " : " addedAt " , " added.desc " : " addedAt % 3Adesc " ,
2021-12-29 15:51:22 +00:00
" viewed.asc " : " lastViewedAt " , " viewed.desc " : " lastViewedAt % 3Adesc " ,
" resolution.asc " : " mediaHeight " , " resolution.desc " : " mediaHeight % 3Adesc " ,
" bitrate.asc " : " mediaBitrate " , " bitrate.desc " : " mediaBitrate % 3Adesc " ,
2021-05-05 18:01:41 +00:00
" random " : " random "
}
2021-05-29 02:33:55 +00:00
show_sorts = {
2021-05-05 18:01:41 +00:00
" title.asc " : " titleSort " , " title.desc " : " titleSort % 3Adesc " ,
" year.asc " : " year " , " year.desc " : " year % 3Adesc " ,
" originally_available.asc " : " originallyAvailableAt " , " originally_available.desc " : " originallyAvailableAt % 3Adesc " ,
2021-05-27 17:40:35 +00:00
" release.asc " : " originallyAvailableAt " , " release.desc " : " originallyAvailableAt % 3Adesc " ,
2021-05-05 18:01:41 +00:00
" critic_rating.asc " : " rating " , " critic_rating.desc " : " rating % 3Adesc " ,
" audience_rating.asc " : " audienceRating " , " audience_rating.desc " : " audienceRating % 3Adesc " ,
" user_rating.asc " : " userRating " , " user_rating.desc " : " userRating % 3Adesc " ,
" content_rating.asc " : " contentRating " , " content_rating.desc " : " contentRating % 3Adesc " ,
2021-12-29 15:51:22 +00:00
" unplayed.asc " : " unviewedLeafCount " , " unplayed.desc " : " unviewedLeafCount % 3Adesc " ,
2021-05-05 18:01:41 +00:00
" episode_added.asc " : " episode.addedAt " , " episode_added.desc " : " episode.addedAt % 3Adesc " ,
2021-12-29 15:51:22 +00:00
" added.asc " : " addedAt " , " added.desc " : " addedAt % 3Adesc " ,
" viewed.asc " : " lastViewedAt " , " viewed.desc " : " lastViewedAt % 3Adesc " ,
2021-05-05 18:01:41 +00:00
" random " : " random "
}
2021-05-29 02:33:55 +00:00
season_sorts = {
2021-05-05 18:01:41 +00:00
" season.asc " : " season.index % 2Cseason.titleSort " , " season.desc " : " season.index % 3Adesc % 2Cseason.titleSort " ,
" show.asc " : " show.titleSort % 2Cindex " , " show.desc " : " show.titleSort % 3Adesc % 2Cindex " ,
" user_rating.asc " : " userRating " , " user_rating.desc " : " userRating % 3Adesc " ,
" added.asc " : " addedAt " , " added.desc " : " addedAt % 3Adesc " ,
" random " : " random "
}
2021-05-29 02:33:55 +00:00
episode_sorts = {
2021-05-05 18:01:41 +00:00
" title.asc " : " titleSort " , " title.desc " : " titleSort % 3Adesc " ,
" show.asc " : " show.titleSort % 2Cseason.index % 3AnullsLast % 2Cepisode.index % 3AnullsLast % 2Cepisode.originallyAvailableAt % 3AnullsLast % 2Cepisode.titleSort % 2Cepisode.id " ,
" show.desc " : " show.titleSort % 3Adesc % 2Cseason.index % 3AnullsLast % 2Cepisode.index % 3AnullsLast % 2Cepisode.originallyAvailableAt % 3AnullsLast % 2Cepisode.titleSort % 2Cepisode.id " ,
" year.asc " : " year " , " year.desc " : " year % 3Adesc " ,
" originally_available.asc " : " originallyAvailableAt " , " originally_available.desc " : " originallyAvailableAt % 3Adesc " ,
2021-05-27 17:40:35 +00:00
" release.asc " : " originallyAvailableAt " , " release.desc " : " originallyAvailableAt % 3Adesc " ,
2021-05-05 18:01:41 +00:00
" critic_rating.asc " : " rating " , " critic_rating.desc " : " rating % 3Adesc " ,
" audience_rating.asc " : " audienceRating " , " audience_rating.desc " : " audienceRating % 3Adesc " ,
" user_rating.asc " : " userRating " , " user_rating.desc " : " userRating % 3Adesc " ,
" duration.asc " : " duration " , " duration.desc " : " duration % 3Adesc " ,
2021-12-29 15:51:22 +00:00
" progress.asc " : " viewOffset " , " progress.desc " : " viewOffset % 3Adesc " ,
2021-05-05 18:01:41 +00:00
" plays.asc " : " viewCount " , " plays.desc " : " viewCount % 3Adesc " ,
" added.asc " : " addedAt " , " added.desc " : " addedAt % 3Adesc " ,
2021-12-29 15:51:22 +00:00
" viewed.asc " : " lastViewedAt " , " viewed.desc " : " lastViewedAt % 3Adesc " ,
" resolution.asc " : " mediaHeight " , " resolution.desc " : " mediaHeight % 3Adesc " ,
" bitrate.asc " : " mediaBitrate " , " bitrate.desc " : " mediaBitrate % 3Adesc " ,
" random " : " random "
}
artist_sorts = {
" title.asc " : " titleSort " , " title.desc " : " titleSort % 3Adesc " ,
" user_rating.asc " : " userRating " , " user_rating.desc " : " userRating % 3Adesc " ,
" added.asc " : " addedAt " , " added.desc " : " addedAt % 3Adesc " ,
" played.asc " : " lastViewedAt " , " played.desc " : " lastViewedAt % 3Adesc " ,
" plays.asc " : " viewCount " , " plays.desc " : " viewCount % 3Adesc " ,
2021-05-05 18:01:41 +00:00
" random " : " random "
}
2021-12-29 15:51:22 +00:00
album_sorts = {
" title.asc " : " titleSort " , " title.desc " : " titleSort % 3Adesc " ,
" album_artist.asc " : " artist.titleSort % 2Calbum.titleSort % 2Calbum.index % 2Calbum.id % 2Calbum.originallyAvailableAt " ,
" album_artist.desc " : " artist.titleSort % 3Adesc % 2Calbum.titleSort % 2Calbum.index % 2Calbum.id % 2Calbum.originallyAvailableAt " ,
2022-01-11 01:23:56 +00:00
" year.asc " : " year " , " year.desc " : " year % 3Adesc " ,
2021-12-29 15:51:22 +00:00
" originally_available.asc " : " originallyAvailableAt " , " originally_available.desc " : " originallyAvailableAt % 3Adesc " ,
" release.asc " : " originallyAvailableAt " , " release.desc " : " originallyAvailableAt % 3Adesc " ,
" critic_rating.asc " : " rating " , " critic_rating.desc " : " rating % 3Adesc " ,
" user_rating.asc " : " userRating " , " user_rating.desc " : " userRating % 3Adesc " ,
" added.asc " : " addedAt " , " added.desc " : " addedAt % 3Adesc " ,
" played.asc " : " lastViewedAt " , " played.desc " : " lastViewedAt % 3Adesc " ,
" plays.asc " : " viewCount " , " plays.desc " : " viewCount % 3Adesc " ,
" random " : " random "
}
track_sorts = {
" title.asc " : " titleSort " , " title.desc " : " titleSort % 3Adesc " ,
" album_artist.asc " : " artist.titleSort % 2Calbum.titleSort % 2Calbum.year % 2Ctrack.absoluteIndex % 2Ctrack.index % 2Ctrack.titleSort % 2Ctrack.id " ,
" album_artist.desc " : " artist.titleSort % 3Adesc % 2Calbum.titleSort % 2Calbum.year % 2Ctrack.absoluteIndex % 2Ctrack.index % 2Ctrack.titleSort % 2Ctrack.id " ,
" artist.asc " : " originalTitle " , " artist.desc " : " originalTitle % 3Adesc " ,
" album.asc " : " album.titleSort " , " album.desc " : " album.titleSort % 3Adesc " ,
" user_rating.asc " : " userRating " , " user_rating.desc " : " userRating % 3Adesc " ,
" duration.asc " : " duration " , " duration.desc " : " duration % 3Adesc " ,
" plays.asc " : " viewCount " , " plays.desc " : " viewCount % 3Adesc " ,
" added.asc " : " addedAt " , " added.desc " : " addedAt % 3Adesc " ,
" played.asc " : " lastViewedAt " , " played.desc " : " lastViewedAt % 3Adesc " ,
" rated.asc " : " lastRatedAt " , " rated.desc " : " lastRatedAt % 3Adesc " ,
" popularity.asc " : " ratingCount " , " popularity.desc " : " ratingCount % 3Adesc " ,
" bitrate.asc " : " mediaBitrate " , " bitrate.desc " : " mediaBitrate % 3Adesc " ,
" random " : " random "
}
sort_types = {
" movies " : ( 1 , movie_sorts ) ,
" shows " : ( 2 , show_sorts ) ,
" seasons " : ( 3 , season_sorts ) ,
" episodes " : ( 4 , episode_sorts ) ,
" artists " : ( 8 , artist_sorts ) ,
" albums " : ( 9 , album_sorts ) ,
" tracks " : ( 10 , track_sorts )
}
2021-03-30 05:50:53 +00:00
2021-09-21 03:57:16 +00:00
class Plex ( Library ) :
2021-06-02 15:18:37 +00:00
def __init__ ( self , config , params ) :
2021-09-21 03:57:16 +00:00
super ( ) . __init__ ( config , params )
2021-07-14 14:47:20 +00:00
self . plex = params [ " plex " ]
self . url = params [ " plex " ] [ " url " ]
self . token = params [ " plex " ] [ " token " ]
self . timeout = params [ " plex " ] [ " timeout " ]
2021-11-22 20:47:26 +00:00
logger . info ( " " )
2021-03-26 05:43:11 +00:00
try :
2021-07-14 14:47:20 +00:00
self . PlexServer = PlexServer ( baseurl = self . url , token = self . token , session = self . config . session , timeout = self . timeout )
2021-03-26 05:43:11 +00:00
except Unauthorized :
raise Failed ( " Plex Error: Plex token is invalid " )
except ValueError as e :
raise Failed ( f " Plex Error: { e } " )
2021-06-22 20:28:12 +00:00
except ( requests . exceptions . ConnectionError , ParseError ) :
2021-01-20 21:37:59 +00:00
util . print_stacktrace ( )
raise Failed ( " Plex Error: Plex url is invalid " )
2021-11-22 20:47:26 +00:00
self . Plex = None
library_names = [ ]
for s in self . PlexServer . library . sections ( ) :
library_names . append ( s . title )
if s . title == params [ " name " ] :
self . Plex = s
break
2021-03-26 05:43:11 +00:00
if not self . Plex :
2021-12-30 16:32:18 +00:00
raise Failed ( f " Plex Error: Plex Library ' { params [ ' name ' ] } ' not found. Options: { library_names } " )
2021-12-29 15:51:22 +00:00
if self . Plex . type in [ " movie " , " show " , " artist " ] :
2021-08-24 04:33:04 +00:00
self . type = self . Plex . type . capitalize ( )
else :
2021-04-21 18:02:01 +00:00
raise Failed ( f " Plex Error: Plex Library must be a Movies or TV Shows library " )
2021-03-31 04:20:20 +00:00
2021-12-23 17:02:07 +00:00
self . _users = [ ]
2021-04-21 12:49:27 +00:00
self . agent = self . Plex . agent
2021-08-24 04:33:04 +00:00
self . is_movie = self . type == " Movie "
self . is_show = self . type == " Show "
2021-12-29 15:51:22 +00:00
self . is_music = self . type == " Artist "
2021-08-24 04:33:04 +00:00
self . is_other = self . agent == " com.plexapp.agents.none "
if self . is_other :
self . type = " Video "
2021-12-06 07:52:08 +00:00
if self . tmdb_collections and self . is_show :
self . tmdb_collections = None
logger . error ( " Config Error: tmdb_collections only work with Movie Libraries. " )
2021-01-20 21:37:59 +00:00
2021-12-17 14:24:46 +00:00
def notify ( self , text , collection = None , critical = True ) :
self . config . notify ( text , server = self . PlexServer . friendlyName , library = self . name , collection = collection , critical = critical )
2021-12-08 03:31:20 +00:00
def set_server_preroll ( self , preroll ) :
self . PlexServer . settings . get ( ' cinemaTrailersPrerollID ' ) . set ( preroll )
self . PlexServer . settings . save ( )
2021-04-08 20:10:26 +00:00
def get_all_collections ( self ) :
return self . search ( libtype = " collection " )
2021-05-11 01:22:18 +00:00
@retry ( stop_max_attempt_number = 6 , wait_fixed = 10000 , retry_on_exception = util . retry_if_not_plex )
2021-04-08 20:10:26 +00:00
def search ( self , title = None , libtype = None , sort = None , maxresults = None , * * kwargs ) :
return self . Plex . search ( title = title , sort = sort , maxresults = maxresults , libtype = libtype , * * kwargs )
2021-01-20 21:37:59 +00:00
2021-05-11 01:22:18 +00:00
@retry ( stop_max_attempt_number = 6 , wait_fixed = 10000 , retry_on_exception = util . retry_if_not_plex )
2021-05-12 14:25:48 +00:00
def exact_search ( self , title , libtype = None , year = None ) :
if year :
terms = { " title= " : title , " year " : year }
else :
terms = { " title= " : title }
return self . Plex . search ( libtype = libtype , * * terms )
2021-05-11 01:22:18 +00:00
@retry ( stop_max_attempt_number = 6 , wait_fixed = 10000 , retry_on_exception = util . retry_if_not_plex )
2021-05-02 04:22:48 +00:00
def get_labeled_items ( self , label ) :
return self . Plex . search ( label = label )
2021-05-11 01:22:18 +00:00
@retry ( stop_max_attempt_number = 6 , wait_fixed = 10000 , retry_on_exception = util . retry_if_not_plex )
2021-01-20 21:37:59 +00:00
def fetchItem ( self , data ) :
return self . PlexServer . fetchItem ( data )
2022-01-11 01:23:56 +00:00
def get_all ( self , collection_level = None ) :
collection_type = collection_level if collection_level else self . Plex . TYPE
if not collection_level :
collection_level = self . type
logger . info ( f " Loading All { collection_level . capitalize ( ) } s from Library: { self . name } " )
key = f " /library/sections/ { self . Plex . key } /all?includeGuids=1&type= { utils . searchType ( collection_type ) } "
2021-06-02 17:50:11 +00:00
container_start = 0
container_size = plexapi . X_PLEX_CONTAINER_SIZE
results = [ ]
while self . Plex . _totalViewSize is None or container_start < = self . Plex . _totalViewSize :
results . extend ( self . fetchItems ( key , container_start , container_size ) )
util . print_return ( f " Loaded: { container_start } / { self . Plex . _totalViewSize } " )
container_start + = container_size
2022-01-11 01:23:56 +00:00
logger . info ( util . adjust_space ( f " Loaded { self . Plex . _totalViewSize } { collection_level . capitalize ( ) } s " ) )
2021-06-02 17:50:11 +00:00
return results
2021-12-17 14:24:46 +00:00
@retry ( stop_max_attempt_number = 6 , wait_fixed = 10000 , retry_on_exception = util . retry_if_not_plex )
def create_playlist ( self , name , items ) :
return self . PlexServer . createPlaylist ( name , items = items )
2021-06-02 17:50:11 +00:00
@retry ( stop_max_attempt_number = 6 , wait_fixed = 10000 , retry_on_exception = util . retry_if_not_plex )
def fetchItems ( self , key , container_start , container_size ) :
return self . Plex . fetchItems ( key , container_start = container_start , container_size = container_size )
2021-04-08 20:10:26 +00:00
2021-12-17 14:24:46 +00:00
@retry ( stop_max_attempt_number = 6 , wait_fixed = 10000 , retry_on_exception = util . retry_if_not_plex )
def moveItem ( self , obj , item , after ) :
obj . moveItem ( item , after = after )
2021-05-11 01:22:18 +00:00
@retry ( stop_max_attempt_number = 6 , wait_fixed = 10000 , retry_on_exception = util . retry_if_not_plex )
2021-05-02 04:22:48 +00:00
def query ( self , method ) :
return method ( )
2021-05-11 01:22:18 +00:00
@retry ( stop_max_attempt_number = 6 , wait_fixed = 10000 , retry_on_exception = util . retry_if_not_plex )
2021-05-02 04:22:48 +00:00
def query_data ( self , method , data ) :
return method ( data )
2021-09-30 14:22:03 +00:00
@retry ( stop_max_attempt_number = 6 , wait_fixed = 10000 , retry_on_exception = util . retry_if_not_failed )
2021-09-30 20:55:29 +00:00
def query_collection ( self , item , collection , locked = True , add = True ) :
if add :
item . addCollection ( collection , locked = locked )
else :
item . removeCollection ( collection , locked = locked )
2021-09-30 14:22:03 +00:00
2021-05-11 01:22:18 +00:00
@retry ( stop_max_attempt_number = 6 , wait_fixed = 10000 , retry_on_exception = util . retry_if_not_plex )
2021-05-02 04:22:48 +00:00
def collection_mode_query ( self , collection , data ) :
2021-12-19 21:02:10 +00:00
if int ( collection . collectionMode ) not in collection_mode_keys or collection_mode_keys [ int ( collection . collectionMode ) ] != data :
collection . modeUpdate ( mode = data )
logger . info ( f " Detail: collection_order updated Collection Order to { data } " )
2021-05-02 04:22:48 +00:00
2021-05-11 01:22:18 +00:00
@retry ( stop_max_attempt_number = 6 , wait_fixed = 10000 , retry_on_exception = util . retry_if_not_plex )
2021-05-02 04:22:48 +00:00
def collection_order_query ( self , collection , data ) :
collection . sortUpdate ( sort = data )
2021-06-02 18:41:57 +00:00
@retry ( stop_max_attempt_number = 6 , wait_fixed = 10000 , retry_on_exception = util . retry_if_not_plex )
def reload ( self , item ) :
2021-08-12 20:36:38 +00:00
try :
item . reload ( checkFiles = False , includeAllConcerts = False , includeBandwidths = False , includeChapters = False ,
includeChildren = False , includeConcerts = False , includeExternalMedia = False , includeExtras = False ,
includeFields = False , includeGeolocation = False , includeLoudnessRamps = False , includeMarkers = False ,
includeOnDeck = False , includePopularLeaves = False , includeRelated = False ,
includeRelatedCount = 0 , includeReviews = False , includeStations = False )
except ( BadRequest , NotFound ) as e :
util . print_stacktrace ( )
raise Failed ( f " Item Failed to Load: { e } " )
2021-05-08 00:40:07 +00:00
2021-05-11 01:22:18 +00:00
@retry ( stop_max_attempt_number = 6 , wait_fixed = 10000 , retry_on_exception = util . retry_if_not_plex )
2021-05-07 15:37:09 +00:00
def edit_query ( self , item , edits , advanced = False ) :
if advanced :
2021-05-03 04:10:12 +00:00
item . editAdvanced ( * * edits )
else :
item . edit ( * * edits )
2021-06-02 18:41:57 +00:00
self . reload ( item )
2021-05-02 04:22:48 +00:00
2021-05-11 01:22:18 +00:00
@retry ( stop_max_attempt_number = 6 , wait_fixed = 10000 , retry_on_exception = util . retry_if_not_plex )
2021-06-12 15:29:17 +00:00
def _upload_image ( self , item , image ) :
2021-09-21 03:57:16 +00:00
try :
if image . is_poster and image . is_url :
item . uploadPoster ( url = image . location )
elif image . is_poster :
item . uploadPoster ( filepath = image . location )
elif image . is_url :
item . uploadArt ( url = image . location )
else :
item . uploadArt ( filepath = image . location )
self . reload ( item )
except BadRequest as e :
2022-01-11 01:23:56 +00:00
item . refresh ( )
2021-09-21 03:57:16 +00:00
raise Failed ( e )
2021-04-08 20:10:26 +00:00
2021-06-30 03:08:38 +00:00
@retry ( stop_max_attempt_number = 6 , wait_fixed = 10000 , retry_on_exception = util . retry_if_not_plex )
2021-07-14 14:47:20 +00:00
def upload_file_poster ( self , item , image ) :
2021-06-30 03:08:38 +00:00
item . uploadPoster ( filepath = image )
self . reload ( item )
2021-12-31 04:51:46 +00:00
@retry ( stop_max_attempt_number = 6 , wait_fixed = 10000 , retry_on_exception = util . retry_if_not_plex )
def get_genres ( self ) :
return [ genre . title for genre in self . Plex . listFilterChoices ( " genre " ) ]
2021-04-08 20:10:26 +00:00
@retry ( stop_max_attempt_number = 6 , wait_fixed = 10000 , retry_on_exception = util . retry_if_not_failed )
2021-05-05 18:01:41 +00:00
def get_search_choices ( self , search_name , title = True ) :
2021-06-16 03:20:45 +00:00
final_search = search_translation [ search_name ] if search_name in search_translation else search_name
2021-07-26 18:52:32 +00:00
final_search = show_translation [ final_search ] if self . is_show and final_search in show_translation else final_search
2021-04-01 15:34:02 +00:00
try :
2021-12-17 00:16:08 +00:00
names = [ ]
2021-04-18 15:36:43 +00:00
choices = { }
2022-01-14 17:57:48 +00:00
use_title = title and final_search not in [ " contentRating " , " audioLanguage " , " subtitleLanguage " , " resolution " ]
2021-06-16 03:20:45 +00:00
for choice in self . Plex . listFilterChoices ( final_search ) :
2021-12-18 23:02:22 +00:00
if choice . title not in names :
names . append ( choice . title )
if choice . key not in names :
names . append ( choice . key )
2022-01-14 17:57:48 +00:00
choices [ choice . title ] = choice . title if use_title else choice . key
choices [ choice . key ] = choice . title if use_title else choice . key
choices [ choice . title . lower ( ) ] = choice . title if use_title else choice . key
choices [ choice . key . lower ( ) ] = choice . title if use_title else choice . key
2021-12-17 00:16:08 +00:00
return choices , names
2021-04-01 15:34:02 +00:00
except NotFound :
2021-07-26 18:52:32 +00:00
logger . debug ( f " Search Attribute: { final_search } " )
2021-12-13 07:30:19 +00:00
raise Failed ( f " Plex Error: plex_search attribute: { search_name } not supported " )
2021-03-26 05:43:11 +00:00
2021-05-11 01:22:18 +00:00
@retry ( stop_max_attempt_number = 6 , wait_fixed = 10000 , retry_on_exception = util . retry_if_not_plex )
2021-05-02 04:22:48 +00:00
def get_labels ( self ) :
return { label . title : label . key for label in self . Plex . listFilterChoices ( field = " label " ) }
2021-05-11 01:22:18 +00:00
@retry ( stop_max_attempt_number = 6 , wait_fixed = 10000 , retry_on_exception = util . retry_if_not_plex )
2021-05-02 04:22:48 +00:00
def _query ( self , key , post = False , put = False ) :
if post : method = self . Plex . _server . _session . post
elif put : method = self . Plex . _server . _session . put
else : method = None
2021-06-30 15:07:02 +00:00
return self . Plex . _server . query ( key , method = method )
2021-05-02 04:22:48 +00:00
2021-12-23 17:02:07 +00:00
@property
def users ( self ) :
if not self . _users :
users = [ ]
for user in self . PlexServer . myPlexAccount ( ) . users ( ) :
if self . PlexServer . machineIdentifier in [ s . machineIdentifier for s in user . servers ] :
users . append ( user . title )
self . _users = users
return self . _users
2021-09-30 20:55:29 +00:00
def alter_collection ( self , item , collection , smart_label_collection = False , add = True ) :
2021-09-30 14:22:03 +00:00
if smart_label_collection :
2021-09-30 20:55:29 +00:00
self . query_data ( item . addLabel if add else item . removeLabel , collection )
2021-09-30 14:22:03 +00:00
else :
locked = True
if self . agent in [ " tv.plex.agents.movie " , " tv.plex.agents.series " ] :
field = next ( ( f for f in item . fields if f . name == " collection " ) , None )
locked = field is not None
2021-09-30 20:55:29 +00:00
self . query_collection ( item , collection , locked = locked , add = add )
2021-09-30 14:22:03 +00:00
2021-08-02 14:24:13 +00:00
def move_item ( self , collection , item , after = None ) :
2021-07-29 13:36:30 +00:00
key = f " { collection . key } /items/ { item } /move "
if after :
key + = f " ?after= { after } "
self . _query ( key , put = True )
2021-05-17 14:35:47 +00:00
def smart_label_url ( self , title , sort ) :
2021-05-02 04:22:48 +00:00
labels = self . get_labels ( )
if title not in labels :
raise Failed ( f " Plex Error: Label: { title } does not exist " )
2021-05-05 18:01:41 +00:00
smart_type = 1 if self . is_movie else 2
2021-05-29 02:33:55 +00:00
sort_type = movie_sorts [ sort ] if self . is_movie else show_sorts [ sort ]
2021-05-17 14:35:47 +00:00
return smart_type , f " ?type= { smart_type } &sort= { sort_type } &label= { labels [ title ] } "
2021-05-02 04:22:48 +00:00
2021-05-23 02:42:00 +00:00
def test_smart_filter ( self , uri_args ) :
logger . debug ( f " Smart Collection Test: { uri_args } " )
test_items = self . get_filter_items ( uri_args )
if len ( test_items ) < 1 :
raise Failed ( f " Plex Error: No items for smart filter: { uri_args } " )
2021-05-05 18:01:41 +00:00
def create_smart_collection ( self , title , smart_type , uri_args ) :
2021-05-23 02:42:00 +00:00
self . test_smart_filter ( uri_args )
2021-05-02 04:22:48 +00:00
args = {
2021-05-05 18:01:41 +00:00
" type " : smart_type ,
2021-05-02 04:22:48 +00:00
" title " : title ,
" smart " : 1 ,
" sectionId " : self . Plex . key ,
" uri " : self . build_smart_filter ( uri_args )
}
self . _query ( f " /library/collections { utils . joinArgs ( args ) } " , post = True )
def get_smart_filter_from_uri ( self , uri ) :
smart_filter = parse . parse_qs ( parse . urlparse ( uri . replace ( " /#!/ " , " / " ) ) . query ) [ " key " ] [ 0 ]
2021-05-05 18:01:41 +00:00
args = smart_filter [ smart_filter . index ( " ? " ) : ]
return self . build_smart_filter ( args ) , int ( args [ args . index ( " type= " ) + 5 : args . index ( " type= " ) + 6 ] )
2021-05-02 04:22:48 +00:00
def build_smart_filter ( self , uri_args ) :
return f " server:// { self . PlexServer . machineIdentifier } /com.plexapp.plugins.library/library/sections/ { self . Plex . key } /all { uri_args } "
def update_smart_collection ( self , collection , uri_args ) :
2021-05-23 02:42:00 +00:00
self . test_smart_filter ( uri_args )
2021-05-02 04:22:48 +00:00
self . _query ( f " /library/collections/ { collection . ratingKey } /items { utils . joinArgs ( { ' uri ' : self . build_smart_filter ( uri_args ) } ) } " , put = True )
def smart_filter ( self , collection ) :
2021-06-16 18:20:48 +00:00
smart_filter = self . get_collection ( collection ) . content
2021-05-02 04:22:48 +00:00
return smart_filter [ smart_filter . index ( " ? " ) : ]
2021-04-21 18:02:01 +00:00
2021-06-30 15:07:02 +00:00
def collection_visibility ( self , collection ) :
try :
attrs = self . _query ( f " /hubs/sections/ { self . Plex . key } /manage?metadataItemId= { collection . ratingKey } " ) [ 0 ] . attrib
return {
" library " : utils . cast ( bool , attrs . get ( " promotedToRecommended " , " 0 " ) ) ,
" home " : utils . cast ( bool , attrs . get ( " promotedToOwnHome " , " 0 " ) ) ,
" shared " : utils . cast ( bool , attrs . get ( " promotedToSharedHome " , " 0 " ) )
}
except IndexError :
return { " library " : False , " home " : False , " shared " : False }
def collection_visibility_update ( self , collection , visibility = None , library = None , home = None , shared = None ) :
if visibility is None :
visibility = self . collection_visibility ( collection )
key = f " /hubs/sections/ { self . Plex . key } /manage?metadataItemId= { collection . ratingKey } "
key + = f " &promotedToRecommended= { 1 if ( library is None and visibility [ ' library ' ] ) or library else 0 } "
key + = f " &promotedToOwnHome= { 1 if ( home is None and visibility [ ' home ' ] ) or home else 0 } "
key + = f " &promotedToSharedHome= { 1 if ( shared is None and visibility [ ' shared ' ] ) or shared else 0 } "
self . _query ( key , post = True )
2021-12-17 14:24:46 +00:00
def get_playlist ( self , title ) :
try :
return self . PlexServer . playlist ( title )
except NotFound :
raise Failed ( f " Plex Error: Playlist { title } not found " )
2021-01-20 21:37:59 +00:00
def get_collection ( self , data ) :
2021-05-02 04:22:48 +00:00
if isinstance ( data , int ) :
2021-07-26 20:29:28 +00:00
return self . fetchItem ( data )
2021-06-16 18:20:48 +00:00
elif isinstance ( data , Collection ) :
2021-07-26 20:29:28 +00:00
return data
2021-05-02 04:22:48 +00:00
else :
2021-12-22 00:22:07 +00:00
cols = self . search ( title = str ( data ) , libtype = " collection " )
for d in cols :
2021-07-26 20:29:28 +00:00
if d . title == data :
return d
2021-12-22 00:22:07 +00:00
for d in cols :
logger . debug ( f " Found: { d . title } " )
logger . debug ( f " Looking for: { data } " )
2021-06-16 18:20:48 +00:00
raise Failed ( f " Plex Error: Collection { data } not found " )
2021-01-20 21:37:59 +00:00
def validate_collections ( self , collections ) :
valid_collections = [ ]
for collection in collections :
try : valid_collections . append ( self . get_collection ( collection ) )
except Failed as e : logger . error ( e )
if len ( valid_collections ) == 0 :
2021-02-24 06:44:06 +00:00
raise Failed ( f " Collection Error: No valid Plex Collections in { collections } " )
2021-01-20 21:37:59 +00:00
return valid_collections
2021-08-07 06:01:21 +00:00
def get_rating_keys ( self , method , data ) :
2021-04-03 18:00:05 +00:00
items = [ ]
if method == " plex_all " :
2022-01-11 01:23:56 +00:00
logger . info ( f " Processing Plex All { data . capitalize ( ) } s " )
items = self . get_all ( collection_level = data )
2021-04-03 18:00:05 +00:00
elif method == " plex_search " :
2021-05-29 02:33:55 +00:00
util . print_multiline ( data [ 1 ] , info = True )
items = self . get_filter_items ( data [ 2 ] )
2021-04-03 18:00:05 +00:00
elif method == " plex_collectionless " :
good_collections = [ ]
2021-08-01 04:35:42 +00:00
logger . info ( f " Processing Plex Collectionless " )
2021-05-09 05:37:45 +00:00
logger . info ( " Collections Excluded " )
2021-04-03 18:00:05 +00:00
for col in self . get_all_collections ( ) :
keep_collection = True
for pre in data [ " exclude_prefix " ] :
if col . title . startswith ( pre ) or ( col . titleSort and col . titleSort . startswith ( pre ) ) :
keep_collection = False
2021-05-09 05:37:45 +00:00
logger . info ( f " { col . title } excluded by prefix match { pre } " )
2021-04-03 18:00:05 +00:00
break
if keep_collection :
for ext in data [ " exclude " ] :
if col . title == ext or ( col . titleSort and col . titleSort == ext ) :
keep_collection = False
2021-05-09 05:37:45 +00:00
logger . info ( f " { col . title } excluded by exact match " )
2021-04-03 18:00:05 +00:00
break
if keep_collection :
2021-05-05 14:50:40 +00:00
logger . info ( f " Collection Passed: { col . title } " )
2021-05-05 18:01:41 +00:00
good_collections . append ( col )
2021-05-09 05:37:45 +00:00
logger . info ( " " )
logger . info ( " Collections Not Excluded (Items in these collections are not added to Collectionless) " )
for col in good_collections :
logger . info ( col . title )
2021-05-05 18:01:41 +00:00
collection_indexes = [ c . index for c in good_collections ]
2021-04-08 20:10:26 +00:00
all_items = self . get_all ( )
2021-04-03 18:00:05 +00:00
for i , item in enumerate ( all_items , 1 ) :
2021-05-26 13:25:32 +00:00
util . print_return ( f " Processing: { i } / { len ( all_items ) } { item . title } " )
2021-04-03 18:00:05 +00:00
add_item = True
2021-06-02 18:41:57 +00:00
self . reload ( item )
2021-04-03 18:00:05 +00:00
for collection in item . collections :
2021-05-05 18:01:41 +00:00
if collection . id in collection_indexes :
2021-04-03 18:00:05 +00:00
add_item = False
break
if add_item :
items . append ( item )
2021-08-24 04:33:04 +00:00
logger . info ( util . adjust_space ( f " Processed { len ( all_items ) } { self . type } s " ) )
2021-04-03 18:00:05 +00:00
else :
raise Failed ( f " Plex Error: Method { method } not supported " )
if len ( items ) > 0 :
2021-12-14 05:51:36 +00:00
ids = [ ( item . ratingKey , " ratingKey " ) for item in items ]
2021-08-07 06:01:21 +00:00
logger . debug ( " " )
logger . debug ( f " { len ( ids ) } Keys Found: { ids } " )
return ids
2021-04-03 18:00:05 +00:00
else :
raise Failed ( " Plex Error: No Items found in Plex " )
2021-05-02 04:22:48 +00:00
def get_collection_items ( self , collection , smart_label_collection ) :
if smart_label_collection :
2021-06-16 18:20:48 +00:00
return self . get_labeled_items ( collection . title if isinstance ( collection , Collection ) else str ( collection ) )
2021-12-17 14:24:46 +00:00
elif isinstance ( collection , ( Collection , Playlist ) ) :
2021-06-16 18:20:48 +00:00
if collection . smart :
2021-05-23 02:42:00 +00:00
return self . get_filter_items ( self . smart_filter ( collection ) )
2021-05-19 21:07:43 +00:00
else :
return self . query ( collection . items )
2021-05-02 04:22:48 +00:00
else :
return [ ]
2021-05-23 02:42:00 +00:00
def get_filter_items ( self , uri_args ) :
key = f " /library/sections/ { self . Plex . key } /all { uri_args } "
return self . Plex . _search ( key , None , 0 , plexapi . X_PLEX_CONTAINER_SIZE )
2021-05-02 04:22:48 +00:00
def get_collection_name_and_items ( self , collection , smart_label_collection ) :
2021-12-17 14:24:46 +00:00
name = collection . title if isinstance ( collection , ( Collection , Playlist ) ) else str ( collection )
2021-05-02 04:22:48 +00:00
return name , self . get_collection_items ( collection , smart_label_collection )
2021-05-26 14:45:33 +00:00
def get_tmdb_from_map ( self , item ) :
return self . movie_rating_key_map [ item . ratingKey ] if item . ratingKey in self . movie_rating_key_map else None
def get_tvdb_from_map ( self , item ) :
return self . show_rating_key_map [ item . ratingKey ] if item . ratingKey in self . show_rating_key_map else None
2021-02-05 14:56:05 +00:00
def search_item ( self , data , year = None ) :
2021-04-12 04:00:03 +00:00
kwargs = { }
if year is not None :
kwargs [ " year " ] = year
2021-07-26 20:29:28 +00:00
for d in self . search ( title = str ( data ) , * * kwargs ) :
if d . title == data :
return d
return None
2021-02-05 14:56:05 +00:00
2021-04-21 18:02:01 +00:00
def edit_item ( self , item , name , item_type , edits , advanced = False ) :
if len ( edits ) > 0 :
logger . debug ( f " Details Update: { edits } " )
try :
2021-05-03 04:10:12 +00:00
self . edit_query ( item , edits , advanced = advanced )
2021-09-13 04:56:05 +00:00
if advanced and ( " languageOverride " in edits or " useOriginalTitle " in edits ) :
2021-05-02 04:22:48 +00:00
self . query ( item . refresh )
2021-04-21 18:02:01 +00:00
logger . info ( f " { item_type } : { name } { ' Advanced ' if advanced else ' ' } Details Update Successful " )
2021-05-20 19:26:56 +00:00
return True
2021-04-21 18:02:01 +00:00
except BadRequest :
util . print_stacktrace ( )
logger . error ( f " { item_type } : { name } { ' Advanced ' if advanced else ' ' } Details Update Failed " )
2021-05-20 19:26:56 +00:00
return False
2021-05-27 17:40:35 +00:00
def edit_tags ( self , attr , obj , add_tags = None , remove_tags = None , sync_tags = None ) :
2021-08-20 13:13:54 +00:00
display = " "
2021-05-27 17:40:35 +00:00
key = builder . filter_translation [ attr ] if attr in builder . filter_translation else attr
2021-12-29 15:51:22 +00:00
attr_display = attr . replace ( " _ " , " " ) . title ( )
attr_call = attr_display . replace ( " " , " " )
2021-12-06 07:51:06 +00:00
if add_tags or remove_tags or sync_tags is not None :
2021-06-24 05:57:55 +00:00
_add_tags = add_tags if add_tags else [ ]
2021-08-05 19:41:59 +00:00
_remove_tags = [ t . lower ( ) for t in remove_tags ] if remove_tags else [ ]
_sync_tags = [ t . lower ( ) for t in sync_tags ] if sync_tags else [ ]
2021-07-12 02:23:14 +00:00
try :
2021-08-18 16:13:41 +00:00
self . reload ( obj )
2021-07-12 02:23:14 +00:00
_item_tags = [ item_tag . tag . lower ( ) for item_tag in getattr ( obj , key ) ]
except BadRequest :
_item_tags = [ ]
_add = [ f " { t [ : 1 ] . upper ( ) } { t [ 1 : ] } " for t in _add_tags + _sync_tags if t . lower ( ) not in _item_tags ]
2021-12-03 04:23:22 +00:00
_remove = [ t for t in _item_tags if ( sync_tags is not None and t not in _sync_tags ) or t in _remove_tags ]
2021-06-24 05:57:55 +00:00
if _add :
2021-12-29 15:51:22 +00:00
self . query_data ( getattr ( obj , f " add { attr_call } " ) , _add )
2021-08-20 13:13:54 +00:00
display + = f " + { ' , + ' . join ( _add ) } "
2021-07-12 02:23:14 +00:00
if _remove :
2021-12-29 15:51:22 +00:00
self . query_data ( getattr ( obj , f " remove { attr_call } " ) , _remove )
2021-08-20 13:13:54 +00:00
display + = f " - { ' , - ' . join ( _remove ) } "
if len ( display ) > 0 :
2021-12-29 15:51:22 +00:00
logger . info ( f " { obj . title [ : 25 ] : <25 } | { attr_display } | { display } " )
2021-08-20 13:13:54 +00:00
return len ( display ) > 0
2021-04-21 18:02:01 +00:00
2021-12-19 16:17:03 +00:00
def find_assets ( self , item , name = None , upload = True , overlay = None , folders = None , create = None ) :
2021-12-19 00:48:11 +00:00
if isinstance ( item , Movie ) :
name = os . path . basename ( os . path . dirname ( str ( item . locations [ 0 ] ) ) )
2022-01-19 15:41:08 +00:00
elif isinstance ( item , ( Artist , Show ) ) :
2021-12-19 00:48:11 +00:00
name = os . path . basename ( str ( item . locations [ 0 ] ) )
2022-01-24 16:08:44 +00:00
elif isinstance ( item , ( Collection , Playlist ) ) :
2021-12-19 00:48:11 +00:00
name = name if name else item . title
else :
2022-01-06 20:23:50 +00:00
return None , None , None
2021-12-19 16:17:03 +00:00
if not folders :
folders = self . asset_folders
if not create :
create = self . create_asset_folders
2022-01-06 20:23:50 +00:00
found_folder = None
2021-07-14 14:47:20 +00:00
poster = None
background = None
2021-06-22 20:28:12 +00:00
for ad in self . asset_directory :
item_dir = None
2021-12-19 16:17:03 +00:00
if folders :
2021-06-22 20:28:12 +00:00
if os . path . isdir ( os . path . join ( ad , name ) ) :
item_dir = os . path . join ( ad , name )
else :
2021-12-13 04:57:38 +00:00
for n in range ( 1 , self . asset_depth + 1 ) :
new_path = ad
for i in range ( 1 , n + 1 ) :
new_path = os . path . join ( new_path , " * " )
matches = util . glob_filter ( os . path . join ( new_path , name ) )
if len ( matches ) > 0 :
item_dir = os . path . abspath ( matches [ 0 ] )
break
2021-06-22 20:28:12 +00:00
if item_dir is None :
2021-05-02 07:40:39 +00:00
continue
2022-01-06 20:23:50 +00:00
found_folder = item_dir
2021-06-22 20:28:12 +00:00
poster_filter = os . path . join ( item_dir , " poster.* " )
background_filter = os . path . join ( item_dir , " background.* " )
2021-05-02 07:40:39 +00:00
else :
2021-08-10 15:33:32 +00:00
poster_filter = os . path . join ( ad , f " { name } .* " )
background_filter = os . path . join ( ad , f " { name } _background.* " )
2021-12-19 00:48:11 +00:00
poster_matches = util . glob_filter ( poster_filter )
if len ( poster_matches ) > 0 :
poster = ImageData ( " asset_directory " , os . path . abspath ( poster_matches [ 0 ] ) , prefix = f " { item . title } ' s " , is_url = False )
background_matches = util . glob_filter ( background_filter )
if len ( background_matches ) > 0 :
background = ImageData ( " asset_directory " , os . path . abspath ( background_matches [ 0 ] ) , prefix = f " { item . title } ' s " , is_poster = False , is_url = False )
if item_dir and self . dimensional_asset_rename and ( not poster or not background ) :
for file in util . glob_filter ( os . path . join ( item_dir , " *.* " ) ) :
if file . lower ( ) . endswith ( ( " .jpg " , " .png " , " .jpeg " ) ) :
2021-12-19 18:48:30 +00:00
image = Image . open ( file )
2021-12-19 00:48:11 +00:00
_w , _h = image . size
image . close ( )
if not poster and _h > _w :
new_path = os . path . join ( os . path . dirname ( file ) , f " poster { os . path . splitext ( file ) [ 1 ] . lower ( ) } " )
os . rename ( file , new_path )
poster = ImageData ( " asset_directory " , os . path . abspath ( new_path ) , prefix = f " { item . title } ' s " , is_url = False )
elif not background and _w > _h :
new_path = os . path . join ( os . path . dirname ( file ) , f " background { os . path . splitext ( file ) [ 1 ] . lower ( ) } " )
os . rename ( file , new_path )
background = ImageData ( " asset_directory " , os . path . abspath ( new_path ) , prefix = f " { item . title } ' s " , is_poster = False , is_url = False )
if poster and background :
break
2021-07-01 18:21:14 +00:00
if poster or background :
2021-12-19 00:48:11 +00:00
if upload :
2021-12-19 14:40:26 +00:00
self . upload_images ( item , poster = poster , background = background , overlay = overlay )
2021-12-19 00:48:11 +00:00
else :
2022-01-06 20:23:50 +00:00
return poster , background , item_dir
2021-12-19 00:48:11 +00:00
if isinstance ( item , Show ) :
2021-12-08 06:23:23 +00:00
missing_assets = " "
found_season = False
2021-05-02 07:40:39 +00:00
for season in self . query ( item . seasons ) :
2021-11-12 07:38:20 +00:00
season_name = f " Season { ' 0 ' if season . seasonNumber < 10 else ' ' } { season . seasonNumber } "
2021-06-22 20:28:12 +00:00
if item_dir :
2021-11-12 07:38:20 +00:00
season_poster_filter = os . path . join ( item_dir , f " { season_name } .* " )
season_background_filter = os . path . join ( item_dir , f " { season_name } _background.* " )
2021-05-02 07:40:39 +00:00
else :
2021-11-12 07:38:20 +00:00
season_poster_filter = os . path . join ( ad , f " { name } _ { season_name } .* " )
season_background_filter = os . path . join ( ad , f " { name } _ { season_name } _background.* " )
season_poster = None
season_background = None
2021-12-08 06:23:23 +00:00
matches = util . glob_filter ( season_poster_filter )
2021-05-02 07:40:39 +00:00
if len ( matches ) > 0 :
2021-06-30 15:02:55 +00:00
season_poster = ImageData ( " asset_directory " , os . path . abspath ( matches [ 0 ] ) , prefix = f " { item . title } Season { season . seasonNumber } ' s " , is_url = False )
2021-12-08 06:23:23 +00:00
found_season = True
elif season . seasonNumber > 0 :
missing_assets + = f " \n Missing Season { season . seasonNumber } Poster "
2021-11-12 07:38:20 +00:00
matches = util . glob_filter ( season_background_filter )
if len ( matches ) > 0 :
season_background = ImageData ( " asset_directory " , os . path . abspath ( matches [ 0 ] ) , prefix = f " { item . title } Season { season . seasonNumber } ' s " , is_poster = False , is_url = False )
if season_poster or season_background :
self . upload_images ( season , poster = season_poster , background = season_background )
2021-05-02 07:40:39 +00:00
for episode in self . query ( season . episodes ) :
2021-06-22 20:28:12 +00:00
if item_dir :
episode_filter = os . path . join ( item_dir , f " { episode . seasonEpisode . upper ( ) } .* " )
2021-05-02 07:40:39 +00:00
else :
2021-08-10 15:33:32 +00:00
episode_filter = os . path . join ( ad , f " { name } _ { episode . seasonEpisode . upper ( ) } .* " )
matches = util . glob_filter ( episode_filter )
2021-05-02 07:40:39 +00:00
if len ( matches ) > 0 :
2021-06-30 15:02:55 +00:00
episode_poster = ImageData ( " asset_directory " , os . path . abspath ( matches [ 0 ] ) , prefix = f " { item . title } { episode . seasonEpisode . upper ( ) } ' s " , is_url = False )
2021-06-22 20:28:12 +00:00
self . upload_images ( episode , poster = episode_poster )
2021-12-08 06:23:23 +00:00
if self . show_missing_season_assets and found_season and missing_assets :
util . print_multiline ( f " Missing Season Posters for { item . title } { missing_assets } " , info = True )
2022-01-19 15:57:43 +00:00
if isinstance ( item , Artist ) :
missing_assets = " "
found_album = False
for album in self . query ( item . albums ) :
if item_dir :
album_poster_filter = os . path . join ( item_dir , f " { album . title } .* " )
album_background_filter = os . path . join ( item_dir , f " { album . title } _background.* " )
else :
album_poster_filter = os . path . join ( ad , f " { name } _ { album . title } .* " )
album_background_filter = os . path . join ( ad , f " { name } _ { album . title } _background.* " )
album_poster = None
album_background = None
matches = util . glob_filter ( album_poster_filter )
if len ( matches ) > 0 :
album_poster = ImageData ( " asset_directory " , os . path . abspath ( matches [ 0 ] ) , prefix = f " { item . title } Album { album . title } ' s " , is_url = False )
found_album = True
else :
missing_assets + = f " \n Missing Album { album . title } Poster "
matches = util . glob_filter ( album_background_filter )
if len ( matches ) > 0 :
album_background = ImageData ( " asset_directory " , os . path . abspath ( matches [ 0 ] ) , prefix = f " { item . title } Album { album . title } ' s " , is_poster = False , is_url = False )
if album_poster or album_background :
self . upload_images ( album , poster = album_poster , background = album_background )
if self . show_missing_season_assets and found_album and missing_assets :
util . print_multiline ( f " Missing Album Posters for { item . title } { missing_assets } " , info = True )
2022-01-06 20:23:50 +00:00
2021-12-19 14:40:26 +00:00
if isinstance ( item , ( Movie , Show ) ) and not poster and overlay :
2021-12-19 16:17:03 +00:00
self . upload_images ( item , overlay = overlay )
if create and folders and not found_folder :
2022-01-06 20:23:50 +00:00
found_folder = os . path . join ( self . asset_directory [ 0 ] , name )
os . makedirs ( found_folder , exist_ok = True )
logger . info ( f " Asset Directory Created: { found_folder } " )
2021-12-19 16:17:03 +00:00
elif isinstance ( item , ( Movie , Show ) ) and not overlay and folders and not found_folder :
2021-12-31 04:34:04 +00:00
logger . warning ( f " Asset Warning: No asset folder found called ' { name } ' " )
2021-12-19 00:48:11 +00:00
elif isinstance ( item , ( Movie , Show ) ) and not poster and not background and self . show_missing_assets :
2021-12-31 04:34:04 +00:00
logger . warning ( f " Asset Warning: No poster or background found in an assets folder for ' { name } ' " )
2022-01-06 20:23:50 +00:00
return None , None , found_folder