예제 #1
0
def findplayingmedia():

	# Connect Remotely but slower
	#account = MyPlexAccount(credentials.login['username'], credentials.login['password'])
	#plex = account.resource(credentials.login['plexhost']).connect() # returns a PlexServer instance

	# Connect faster locally with token credential
	plex = PlexServer(credentials.login['baseurl'], credentials.login['token'])

	mediainfos = []
	for session in plex.sessions():

		mediadict = {}

		playheadseconds = session.viewOffset // 1000

		#Display all instances of the players.
		#for player in session.players:
		#	print("Player: " + player.device)


		# this checks if the media being returned is a movie or TV show. 
		if (session.type == "episode"):
			mediadict["title"] = session.grandparentTitle + " - " + session.title
			mediadict["type"] = session.type

		elif (session.type == "movie"):
			mediadict["title"] = session.title
			mediadict["type"] = session.type

		else:
			print ("not the correct media type playing:  " + session.type)
			mediadict["title"] = "Music"
			mediadict["type"] = session.type
			mediadict["convert"] = False

		if (session.type == "episode") or (session.type == "movie"):
			# Only for printing out the playehead, checking for accuracy, not otherwise necessary.
			#minutes, seconds = divmod(int(playheadseconds), 60)
			#print("Current Playhead Location:  " +  str(minutes) + ":" + str(seconds))

			#searches plex's library for media that matches the key of the session.
			#this is because I couldn't find a way to consistently get the file path. Sometimes
			#it would work, sometimes it wouldn't. This made it always work.
			playingmedia = plex.fetchItem(session.key)
			playingmediafilepath = playingmedia.media[0].parts[0].file

			mediadict["playhead"] = playheadseconds
			mediadict["path"] = playingmediafilepath
			mediadict["convert"] = True

		else:			
			mediadict["playhead"] = 0
			mediadict["path"] = "No Path"

		mediadict["id"] = id_generator(8)
		mediainfos.append(mediadict)

	return (mediainfos)
예제 #2
0
class PlexWrapper(object):
    def __init__(self):
        baseurl = os.environ.get("PLEX_BASE_URL")
        token = os.environ.get("PLEX_TOKEN")
        verify_ssl = os.environ.get("BYPASS_SSL_VERIFY", "0") != "1"

        session = requests.Session()
        session.verify = verify_ssl
        self.plex = PlexServer(baseurl,
                               token,
                               session=session,
                               timeout=(60 * 60))

    def get_dupe_movies(self):
        dupes = []
        section = self.plex.library.section(title="4K Movies")
        for movie in section.search(duplicate=True):
            if len(movie.media) > 1:
                dupes.append(self.movie_to_dict(movie))
        return dupes

    def get_movie_sample_files(self):
        movies = []
        section = self.plex.library.section(title="4K Movies")
        for movie in section.all():
            samples = []
            for media in movie.media:
                if media.duration is None or media.duration < (5 * 60 * 1000):
                    samples.append(self.media_to_dict(media))
            if len(samples) > 0:
                _movie = self.movie_to_dict(movie)
                _movie['media'] = samples
                movies.append(_movie)

        return movies

    def get_movie(self, media_id):
        return self.plex.fetchItem(media_id)

    @classmethod
    def video_to_dict(cls, video: Video) -> dict:
        # https://python-plexapi.readthedocs.io/en/latest/modules/video.html#plexapi.video.Video
        return {
            'addedAt': str(video.addedAt),
            'key': video.key,
            'lastViewedAt': str(video.lastViewedAt),
            'librarySectionID': video.librarySectionID,
            'summary': video.summary,
            'thumb': video.thumb,
            'title': video.title,
            'titleSort': video.titleSort,
            'type': video.type,
            'updatedAt': str(video.updatedAt),
            'viewCount': str(video.viewCount),
        }

    @classmethod
    def movie_to_dict(cls, movie: Movie) -> dict:
        # https://python-plexapi.readthedocs.io/en/latest/modules/video.html#plexapi.video.Movie
        return {
            **cls.video_to_dict(movie), 'duration': movie.duration,
            'guid': movie.guid,
            'originalTitle': movie.originalTitle,
            'originallyAvailableAt': str(movie.originallyAvailableAt),
            'rating': movie.rating,
            'ratingImage': movie.ratingImage,
            'studio': movie.studio,
            'tagline': movie.tagline,
            'userRating': movie.userRating,
            'year': movie.year,
            'media': [cls.media_to_dict(media) for media in movie.media]
        }

    @classmethod
    def media_to_dict(cls, media: Media) -> dict:
        # https://python-plexapi.readthedocs.io/en/latest/modules/media.html#plexapi.media.Media
        return {
            'id':
            media.id,
            # 'initpath': media.initpath,
            # 'video': media.video,
            'aspectRatio':
            media.aspectRatio,
            'audioChannels':
            media.audioChannels,
            'audioCodec':
            media.audioCodec,
            'bitrate':
            media.bitrate,
            'container':
            media.container,
            'duration':
            media.duration,
            'width':
            media.width,
            'height':
            media.height,
            'has64bitOffsets':
            media.has64bitOffsets,
            'optimizedForStreaming':
            media.optimizedForStreaming,
            'target':
            media.target,
            'title':
            media.title,
            'videoCodec':
            media.videoCodec,
            'videoFrameRate':
            media.videoFrameRate,
            'videoResolution':
            media.videoResolution,
            'videoProfile':
            media.videoProfile,
            'parts':
            [cls.media_part_to_dict(media_part) for media_part in media.parts]
        }

    @classmethod
    def media_part_to_dict(cls, media_part: MediaPart) -> dict:
        # https://python-plexapi.readthedocs.io/en/latest/modules/media.html#plexapi.media.MediaPart
        return {
            'id':
            media_part.id,
            # 'media_id': media_part.media.id,
            # 'initpath': media_part.initpath,
            'container':
            media_part.container,
            'duration':
            media_part.duration,
            'file':
            media_part.file,
            'indexes':
            media_part.indexes,
            'key':
            media_part.key,
            'size':
            media_part.size,
            'exists':
            media_part.exists,
            'accessible':
            media_part.accessible,
            'streams': [
                cls.media_part_stream_to_dict(media_part_stream)
                for media_part_stream in media_part.videoStreams()
            ]
        }

    @classmethod
    def media_part_stream_to_dict(cls,
                                  media_part_stream: MediaPartStream) -> dict:
        # https://python-plexapi.readthedocs.io/en/latest/modules/media.html#plexapi.media.MediaPartStream
        return {
            'id': media_part_stream.id,
            # 'media_id': media_part.media.id,
            # 'initpath': media_part.initpath,
            'codec': media_part_stream.codec,
            'codecID': media_part_stream.codecID,
            'language': media_part_stream.language,
            'languageCode': media_part_stream.languageCode,
            'selected': media_part_stream.selected,
            'type': media_part_stream.type
        }
예제 #3
0
class PlexAPI:
    def __init__(self, params, TMDb, TVDb):
        try:
            self.PlexServer = PlexServer(params["plex"]["url"], params["plex"]["token"], timeout=params["plex"]["timeout"])
        except Unauthorized:
            raise Failed("Plex Error: Plex token is invalid")
        except ValueError as e:
            raise Failed(f"Plex Error: {e}")
        except requests.exceptions.ConnectionError:
            util.print_stacktrace()
            raise Failed("Plex Error: Plex url is invalid")
        self.Plex = next((s for s in self.PlexServer.library.sections() if s.title == params["name"]), None)
        if not self.Plex:
            raise Failed(f"Plex Error: Plex Library {params['name']} not found")
        if self.Plex.type not in ["movie", "show"]:
            raise Failed(f"Plex Error: Plex Library must be a Movies or TV Shows library")

        self.agent = self.Plex.agent
        self.is_movie = self.Plex.type == "movie"
        self.is_show = self.Plex.type == "show"

        logger.info(f"Using Metadata File: {params['metadata_path']}")
        try:
            self.data, ind, bsi = yaml.util.load_yaml_guess_indent(open(params["metadata_path"], encoding="utf-8"))
        except yaml.scanner.ScannerError as ye:
            raise Failed(f"YAML Error: {util.tab_new_lines(ye)}")
        except Exception as e:
            util.print_stacktrace()
            raise Failed(f"YAML Error: {e}")

        def get_dict(attribute):
            if attribute in self.data:
                if self.data[attribute]:
                    if isinstance(self.data[attribute], dict):
                        return self.data[attribute]
                    else:
                        logger.warning(f"Config Warning: {attribute} must be a dictionary")
                else:
                    logger.warning(f"Config Warning: {attribute} attribute is blank")
            return None

        self.metadata = get_dict("metadata")
        self.templates = get_dict("templates")
        self.collections = get_dict("collections")

        if self.metadata is None and self.collections is None:
            raise Failed("YAML Error: metadata attributes or collections attribute required")

        if params["asset_directory"]:
            for ad in params["asset_directory"]:
                logger.info(f"Using Asset Directory: {ad}")

        self.TMDb = TMDb
        self.TVDb = TVDb
        self.Radarr = None
        self.Sonarr = None
        self.Tautulli = None
        self.name = params["name"]
        self.missing_path = os.path.join(os.path.dirname(os.path.abspath(params["metadata_path"])), f"{os.path.splitext(os.path.basename(params['metadata_path']))[0]}_missing.yml")
        self.metadata_path = params["metadata_path"]
        self.asset_directory = params["asset_directory"]
        self.asset_folders = params["asset_folders"]
        self.assets_for_all = params["assets_for_all"]
        self.sync_mode = params["sync_mode"]
        self.show_unmanaged = params["show_unmanaged"]
        self.show_filtered = params["show_filtered"]
        self.show_missing = params["show_missing"]
        self.save_missing = params["save_missing"]
        self.mass_genre_update = params["mass_genre_update"]
        self.plex = params["plex"]
        self.url = params["plex"]["url"]
        self.token = params["plex"]["token"]
        self.timeout = params["plex"]["timeout"]
        self.missing = {}
        self.run_again = []

    def get_all_collections(self):
        return self.search(libtype="collection")

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    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)

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def fetchItem(self, data):
        return self.PlexServer.fetchItem(data)

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def get_all(self):
        return self.Plex.all()

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def server_search(self, data):
        return self.PlexServer.search(data)

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def add_collection(self, item, name):
        item.addCollection(name)

    @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed)
    def get_search_choices(self, search_name):
        try:
            choices = {}
            for choice in self.Plex.listFilterChoices(search_name):
                choices[choice.title.lower()] = choice.title
                choices[choice.key.lower()] = choice.title
            return choices
        except NotFound:
            raise Failed(f"Collection Error: plex search attribute: {search_name} only supported with Plex's New TV Agent")

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def refresh_item(self, rating_key):
        requests.put(f"{self.url}/library/metadata/{rating_key}/refresh?X-Plex-Token={self.token}")

    def validate_search_list(self, data, search_name):
        final_search = search_translation[search_name] if search_name in search_translation else search_name
        search_choices = self.get_search_choices(final_search)
        valid_list = []
        for value in util.get_list(data):
            if str(value).lower() in search_choices:
                valid_list.append(search_choices[str(value).lower()])
            else:
                logger.error(f"Plex Error: {search_name}: {value} not found")
        return valid_list

    def get_collection(self, data):
        collection = util.choose_from_list(self.search(title=str(data), libtype="collection"), "collection", str(data), exact=True)
        if collection:                              return collection
        else:                                       raise Failed(f"Plex Error: Collection {data} not found")

    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:
            raise Failed(f"Collection Error: No valid Plex Collections in {collections}")
        return valid_collections

    def get_items(self, method, data, status_message=True):
        if status_message:
            logger.debug(f"Data: {data}")
        pretty = util.pretty_names[method] if method in util.pretty_names else method
        media_type = "Movie" if self.is_movie else "Show"
        items = []
        if method == "plex_all":
            if status_message:
                logger.info(f"Processing {pretty} {media_type}s")
            items = self.get_all()
        elif method == "plex_collection":
            if status_message:
                logger.info(f"Processing {pretty} {data}")
            items = data.items()
        elif method == "plex_search":
            search_terms = {}
            has_processed = False
            search_limit = None
            search_sort = None
            for search_method, search_data in data.items():
                if search_method == "limit":
                    search_limit = search_data
                elif search_method == "sort_by":
                    search_sort = search_data
                else:
                    search, modifier = os.path.splitext(str(search_method).lower())
                    final_search = search_translation[search] if search in search_translation else search
                    if search in ["added", "originally_available"] and modifier == "":
                        final_mod = ">>"
                    elif search in ["added", "originally_available"] and modifier == ".not":
                        final_mod = "<<"
                    elif search in ["critic_rating", "audience_rating"] and modifier == ".greater":
                        final_mod = "__gte"
                    elif search in ["critic_rating", "audience_rating"] and modifier == ".less":
                        final_mod = "__lt"
                    else:
                        final_mod = modifiers[modifier] if modifier in modifiers else ""
                    final_method = f"{final_search}{final_mod}"

                    if search == "duration":
                        search_terms[final_method] = search_data * 60000
                    elif search in ["added", "originally_available"] and modifier in ["", ".not"]:
                        search_terms[final_method] = f"{search_data}d"
                    else:
                        search_terms[final_method] = search_data

                    if status_message:
                        if search in ["added", "originally_available"] or modifier in [".greater", ".less", ".before", ".after"]:
                            ors = f"{search_method}({search_data}"
                        else:
                            ors = ""
                            conjunction = " AND " if final_mod == "&" else " OR "
                            for o, param in enumerate(search_data):
                                or_des = conjunction if o > 0 else f"{search_method}("
                                ors += f"{or_des}{param}"
                        if has_processed:
                            logger.info(f"\t\t      AND {ors})")
                        else:
                            logger.info(f"Processing {pretty}: {ors})")
                            has_processed = True
            if status_message:
                if search_sort:
                    logger.info(f"\t\t      SORT BY {search_sort})")
                if search_limit:
                    logger.info(f"\t\t      LIMIT {search_limit})")
                logger.debug(f"Search: {search_terms}")
            return self.search(sort=sorts[search_sort], maxresults=search_limit, **search_terms)
        elif method == "plex_collectionless":
            good_collections = []
            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
                        break
                if keep_collection:
                    for ext in data["exclude"]:
                        if col.title == ext or (col.titleSort and col.titleSort == ext):
                            keep_collection = False
                            break
                if keep_collection:
                    good_collections.append(col.index)
            all_items = self.get_all()
            length = 0
            for i, item in enumerate(all_items, 1):
                length = util.print_return(length, f"Processing: {i}/{len(all_items)} {item.title}")
                add_item = True
                item.reload()
                for collection in item.collections:
                    if collection.id in good_collections:
                        add_item = False
                        break
                if add_item:
                    items.append(item)
            util.print_end(length, f"Processed {len(all_items)} {'Movies' if self.is_movie else 'Shows'}")
        else:
            raise Failed(f"Plex Error: Method {method} not supported")
        if len(items) > 0:
            return items
        else:
            raise Failed("Plex Error: No Items found in Plex")

    def add_missing(self, collection, items, is_movie):
        col_name = collection.encode("ascii", "replace").decode()
        if col_name not in self.missing:
            self.missing[col_name] = {}
        section = "Movies Missing (TMDb IDs)" if is_movie else "Shows Missing (TVDb IDs)"
        if section not in self.missing[col_name]:
            self.missing[col_name][section] = {}
        for title, item_id in items:
            self.missing[col_name][section][int(item_id)] = str(title).encode("ascii", "replace").decode()
        with open(self.missing_path, "w"): pass
        try:
            yaml.round_trip_dump(self.missing, open(self.missing_path, "w"))
        except yaml.scanner.ScannerError as e:
            logger.error(f"YAML Error: {util.tab_new_lines(e)}")

    def add_to_collection(self, collection, items, filters, show_filtered, rating_key_map, movie_map, show_map):
        name = collection.title if isinstance(collection, Collections) else collection
        collection_items = collection.items() if isinstance(collection, Collections) else []
        total = len(items)
        max_length = len(str(total))
        length = 0
        for i, item in enumerate(items, 1):
            try:
                current = self.fetchItem(item.ratingKey if isinstance(item, (Movie, Show)) else int(item))
                if not isinstance(current, (Movie, Show)):
                    raise NotFound
            except (BadRequest, NotFound):
                logger.error(f"Plex Error: Item {item} not found")
                continue
            match = True
            if filters:
                length = util.print_return(length, f"Filtering {(' ' * (max_length - len(str(i)))) + str(i)}/{total} {current.title}")
                for filter_method, filter_data in filters:
                    modifier = filter_method[-4:]
                    method = filter_method[:-4] if modifier in [".not", ".lte", ".gte"] else filter_method
                    method_name = filter_alias[method] if method in filter_alias else method
                    if method_name == "max_age":
                        threshold_date = datetime.now() - timedelta(days=filter_data)
                        if current.originallyAvailableAt is None or current.originallyAvailableAt < threshold_date:
                            match = False
                            break
                    elif method_name == "original_language":
                        movie = None
                        for key, value in movie_map.items():
                            if current.ratingKey in value:
                                try:
                                    movie = self.TMDb.get_movie(key)
                                    break
                                except Failed:
                                    pass
                        if movie is None:
                            logger.warning(f"Filter Error: No TMDb ID found for {current.title}")
                            continue
                        if (modifier == ".not" and movie.original_language in filter_data) or (modifier != ".not" and movie.original_language not in filter_data):
                            match = False
                            break
                    elif method_name == "audio_track_title":
                        jailbreak = False
                        for media in current.media:
                            for part in media.parts:
                                for audio in part.audioStreams():
                                    for check_title in filter_data:
                                        title = audio.title if audio.title else ""
                                        if check_title.lower() in title.lower():
                                            jailbreak = True
                                            break
                                    if jailbreak: break
                                if jailbreak: break
                            if jailbreak: break
                        if (jailbreak and modifier == ".not") or (not jailbreak and modifier != ".not"):
                            match = False
                            break
                    elif modifier in [".gte", ".lte"]:
                        if method_name == "vote_count":
                            tmdb_item = None
                            for key, value in movie_map.items():
                                if current.ratingKey in value:
                                    try:
                                        tmdb_item = self.TMDb.get_movie(key) if self.is_movie else self.TMDb.get_show(key)
                                        break
                                    except Failed:
                                        pass
                            if tmdb_item is None:
                                logger.warning(f"Filter Error: No TMDb ID found for {current.title}")
                                continue
                            attr = tmdb_item.vote_count
                        else:
                            attr = getattr(current, method_name) / 60000 if method_name == "duration" else getattr(current, method_name)
                        if attr is None or (modifier == ".lte" and attr > filter_data) or (modifier == ".gte" and attr < filter_data):
                            match = False
                            break
                    else:
                        attrs = []
                        if method_name in ["video_resolution", "audio_language", "subtitle_language"]:
                            for media in current.media:
                                if method_name == "video_resolution":
                                    attrs.extend([media.videoResolution])
                                for part in media.parts:
                                    if method_name == "audio_language":
                                        attrs.extend([a.language for a in part.audioStreams()])
                                    if method_name == "subtitle_language":
                                        attrs.extend([s.language for s in part.subtitleStreams()])
                        elif method_name in ["contentRating", "studio", "year", "rating", "originallyAvailableAt"]:
                            attrs = [str(getattr(current, method_name))]
                        elif method_name in ["actors", "countries", "directors", "genres", "writers", "collections"]:
                            attrs = [getattr(x, "tag") for x in getattr(current, method_name)]
                        else:
                            raise Failed(f"Filter Error: filter: {method_name} not supported")

                        if (not list(set(filter_data) & set(attrs)) and modifier != ".not") or (list(set(filter_data) & set(attrs)) and modifier == ".not"):
                            match = False
                            break
                length = util.print_return(length, f"Filtering {(' ' * (max_length - len(str(i)))) + str(i)}/{total} {current.title}")
            if match:
                util.print_end(length, f"{name} Collection | {'=' if current in collection_items else '+'} | {current.title}")
                if current in collection_items:             rating_key_map[current.ratingKey] = None
                else:                                       self.add_collection(current, name)
            elif show_filtered is True:
                logger.info(f"{name} Collection | X | {current.title}")
        media_type = f"{'Movie' if self.is_movie else 'Show'}{'s' if total > 1 else ''}"
        util.print_end(length, f"{total} {media_type} Processed")
        return rating_key_map

    def search_item(self, data, year=None):
        kwargs = {}
        if year is not None:
            kwargs["year"] = year
        return util.choose_from_list(self.search(title=str(data), **kwargs), "movie" if self.is_movie else "show", str(data), exact=True)

    def edit_item(self, item, name, item_type, edits, advanced=False):
        if len(edits) > 0:
            logger.debug(f"Details Update: {edits}")
            try:
                if advanced:
                    item.editAdvanced(**edits)
                else:
                    item.edit(**edits)
                item.reload()
                if advanced and "languageOverride" in edits:
                    self.refresh_item(item.ratingKey)
                logger.info(f"{item_type}: {name}{' Advanced' if advanced else ''} Details Update Successful")
            except BadRequest:
                util.print_stacktrace()
                logger.error(f"{item_type}: {name}{' Advanced' if advanced else ''} Details Update Failed")

    def update_metadata(self, TMDb, test):
        logger.info("")
        util.separator(f"{self.name} Library Metadata")
        logger.info("")
        if not self.metadata:
            raise Failed("No metadata to edit")
        for mapping_name, meta in self.metadata.items():
            methods = {mm.lower(): mm for mm in meta}
            if test and ("test" not in methods or meta[methods["test"]] is not True):
                continue

            updated = False
            edits = {}
            advance_edits = {}
            def add_edit(name, current, group, alias, key=None, value=None, var_type="str"):
                if value or name in alias:
                    if value or group[alias[name]]:
                        if key is None:         key = name
                        if value is None:       value = group[alias[name]]
                        try:
                            if var_type == "date":
                                final_value = util.check_date(value, name, return_string=True, plex_date=True)
                            elif var_type == "float":
                                final_value = util.check_number(value, name, number_type="float", minimum=0, maximum=10)
                            else:
                                final_value = value
                            if str(current) != str(final_value):
                                edits[f"{key}.value"] = final_value
                                edits[f"{key}.locked"] = 1
                                logger.info(f"Detail: {name} updated to {final_value}")
                        except Failed as ee:
                            logger.error(ee)
                    else:
                        logger.error(f"Metadata Error: {name} attribute is blank")

            def add_advanced_edit(attr, obj, group, alias, show_library=False, new_agent=False):
                key, options = advance_keys[attr]
                if attr in alias:
                    if new_agent and self.agent not in new_plex_agents:
                        logger.error(f"Metadata Error: {attr} attribute only works for with the New Plex Movie Agent and New Plex TV Agent")
                    elif show_library and not self.is_show:
                        logger.error(f"Metadata Error: {attr} attribute only works for show libraries")
                    elif group[alias[attr]]:
                        method_data = str(group[alias[attr]]).lower()
                        if method_data not in options:
                            logger.error(f"Metadata Error: {group[alias[attr]]} {attr} attribute invalid")
                        elif getattr(obj, key) != options[method_data]:
                            advance_edits[key] = options[method_data]
                            logger.info(f"Detail: {attr} updated to {method_data}")
                    else:
                        logger.error(f"Metadata Error: {attr} attribute is blank")

            def edit_tags(attr, obj, group, alias, key=None, extra=None, movie_library=False):
                if key is None:
                    key = f"{attr}s"
                if attr in alias and f"{attr}.sync" in alias:
                    logger.error(f"Metadata Error: Cannot use {attr} and {attr}.sync together")
                elif attr in alias or f"{attr}.sync" in alias:
                    attr_key = attr if attr in alias else f"{attr}.sync"
                    if movie_library and not self.is_movie:
                        logger.error(f"Metadata Error: {attr_key} attribute only works for movie libraries")
                    elif group[alias[attr_key]] or extra:
                        item_tags = [item_tag.tag for item_tag in getattr(obj, key)]
                        input_tags = []
                        if group[alias[attr_key]]:
                            input_tags.extend(util.get_list(group[alias[attr_key]]))
                        if extra:
                            input_tags.extend(extra)
                        if f"{attr}.sync" in alias:
                            remove_method = getattr(obj, f"remove{attr.capitalize()}")
                            for tag in (t for t in item_tags if t not in input_tags):
                                updated = True
                                remove_method(tag)
                                logger.info(f"Detail: {attr.capitalize()} {tag} removed")
                        add_method = getattr(obj, f"add{attr.capitalize()}")
                        for tag in (t for t in input_tags if t not in item_tags):
                            updated = True
                            add_method(tag)
                            logger.info(f"Detail: {attr.capitalize()} {tag} added")
                    else:
                        logger.error(f"Metadata Error: {attr} attribute is blank")

            def set_image(attr, obj, group, alias, is_background=False):
                if group[alias[attr]]:
                    message = f"{'background' if is_background else 'poster'} to [{'File' if attr.startswith('file') else 'URL'}] {group[alias[attr]]}"
                    if group[alias[attr]] and attr.startswith("url") and is_background:
                        obj.uploadArt(url=group[alias[attr]])
                    elif group[alias[attr]] and attr.startswith("url"):
                        obj.uploadPoster(url=group[alias[attr]])
                    elif group[alias[attr]] and attr.startswith("file") and is_background:
                        obj.uploadArt(filepath=group[alias[attr]])
                    elif group[alias[attr]] and attr.startswith("file"):
                        obj.uploadPoster(filepath=group[alias[attr]])
                    logger.info(f"Detail: {attr} updated {message}")
                else:
                    logger.error(f"Metadata Error: {attr} attribute is blank")

            def set_images(obj, group, alias):
                if "url_poster" in alias:
                    set_image("url_poster", obj, group, alias)
                elif "file_poster" in alias:
                    set_image("file_poster", obj, group, alias)
                if "url_background" in alias:
                    set_image("url_background", obj, group, alias, is_background=True)
                elif "file_background" in alias:
                    set_image("file_background", obj, group, alias, is_background=True)

            logger.info("")
            util.separator()
            logger.info("")
            year = None
            if "year" in methods:
                year = util.check_number(meta[methods["year"]], "year", minimum=1800, maximum=datetime.now().year + 1)

            title = mapping_name
            if "title" in methods:
                if meta[methods["title"]] is None:              logger.error("Metadata Error: title attribute is blank")
                else:                                           title = meta[methods["title"]]

            item = self.search_item(title, year=year)

            if item is None:
                item = self.search_item(f"{title} (SUB)", year=year)

            if item is None and "alt_title" in methods:
                if meta[methods["alt_title"]] is None:
                    logger.error("Metadata Error: alt_title attribute is blank")
                else:
                    alt_title = meta["alt_title"]
                    item = self.search_item(alt_title, year=year)

            if item is None:
                logger.error(f"Plex Error: Item {mapping_name} not found")
                logger.error(f"Skipping {mapping_name}")
                continue

            item_type = "Movie" if self.is_movie else "Show"
            logger.info(f"Updating {item_type}: {title}...")

            tmdb_item = None
            tmdb_is_movie = None
            if ("tmdb_show" in methods or "tmdb_id" in methods) and "tmdb_movie" in methods:
                logger.error("Metadata Error: Cannot use tmdb_movie and tmdb_show when editing the same metadata item")

            if "tmdb_show" in methods or "tmdb_id" in methods or "tmdb_movie" in methods:
                try:
                    if "tmdb_show" in methods or "tmdb_id" in methods:
                        data = meta[methods["tmdb_show" if "tmdb_show" in methods else "tmdb_id"]]
                        if data is None:
                            logger.error("Metadata Error: tmdb_show attribute is blank")
                        else:
                            tmdb_is_movie = False
                            tmdb_item = TMDb.get_show(util.regex_first_int(data, "Show"))
                    elif "tmdb_movie" in methods:
                        if meta[methods["tmdb_movie"]] is None:
                            logger.error("Metadata Error: tmdb_movie attribute is blank")
                        else:
                            tmdb_is_movie = True
                            tmdb_item = TMDb.get_movie(util.regex_first_int(meta[methods["tmdb_movie"]], "Movie"))
                except Failed as e:
                    logger.error(e)

            originally_available = None
            original_title = None
            rating = None
            studio = None
            tagline = None
            summary = None
            genres = []
            if tmdb_item:
                originally_available = tmdb_item.release_date if tmdb_is_movie else tmdb_item.first_air_date
                if tmdb_item and tmdb_is_movie is True and tmdb_item.original_title != tmdb_item.title:
                    original_title = tmdb_item.original_title
                elif tmdb_item and tmdb_is_movie is False and tmdb_item.original_name != tmdb_item.name:
                    original_title = tmdb_item.original_name
                rating = tmdb_item.vote_average
                if tmdb_is_movie is True and tmdb_item.production_companies:
                    studio = tmdb_item.production_companies[0].name
                elif tmdb_is_movie is False and tmdb_item.networks:
                    studio = tmdb_item.networks[0].name
                tagline = tmdb_item.tagline if len(tmdb_item.tagline) > 0 else None
                summary = tmdb_item.overview
                genres = [genre.name for genre in tmdb_item.genres]

            edits = {}
            add_edit("title", item.title, meta, methods, value=title)
            add_edit("sort_title", item.titleSort, meta, methods, key="titleSort")
            add_edit("originally_available", str(item.originallyAvailableAt)[:-9], meta, methods, key="originallyAvailableAt", value=originally_available, var_type="date")
            add_edit("critic_rating", item.rating, meta, methods, value=rating, key="rating", var_type="float")
            add_edit("audience_rating", item.audienceRating, meta, methods, key="audienceRating", var_type="float")
            add_edit("content_rating", item.contentRating, meta, methods, key="contentRating")
            add_edit("original_title", item.originalTitle, meta, methods, key="originalTitle", value=original_title)
            add_edit("studio", item.studio, meta, methods, value=studio)
            add_edit("tagline", item.tagline, meta, methods, value=tagline)
            add_edit("summary", item.summary, meta, methods, value=summary)
            self.edit_item(item, mapping_name, item_type, edits)

            advance_edits = {}
            add_advanced_edit("episode_sorting", item, meta, methods, show_library=True)
            add_advanced_edit("keep_episodes", item, meta, methods, show_library=True)
            add_advanced_edit("delete_episodes", item, meta, methods, show_library=True)
            add_advanced_edit("season_display", item, meta, methods, show_library=True)
            add_advanced_edit("episode_ordering", item, meta, methods, show_library=True)
            add_advanced_edit("metadata_language", item, meta, methods, new_agent=True)
            add_advanced_edit("use_original_title", item, meta, methods, new_agent=True)
            self.edit_item(item, mapping_name, item_type, advance_edits, advanced=True)

            edit_tags("genre", item, meta, methods, extra=genres)
            edit_tags("label", item, meta, methods)
            edit_tags("collection", item, meta, methods)
            edit_tags("country", item, meta, methods, key="countries", movie_library=True)
            edit_tags("director", item, meta, methods, movie_library=True)
            edit_tags("producer", item, meta, methods, movie_library=True)
            edit_tags("writer", item, meta, methods, movie_library=True)

            logger.info(f"{item_type}: {mapping_name} Details Update {'Complete' if updated else 'Not Needed'}")

            set_images(item, meta, methods)

            if "seasons" in methods and self.is_show:
                if meta[methods["seasons"]]:
                    for season_id in meta[methods["seasons"]]:
                        updated = False
                        logger.info("")
                        logger.info(f"Updating season {season_id} of {mapping_name}...")
                        if isinstance(season_id, int):
                            season = None
                            for s in item.seasons():
                                if s.index == season_id:
                                    season = s
                                    break
                            if season is None:
                                logger.error(f"Metadata Error: Season: {season_id} not found")
                            else:
                                season_dict = meta[methods["seasons"]][season_id]
                                season_methods = {sm.lower(): sm for sm in season_dict}

                                if "title" in season_methods and season_dict[season_methods["title"]]:
                                    title = season_dict[season_methods["title"]]
                                else:
                                    title = season.title
                                if "sub" in season_methods:
                                    if season_dict[season_methods["sub"]] is None:
                                        logger.error("Metadata Error: sub attribute is blank")
                                    elif season_dict[season_methods["sub"]] is True and "(SUB)" not in title:
                                        title = f"{title} (SUB)"
                                    elif season_dict[season_methods["sub"]] is False and title.endswith(" (SUB)"):
                                        title = title[:-6]
                                    else:
                                        logger.error("Metadata Error: sub attribute must be True or False")

                                edits = {}
                                add_edit("title", season.title, season_dict, season_methods, value=title)
                                add_edit("summary", season.summary, season_dict, season_methods)
                                self.edit_item(season, season_id, "Season", edits)
                                set_images(season, season_dict, season_methods)
                        else:
                            logger.error(f"Metadata Error: Season: {season_id} invalid, it must be an integer")
                        logger.info(f"Season {season_id} of {mapping_name} Details Update {'Complete' if updated else 'Not Needed'}")
                else:
                    logger.error("Metadata Error: seasons attribute is blank")
            elif "seasons" in methods:
                logger.error("Metadata Error: seasons attribute only works for show libraries")

            if "episodes" in methods and self.is_show:
                if meta[methods["episodes"]]:
                    for episode_str in meta[methods["episodes"]]:
                        updated = False
                        logger.info("")
                        match = re.search("[Ss]\\d+[Ee]\\d+", episode_str)
                        if match:
                            output = match.group(0)[1:].split("E" if "E" in match.group(0) else "e")
                            season_id = int(output[0])
                            episode_id = int(output[1])
                            logger.info(f"Updating episode S{season_id}E{episode_id} of {mapping_name}...")
                            try:                                episode = item.episode(season=season_id, episode=episode_id)
                            except NotFound:                    logger.error(f"Metadata Error: episode {episode_id} of season {season_id} not found")
                            else:
                                episode_dict = meta[methods["episodes"]][episode_str]
                                episode_methods = {em.lower(): em for em in episode_dict}

                                if "title" in episode_methods and episode_dict[episode_methods["title"]]:
                                    title = episode_dict[episode_methods["title"]]
                                else:
                                    title = episode.title
                                if "sub" in episode_dict:
                                    if episode_dict[episode_methods["sub"]] is None:
                                        logger.error("Metadata Error: sub attribute is blank")
                                    elif episode_dict[episode_methods["sub"]] is True and "(SUB)" not in title:
                                        title = f"{title} (SUB)"
                                    elif episode_dict[episode_methods["sub"]] is False and title.endswith(" (SUB)"):
                                        title = title[:-6]
                                    else:
                                        logger.error("Metadata Error: sub attribute must be True or False")
                                edits = {}
                                add_edit("title", episode.title, episode_dict, episode_methods, value=title)
                                add_edit("sort_title", episode.titleSort, episode_dict, episode_methods, key="titleSort")
                                add_edit("rating", episode.rating, episode_dict, episode_methods)
                                add_edit("originally_available", str(episode.originallyAvailableAt)[:-9], episode_dict, episode_methods, key="originallyAvailableAt")
                                add_edit("summary", episode.summary, episode_dict, episode_methods)
                                self.edit_item(episode, f"{season_id} Episode: {episode_id}", "Season", edits)
                                edit_tags("director", episode, episode_dict, episode_methods)
                                edit_tags("writer", episode, episode_dict, episode_methods)
                                set_images(episode, episode_dict, episode_methods)
                            logger.info(f"Episode S{episode_id}E{season_id}  of {mapping_name} Details Update {'Complete' if updated else 'Not Needed'}")
                        else:
                            logger.error(f"Metadata Error: episode {episode_str} invalid must have S##E## format")
                else:
                    logger.error("Metadata Error: episodes attribute is blank")
            elif "episodes" in methods:
                logger.error("Metadata Error: episodes attribute only works for show libraries")
예제 #4
0
PLEX_URL = ''
PLEX_TOKEN = ''

# Environmental Variables
PLEX_URL = os.getenv('PLEX_URL', PLEX_URL)
PLEX_TOKEN = os.getenv('PLEX_TOKEN', PLEX_TOKEN)

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('--rating_key', required=True, type=int)
    parser.add_argument('--collection', required=True)
    parser.add_argument('--days', required=True, type=int)
    opts = parser.parse_args()

    threshold_date = datetime.now() - timedelta(days=opts.days)

    plex = PlexServer(PLEX_URL, PLEX_TOKEN)

    movie = plex.fetchItem(opts.rating_key)

    if movie.originallyAvailableAt >= threshold_date:
        movie.addCollection(opts.collection)
        print("Added collection '{}' to '{}'.".format(
            opts.collection, movie.title.encode('UTF-8')))

    for m in movie.section().search(collection=opts.collection):
        if m.originallyAvailableAt < threshold_date:
            m.removeCollection(opts.collection)
            print("Removed collection '{}' from '{}'.".format(
                opts.collection, m.title.encode('UTF-8')))
예제 #5
0
import os
from plexapi.server import PlexServer

PLEX_URL = ''
PLEX_TOKEN = ''

# Environmental Variables
PLEX_URL = os.getenv('PLEX_URL', PLEX_URL)
PLEX_USER_TOKEN = os.getenv('PLEX_USER_TOKEN', PLEX_TOKEN)

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('--rating_key', required=True, type=int)
    parser.add_argument('--filename', required=True)
    opts = parser.parse_args()

    plex = PlexServer(PLEX_URL, PLEX_USER_TOKEN)

    for episode in plex.fetchItem(opts.rating_key).season().episodes():
        if episode.ratingKey == opts.rating_key:
            continue
        if any(opts.filename in part.file for media in episode.media
               for part in media.parts):
            print(
                "Marking multi-episode file '{grandparentTitle} - S{parentIndex}E{index}' as watched."
                .format(
                    grandparentTitle=episode.grandparentTitle.encode('UTF-8'),
                    parentIndex=str(episode.parentIndex).zfill(2),
                    index=str(episode.index).zfill(2)))
            episode.markWatched()
예제 #6
0
from plexapi.server import PlexServer

PLEX_URL = ''
PLEX_TOKEN = ''

plex = PlexServer(PLEX_URL, PLEX_TOKEN)

COLLECTIONAME = 'My Fav Series'
TOPLEVELFOLDERNAME = 'Series Name'
LIBRARYNAME = 'Audio Books'

abLibrary = plex.library.section(LIBRARYNAME)

albums = []
for folder in abLibrary.folders():
    if folder.title == TOPLEVELFOLDERNAME:
        for series in folder.allSubfolders():
            trackKey = series.key
            try:
                track = plex.fetchItem(trackKey)
                albumKey = track.parentKey
                album = plex.fetchItem(albumKey)
                albums.append(album)
            except Exception:
                # print('{} contains additional subfolders that were likely captured. \n[{}].'
                #       .format(series.title, ', '.join([x.title for x in series.allSubfolders()])))
                pass

for album in list(set(albums)):
    print('Adding {} to collection {}.'.format(album.title, COLLECTIONAME))
    album.addCollection(COLLECTIONAME)
예제 #7
0
        else:
            logger.error("Missing --libraries or --allLibraries")
            exit()

    if opts.action == 'update':
        logger.info("Deleting the playlist(s)...")
        for data in playlist_dict['data']:
            delete_playlist(data, title)
            logger.info('Creating playlist(s)...')
        for data in playlist_dict['data']:
            create_playlist(title, keys_list, data['server'], data['user'])

    if opts.action == 'add':
        if opts.jbop == 'collection':
            logger.info('Creating collection(s)...')
            for key in keys_list:
                item = plex.fetchItem(int(key))
                item.addCollection([title])

        elif opts.jbop == 'label':
            logger.info('Creating label(s)...')
            for key in keys_list:
                item = plex.fetchItem(int(key))
                item.addLabel([title])
        else:
            logger.info('Creating playlist(s)...')
            for data in playlist_dict['data']:
                create_playlist(title, keys_list, data['server'], data['user'])

    logger.info("Done.")
예제 #8
0
class PlexWrapper(object):
    def __init__(self):
        self.baseurl = os.environ.get("PLEX_BASE_URL")
        token = os.environ.get("PLEX_TOKEN")
        self.maxresults = int(os.environ.get("MAXRESULTS", 50))
        verify_ssl = os.environ.get("BYPASS_SSL_VERIFY", "0") != "1"
        self.libraries = [
            x.strip()
            for x in os.environ.get("LIBRARY_NAMES", "Movies").split(";")
            if x.strip() != ""
        ]

        session = requests.Session()
        session.verify = verify_ssl
        self.plex = PlexServer(self.baseurl,
                               token,
                               session=session,
                               timeout=(60 * 60))

    def _get_sections(self):
        return [
            self.plex.library.section(title=library)
            for library in self.libraries
        ]

    def get_server_info(self):
        return {
            'name': self.plex.friendlyName,
            'url': self.baseurl + '/web/index.html'
        }

    def get_dupe_content(self):
        dupes = []
        for section in self._get_sections():
            for movie in section.search(duplicate=True,
                                        maxresults=self.maxresults,
                                        libtype='movie'):
                if len(movie.media) > 1:
                    dupes.append(self.movie_to_dict(movie, section.title))
            for episode in section.search(duplicate=True,
                                          maxresults=self.maxresults,
                                          libtype='episode'):
                if len(episode.media) > 1:
                    dupes.append(self.episode_to_dict(episode, section.title))
        return dupes

    def get_content_sample_files(self):
        content = []

        for section in self._get_sections():
            for mediaContent in section.all():
                samples = []
                if mediaContent.TYPE != 'movie' or mediaContent.TYPE != 'episode':
                    continue
                for media in mediaContent.media:
                    if media.duration is None or media.duration < (5 * 60 *
                                                                   1000):
                        samples.append(self.media_to_dict(media))
                if len(samples) > 0:
                    _media = dict()
                    if mediaContent.TYPE == 'movie':
                        _media = self.movie_to_dict(mediaContent,
                                                    section.title)
                    elif mediaContent.TYPE == 'episode':
                        _media = self.episode_to_dict(mediaContent,
                                                      section.title)
                    _media["media"] = samples
                    content.append(_media)
        return content

    def get_content(self, media_id):
        return self.plex.fetchItem(media_id)

    def video_to_dict(self, video: Video) -> dict:
        # https://python-plexapi.readthedocs.io/en/latest/modules/video.html#plexapi.video.Video
        return {
            "addedAt":
            str(video.addedAt),
            "key":
            video.key,
            "lastViewedAt":
            str(video.lastViewedAt),
            "librarySectionID":
            video.librarySectionID,
            "summary":
            video.summary,
            "thumbUrl":
            video.thumbUrl,
            "title":
            video.title,
            "titleSort":
            video.titleSort,
            "type":
            video.type,
            "updatedAt":
            str(video.updatedAt),
            "viewCount":
            str(video.viewCount),
            "url":
            self.baseurl + '/web/index.html#!/server/' +
            self.plex.machineIdentifier + '/details?key=' +
            urllib.parse.quote_plus(video.key)
        }

    def movie_to_dict(self, movie: Movie, library: str) -> dict:
        # https://python-plexapi.readthedocs.io/en/latest/modules/video.html#plexapi.video.Movie
        return {
            **self.video_to_dict(movie),
            "contentType": 'movie',
            "library": library,
            "duration": movie.duration,
            "guid": movie.guid,
            "originalTitle": movie.originalTitle,
            "originallyAvailableAt": str(movie.originallyAvailableAt),
            "rating": movie.rating,
            "ratingImage": movie.ratingImage,
            "studio": movie.studio,
            "tagline": movie.tagline,
            "userRating": movie.userRating,
            "year": movie.year,
            "media": [self.media_to_dict(media) for media in movie.media],
        }

    def episode_to_dict(self, episode: Episode, library: str) -> dict:
        # https://python-plexapi.readthedocs.io/en/latest/modules/video.html#plexapi.video.Movie
        return {
            **self.video_to_dict(episode),
            "contentType": 'episode',
            "library": library,
            "duration": episode.duration,
            "guid": episode.guid,
            "originalTitle": episode.title,
            "originallyAvailableAt": str(episode.originallyAvailableAt),
            "rating": episode.rating,
            "year": episode.year,
            "seasonNumber": episode.seasonNumber,
            "seasonEpisode": episode.seasonEpisode,
            "seriesTitle": episode.grandparentTitle,
            "media": [self.media_to_dict(media) for media in episode.media],
        }

    @classmethod
    def media_to_dict(cls, media: Media) -> dict:
        # https://python-plexapi.readthedocs.io/en/latest/modules/media.html#plexapi.media.Media
        return {
            "id":
            media.id,
            # 'initpath': media.initpath,
            # 'video': media.video,
            "aspectRatio":
            media.aspectRatio,
            "audioChannels":
            media.audioChannels,
            "audioCodec":
            media.audioCodec,
            "bitrate":
            media.bitrate,
            "container":
            media.container,
            "duration":
            media.duration,
            "width":
            media.width,
            "height":
            media.height,
            "has64bitOffsets":
            media.has64bitOffsets,
            "optimizedForStreaming":
            media.optimizedForStreaming,
            "target":
            media.target,
            "title":
            media.title,
            "videoCodec":
            media.videoCodec,
            "videoFrameRate":
            media.videoFrameRate,
            "videoResolution":
            media.videoResolution,
            "videoProfile":
            media.videoProfile,
            "parts":
            [cls.media_part_to_dict(media_part) for media_part in media.parts],
        }

    @classmethod
    def media_part_to_dict(cls, media_part: MediaPart) -> dict:
        # https://python-plexapi.readthedocs.io/en/latest/modules/media.html#plexapi.media.MediaPart
        return {
            "id":
            media_part.id,
            # 'media_id': media_part.media.id,
            # 'initpath': media_part.initpath,
            "container":
            media_part.container,
            "duration":
            media_part.duration,
            "file":
            media_part.file,
            "indexes":
            media_part.indexes,
            "key":
            media_part.key,
            "size":
            media_part.size,
            "exists":
            media_part.exists,
            "accessible":
            media_part.accessible,
            "streams": [
                cls.media_part_stream_to_dict(media_part_stream)
                for media_part_stream in media_part.videoStreams()
            ],
        }

    @classmethod
    def media_part_stream_to_dict(cls,
                                  media_part_stream: MediaPartStream) -> dict:
        # https://python-plexapi.readthedocs.io/en/latest/modules/media.html#plexapi.media.MediaPartStream
        return {
            "id": media_part_stream.id,
            # 'media_id': media_part.media.id,
            # 'initpath': media_part.initpath,
            "codec": media_part_stream.codec,
            "codecID": media_part_stream.codecID,
            "language": media_part_stream.language,
            "languageCode": media_part_stream.languageCode,
            "selected": media_part_stream.selected,
            "type": media_part_stream.type,
        }
예제 #9
0
    'Chromecast': 'Chromecast message'
}

USER_IGNORE = ('')  # ('Username','User2')

PLEXPY_LOG = 'Killing {user}\'s stream of {title} due to video transcoding of {original} content'
##

sess = requests.Session()
sess.verify = False
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess)

if __name__ == '__main__':

    rating_key = sys.argv[1]
    item = plex.fetchItem(int(rating_key))
    orig_quality = item.media[0].videoResolution
    # print(orig_quality)

    for session in plex.sessions():
        username = session.usernames[0]
        media_type = session.type
        if username not in USER_IGNORE and media_type != 'track':
            title = session.title
            sess_rating = session.key.split('/')[3]
            trans_dec = session.transcodeSessions[0].videoDecision
            if sess_rating == str(
                    rating_key
            ) and orig_quality in TARGET_QUALITY and trans_dec == 'transcode':
                reason = DEVICES.get(session.players[0].platform,
                                     DEFAULT_REASON)
예제 #10
0
def cli(plex_url, plex_token, plex_section, myshows_api_url, myshows_username,
        myshows_password, cache_dir, what_if):
    try:
        myshows = MyShows(myshows_api_url, myshows_username, myshows_password)
    except Exception as exc:
        print(exc)
        log.error(exc)
        sys.exit(1)

    try:
        plex_instance = PlexServer(plex_url, plex_token)
    except Exception as exc:
        print('No Plex Media Server found at {}'.format(plex_url))
        log.error(exc)
        sys.exit(1)

    plex = Plex(plex_instance)
    try:
        watched_episodes = plex.get_watched_episodes(plex_section)
        myshows_series_id = {}

        try:
            with open('{}/series_cache'.format(cache_dir), 'rb') as cache_file:
                series_cache = pickle.load(cache_file)
        except EOFError:
            series_cache = []
        series_cache_size = len(series_cache)
        for entry in watched_episodes:
            if entry.ratingKey in series_cache:
                continue
            entry_key = (entry.grandparentTitle,
                         plex_instance.fetchItem(
                             entry.grandparentRatingKey).year)
            if entry_key not in myshows_series_id:
                series_id = myshows.get_series_id(
                    entry.grandparentTitle,
                    plex_instance.fetchItem(entry.grandparentRatingKey).year)
                myshows_series_id.update({entry_key: series_id})
            else:
                series_id = myshows_series_id[entry_key]

            if series_id:
                episode_id = myshows.get_episode_id(series_id,
                                                    entry.parentIndex,
                                                    entry.index)
                if episode_id:
                    myshows_watched_episodes = myshows.get_watched_episodes_id(
                        series_id)
                    if not myshows_watched_episodes or episode_id not in myshows_watched_episodes:
                        info = myshows.get_episode_info(episode_id)
                        if what_if:
                            if info:
                                print(
                                    '{} season {} episode {} will be marked as watched'
                                    .format(info['series_title'],
                                            info['season'], info['episode']))
                                log.info(
                                    '{} season {} episode {} will be marked as watched'
                                    .format(info['series_title'],
                                            info['season'], info['episode']))
                            else:
                                print('Episode with id {} not found'.format(
                                    episode_id))
                                log.warning(
                                    'Episode with id {} not found'.format(
                                        episode_id))
                        else:
                            if myshows.mark_episode_as_watch(episode_id):
                                print(
                                    '{} season {} episode {} mark as watched'.
                                    format(info['series_title'],
                                           info['season'], info['episode']))
                                log.info(
                                    '{} season {} episode {} mark as watched'.
                                    format(info['series_title'],
                                           info['season'], info['episode']))
                                series_cache.append(entry.ratingKey)
                    else:
                        series_cache.append(entry.ratingKey)
                else:
                    print('{} season {} episode {} not found'.format(
                        entry.grandparentTitle, entry.parentIndex,
                        entry.index))
                    log.warning('{} season {} episode {} not found'.format(
                        entry.grandparentTitle, entry.parentIndex,
                        entry.index))

            else:
                print('Series {} not found'.format(entry.grandparentTitle))
                log.warning('Series {} not found'.format(
                    entry.grandparentTitle))
        if len(series_cache) != series_cache_size:
            with open('{}/series_cache'.format(cache_dir),
                      'wb+') as cache_file:
                pickle.dump(series_cache, cache_file)

    except Exception as exc:
        print(exc)
        log.error(exc)
        sys.exit(1)
예제 #11
0
class PlexAPI:
    def __init__(self, params, TMDb, TVDb):
        try:
            self.PlexServer = PlexServer(params["plex"]["url"],
                                         params["plex"]["token"],
                                         timeout=params["plex"]["timeout"])
        except Unauthorized:
            raise Failed("Plex Error: Plex token is invalid")
        except ValueError as e:
            raise Failed("Plex Error: {}".format(e))
        except requests.exceptions.ConnectionError as e:
            util.print_stacktrace()
            raise Failed("Plex Error: Plex url is invalid")
        self.is_movie = params["library_type"] == "movie"
        self.is_show = params["library_type"] == "show"
        self.Plex = next(
            (s for s in self.PlexServer.library.sections()
             if s.title == params["name"] and (
                 (self.is_movie and isinstance(s, MovieSection)) or
                 (self.is_show and isinstance(s, ShowSection)))), None)
        if not self.Plex:
            raise Failed("Plex Error: Plex Library {} not found".format(
                params["name"]))
        try:
            self.data, ind, bsi = yaml.util.load_yaml_guess_indent(
                open(params["metadata_path"], encoding="utf-8"))
        except yaml.scanner.ScannerError as e:
            raise Failed("YAML Error: {}".format(
                str(e).replace("\n", "\n|\t      ")))

        def get_dict(attribute):
            if attribute in self.data:
                if self.data[attribute]:
                    if isinstance(self.data[attribute], dict):
                        return self.data[attribute]
                    else:
                        logger.waring(
                            "Config Warning: {} must be a dictionary".format(
                                attribute))
                else:
                    logger.warning(
                        "Config Warning: {} attribute is blank".format(
                            attribute))
            return None

        self.metadata = get_dict("metadata")
        self.templates = get_dict("templates")
        self.collections = get_dict("collections")

        if self.metadata is None and self.collections is None:
            raise Failed(
                "YAML Error: metadata attributes or collections attribute required"
            )

        if params["asset_directory"]:
            for ad in params["asset_directory"]:
                logger.info("Using Asset Directory: {}".format(ad))

        self.TMDb = TMDb
        self.TVDb = TVDb
        self.Radarr = None
        self.Sonarr = None
        self.Tautulli = None
        self.name = params["name"]
        self.missing_path = os.path.join(
            os.path.dirname(os.path.abspath(params["metadata_path"])),
            "{}_missing.yml".format(
                os.path.splitext(os.path.basename(
                    params["metadata_path"]))[0]))
        self.metadata_path = params["metadata_path"]
        self.asset_directory = params["asset_directory"]
        self.sync_mode = params["sync_mode"]
        self.show_unmanaged = params["show_unmanaged"]
        self.show_filtered = params["show_filtered"]
        self.show_missing = params["show_missing"]
        self.save_missing = params["save_missing"]
        self.plex = params["plex"]
        self.timeout = params["plex"]["timeout"]
        self.missing = {}

    def add_Radarr(self, Radarr):
        self.Radarr = Radarr

    def add_Sonarr(self, Sonarr):
        self.Sonarr = Sonarr

    def add_Tautulli(self, Tautulli):
        self.Tautulli = Tautulli

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def search(self, title, libtype=None, year=None):
        if libtype is not None and year is not None:
            return self.Plex.search(title=title, year=year, libtype=libtype)
        elif libtype is not None:
            return self.Plex.search(title=title, libtype=libtype)
        elif year is not None:
            return self.Plex.search(title=title, year=year)
        else:
            return self.Plex.search(title=title)

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def fetchItem(self, data):
        return self.PlexServer.fetchItem(data)

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def server_search(self, data):
        return self.PlexServer.search(data)

    def get_all_collections(self):
        return self.Plex.search(libtype="collection")

    def get_collection(self, data):
        collection = util.choose_from_list(self.search(str(data),
                                                       libtype="collection"),
                                           "collection",
                                           str(data),
                                           exact=True)
        if collection: return collection
        else: raise Failed("Plex Error: Collection {} not found".format(data))

    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:
            raise Failed(
                "Collection Error: No valid Plex Collections in {}".format(
                    collections[c][m]))
        return valid_collections

    def add_missing(self, collection, items, is_movie):
        col_name = collection.encode("ascii", "replace").decode()
        if col_name not in self.missing:
            self.missing[col_name] = {}
        section = "Movies Missing (TMDb IDs)" if is_movie else "Shows Missing (TVDb IDs)"
        if section not in self.missing[col_name]:
            self.missing[col_name][section] = {}
        for title, item_id in items:
            self.missing[col_name][section][int(item_id)] = str(title).encode(
                "ascii", "replace").decode()
        with open(self.missing_path, "w"):
            pass
        try:
            yaml.round_trip_dump(self.missing, open(self.missing_path, "w"))
        except yaml.scanner.ScannerError as e:
            logger.error("YAML Error: {}".format(
                str(e).replace("\n", "\n|\t      ")))

    def add_to_collection(self, collection, items, filters, show_filtered, map,
                          movie_map, show_map):
        name = collection.title if isinstance(collection,
                                              Collections) else collection
        collection_items = collection.items() if isinstance(
            collection, Collections) else []
        total = len(items)
        max_length = len(str(total))
        length = 0
        for i, item in enumerate(items, 1):
            try:
                current = self.fetchItem(
                    item.ratingKey if isinstance(item, (Movie,
                                                        Show)) else int(item))
            except (BadRequest, NotFound):
                logger.error("Plex Error: Item {} not found".format(item))
                continue
            match = True
            if filters:
                length = util.print_return(
                    length, "Filtering {}/{} {}".format(
                        (" " * (max_length - len(str(i)))) + str(i), total,
                        current.title))
                for f in filters:
                    modifier = f[0][-4:]
                    method = util.filter_alias[f[0][:-4]] if modifier in [
                        ".not", ".lte", ".gte"
                    ] else util.filter_alias[f[0]]
                    if method == "max_age":
                        threshold_date = datetime.now() - timedelta(days=f[1])
                        attr = getattr(current, "originallyAvailableAt")
                        if attr is None or attr < threshold_date:
                            match = False
                            break
                    elif method == "original_language":
                        terms = util.get_list(f[1], lower=True)
                        tmdb_id = None
                        movie = None
                        for key, value in movie_map.items():
                            if current.ratingKey == value:
                                try:
                                    movie = self.TMDb.get_movie(key)
                                    break
                                except Failed:
                                    pass
                        if movie is None:
                            logger.warning(
                                "Filter Error: No TMDb ID found for {}".format(
                                    current.title))
                            continue
                        if (modifier == ".not"
                                and movie.original_language in terms) or (
                                    modifier != ".not"
                                    and movie.original_language not in terms):
                            match = False
                            break
                    elif modifier in [".gte", ".lte"]:
                        if method == "originallyAvailableAt":
                            threshold_date = datetime.strptime(
                                f[1], "%m/%d/%y")
                            attr = getattr(current, "originallyAvailableAt")
                            if (modifier == ".lte" and attr > threshold_date
                                ) or (modifier == ".gte"
                                      and attr < threshold_date):
                                match = False
                                break
                        elif method in ["year", "rating"]:
                            attr = getattr(current, method)
                            if (modifier == ".lte"
                                    and attr > f[1]) or (modifier == ".gte"
                                                         and attr < f[1]):
                                match = False
                                break
                    else:
                        terms = util.get_list(f[1])
                        if method in [
                                "video_resolution", "audio_language",
                                "subtitle_language"
                        ]:
                            for media in current.media:
                                if method == "video_resolution":
                                    attrs = [media.videoResolution]
                                for part in media.parts:
                                    if method == "audio_language":
                                        attrs = ([
                                            a.language
                                            for a in part.audioStreams()
                                        ])
                                    if method == "subtitle_language":
                                        attrs = ([
                                            s.language
                                            for s in part.subtitleStreams()
                                        ])
                        elif method in [
                                "contentRating", "studio", "year", "rating",
                                "originallyAvailableAt"
                        ]:
                            attrs = [str(getattr(current, method))]
                        elif method in [
                                "actors", "countries", "directors", "genres",
                                "writers", "collections"
                        ]:
                            attrs = [
                                getattr(x, "tag")
                                for x in getattr(current, method)
                            ]

                        if (not list(set(terms) & set(attrs)) and modifier !=
                                ".not") or (list(set(terms) & set(attrs))
                                            and modifier == ".not"):
                            match = False
                            break
                length = util.print_return(
                    length, "Filtering {}/{} {}".format(
                        (" " * (max_length - len(str(i)))) + str(i), total,
                        current.title))
            if match:
                util.print_end(
                    length, "{} Collection | {} | {}".format(
                        name, "=" if current in collection_items else "+",
                        current.title))
                if current in collection_items: map[current.ratingKey] = None
                else: current.addCollection(name)
            elif show_filtered is True:
                logger.info("{} Collection | X | {}".format(
                    name, current.title))
        media_type = "{}{}".format("Movie" if self.is_movie else "Show",
                                   "s" if total > 1 else "")
        util.print_end(length, "{} {} Processed".format(total, media_type))
        return map

    def search_item(self, data, year=None):
        return util.choose_from_list(self.search(data, year=year),
                                     "movie" if self.is_movie else "show",
                                     str(data),
                                     exact=True)

    def update_metadata(self, TMDb, test):
        logger.info("")
        util.seperator("{} Library Metadata".format(self.name))
        logger.info("")
        if not self.metadata:
            raise Failed("No metadata to edit")
        for m in self.metadata:
            if test and ("test" not in self.metadata[m]
                         or self.metadata[m]["test"] is not True):
                continue
            logger.info("")
            util.seperator()
            logger.info("")
            year = None
            if "year" in self.metadata[m]:
                now = datetime.datetime.now()
                if self.metadata[m]["year"] is None:
                    logger.error("Metadata Error: year attribute is blank")
                elif not isinstance(self.metadata[m]["year"], int):
                    logger.error(
                        "Metadata Error: year attribute must be an integer")
                elif self.metadata[m]["year"] not in range(1800, now.year + 2):
                    logger.error(
                        "Metadata Error: year attribute must be between 1800-{}"
                        .format(now.year + 1))
                else:
                    year = self.metadata[m]["year"]

            title = m
            if "title" in self.metadata[m]:
                if self.metadata[m]["title"] is None:
                    logger.error("Metadata Error: title attribute is blank")
                else:
                    title = self.metadata[m]["title"]

            item = self.search_item(title, year=year)

            if item is None:
                item = self.search_item("{} (SUB)".format(title), year=year)

            if item is None and "alt_title" in self.metadata[m]:
                if self.metadata[m]["alt_title"] is None:
                    logger.error(
                        "Metadata Error: alt_title attribute is blank")
                else:
                    alt_title = self.metadata[m]["alt_title"]
                    item = self.search_item(alt_title, year=year)

            if item is None:
                logger.error("Plex Error: Item {} not found".format(m))
                logger.error("Skipping {}".format(m))
                continue

            logger.info("Updating {}: {}...".format(
                "Movie" if self.is_movie else "Show", title))

            tmdb_item = None
            try:
                if "tmdb_id" in self.metadata[m]:
                    if self.metadata[m]["tmdb_id"] is None:
                        logger.error(
                            "Metadata Error: tmdb_id attribute is blank")
                    elif self.is_show:
                        logger.error(
                            "Metadata Error: tmdb_id attribute only works with movie libraries"
                        )
                    else:
                        tmdb_item = TMDb.get_show(
                            util.regex_first_int(self.metadata[m]["tmdb_id"],
                                                 "Show"))
            except Failed as e:
                logger.error(e)

            originally_available = tmdb_item.first_air_date if tmdb_item else None
            rating = tmdb_item.vote_average if tmdb_item else None
            original_title = tmdb_item.original_name if tmdb_item and tmdb_item.original_name != tmdb_item.name else None
            studio = tmdb_item.networks[0].name if tmdb_item else None
            tagline = tmdb_item.tagline if tmdb_item and len(
                tmdb_item.tagline) > 0 else None
            summary = tmdb_item.overview if tmdb_item else None

            edits = {}

            def add_edit(name, current, group, key=None, value=None):
                if value or name in group:
                    if value or group[name]:
                        if key is None: key = name
                        if value is None: value = group[name]
                        if str(current) != str(value):
                            edits["{}.value".format(key)] = value
                            edits["{}.locked".format(key)] = 1
                    else:
                        logger.error(
                            "Metadata Error: {} attribute is blank".format(
                                name))

            add_edit("title", item.title, self.metadata[m], value=title)
            add_edit("sort_title",
                     item.titleSort,
                     self.metadata[m],
                     key="titleSort")
            add_edit("originally_available",
                     str(item.originallyAvailableAt)[:-9],
                     self.metadata[m],
                     key="originallyAvailableAt",
                     value=originally_available)
            add_edit("rating", item.rating, self.metadata[m], value=rating)
            add_edit("content_rating",
                     item.contentRating,
                     self.metadata[m],
                     key="contentRating")
            item_original_title = item.originalTitle if self.is_movie else item._data.attrib.get(
                "originalTitle")
            add_edit("original_title",
                     item_original_title,
                     self.metadata[m],
                     key="originalTitle",
                     value=original_title)
            add_edit("studio", item.studio, self.metadata[m], value=studio)
            item_tagline = item.tagline if self.is_movie else item._data.attrib.get(
                "tagline")
            add_edit("tagline", item_tagline, self.metadata[m], value=tagline)
            add_edit("summary", item.summary, self.metadata[m], value=summary)
            if len(edits) > 0:
                logger.debug("Details Update: {}".format(edits))
                try:
                    item.edit(**edits)
                    item.reload()
                    logger.info("{}: {} Details Update Successful".format(
                        "Movie" if self.is_movie else "Show", m))
                except BadRequest:
                    util.print_stacktrace()
                    logger.error("{}: {} Details Update Failed".format(
                        "Movie" if self.is_movie else "Show", m))
            else:
                logger.info("{}: {} Details Update Not Needed".format(
                    "Movie" if self.is_movie else "Show", m))

            genres = []

            if tmdb_item:
                genres.extend([genre.name for genre in tmdb_item.genres])

            if "genre" in self.metadata[m]:
                if self.metadata[m]["genre"]:
                    genres.extend(util.get_list(self.metadata[m]["genre"]))
                else:
                    logger.error("Metadata Error: genre attribute is blank")

            if len(genres) > 0:
                item_genres = [genre.tag for genre in item.genres]
                if "genre_sync_mode" in self.metadata[m]:
                    if self.metadata[m]["genre_sync_mode"] is None:
                        logger.error(
                            "Metadata Error: genre_sync_mode attribute is blank defaulting to append"
                        )
                    elif self.metadata[m]["genre_sync_mode"] not in [
                            "append", "sync"
                    ]:
                        logger.error(
                            "Metadata Error: genre_sync_mode attribute must be either 'append' or 'sync' defaulting to append"
                        )
                    elif self.metadata[m]["genre_sync_mode"] == "sync":
                        for genre in (g for g in item_genres
                                      if g not in genres):
                            item.removeGenre(genre)
                            logger.info(
                                "Detail: Genre {} removed".format(genre))
                for genre in (g for g in genres if g not in item_genres):
                    item.addGenre(genre)
                    logger.info("Detail: Genre {} added".format(genre))

            if "label" in self.metadata[m]:
                if self.metadata[m]["label"]:
                    item_labels = [label.tag for label in item.labels]
                    labels = util.get_list(self.metadata[m]["label"])
                    if "label_sync_mode" in self.metadata[m]:
                        if self.metadata[m]["label_sync_mode"] is None:
                            logger.error(
                                "Metadata Error: label_sync_mode attribute is blank defaulting to append"
                            )
                        elif self.metadata[m]["label_sync_mode"] not in [
                                "append", "sync"
                        ]:
                            logger.error(
                                "Metadata Error: label_sync_mode attribute must be either 'append' or 'sync' defaulting to append"
                            )
                        elif self.metadata[m]["label_sync_mode"] == "sync":
                            for label in (l for l in item_labels
                                          if l not in labels):
                                item.removeLabel(label)
                                logger.info(
                                    "Detail: Label {} removed".format(label))
                    for label in (l for l in labels if l not in item_labels):
                        item.addLabel(label)
                        logger.info("Detail: Label {} added".format(label))
                else:
                    logger.error("Metadata Error: label attribute is blank")

            if "seasons" in self.metadata[m] and self.is_show:
                if self.metadata[m]["seasons"]:
                    for season_id in self.metadata[m]["seasons"]:
                        logger.info("")
                        logger.info("Updating season {} of {}...".format(
                            season_id, m))
                        if isinstance(season_id, int):
                            try:
                                season = item.season(season_id)
                            except NotFound:
                                logger.error(
                                    "Metadata Error: Season: {} not found".
                                    format(season_id))
                            else:

                                if "title" in self.metadata[m]["seasons"][
                                        season_id] and self.metadata[m][
                                            "seasons"][season_id]["title"]:
                                    title = self.metadata[m]["seasons"][
                                        season_id]["title"]
                                else:
                                    title = season.title
                                if "sub" in self.metadata[m]["seasons"][
                                        season_id]:
                                    if self.metadata[m]["seasons"][season_id][
                                            "sub"] is None:
                                        logger.error(
                                            "Metadata Error: sub attribute is blank"
                                        )
                                    elif self.metadata[m]["seasons"][season_id][
                                            "sub"] is True and "(SUB)" not in title:
                                        title = "{} (SUB)".format(title)
                                    elif self.metadata[m]["seasons"][season_id][
                                            "sub"] is False and title.endswith(
                                                " (SUB)"):
                                        title = title[:-6]
                                    else:
                                        logger.error(
                                            "Metadata Error: sub attribute must be True or False"
                                        )

                                edits = {}
                                add_edit(
                                    "title",
                                    season.title,
                                    self.metadata[m]["seasons"][season_id],
                                    value=title)
                                add_edit(
                                    "summary", season.summary,
                                    self.metadata[m]["seasons"][season_id])
                                if len(edits) > 0:
                                    logger.debug(
                                        "Season: {} Details Update: {}".format(
                                            season_id, edits))
                                    try:
                                        season.edit(**edits)
                                        season.reload()
                                        logger.info(
                                            "Season: {} Details Update Successful"
                                            .format(season_id))
                                    except BadRequest:
                                        util.print_stacktrace()
                                        logger.error(
                                            "Season: {} Details Update Failed".
                                            format(season_id))
                                else:
                                    logger.info(
                                        "Season: {} Details Update Not Needed".
                                        format(season_id))
                        else:
                            logger.error(
                                "Metadata Error: Season: {} invalid, it must be an integer"
                                .format(season_id))
                else:
                    logger.error("Metadata Error: seasons attribute is blank")

            if "episodes" in self.metadata[m] and self.is_show:
                if self.metadata[m]["episodes"]:
                    for episode_str in self.metadata[m]["episodes"]:
                        logger.info("")
                        match = re.search("[Ss]{1}\d+[Ee]{1}\d+", episode_str)
                        if match:
                            output = match.group(0)[1:].split(
                                "E" if "E" in m.group(0) else "e")
                            episode_id = int(output[0])
                            season_id = int(output[1])
                            logger.info(
                                "Updating episode S{}E{} of {}...".format(
                                    episode_id, season_id, m))
                            try:
                                episode = item.episode(season=season_id,
                                                       episode=episode_id)
                            except NotFound:
                                logger.error(
                                    "Metadata Error: episode {} of season {} not found"
                                    .format(episode_id, season_id))
                            else:
                                if "title" in self.metadata[m]["episodes"][
                                        episode_str] and self.metadata[m][
                                            "episodes"][episode_str]["title"]:
                                    title = self.metadata[m]["episodes"][
                                        episode_str]["title"]
                                else:
                                    title = episode.title
                                if "sub" in self.metadata[m]["episodes"][
                                        episode_str]:
                                    if self.metadata[m]["episodes"][
                                            episode_str]["sub"] is None:
                                        logger.error(
                                            "Metadata Error: sub attribute is blank"
                                        )
                                    elif self.metadata[m]["episodes"][episode_str][
                                            "sub"] is True and "(SUB)" not in title:
                                        title = "{} (SUB)".format(title)
                                    elif self.metadata[m]["episodes"][
                                            episode_str][
                                                "sub"] is False and title.endswith(
                                                    " (SUB)"):
                                        title = title[:-6]
                                    else:
                                        logger.error(
                                            "Metadata Error: sub attribute must be True or False"
                                        )
                                edits = {}
                                add_edit(
                                    "title",
                                    episode.title,
                                    self.metadata[m]["episodes"][episode_str],
                                    value=title)
                                add_edit(
                                    "sort_title",
                                    episode.titleSort,
                                    self.metadata[m]["episodes"][episode_str],
                                    key="titleSort")
                                add_edit(
                                    "rating", episode.rating,
                                    self.metadata[m]["episodes"][episode_str])
                                add_edit(
                                    "originally_available",
                                    str(episode.originallyAvailableAt)[:-9],
                                    self.metadata[m]["episodes"][episode_str],
                                    key="originallyAvailableAt")
                                add_edit(
                                    "summary", episode.summary,
                                    self.metadata[m]["episodes"][episode_str])
                                if len(edits) > 0:
                                    logger.debug(
                                        "Season: {} Episode: {} Details Update: {}"
                                        .format(season_id, episode_id, edits))
                                    try:
                                        episode.edit(**edits)
                                        episode.reload()
                                        logger.info(
                                            "Season: {} Episode: {} Details Update Successful"
                                            .format(season_id, episode_id))
                                    except BadRequest:
                                        util.print_stacktrace()
                                        logger.error(
                                            "Season: {} Episode: {} Details Update Failed"
                                            .format(season_id, episode_id))
                                else:
                                    logger.info(
                                        "Season: {} Episode: {} Details Update Not Needed"
                                        .format(season_id, episode_id))
                        else:
                            logger.error(
                                "Metadata Error: episode {} invlaid must have S##E## format"
                                .format(episode_str))
                else:
                    logger.error("Metadata Error: episodes attribute is blank")