Files
wizarr/app/blueprints/api/models.py
Matthieu B 6ebafbf328 fix: deduplicate wizard steps in multi-server setups and add bundle selection to API
Default wizard steps were duplicated once per server instance of the same
type (e.g., 2 Plex servers → 6 steps instead of 3). Deduplicate the
server type list before loading steps so each type's steps appear only once.

Also add the missing wizard_bundle_id parameter to the invitation creation
API, matching the existing web UI capability.

Closes #1219
2026-04-01 22:33:27 +02:00

269 lines
8.5 KiB
Python

"""OpenAPI models for Flask-RESTX API documentation."""
from flask_restx import fields
from app.extensions import api
# Status Models
status_model = api.model(
"Status",
{
"users": fields.Integer(description="Total number of users"),
"invites": fields.Integer(description="Total number of invitations"),
"pending": fields.Integer(description="Number of pending invitations"),
"expired": fields.Integer(description="Number of expired invitations"),
},
)
# Admin Models
admin_model = api.model(
"Admin",
{
"id": fields.Integer(description="Admin ID"),
"username": fields.String(description="Admin username"),
"passkeys": fields.Integer(description="Number of passkeys for this admin"),
"created": fields.DateTime(description="Creation date (ISO format)"),
},
)
admin_list_model = api.model(
"AdminList",
{
"admins": fields.List(fields.Nested(admin_model)),
"count": fields.Integer(description="Total number of admins"),
},
)
# User Models
user_model = api.model(
"User",
{
"id": fields.Integer(description="User ID"),
"username": fields.String(description="Username"),
"email": fields.String(description="Email address"),
"server": fields.String(description="Media server name"),
"server_type": fields.String(
description="Type of media server (plex, jellyfin, etc.)"
),
"expires": fields.DateTime(description="Expiration date (ISO format)"),
"created_at": fields.DateTime(description="Creation date (ISO format)"),
},
)
user_list_model = api.model(
"UserList",
{
"users": fields.List(fields.Nested(user_model)),
"count": fields.Integer(description="Total number of users"),
},
)
user_extend_request = api.model(
"UserExtendRequest",
{
"days": fields.Integer(
description="Number of days to extend (default: 30)", default=30
),
},
)
user_extend_response = api.model(
"UserExtendResponse",
{
"message": fields.String(description="Success message"),
"new_expiry": fields.DateTime(description="New expiration date"),
},
)
user_update_expiry_request = api.model(
"UserUpdateExpiryRequest",
{
"expires": fields.DateTime(
description="New expiration date (ISO format). Use null for unlimited access.",
required=False,
allow_null=True,
),
},
)
user_update_expiry_response = api.model(
"UserUpdateExpiryResponse",
{
"message": fields.String(description="Success message"),
"new_expiry": fields.DateTime(
description="New expiration date (null for unlimited)",
allow_null=True,
),
},
)
# Invitation Models
invitation_model = api.model(
"Invitation",
{
"id": fields.Integer(description="Invitation ID"),
"code": fields.String(description="Invitation code"),
"url": fields.String(description="Ready-to-use invitation URL"),
"status": fields.String(
description="Invitation status", enum=["pending", "used", "expired"]
),
"created": fields.DateTime(description="Creation date (ISO format)"),
"expires": fields.DateTime(description="Expiration date (ISO format)"),
"used_at": fields.DateTime(description="Date when invitation was used"),
"used_by": fields.String(description="Username who used the invitation"),
"duration": fields.String(
description='User access duration in days or "unlimited"'
),
"unlimited": fields.Boolean(description="Whether user access is unlimited"),
"specific_libraries": fields.List(
fields.Integer, description="Specific library IDs if restricted"
),
"display_name": fields.String(description="Display name for the invitation"),
"server_names": fields.List(fields.String, description="List of server names"),
"uses_global_setting": fields.Boolean(
description="Whether display name comes from global setting"
),
},
)
invitation_list_model = api.model(
"InvitationList",
{
"invitations": fields.List(fields.Nested(invitation_model)),
"count": fields.Integer(description="Total number of invitations"),
},
)
invitation_create_request = api.model(
"InvitationCreateRequest",
{
"server_ids": fields.List(
fields.Integer,
required=False,
description="Array of server IDs (required, but validated by API for better error messages)",
),
"expires_in_days": fields.Integer(
description="Days until invitation expires (1, 7, 30, or null)",
enum=[1, 7, 30],
),
"duration": fields.String(
description='User access duration in days or "unlimited"',
default="unlimited",
),
"unlimited": fields.Boolean(
description="Whether user access is unlimited", default=True
),
"library_ids": fields.List(
fields.Integer, description="Array of library IDs to grant access to"
),
"allow_downloads": fields.Boolean(
description="Allow user downloads", default=False
),
"allow_live_tv": fields.Boolean(
description="Allow live TV access", default=False
),
"allow_mobile_uploads": fields.Boolean(
description="Allow mobile uploads", default=False
),
"wizard_bundle_id": fields.Integer(
required=False,
description="Wizard bundle ID to use for this invitation (omit for automatic selection)",
),
},
)
invitation_create_response = api.model(
"InvitationCreateResponse",
{
"message": fields.String(description="Success message"),
"invitation": fields.Nested(invitation_model),
},
)
# Library Models
library_model = api.model(
"Library",
{
"id": fields.Integer(description="Library ID"),
"name": fields.String(description="Library name"),
"external_id": fields.String(
description="External library ID from media server"
),
"server_id": fields.Integer(description="Server ID this library belongs to"),
"server_name": fields.String(description="Server name this library belongs to"),
"enabled": fields.Boolean(description="Whether this library is enabled"),
},
)
library_list_model = api.model(
"LibraryList",
{
"libraries": fields.List(fields.Nested(library_model)),
"count": fields.Integer(description="Total number of libraries"),
},
)
# Server Models
server_model = api.model(
"Server",
{
"id": fields.Integer(description="Server ID"),
"name": fields.String(description="Server name"),
"server_type": fields.String(
description="Server type (plex, jellyfin, emby, etc.)"
),
"server_url": fields.String(description="Internal server URL"),
"external_url": fields.String(description="External server URL"),
"verified": fields.Boolean(description="Whether server connection is verified"),
"allow_downloads": fields.Boolean(description="Whether downloads are allowed"),
"allow_live_tv": fields.Boolean(description="Whether live TV is allowed"),
"allow_mobile_uploads": fields.Boolean(
description="Whether mobile uploads are allowed"
),
"created_at": fields.DateTime(description="Server creation date"),
},
)
server_list_model = api.model(
"ServerList",
{
"servers": fields.List(fields.Nested(server_model)),
"count": fields.Integer(description="Total number of servers"),
},
)
# API Key Models
api_key_model = api.model(
"ApiKey",
{
"id": fields.Integer(description="API key ID"),
"name": fields.String(description="API key name"),
"created_at": fields.DateTime(description="Creation date"),
"last_used_at": fields.DateTime(description="Last used date"),
"created_by": fields.String(description="Username who created the key"),
},
)
api_key_list_model = api.model(
"ApiKeyList",
{
"api_keys": fields.List(fields.Nested(api_key_model)),
"count": fields.Integer(description="Total number of API keys"),
},
)
# Error Models
error_model = api.model(
"Error",
{
"error": fields.String(description="Error message"),
},
)
success_message_model = api.model(
"SuccessMessage",
{
"message": fields.String(description="Success message"),
},
)