version 1.14.0

pull/514/head
meisnate12 3 years ago
parent 63acb4abc5
commit 29cc04f6c4

@ -1 +1 @@
1.13.3-develop1222 1.14.0

@ -42,7 +42,8 @@ method_alias = {
"minimum_tag_percentage": "min_tag_percent", "minimumtagrank": "min_tag_percent", "minimum_tag_rank": "min_tag_percent", "minimum_tag_percentage": "min_tag_percent", "minimumtagrank": "min_tag_percent", "minimum_tag_rank": "min_tag_percent",
"anilist_tag": "anilist_search", "anilist_genre": "anilist_search", "anilist_season": "anilist_search", "anilist_tag": "anilist_search", "anilist_genre": "anilist_search", "anilist_season": "anilist_search",
"mal_producer": "mal_studio", "mal_licensor": "mal_studio", "mal_producer": "mal_studio", "mal_licensor": "mal_studio",
"trakt_recommended": "trakt_recommended_weekly", "trakt_watched": "trakt_watched_weekly", "trakt_collected": "trakt_collected_weekly" "trakt_recommended": "trakt_recommended_weekly", "trakt_watched": "trakt_watched_weekly", "trakt_collected": "trakt_collected_weekly",
"collection_changes_webhooks": "changes_webhooks"
} }
filter_translation = { filter_translation = {
"actor": "actors", "actor": "actors",
@ -85,10 +86,11 @@ boolean_details = [
scheduled_boolean = ["visible_library", "visible_home", "visible_shared"] scheduled_boolean = ["visible_library", "visible_home", "visible_shared"]
string_details = ["sort_title", "content_rating", "name_mapping"] string_details = ["sort_title", "content_rating", "name_mapping"]
ignored_details = [ ignored_details = [
"smart_filter", "smart_label", "smart_url", "run_again", "schedule", "sync_mode", "template", "test", "delete_not_scheduled", "smart_filter", "smart_label", "smart_url", "run_again", "schedule", "sync_mode", "template", "test",
"tmdb_person", "build_collection", "collection_order", "collection_level", "validate_builders", "collection_name", "sort_by", "libraries", "sync_to_users" "delete_not_scheduled", "tmdb_person", "build_collection", "collection_order", "collection_level",
"validate_builders", "sort_by", "libraries", "sync_to_users", "collection_name", "playlist_name", "name"
] ]
details = ["ignore_ids", "ignore_imdb_ids", "server_preroll", "collection_changes_webhooks", "collection_mode", details = ["ignore_ids", "ignore_imdb_ids", "server_preroll", "changes_webhooks", "collection_mode",
"collection_minimum", "label"] + boolean_details + scheduled_boolean + string_details "collection_minimum", "label"] + boolean_details + scheduled_boolean + string_details
collectionless_details = ["collection_order", "plex_collectionless", "label", "label_sync_mode", "test"] + \ collectionless_details = ["collection_order", "plex_collectionless", "label", "label_sync_mode", "test"] + \
poster_details + background_details + summary_details + string_details poster_details + background_details + summary_details + string_details
@ -101,7 +103,7 @@ sonarr_details = [
"sonarr_quality", "sonarr_season", "sonarr_search", "sonarr_cutoff_search", "sonarr_tag" "sonarr_quality", "sonarr_season", "sonarr_search", "sonarr_cutoff_search", "sonarr_tag"
] ]
parts_collection_valid = [ parts_collection_valid = [
"plex_search", "trakt_list", "trakt_list_details", "collection_mode", "label", "visible_library", "collection_changes_webhooks" "plex_search", "trakt_list", "trakt_list_details", "collection_mode", "label", "visible_library", "changes_webhooks"
"visible_home", "visible_shared", "show_missing", "save_missing", "missing_only_released", "server_preroll", "visible_home", "visible_shared", "show_missing", "save_missing", "missing_only_released", "server_preroll",
"item_lock_background", "item_lock_poster", "item_lock_title", "item_refresh", "imdb_list" "item_lock_background", "item_lock_poster", "item_lock_title", "item_refresh", "imdb_list"
] + summary_details + poster_details + background_details + string_details ] + summary_details + poster_details + background_details + string_details
@ -168,13 +170,13 @@ custom_sort_builders = [
"mal_popular", "mal_favorite", "mal_suggested", "mal_userlist", "mal_season", "mal_genre", "mal_studio" "mal_popular", "mal_favorite", "mal_suggested", "mal_userlist", "mal_season", "mal_genre", "mal_studio"
] ]
playlist_attributes = [ playlist_attributes = [
"playlist_name", "filters", "name_mapping", "show_filtered", "show_missing", "save_missing", "filters", "name_mapping", "show_filtered", "show_missing", "save_missing",
"missing_only_released", "only_filter_missing", "delete_below_minimum", "ignore_ids", "ignore_imdb_ids", "missing_only_released", "only_filter_missing", "delete_below_minimum", "ignore_ids", "ignore_imdb_ids",
"server_preroll", "collection_changes_webhooks", "collection_minimum" "server_preroll", "changes_webhooks", "collection_minimum",
] + custom_sort_builders + summary_details + poster_details + background_details + radarr_details + sonarr_details ] + custom_sort_builders + summary_details + poster_details + radarr_details + sonarr_details
class CollectionBuilder: class CollectionBuilder:
def __init__(self, config, library, metadata, name, no_missing, data, playlist=False): def __init__(self, config, library, metadata, name, no_missing, data, playlist=False, valid_users=None):
self.config = config self.config = config
self.library = library self.library = library
self.metadata = metadata self.metadata = metadata
@ -182,6 +184,7 @@ class CollectionBuilder:
self.no_missing = no_missing self.no_missing = no_missing
self.data = data self.data = data
self.playlist = playlist self.playlist = playlist
self.valid_users = valid_users
self.language = self.library.Plex.language self.language = self.library.Plex.language
self.details = { self.details = {
"show_filtered": self.library.show_filtered, "show_filtered": self.library.show_filtered,
@ -194,7 +197,7 @@ class CollectionBuilder:
"create_asset_folders": self.library.create_asset_folders, "create_asset_folders": self.library.create_asset_folders,
"delete_below_minimum": self.library.delete_below_minimum, "delete_below_minimum": self.library.delete_below_minimum,
"delete_not_scheduled": self.library.delete_not_scheduled, "delete_not_scheduled": self.library.delete_not_scheduled,
"collection_changes_webhooks": self.library.collection_changes_webhooks "changes_webhooks": self.library.changes_webhooks
} }
self.item_details = {} self.item_details = {}
self.radarr_details = {} self.radarr_details = {}
@ -232,13 +235,21 @@ class CollectionBuilder:
methods = {m.lower(): m for m in self.data} methods = {m.lower(): m for m in self.data}
if f"{self.type}_name" in methods: if "name" in methods:
name = self.data[methods["name"]]
elif f"{self.type}_name" in methods:
logger.warning(f"Config Warning: Running {self.type}_name as name")
name = self.data[methods[f"{self.type}_name"]]
else:
name = None
if name:
logger.debug("") logger.debug("")
logger.debug(f"Validating Method: {self.type}_name") logger.debug("Validating Method: name")
if not self.data[methods[f"{self.type}_name"]]: if not name:
raise Failed(f"{self.Type} Error: {self.type}_name attribute is blank") raise Failed(f"{self.Type} Error: name attribute is blank")
logger.debug(f"Value: {self.data[methods['{}_name'.format(self.type)]]}") logger.debug(f"Value: {name}")
self.name = self.data[methods[f"{self.type}_name"]] self.name = name
else: else:
self.name = self.mapping_name self.name = self.mapping_name
@ -269,13 +280,13 @@ class CollectionBuilder:
suffix = "" suffix = ""
if self.details["delete_not_scheduled"]: if self.details["delete_not_scheduled"]:
try: try:
self.obj = self.library.get_collection(self.name) self.obj = self.library.get_playlist(self.name) if self.playlist else self.library.get_collection(self.name)
self.delete_collection() util.print_multiline(self.delete())
self.deleted = True self.deleted = True
suffix = f" and was deleted" suffix = f" and was deleted"
except Failed: except Failed:
suffix = f" and could not be found to delete" suffix = f" and could not be found to delete"
raise NotScheduled(f"{e}\n\nCollection {self.name} not scheduled to run{suffix}") raise NotScheduled(f"{e}\n\n{self.Type} {self.name} not scheduled to run{suffix}")
self.collectionless = "plex_collectionless" in methods and not self.playlist self.collectionless = "plex_collectionless" in methods and not self.playlist
@ -674,7 +685,7 @@ class CollectionBuilder:
self.details["label.sync"] = util.get_list(method_data) if method_data else [] self.details["label.sync"] = util.get_list(method_data) if method_data else []
else: else:
self.details[method_final] = util.get_list(method_data) if method_data else [] self.details[method_final] = util.get_list(method_data) if method_data else []
elif method_name == "collection_changes_webhooks": elif method_name == "changes_webhooks":
self.details[method_name] = self._parse(method_name, method_data, datatype="list") self.details[method_name] = self._parse(method_name, method_data, datatype="list")
elif method_name in scheduled_boolean: elif method_name in scheduled_boolean:
if isinstance(method_data, bool): if isinstance(method_data, bool):
@ -1570,7 +1581,7 @@ class CollectionBuilder:
else: else:
self.library.alter_collection(item, name, smart_label_collection=self.smart_label_collection) self.library.alter_collection(item, name, smart_label_collection=self.smart_label_collection)
amount_added += 1 amount_added += 1
if self.details["collection_changes_webhooks"]: if self.details["changes_webhooks"]:
if item.ratingKey in self.library.movie_rating_key_map: if item.ratingKey in self.library.movie_rating_key_map:
add_id = self.library.movie_rating_key_map[item.ratingKey] add_id = self.library.movie_rating_key_map[item.ratingKey]
elif item.ratingKey in self.library.show_rating_key_map: elif item.ratingKey in self.library.show_rating_key_map:
@ -1605,7 +1616,7 @@ class CollectionBuilder:
else: else:
self.library.alter_collection(item, self.name, smart_label_collection=self.smart_label_collection, add=False) self.library.alter_collection(item, self.name, smart_label_collection=self.smart_label_collection, add=False)
amount_removed += 1 amount_removed += 1
if self.details["collection_changes_webhooks"]: if self.details["changes_webhooks"]:
if item.ratingKey in self.library.movie_rating_key_map: if item.ratingKey in self.library.movie_rating_key_map:
remove_id = self.library.movie_rating_key_map[item.ratingKey] remove_id = self.library.movie_rating_key_map[item.ratingKey]
elif item.ratingKey in self.library.show_rating_key_map: elif item.ratingKey in self.library.show_rating_key_map:
@ -1950,10 +1961,6 @@ class CollectionBuilder:
os.remove(og_image) os.remove(og_image)
self.config.Cache.update_image_map(item.ratingKey, self.library.image_table_name, "", "") self.config.Cache.update_image_map(item.ratingKey, self.library.image_table_name, "", "")
def delete_collection(self):
if self.obj:
self.library.query(self.obj.delete)
def load_collection(self): def load_collection(self):
if not self.obj and self.smart_url: if not self.obj and self.smart_url:
self.library.create_smart_collection(self.name, self.smart_type_key, self.smart_url) self.library.create_smart_collection(self.name, self.smart_type_key, self.smart_url)
@ -2141,25 +2148,27 @@ class CollectionBuilder:
user_playlist = user_server.playlist(title) user_playlist = user_server.playlist(title)
user_playlist.delete() user_playlist.delete()
def delete_playlist(self, users): def delete(self):
output = ""
if self.obj: if self.obj:
self.library.query(self.obj.delete) self.library.query(self.obj.delete)
logger.info("") output = f"{self.Type} {self.obj.title} deleted"
logger.info(f"Playlist {self.obj.title} deleted") if self.playlist:
if users: if self.valid_users:
for user in users: for user in self.valid_users:
try: try:
self.delete_user_playlist(self.obj.title, user) self.delete_user_playlist(self.obj.title, user)
logger.info(f"Playlist {self.obj.title} deleted on User {user}") output += f"\nPlaylist {self.obj.title} deleted on User {user}"
except NotFound: except NotFound:
logger.error(f"Playlist {self.obj.title} not found on User {user}") output += f"\nPlaylist {self.obj.title} not found on User {user}"
return output
def sync_playlist(self, users):
if self.obj and users: def sync_playlist(self):
if self.obj and self.valid_users:
logger.info("") logger.info("")
util.separator(f"Syncing Playlist to Users", space=False, border=False) util.separator(f"Syncing Playlist to Users", space=False, border=False)
logger.info("") logger.info("")
for user in users: for user in self.valid_users:
try: try:
self.delete_user_playlist(self.obj.title, user) self.delete_user_playlist(self.obj.title, user)
except NotFound: except NotFound:
@ -2168,12 +2177,12 @@ class CollectionBuilder:
logger.info(f"Playlist: {self.name} synced to {user}") logger.info(f"Playlist: {self.name} synced to {user}")
def send_notifications(self, playlist=False): def send_notifications(self, playlist=False):
if self.obj and self.details["collection_changes_webhooks"] and \ if self.obj and self.details["changes_webhooks"] and \
(self.created or len(self.notification_additions) > 0 or len(self.notification_removals) > 0): (self.created or len(self.notification_additions) > 0 or len(self.notification_removals) > 0):
self.obj.reload() self.obj.reload()
try: try:
self.library.Webhooks.collection_hooks( self.library.Webhooks.collection_hooks(
self.details["collection_changes_webhooks"], self.details["changes_webhooks"],
self.obj, self.obj,
poster_url=self.collection_poster.location if self.collection_poster and self.collection_poster.is_url else None, poster_url=self.collection_poster.location if self.collection_poster and self.collection_poster.is_url else None,
background_url=self.collection_background.location if self.collection_background and self.collection_background.is_url else None, background_url=self.collection_background.location if self.collection_background and self.collection_background.is_url else None,

@ -33,7 +33,7 @@ sync_modes = {"append": "Only Add Items to the Collection or Playlist", "sync":
mass_update_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb"} mass_update_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb"}
class ConfigFile: class ConfigFile:
def __init__(self, default_dir, attrs, read_only=False): def __init__(self, default_dir, attrs, read_only):
logger.info("Locating config...") logger.info("Locating config...")
config_file = attrs["config_file"] config_file = attrs["config_file"]
if config_file and os.path.exists(config_file): self.config_path = os.path.abspath(config_file) if config_file and os.path.exists(config_file): self.config_path = os.path.abspath(config_file)
@ -94,13 +94,14 @@ class ConfigFile:
hooks("collection_creation") hooks("collection_creation")
hooks("collection_addition") hooks("collection_addition")
hooks("collection_removal") hooks("collection_removal")
new_config["libraries"][library]["webhooks"]["collection_changes"] = changes if changes else None hooks("collection_changes")
new_config["libraries"][library]["webhooks"]["changes"] = changes if changes else None
if "libraries" in new_config: new_config["libraries"] = new_config.pop("libraries") if "libraries" in new_config: new_config["libraries"] = new_config.pop("libraries")
if "playlists" in new_config: new_config["playlists"] = new_config.pop("playlists") if "playlists" in new_config: new_config["playlists"] = new_config.pop("playlists")
if "settings" in new_config: new_config["settings"] = new_config.pop("settings") if "settings" in new_config: new_config["settings"] = new_config.pop("settings")
if "webhooks" in new_config: if "webhooks" in new_config:
temp = new_config.pop("webhooks") temp = new_config.pop("webhooks")
if "collection_changes" not in temp: if "changes" not in temp:
changes = [] changes = []
def hooks(attr): def hooks(attr):
if attr in temp: if attr in temp:
@ -110,7 +111,8 @@ class ConfigFile:
hooks("collection_creation") hooks("collection_creation")
hooks("collection_addition") hooks("collection_addition")
hooks("collection_removal") hooks("collection_removal")
temp["collection_changes"] = changes if changes else None hooks("collection_changes")
temp["changes"] = changes if changes else None
new_config["webhooks"] = temp new_config["webhooks"] = temp
if "plex" in new_config: new_config["plex"] = new_config.pop("plex") if "plex" in new_config: new_config["plex"] = new_config.pop("plex")
if "tmdb" in new_config: new_config["tmdb"] = new_config.pop("tmdb") if "tmdb" in new_config: new_config["tmdb"] = new_config.pop("tmdb")
@ -252,7 +254,7 @@ class ConfigFile:
"error": check_for_attribute(self.data, "error", parent="webhooks", var_type="list", default_is_none=True), "error": check_for_attribute(self.data, "error", parent="webhooks", var_type="list", default_is_none=True),
"run_start": check_for_attribute(self.data, "run_start", parent="webhooks", var_type="list", default_is_none=True), "run_start": check_for_attribute(self.data, "run_start", parent="webhooks", var_type="list", default_is_none=True),
"run_end": check_for_attribute(self.data, "run_end", parent="webhooks", var_type="list", default_is_none=True), "run_end": check_for_attribute(self.data, "run_end", parent="webhooks", var_type="list", default_is_none=True),
"collection_changes": check_for_attribute(self.data, "collection_changes", parent="webhooks", var_type="list", default_is_none=True) "changes": check_for_attribute(self.data, "changes", parent="webhooks", var_type="list", default_is_none=True)
} }
if self.general["cache"]: if self.general["cache"]:
util.separator() util.separator()
@ -534,7 +536,7 @@ class ConfigFile:
params["ignore_imdb_ids"] = check_for_attribute(lib, "ignore_imdb_ids", parent="settings", var_type="list", default_is_none=True, do_print=False, save=False) params["ignore_imdb_ids"] = check_for_attribute(lib, "ignore_imdb_ids", parent="settings", var_type="list", default_is_none=True, do_print=False, save=False)
params["ignore_imdb_ids"].extend([i for i in self.general["ignore_imdb_ids"] if i not in params["ignore_imdb_ids"]]) params["ignore_imdb_ids"].extend([i for i in self.general["ignore_imdb_ids"] if i not in params["ignore_imdb_ids"]])
params["error_webhooks"] = check_for_attribute(lib, "error", parent="webhooks", var_type="list", default=self.webhooks["error"], do_print=False, save=False, default_is_none=True) params["error_webhooks"] = check_for_attribute(lib, "error", parent="webhooks", var_type="list", default=self.webhooks["error"], do_print=False, save=False, default_is_none=True)
params["collection_changes_webhooks"] = check_for_attribute(lib, "collection_changes", parent="webhooks", var_type="list", default=self.webhooks["collection_changes"], do_print=False, save=False, default_is_none=True) params["changes_webhooks"] = check_for_attribute(lib, "changes", parent="webhooks", var_type="list", default=self.webhooks["changes"], do_print=False, save=False, default_is_none=True)
params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", parent="settings", var_type="bool", default=self.general["assets_for_all"], do_print=False, save=False) params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", parent="settings", var_type="bool", default=self.general["assets_for_all"], do_print=False, save=False)
params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=False) params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=False)
params["mass_audience_rating_update"] = check_for_attribute(lib, "mass_audience_rating_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=False) params["mass_audience_rating_update"] = check_for_attribute(lib, "mass_audience_rating_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=False)

@ -74,7 +74,7 @@ class Library(ABC):
self.tmdb_collections = params["tmdb_collections"] self.tmdb_collections = params["tmdb_collections"]
self.genre_mapper = params["genre_mapper"] self.genre_mapper = params["genre_mapper"]
self.error_webhooks = params["error_webhooks"] self.error_webhooks = params["error_webhooks"]
self.collection_changes_webhooks = params["collection_changes_webhooks"] self.changes_webhooks = params["changes_webhooks"]
self.split_duplicates = params["split_duplicates"] # TODO: Here or just in Plex? self.split_duplicates = params["split_duplicates"] # TODO: Here or just in Plex?
self.clean_bundles = params["plex"]["clean_bundles"] # TODO: Here or just in Plex? self.clean_bundles = params["plex"]["clean_bundles"] # TODO: Here or just in Plex?
self.empty_trash = params["plex"]["empty_trash"] # TODO: Here or just in Plex? self.empty_trash = params["plex"]["empty_trash"] # TODO: Here or just in Plex?

@ -34,7 +34,7 @@ parser.add_argument("-rc", "-cl", "--collection", "--collections", "--run-collec
parser.add_argument("-rl", "-l", "--library", "--libraries", "--run-library", "--run-libraries", dest="libraries", help="Process only specified libraries (comma-separated list)", type=str) parser.add_argument("-rl", "-l", "--library", "--libraries", "--run-library", "--run-libraries", dest="libraries", help="Process only specified libraries (comma-separated list)", type=str)
parser.add_argument("-nc", "--no-countdown", dest="no_countdown", help="Run without displaying the countdown", action="store_true", default=False) parser.add_argument("-nc", "--no-countdown", dest="no_countdown", help="Run without displaying the countdown", action="store_true", default=False)
parser.add_argument("-nm", "--no-missing", dest="no_missing", help="Run without running the missing section", action="store_true", default=False) parser.add_argument("-nm", "--no-missing", dest="no_missing", help="Run without running the missing section", action="store_true", default=False)
parser.add_argument("-ro", "--read-only-config", dest="read_only_config", help="Config must be read only", action="store_true", default=False) parser.add_argument("-ro", "--read-only-config", dest="read_only_config", help="Run without writing to the config", action="store_true", default=False)
parser.add_argument("-d", "--divider", dest="divider", help="Character that divides the sections (Default: '=')", default="=", type=str) parser.add_argument("-d", "--divider", dest="divider", help="Character that divides the sections (Default: '=')", default="=", type=str)
parser.add_argument("-w", "--width", dest="width", help="Screen Width (Default: 100)", default=100, type=int) parser.add_argument("-w", "--width", dest="width", help="Screen Width (Default: 100)", default=100, type=int)
args = parser.parse_args() args = parser.parse_args()
@ -721,10 +721,9 @@ def run_collection(config, library, metadata, requested_collections):
logger.info(f"Collection Minimum: {builder.minimum} not met for {mapping_name} Collection") logger.info(f"Collection Minimum: {builder.minimum} not met for {mapping_name} Collection")
valid = False valid = False
if builder.details["delete_below_minimum"] and builder.obj: if builder.details["delete_below_minimum"] and builder.obj:
builder.delete_collection()
builder.deleted = True
logger.info("") logger.info("")
logger.info(f"Collection {builder.obj.title} deleted") util.print_multiline(builder.delete(), info=True)
builder.deleted = True
if builder.do_missing and (len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0): if builder.do_missing and (len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0):
if builder.details["show_missing"] is True: if builder.details["show_missing"] is True:
@ -875,7 +874,7 @@ def run_playlists(config):
util.separator(f"Validating {mapping_name} Attributes", space=False, border=False) util.separator(f"Validating {mapping_name} Attributes", space=False, border=False)
builder = CollectionBuilder(config, pl_libraries[0], playlist_file, mapping_name, no_missing, builder = CollectionBuilder(config, pl_libraries[0], playlist_file, mapping_name, no_missing,
playlist_attrs, playlist=True) playlist_attrs, playlist=True, valid_users=valid_users)
logger.info("") logger.info("")
util.separator(f"Running {mapping_name} Playlist", space=False, border=False) util.separator(f"Running {mapping_name} Playlist", space=False, border=False)
@ -1043,7 +1042,8 @@ def run_playlists(config):
logger.info(f"Playlist Minimum: {builder.minimum} not met for {mapping_name} Playlist") logger.info(f"Playlist Minimum: {builder.minimum} not met for {mapping_name} Playlist")
valid = False valid = False
if builder.details["delete_below_minimum"] and builder.obj: if builder.details["delete_below_minimum"] and builder.obj:
builder.delete_playlist(valid_users) logger.info("")
util.print_multiline(builder.delete(), info=True)
builder.deleted = True builder.deleted = True
if builder.do_missing and (len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0): if builder.do_missing and (len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0):
@ -1086,7 +1086,7 @@ def run_playlists(config):
builder.sort_collection() builder.sort_collection()
if valid: if valid:
builder.sync_playlist(valid_users) builder.sync_playlist()
builder.send_notifications(playlist=True) builder.send_notifications(playlist=True)

Loading…
Cancel
Save