from abc import abstractmethod
from typing import Union, Optional, List, TYPE_CHECKING
from arrapi import NotFound, Invalid, Exists, Excluded
from arrapi.objs.base import BaseObj
if TYPE_CHECKING:
from arrapi.objs.simple import RootFolder
class ReloadObj(BaseObj):
def __init__(self, arr, data, load=False):
super().__init__(arr, data)
if load:
self._load(None)
@abstractmethod
def _load(self, data):
self._partial = data is not None
super()._load(self._full_load() if data is None else data)
@abstractmethod
def _full_load(self):
pass
def reload(self):
""" Reloads the Object """
self._load(None)
[docs]
class QualityProfile(ReloadObj):
""" Represents a single Quality Profile.
Attributes:
id (int): ID of the Quality Profile.
name (str): Name of the Quality Profile.
"""
def _load(self, data):
super()._load(data)
self.id = self._parse(attrs="id", value_type="int")
self.name = self._parse(attrs="name")
self._finish(self.name)
def _full_load(self):
return self._raw.get_qualityProfileId(self.id)
[docs]
class LanguageProfile(ReloadObj):
""" Represents a single Language Profile.
Attributes:
id (int): ID of the Language Profile.
name (str): Name of the Language Profile.
"""
def _load(self, data):
super()._load(data)
self.id = self._parse(attrs="id", value_type="int")
self.name = self._parse(attrs="name")
self._finish(self.name)
def _full_load(self):
return self._raw.get_languageProfileId(self.id)
[docs]
class SystemStatus(ReloadObj):
""" Represents the System Status.
Attributes:
version (str): Version of the Arr Instance.
Check dir(SystemStatus) for all attribute as the rest are auto built.
"""
def __init__(self, arr):
super().__init__(arr, None)
def _load(self, data):
super()._load(None)
self.version = ""
for key, value in self._data.items():
if key.startswith("is") or key == "migrationVersion":
setattr(self, key, self._parse(data=value, value_type="int"))
elif key.endswith("Time"):
setattr(self, key, self._parse(data=value, value_type="date"))
elif key == "version":
self.version = value
else:
setattr(self, key, value)
self._finish(self.version)
def _full_load(self):
return self._raw.get_system_status()
[docs]
class Tag(ReloadObj):
""" Represents a single Tag.
Attributes:
id (int): ID of the Tag.
label (str): Label of the Tag.
detail (bool): If the Tag was loaded with details.
delayProfileIds (List[int]): Delay Profile IDs. (Only when loaded with details)
notificationIds (List[int]): Notification IDs. (Only when loaded with details)
restrictionIds (List[int]): Restriction IDs. (Only when loaded with details)
importListIds (List[int]): Import List IDs. (Only when loaded with details)
movieIds (List[int]): Radarr Movie IDs. (Only when loaded with details using :class:`~arrapi.apis.radarr.RadarrAPI`)
seriesIds (List[int]): Sonarr Series IDs. (Only when loaded with details using :class:`~arrapi.apis.radarr.SonarrAPI`)
artistIds (List[int]): Lidarr Artist IDs. (Only when loaded with details using :class:`~arrapi.apis.lidarr.LidarrAPI`)
authorIds (List[int]): Readarr Author IDs. (Only when loaded with details using :class:`~arrapi.apis.readarr.ReadarrAPI`)
"""
def _load(self, data):
super()._load(data)
self.label = self._parse(attrs="label")
self.id = self._parse(attrs="id", value_type="int", default_is_none=True)
self.detail = False
if "delayProfileIds" in self._data:
self.detail = True
self.delayProfileIds = self._parse(attrs="delayProfileIds", value_type="int", is_list=True)
self.notificationIds = self._parse(attrs="notificationIds", value_type="int", is_list=True)
self.restrictionIds = self._parse(attrs="restrictionIds", value_type="int", is_list=True)
self.importListIds = self._parse(attrs="importListIds", value_type="int", is_list=True)
if "movieIds" in self._data:
self.movieIds = self._parse(attrs="movieIds", value_type="int", is_list=True)
if "seriesIds" in self._data:
self.seriesIds = self._parse(attrs="seriesIds", value_type="int", is_list=True)
if "artistIds" in self._data:
self.artistIds = self._parse(attrs="artistIds", value_type="int", is_list=True)
if "authorIds" in self._data:
self.authorIds = self._parse(attrs="authorIds", value_type="int", is_list=True)
self._finish(self.label)
def __str__(self):
return f"[{self.id}:{self._name}]" if self.id and self._name else f"[Tag:{self.id}]"
def _full_load(self):
return self._raw.get_tag_id(self.id, detail=self.detail)
[docs]
def edit(self, label: str) -> None:
""" Edit the :class:`~arrapi.objs.reload.Tag`.
Parameters:
label (str): Label to change tag to.
"""
self._load(self._raw.put_tag_id(self.id, label))
[docs]
def delete(self) -> None:
""" Delete the :class:`~arrapi.objs.reload.Tag`."""
self._raw.delete_tag_id(self.id)
[docs]
class Command(ReloadObj):
""" Represents a single Command.
Attributes:
name (str): Name.
commandName (str): Full named.
message (str): Status Message.
body (dict): Body of dict sent with the command.
priority (str): Priority level.
status (str): Status state.
queued (datetime): Datetime queued.
started (datetime): Datetime started.
ended (datetime): Datetime ended.
duration (str): Duration.
exception (str): Exception message if there is a failure.
trigger (str): Command trigger.
stateChangeTime (datetime): Datetime the sate was cahnged.
sendUpdatesToClient (bool): Send Updates To Client.
updateScheduledTask (bool):Update Scheduled Task.
id (int): ID of the Command.
"""
def _load(self, data):
super()._load(data)
self.name = self._parse(attrs="name")
self.commandName = self._parse(attrs="commandName")
self.message = self._parse(attrs="message")
self.body = self._parse(attrs="body", value_type="dict")
self.priority = self._parse(attrs="priority")
self.status = self._parse(attrs="status")
self.queued = self._parse(attrs="queued", value_type="date")
self.started = self._parse(attrs="started", value_type="date")
self.ended = self._parse(attrs="ended", value_type="date")
self.duration = self._parse(attrs="duration")
self.exception = self._parse(attrs="exception")
self.trigger = self._parse(attrs="trigger")
self.sendUpdatesToClient = self._parse(attrs="sendUpdatesToClient", value_type="bool")
self.updateScheduledTask = self._parse(attrs="updateScheduledTask", value_type="bool")
self.stateChangeTime = self._parse(attrs="stateChangeTime", value_type="date")
self.lastExecutionTime = self._parse(attrs="lastExecutionTime", value_type="date")
self.id = self._parse(attrs="id", value_type="int")
self._finish(self.name)
def _full_load(self):
return self._raw.get_command_id(self.id)
[docs]
class Movie(ReloadObj):
""" Represents a single Movie.
Attributes:
id (int): ID of the Movie.
title (str): Title of the Movie.
sortTitle (str): Sort Title of the Movie.
sizeOnDisk (int): Movie Size On Disk.
status (str): Status of the Movie.
overview (str): Overview of the Movie.
inCinemas (datetime): Date the Movie was in Cinemas.
physicalRelease (datetime): Date the Movie was Physically Released.
digitalRelease (datetime): Date the Movie was Digitally Released.
images (List[:class:`~arrapi.objs.simple.Image`]): List of Images for the Movie.
website (str): Website of the Movie.
year (int): Year of the Movie.
hasFile (bool): If the Movie has a file.
youTubeTrailerId (str): YouTube Trailer ID for the Movie.
studio (str): Studio of the Movie.
path (str): Path of the Movie.
monitored (bool): If the Movie is monitored.
minimumAvailability (str): Minimum Availability of the Movie.
isAvailable (bool): If the Movie is Available.
folderName (str): Folder Name of the Movie.
folder (str): Folder name.
runtime (int): Runtime of the Movie.
cleanTitle (str): Clean Title of the Movie.
imdbId (str): IMDb ID of the Movie.
tmdbId (int): TMDb ID of the Movie.
titleSlug (str): Title Slug of the Movie.
certification (str): Certification of the Movie.
genres (List[str]): List of genres for the Movie.
tags (List[:class:`~arrapi.objs.reload.Tag`]): List of tags for the Movie.
added (datetime): Date the Movie was added to Radarr.
tagsIds (List[int]): List of tag ids for the Movie.
rating_votes (int): Number of votes for the Movie.
rating_value (float): Rating of the Movie.
originalTitle (str): Original Title of the Movie. (Radarr v3 Only)
digitalRelease (datetime): Date the Movie was Released Digitally. (Radarr v3 Only)
qualityProfileId (int): Quality Profile ID of the Movie. (Radarr v3 Only)
qualityProfile (:class:`~arrapi.objs.reload.QualityProfile`): Quality Profile of the Movie. (Radarr v3 Only)
collection_name (str): Collection Name of the Movie. (Radarr v3 Only)
collection_tmdbId (int): TMDb Collection ID for the Movie. (Radarr v3 Only)
downloaded (bool): If the Movie has been Downloaded. (Radarr v2 Only)
profileId (int): Quality Profile ID of the Movie. (Radarr v2 Only)
profile (:class:`~arrapi.objs.reload.QualityProfile`): Quality Profile of the Movie. (Radarr v2 Only)
"""
def __init__(self, radarr, data=None, movie_id=None, tmdb_id=None, imdb_id=None):
self._loading = True
self.id = movie_id
self.tmdbId = tmdb_id
self.imdbId = imdb_id
super().__init__(radarr, data, load=movie_id or tmdb_id or imdb_id)
def _load(self, data):
super()._load(data)
self.title = self._parse(attrs="title")
self.sortTitle = self._parse(attrs="sortTitle")
self.sizeOnDisk = self._parse(attrs="sizeOnDisk", value_type="int")
self.status = self._parse(attrs="status")
self.overview = self._parse(attrs="overview")
self.inCinemas = self._parse(attrs="inCinemas", value_type="date")
self.physicalRelease = self._parse(attrs="physicalRelease", value_type="date")
self.digitalRelease = self._parse(attrs="digitalRelease", value_type="date")
self.images = self._parse(attrs="images", value_type="image", is_list=True)
self.website = self._parse(attrs="website")
self.year = self._parse(attrs="year", value_type="int")
self.hasFile = self._parse(attrs="hasFile", value_type="bool")
self.youTubeTrailerId = self._parse(attrs="youTubeTrailerId")
self.studio = self._parse(attrs="studio")
self.path = self._parse(attrs="path")
self.monitored = self._parse(attrs="monitored", value_type="bool")
self.minimumAvailability = self._parse(attrs="minimumAvailability")
self.isAvailable = self._parse(attrs="isAvailable", value_type="bool")
self.folderName = self._parse(attrs="folderName")
self.folder = self._parse(attrs="folder")
self.runtime = self._parse(attrs="runtime", value_type="int")
self.cleanTitle = self._parse(attrs="cleanTitle")
self.imdbId = self._parse(attrs="imdbId")
self.tmdbId = self._parse(attrs="tmdbId", value_type="int", default_is_none=True)
self.titleSlug = self._parse(attrs="titleSlug")
self.certification = self._parse(attrs="certification")
self.genres = self._parse(attrs="genres", is_list=True)
self.tagsIds = self._parse(attrs="tags", value_type="int", is_list=True)
self.tags = self._parse(attrs="tags", value_type="intTag", is_list=True)
self.added = self._parse(attrs="added", value_type="date")
self.rating_votes = self._parse(attrs=["rating", "votes"], value_type="int")
self.rating_value = self._parse(attrs=["rating", "value"], value_type="float")
self.collection = self._parse(attrs="collection", value_type="collection")
self.id = self._parse(attrs="id", value_type="int", default_is_none=True)
if self._raw.new_codebase:
self.originalTitle = self._parse(attrs="originalTitle")
self.digitalRelease = self._parse(attrs="digitalRelease", value_type="date")
self.qualityProfileId = self._parse(attrs="qualityProfileId", value_type="int")
self.qualityProfile = self._parse(attrs="qualityProfileId", value_type="intQualityProfile")
self.collection_name = self._parse(attrs=["collection", "name"])
self.collection_tmdbId = self._parse(attrs=["collection", "tmdbId"], value_type="int", default_is_none=True)
else:
self.downloaded = self._parse(attrs="downloaded", value_type="bool")
self.profileId = self._parse(attrs="profileId", value_type="int")
self.profile = self._parse(attrs="profileId", value_type="intQualityProfile")
self._finish(self.title)
def _full_load(self):
if self.id:
return self._raw.get_movie_id(self.id)
elif self.tmdbId or self.imdbId:
items = self._raw.get_movie_lookup(f"tmdb:{self.tmdbId}" if self.tmdbId else f"imdb:{self.imdbId}")
if items:
return items[0]
else:
raise NotFound("Item Not Found")
else:
raise Invalid("Load Failed: No Load Input")
def _get_add_data(self, options, path=None):
if self.id:
raise Exists(f"{self.title} is already in Radarr")
self._data.pop("Id", None)
self._data["monitored"] = options["monitor"]
if path:
if not path.startswith(options["root_folder"]):
raise Invalid(f"Individual Path: {path} must start with the Root Folder: {options['root_folder']} ")
self._data["path"] = path
else:
self._data["rootFolderPath"] = options["root_folder"]
self._data["rootFolderPath"] = options["root_folder"]
self._data["qualityProfileId" if self._raw.new_codebase else "profileId"] = options["quality_profile"]
self._data["minimumAvailability"] = options["minimum_availability"]
self._data["addOptions"] = {"searchForMovie": options["search"]}
if "tags" in options:
self._data["tags"] = options["tags"]
return self._data
[docs]
def add(self,
root_folder: Union[str, int, "RootFolder"],
quality_profile: Union[str, int, "QualityProfile"],
monitor: bool = True,
search: bool = True,
minimum_availability: str = "announced",
tags: Optional[List[Union[str, int, Tag]]] = None
) -> None:
""" Add this Movie to Radarr.
Parameters:
root_folder (Union[str, int, RootFolder]): Root Folder for the Movie.
quality_profile (Union[str, int, QualityProfile]): Quality Profile for the Movie.
monitor (bool): Monitor the Movie.
search (bool): Search for the Movie after adding.
minimum_availability (str): Minimum Availability for the Movie. Valid options are announced, inCinemas, released, or preDB.
tags (Optional[List[Union[str, int, Tag]]]): Tags to be added to the Movie.
Raises:
:class:`~arrapi.exceptions.Invalid`: When one of the options given is invalid.
:class:`~arrapi.exceptions.Exists`: When the Movie already Exists in Radarr.
"""
if self._arr.exclusions and self.tmdbId in self._arr.exclusions:
raise Excluded(f"TMDb ID: {self.tmdbId} is excluded from being added.")
self._load(self._raw.post_movie(self._get_add_data(self._arr._validate_add_options(
root_folder,
quality_profile,
monitor=monitor,
search=search,
minimum_availability=minimum_availability,
tags=tags
))))
[docs]
def edit(self,
path: Optional[str] = None,
move_files: bool = False,
quality_profile: Optional[Union[str, int, "QualityProfile"]] = None,
monitored: Optional[bool] = None,
minimum_availability: Optional[str] = None,
tags: Optional[List[Union[str, int, Tag]]] = None,
apply_tags: str = "add"
) -> None:
""" Edit this Movie in Radarr.
Parameters:
path (Optional[str]): Path to change the Movie to.
move_files (bool): When changing the path do you want to move the files to the new path.
quality_profile (Optional[Union[str, int, QualityProfile]]): Quality Profile to change the Movie to.
monitored (Optional[bool]): Monitor the Movie.
minimum_availability (Optional[str]): Minimum Availability to change the Movie to. Valid options are announced, inCinemas, released, or preDB.
tags (Optional[List[Union[str, int, Tag]]]): Tags to be added, replaced, or removed from the Movie.
apply_tags (str): How you want to edit the Tags. Valid options are add, replace, or remove.
Raises:
:class:`ValueError`: When theres no options given.
:class:`~arrapi.exceptions.Invalid`: When one of the options given is invalid.
:class:`~arrapi.exceptions.NotFound`: When the Movie hasn't been added to Radarr.
"""
if not self.id:
raise NotFound(f"{self.title} not found Radarr, it must be added before editing")
options = self._arr._validate_edit_options(path=path, move_files=move_files, quality_profile=quality_profile,
monitored=monitored, minimum_availability=minimum_availability,
tags=tags, apply_tags=apply_tags)
valid_move_files = options["path"] if "path" in options else False
for key, value in options.items():
if key == "tags":
tag_type = options["applyTags"]
if tag_type == "add":
self._data[key].extend([t for t in value if t not in self._data["tags"]])
elif tag_type == "remove":
self._data[key] = [t for t in self._data["tags"] if t not in value]
elif tag_type == "replace":
self._data[key] = value
else:
raise Invalid(f"Invalid apply_tags: '{tag_type}' Options: {self._arr.apply_tags_options}")
elif key != ["applyTags", "moveFiles"]:
self._data[key] = value
self._load(self._raw.put_movie_id(self.id, self._data, moveFiles=valid_move_files))
[docs]
def delete(self, addImportExclusion: bool = False, deleteFiles: bool = False) -> None:
""" Delete this Movie from Radarr.
Parameters:
addImportExclusion (bool): Add Import Exclusion for this Movie.
deleteFiles (bool): Delete Files for this Movie.
Raises:
:class:`~arrapi.exceptions.NotFound`: When the Movie hasn't been added to Radarr.
"""
if not self.id:
raise NotFound(f"{self.title} not found Radarr, it must be added before deleting")
self._raw.delete_movie_id(self.id, addImportExclusion=addImportExclusion, deleteFiles=deleteFiles)
self._loading = True
self.id = None
self._loading = False
[docs]
class Series(ReloadObj):
""" Represents a single Series.
Attributes:
id (int): ID of the Series.
title (str): Title of the Series.
sortTitle (str): Sort Title of the Series.
status (str): Status of the Series.
overview (str): Overview of the Series.
seasons (List[:class:`~arrapi.objs.simple.Season`]): List of Seasons in the Series.
nextAiring (datetime): Date the next Episode in the Series Airs.
previousAiring (datetime): Date the latest Episode in the Series Aired.
network (str): Network the Series Airs on.
airTime (str): Time Series Airs.
images (List[:class:`~arrapi.objs.simple.Image`]): List of Images for the Series.
year (int): Year of the Series.
path (str): Path of the Series.
languageProfileId (int): Language Profile ID of the Series.
languageProfile (:class:`~arrapi.objs.reload.LanguageProfile`)): Language Profile of the Series.
seasonFolder (bool): If the Series has Season Folders.
monitored (bool): If the Series is monitored.
useSceneNumbering (bool): If the Series uses Scene Numbering.
folder (str): Folder name.
runtime (int): Runtime of the Series.
cleanTitle (str): Clean Title of the Series.
imdbId (str): IMDb ID of the Series.
tvdbId (int): TVDb ID of the Series.
tvRageId (int): TV Rage ID of the Series.
tvMazeId (int): TV Maze ID of the Series.
titleSlug (str): Title Slug of the Series.
firstAired (datetime): Date the Series First Aired.
seriesType (str): Series Type of the Series.
certification (str): Certification of the Series.
genres (List[str]): List of genres for the Series.
tags (List[:class:`~arrapi.objs.reload.Tag`]): List of tags for the Movie.
added (datetime): Date the Movie was added to Radarr.
tagsIds (List[int]): List of tag ids for the Movie.
rating_votes (int): Number of votes for the Series.
rating_value (float): Rating of the Series.
seasonCount (int): Number of Seasons in the Series.
totalEpisodeCount (int): Total Episodes in the Series.
episodeCount (int): Episodes in the Series.
episodeFileCount (int): Episode File Count for the Series.
sizeOnDisk (int): Size on Disk for the Series.
ended (bool): If the Series has ended. (Sonarr v3 Only)
rootFolderPath (str): Root Folder Path for the Series. (Sonarr v3 Only)
qualityProfileId (int): Quality Profile ID of the Series. (Sonarr v3 Only)
qualityProfile (:class:`~arrapi.objs.reload.QualityProfile`): Quality Profile of the Series. (Sonarr v3 Only)
percentOfEpisodes (float): Percent of Episodes obtained. (Sonarr v3 Only)
profileId (int): Quality Profile ID of the Series. (Sonarr v2 Only)
profile (:class:`~arrapi.objs.reload.QualityProfile`): Quality Profile of the Series. (Sonarr v2 Only)
"""
def __init__(self, sonarr, data=None, series_id=None, tvdb_id=None):
self._loading = True
self.id = series_id
self.tvdbId = tvdb_id
super().__init__(sonarr, data, load=series_id or tvdb_id)
def _load(self, data):
super()._load(data)
self.title = self._parse(attrs="title")
self.sortTitle = self._parse(attrs="sortTitle")
self.status = self._parse(attrs="status")
self.overview = self._parse(attrs="overview")
self.nextAiring = self._parse(attrs="nextAiring", value_type="date")
self.previousAiring = self._parse(attrs="previousAiring", value_type="date")
self.network = self._parse(attrs="network")
self.airTime = self._parse(attrs="airTime")
self.images = self._parse(attrs="images", value_type="image", is_list=True)
self.year = self._parse(attrs="year", value_type="int")
self.path = self._parse(attrs="path")
self.languageProfileId = self._parse(attrs="languageProfileId", value_type="int")
self.languageProfile = self._parse(attrs="languageProfileId", value_type="intLanguageProfile")
self.seasonFolder = self._parse(attrs="seasonFolder", value_type="bool")
self.monitored = self._parse(attrs="monitored", value_type="bool")
self.useSceneNumbering = self._parse(attrs="useSceneNumbering", value_type="bool")
self.folder = self._parse(attrs="folder")
self.runtime = self._parse(attrs="runtime", value_type="int")
self.cleanTitle = self._parse(attrs="cleanTitle")
self.imdbId = self._parse(attrs="imdbId")
self.tvdbId = self._parse(attrs="tvdbId", value_type="int")
self.tvRageId = self._parse(attrs="tvRageId", value_type="int")
self.tvMazeId = self._parse(attrs="tvMazeId", value_type="int")
self.titleSlug = self._parse(attrs="titleSlug")
self.firstAired = self._parse(attrs="firstAired", value_type="date")
self.seriesType = self._parse(attrs="seriesType")
self.certification = self._parse(attrs="certification")
self.genres = self._parse(attrs="genres", is_list=True)
self.tagsIds = self._parse(attrs="tags", value_type="int", is_list=True)
self.tags = self._parse(attrs="tags", value_type="intTag", is_list=True)
self.added = self._parse(attrs="added", value_type="date")
if "rating" in self._data:
self.rating_votes = self._parse(attrs=["rating", "votes"], value_type="int")
self.rating_value = self._parse(attrs=["rating", "value"], value_type="float")
self.id = self._parse(attrs="id", value_type="int", default_is_none=True)
self.seasons = self._parse(attrs="seasons", value_type="season", is_list=True)
if self._raw.new_codebase:
self.ended = self._parse(attrs="ended", value_type="bool")
self.rootFolderPath = self._parse(attrs="rootFolderPath")
self.qualityProfileId = self._parse(attrs="qualityProfileId", value_type="int")
self.qualityProfile = self._parse(attrs="qualityProfileId", value_type="intQualityProfile")
self.seasonCount = self._parse(attrs=["statistics", "seasonCount"], value_type="int")
self.totalEpisodeCount = self._parse(attrs=["statistics", "totalEpisodeCount"], value_type="int")
self.episodeCount = self._parse(attrs=["statistics", "episodeCount"], value_type="int")
self.episodeFileCount = self._parse(attrs=["statistics", "episodeFileCount"], value_type="int")
self.sizeOnDisk = self._parse(attrs=["statistics", "sizeOnDisk"], value_type="int")
self.percentOfEpisodes = self._parse(attrs=["statistics", "percentOfEpisodes"], value_type="float")
else:
self.profileId = self._parse(attrs="profileId", value_type="int")
self.profile = self._parse(attrs="profileId", value_type="intQualityProfile")
self.seasonCount = self._parse(attrs="seasonCount", value_type="int")
self.totalEpisodeCount = self._parse(attrs="totalEpisodeCount", value_type="int")
self.episodeCount = self._parse(attrs="episodeCount", value_type="int")
self.episodeFileCount = self._parse(attrs="episodeFileCount", value_type="int")
self.sizeOnDisk = self._parse(attrs="sizeOnDisk", value_type="int")
self._finish(self.title)
def _full_load(self):
if self.id:
return self._raw.get_series_id(self.id)
elif self.tvdbId:
items = self._raw.get_series_lookup(f"tvdb:{self.tvdbId}")
if items:
return items[0]
else:
raise NotFound("Item Not Found")
else:
raise Invalid("Load Failed: No Load Input")
def _get_add_data(self, options, path=None):
if self.id:
raise Exists(f"{self.title} is already in Sonarr")
self._data.pop("Id", None)
self._data["rootFolderPath"] = options["root_folder"]
if path:
if not path.startswith(options["root_folder"]):
raise Invalid(f"Individual Path: {path} must start with the Root Folder: {options['root_folder']} ")
self._data["path"] = path
else:
self._data["rootFolderPath"] = options["root_folder"]
self._data["monitored"] = options["monitored"]
self._data["qualityProfileId" if self._raw.new_codebase else "profileId"] = options["quality_profile"]
if "language_profile" in options:
self._data["languageProfileId"] = options["language_profile"]
self._data["seriesType"] = options["series_type"]
self._data["seasonFolder"] = options["season_folder"]
self._data["addOptions"] = {
"searchForMissingEpisodes": options["search"],
"searchForCutoffUnmetEpisodes": options["unmet_search"],
"monitor": options["monitor"]
}
if "tags" in options:
self._data["tags"] = options["tags"]
return self._data
[docs]
def add(self,
root_folder: Union[str, int, "RootFolder"],
quality_profile: Union[str, int, "QualityProfile"],
language_profile: Optional[Union[str, int, "LanguageProfile"]] = None,
monitor: str = "all",
season_folder: bool = True,
search: bool = True,
unmet_search: bool = True,
series_type: str = "standard",
tags: Optional[List[Union[str, int, Tag]]] = None) -> None:
""" Add this Series to Sonarr.
Parameters:
root_folder (Union[str, int, RootFolder]): Root Folder for the Series.
quality_profile (Union[str, int, QualityProfile]): Quality Profile for the Series.
language_profile (Optional[Union[str, int, LanguageProfile]]): Language Profile for the Series. Required for older versions.
monitor (bool): How to monitor the Series. Valid options are all, future, missing, existing, pilot, firstSeason, latestSeason, or none.
season_folder (bool): Use Season Folders for the Series.
search (bool): Start search for missing episodes of the Series after adding.
unmet_search (bool): Start search for cutoff unmet episodes of the Series after adding.
series_type (str): Series Type for the Series. Valid options are standard, daily, or anime.
tags (Optional[List[Union[str, int, Tag]]]): Tags to be added to the Series.
Raises:
:class:`~arrapi.exceptions.Invalid`: When one of the options given is invalid.
:class:`~arrapi.exceptions.Exists`: When the Series already Exists in Sonarr.
"""
if self._arr.exclusions and self.tvdbId in self._arr.exclusions:
raise Excluded(f"TVDb ID: {self.tvdbId} is excluded from being added.")
self._load(self._raw.post_series(self._get_add_data(self._arr._validate_add_options(
root_folder,
quality_profile,
language_profile=language_profile,
monitor=monitor,
season_folder=season_folder,
search=search,
unmet_search=unmet_search,
series_type=series_type,
tags=tags
))))
[docs]
def edit(self,
path: Optional[str] = None,
move_files: bool = False,
quality_profile: Optional[Union[str, int, "QualityProfile"]] = None,
language_profile: Optional[Union[str, int, "LanguageProfile"]] = None,
monitor: Optional[str] = None,
monitored: Optional[bool] = None,
season_folder: Optional[bool] = None,
series_type: Optional[str] = None,
tags: Optional[List[Union[str, int, Tag]]] = None,
apply_tags: str = "add"
) -> None:
""" Edit this Series in Sonarr.
Parameters:
path (Optional[str]): Path to change the Series to.
move_files (bool): When changing the path do you want to move the files to the new path.
quality_profile (Optional[Union[str, int, QualityProfile]]): Quality Profile to change the Series to.
language_profile (Optional[Union[str, int, LanguageProfile]]): Language Profile to change the Series to.
monitor (Optional[str]): How you want the Series monitored. Valid options are all, future, missing, existing, pilot, firstSeason, latestSeason, or none.
monitored (Optional[bool]): Monitor the Series.
season_folder (Optional[bool]): Use Season Folders for the Series.
series_type (Optional[str]): Series Type to change the Series to. Valid options are standard, daily, or anime.
tags (Optional[List[Union[str, int, Tag]]]): Tags to be added, replaced, or removed from the Series.
apply_tags (str): How you want to edit the Tags. Valid options are add, replace, or remove.
Raises:
:class:`ValueError`: When theres no options given.
:class:`~arrapi.exceptions.Invalid`: When one of the options given is invalid.
:class:`~arrapi.exceptions.NotFound`: When the Series hasn't been added to Sonarr.
"""
if not self.id:
raise NotFound(f"{self.title} not found in Sonarr, it must be added before editing")
options = self._arr._validate_edit_options(path=path, move_files=move_files, quality_profile=quality_profile,
language_profile=language_profile, monitor=monitor,
monitored=monitored, season_folder=season_folder,
series_type=series_type, tags=tags, apply_tags=apply_tags)
if "monitor" in options:
self._raw.edit_series_monitoring([self.id], options.pop("monitor"))
valid_move_files = options["path"] if "path" in options else False
for key, value in options.items():
if key == "tags":
tag_type = options["applyTags"]
if tag_type == "add":
self._data[key].extend([t for t in value if t not in self._data["tags"]])
elif tag_type == "remove":
self._data[key] = [t for t in self._data["tags"] if t not in value]
elif tag_type == "replace":
self._data[key] = value
else:
raise Invalid(f"Invalid apply_tags: '{tag_type}' Options: {self._arr.apply_tags_options}")
elif key != ["applyTags", "moveFiles"]:
self._data[key] = value
self._load(self._raw.put_series_id(self.id, self._data, moveFiles=valid_move_files))
[docs]
def delete(self, addImportListExclusion: bool = False, deleteFiles: bool = False) -> None:
""" Delete this Series from Sonarr.
Parameters:
addImportListExclusion (bool): Add Import Exclusion for this Series.
deleteFiles (bool): Delete Files for this Series.
Raises:
:class:`~arrapi.exceptions.NotFound`: When the Series hasn't been added to Sonarr.
"""
if not self.id:
raise NotFound(f"{self.title} not found in Sonarr, it must be added before deleting")
self._raw.delete_series_id(self.id, addImportListExclusion=addImportListExclusion, deleteFiles=deleteFiles)
self._loading = True
self.id = None
self._loading = False