323 lines
15 KiB
Python
323 lines
15 KiB
Python
import time
|
|
|
|
from flask import request, abort, jsonify
|
|
from flask_restx import Resource, Namespace, fields
|
|
from src.model.entity.Slide import Slide
|
|
from src.interface.ObController import ObController
|
|
from src.util.utils import str_datetime_to_cron, str_weekdaytime_to_cron, str_to_bool
|
|
from src.service.WebServer import create_require_api_key_decorator
|
|
|
|
# Namespace for slide operations
|
|
slide_ns = Namespace('slides', description='Operations on slides')
|
|
|
|
# Output model for a slide
|
|
slide_output_model = slide_ns.model('SlideOutput', {
|
|
'id': fields.Integer(readOnly=True, description='The unique identifier of a slide'),
|
|
'content_id': fields.Integer(description='The content ID for the slide'),
|
|
'playlist_id': fields.Integer(description='The playlist ID to which the slide belongs'),
|
|
'enabled': fields.Boolean(description='Is the slide enabled?'),
|
|
'delegate_duration': fields.Boolean(description='Should the duration be delegated?'),
|
|
'duration': fields.Integer(description='Duration of the slide'),
|
|
'position': fields.Integer(description='Position of the slide'),
|
|
'is_notification': fields.Boolean(description='Is the slide a notification?'),
|
|
'cron_schedule': fields.String(description='Cron expression for scheduling start'),
|
|
'cron_schedule_end': fields.String(description='Cron expression for scheduling end'),
|
|
})
|
|
|
|
# Input model for updating slide positions
|
|
positions_model = slide_ns.model('SlidePositions', {
|
|
'positions': fields.Raw(required=True, description='A dictionary where keys are slide IDs and values are their new positions')
|
|
})
|
|
|
|
# Parser for basic slide attributes
|
|
slide_base_parser = slide_ns.parser()
|
|
slide_base_parser.add_argument('content_id', type=int, required=True, help='The content ID for the slide')
|
|
slide_base_parser.add_argument('playlist_id', type=int, required=True, help='The playlist ID to which the slide belongs')
|
|
slide_base_parser.add_argument('enabled', type=str_to_bool, default=None, help='Is the slide enabled?')
|
|
slide_base_parser.add_argument('duration', type=int, default=3, help='Duration of the slide')
|
|
slide_base_parser.add_argument('position', type=int, default=999, help='Position of the slide')
|
|
|
|
# Parser for slide attributes (add)
|
|
slide_parser = slide_base_parser.copy()
|
|
slide_parser.add_argument('scheduling', type=str, required=True, help='Scheduling type: loop, datetime or inweek')
|
|
slide_parser.add_argument('delegate_duration', type=str_to_bool, default=None, help='Should the duration be delegated to video\'s duration?')
|
|
slide_parser.add_argument('datetime_start', type=str, required=False, help='Start datetime for scheduling (format: Y-m-d H:M)')
|
|
slide_parser.add_argument('datetime_end', type=str, required=False, help='End datetime for scheduling (format: Y-m-d H:M)')
|
|
slide_parser.add_argument('day_start', type=int, required=False, help='Start day for inweek scheduling (format: 1 for Monday to 7 for Sunday)')
|
|
slide_parser.add_argument('time_start', type=str, required=False, help='Start time for inweek scheduling (format: H:M)')
|
|
slide_parser.add_argument('day_end', type=int, required=False, help='End day for inweek scheduling (format: 1 for Monday to 7 for Sunday)')
|
|
slide_parser.add_argument('time_end', type=str, required=False, help='End time for inweek scheduling (format: H:M)')
|
|
|
|
# Parser for slide notification attributes (add)
|
|
slide_notification_parser = slide_base_parser.copy()
|
|
slide_notification_parser.add_argument('scheduling', type=str, required=True, help='Scheduling type: datetime or cron')
|
|
slide_notification_parser.add_argument('datetime_start', type=str, required=False, help='Start datetime for notification scheduling (format: Y-m-d H:M)')
|
|
slide_notification_parser.add_argument('datetime_end', type=str, required=False, help='End datetime for notification scheduling (format: Y-m-d H:M)')
|
|
slide_notification_parser.add_argument('cron_start', type=str, required=False, help='Cron expression for notification scheduling start (format: * * * * * * *)')
|
|
slide_notification_parser.add_argument('cron_end', type=str, required=False, help='Cron expression for notification scheduling end (format: * * * * * * *)')
|
|
|
|
# Parser for slide attributes (update)
|
|
slide_edit_parser = slide_parser.copy()
|
|
slide_edit_parser.replace_argument('scheduling', type=str, required=False, help='Scheduling type: loop, datetime, or inweek')
|
|
slide_edit_parser.replace_argument('content_id', type=int, required=False, help='The content ID for the slide')
|
|
slide_edit_parser.replace_argument('playlist_id', type=int, required=False, help='The playlist ID to which the slide belongs')
|
|
|
|
# Parser for slide notification attributes (update)
|
|
slide_notification_edit_parser = slide_notification_parser.copy()
|
|
slide_notification_edit_parser.replace_argument('scheduling', type=str, required=False, help='Scheduling type: datetime or cron')
|
|
slide_notification_edit_parser.replace_argument('content_id', type=int, required=False, help='The content ID for the slide')
|
|
slide_notification_edit_parser.replace_argument('playlist_id', type=int, required=False, help='The playlist ID to which the slide belongs')
|
|
|
|
|
|
class SlideApiController(ObController):
|
|
|
|
def register(self):
|
|
self.api().add_namespace(slide_ns, path='/api/slides')
|
|
slide_ns.add_resource(self.create_resource(SlideNotificationResource), '/notifications/<int:slide_id>')
|
|
slide_ns.add_resource(self.create_resource(SlideResource), '/<int:slide_id>')
|
|
slide_ns.add_resource(self.create_resource(SlideAddResource), '/')
|
|
slide_ns.add_resource(self.create_resource(SlideAddNotificationResource), '/notifications')
|
|
slide_ns.add_resource(self.create_resource(SlidePositionResource), '/positions')
|
|
|
|
def create_resource(self, resource_class):
|
|
# Function to inject dependencies into resources
|
|
return type(f'{resource_class.__name__}WithDependencies', (resource_class,), {
|
|
'_model_store': self._model_store,
|
|
'_controller': self,
|
|
'require_api_key': create_require_api_key_decorator(self._web_server)
|
|
})
|
|
|
|
def _add_slide_or_notification(self, data, is_notification=False):
|
|
if not data or 'content_id' not in data:
|
|
abort(400, description="Valid Content ID is required")
|
|
|
|
if not self._model_store.content().get(data.get('content_id')):
|
|
abort(404, description="Content not found")
|
|
|
|
if not data or 'playlist_id' not in data:
|
|
abort(400, description="Valid Playlist ID is required")
|
|
|
|
if not self._model_store.playlist().get(data.get('playlist_id')):
|
|
abort(404, description="Playlist not found")
|
|
|
|
cron_schedule_start, cron_schedule_end = self._resolve_scheduling(data, is_notification=is_notification)
|
|
|
|
slide = Slide(
|
|
content_id=data.get('content_id'),
|
|
enabled=data.get('enabled') if data.get('enabled') is not None else True,
|
|
delegate_duration=data.get('delegate_duration') if data.get('delegate_duration') is not None else False,
|
|
duration=data.get('duration', 3),
|
|
position=data.get('position', 999),
|
|
is_notification=is_notification,
|
|
playlist_id=data.get('playlist_id', None),
|
|
cron_schedule=cron_schedule_start,
|
|
cron_schedule_end=cron_schedule_end
|
|
)
|
|
|
|
slide = self._model_store.slide().add_form(slide)
|
|
self._post_update()
|
|
|
|
return slide.to_dict(), 201
|
|
|
|
def _resolve_scheduling(self, data, is_notification=False):
|
|
try:
|
|
return self._resolve_scheduling_for_notification(data) if is_notification else self._resolve_scheduling_for_slide(data)
|
|
except ValueError as ve:
|
|
abort(400, description=str(ve))
|
|
|
|
def _resolve_scheduling_for_slide(self, data):
|
|
scheduling = data.get('scheduling', 'loop')
|
|
cron_schedule_start = None
|
|
cron_schedule_end = None
|
|
|
|
if scheduling == 'loop':
|
|
pass
|
|
elif scheduling == 'datetime':
|
|
datetime_start = data.get('datetime_start')
|
|
datetime_end = data.get('datetime_end')
|
|
|
|
if not datetime_start:
|
|
abort(400, description="Field datetime_start is required for scheduling='datetime'")
|
|
|
|
cron_schedule_start = str_datetime_to_cron(datetime_str=datetime_start)
|
|
|
|
if datetime_end:
|
|
cron_schedule_end = str_datetime_to_cron(datetime_str=datetime_end)
|
|
elif scheduling == 'inweek':
|
|
day_start = data.get('day_start')
|
|
time_start = data.get('time_start')
|
|
day_end = data.get('day_end')
|
|
time_end = data.get('time_end')
|
|
|
|
if not (day_start and time_start and day_end and time_end):
|
|
abort(400, description="day_start, time_start, day_end, and time_end are required for scheduling='inweek'")
|
|
cron_schedule_start = str_weekdaytime_to_cron(weekday=int(day_start), time_str=time_start)
|
|
cron_schedule_end = str_weekdaytime_to_cron(weekday=int(day_end), time_str=time_end)
|
|
else:
|
|
abort(400, description="Invalid value for slide scheduling. Expected 'loop', 'datetime', or 'inweek'.")
|
|
|
|
return cron_schedule_start, cron_schedule_end
|
|
|
|
def _resolve_scheduling_for_notification(self, data):
|
|
scheduling = data.get('scheduling', 'datetime')
|
|
cron_schedule_start = None
|
|
cron_schedule_end = None
|
|
|
|
if scheduling == 'datetime':
|
|
datetime_start = data.get('datetime_start')
|
|
datetime_end = data.get('datetime_end')
|
|
|
|
if not datetime_start:
|
|
abort(400, description="Field datetime_start is required for scheduling='datetime'")
|
|
|
|
cron_schedule_start = str_datetime_to_cron(datetime_str=datetime_start)
|
|
|
|
if datetime_end:
|
|
cron_schedule_end = str_datetime_to_cron(datetime_str=datetime_end)
|
|
elif scheduling == 'cron':
|
|
cron_schedule_start = data.get('cron_start')
|
|
|
|
if not cron_schedule_start:
|
|
abort(400, description="Field cron_start is required for scheduling='cron'")
|
|
else:
|
|
abort(400, description="Invalid value for notification scheduling. Expected 'datetime' or 'cron'.")
|
|
|
|
return cron_schedule_start, cron_schedule_end
|
|
|
|
def _post_update(self):
|
|
self._model_store.variable().update_by_name("last_slide_update", time.time())
|
|
|
|
|
|
class SlideAddResource(Resource):
|
|
|
|
@slide_ns.expect(slide_parser)
|
|
@slide_ns.marshal_with(slide_output_model, code=201)
|
|
def post(self):
|
|
"""Add a new slide"""
|
|
self.require_api_key()
|
|
data = slide_parser.parse_args()
|
|
return self._controller._add_slide_or_notification(data, is_notification=False)
|
|
|
|
|
|
class SlideAddNotificationResource(Resource):
|
|
|
|
@slide_ns.expect(slide_notification_parser)
|
|
@slide_ns.marshal_with(slide_output_model, code=201)
|
|
def post(self):
|
|
"""Add a new slide notification"""
|
|
self.require_api_key()
|
|
data = slide_notification_parser.parse_args()
|
|
return self._controller._add_slide_or_notification(data, is_notification=True)
|
|
|
|
|
|
class SlideResource(Resource):
|
|
|
|
@slide_ns.marshal_with(slide_output_model)
|
|
def get(self, slide_id):
|
|
"""Get a slide by its ID"""
|
|
self.require_api_key()
|
|
slide = self._model_store.slide().get(slide_id)
|
|
if not slide:
|
|
abort(404, description="Slide not found")
|
|
return slide.to_dict()
|
|
|
|
@slide_ns.expect(slide_edit_parser)
|
|
@slide_ns.marshal_with(slide_output_model)
|
|
def put(self, slide_id):
|
|
"""Edit an existing slide"""
|
|
self.require_api_key()
|
|
data = slide_edit_parser.parse_args()
|
|
|
|
slide = self._model_store.slide().get(slide_id)
|
|
if not slide:
|
|
abort(404, description="Slide not found")
|
|
|
|
cron_schedule_start = slide.cron_schedule
|
|
cron_schedule_end = slide.cron_schedule_end
|
|
|
|
if data.get('scheduling'):
|
|
cron_schedule_start, cron_schedule_end = self._controller._resolve_scheduling(data, is_notification=slide.is_notification)
|
|
|
|
self._model_store.slide().update_form(
|
|
id=slide_id,
|
|
content_id=data.get('content_id', slide.content_id),
|
|
enabled=data.get('enabled', slide.enabled),
|
|
position=data.get('position', slide.position),
|
|
duration=data.get('duration', slide.duration),
|
|
cron_schedule=cron_schedule_start,
|
|
cron_schedule_end=cron_schedule_end
|
|
)
|
|
self._controller._post_update()
|
|
|
|
updated_slide = self._model_store.slide().get(slide_id)
|
|
return updated_slide.to_dict()
|
|
|
|
def delete(self, slide_id):
|
|
"""Delete a slide"""
|
|
self.require_api_key()
|
|
slide = self._model_store.slide().get(slide_id)
|
|
|
|
if not slide:
|
|
abort(404, description="Slide not found")
|
|
|
|
self._model_store.slide().delete(slide_id)
|
|
self._controller._post_update()
|
|
|
|
return '', 204
|
|
|
|
|
|
class SlideNotificationResource(Resource):
|
|
|
|
@slide_ns.expect(slide_notification_edit_parser)
|
|
@slide_ns.marshal_with(slide_output_model)
|
|
def put(self, slide_id):
|
|
"""Edit an existing slide notification"""
|
|
self.require_api_key()
|
|
data = slide_notification_edit_parser.parse_args()
|
|
|
|
slide = self._model_store.slide().get(slide_id)
|
|
if not slide:
|
|
abort(404, description="Slide not found")
|
|
|
|
cron_schedule_start = slide.cron_schedule
|
|
cron_schedule_end = slide.cron_schedule_end
|
|
|
|
if data.get('scheduling'):
|
|
cron_schedule_start, cron_schedule_end = self._controller._resolve_scheduling(data, is_notification=slide.is_notification)
|
|
|
|
self._model_store.slide().update_form(
|
|
id=slide_id,
|
|
content_id=data.get('content_id', slide.content_id),
|
|
enabled=data.get('enabled', slide.enabled),
|
|
position=data.get('position', slide.position),
|
|
delegate_duration=data.get('delegate_duration', slide.delegate_duration),
|
|
duration=data.get('duration', slide.duration),
|
|
cron_schedule=cron_schedule_start,
|
|
cron_schedule_end=cron_schedule_end
|
|
)
|
|
self._controller._post_update()
|
|
|
|
updated_slide = self._model_store.slide().get(slide_id)
|
|
return updated_slide.to_dict()
|
|
|
|
|
|
class SlidePositionResource(Resource):
|
|
|
|
@slide_ns.expect(positions_model)
|
|
def post(self):
|
|
"""Update positions of multiple slides"""
|
|
self.require_api_key()
|
|
data = request.get_json()
|
|
positions = data.get('positions', None) if data else None
|
|
|
|
if not positions:
|
|
abort(400, description="Positions data are required")
|
|
|
|
# Ensure the input is a dictionary with integer keys and values
|
|
if not isinstance(data, dict) or not all(isinstance(k, str) and isinstance(v, int) for k, v in positions.items()):
|
|
abort(400, description="Input must be a dictionary with string keys as slide IDs and integer values as positions")
|
|
|
|
self._model_store.slide().update_positions(positions)
|
|
self._controller._post_update()
|
|
return jsonify({'status': 'ok'})
|