wip swagger

This commit is contained in:
jr-k 2024-08-04 03:55:54 +02:00
parent 46a05cde84
commit c7fc29ba99
8 changed files with 393 additions and 174 deletions

View File

@ -1,38 +1,78 @@
import json
import os
import time
from flask import Flask, request, jsonify, abort
from werkzeug.utils import secure_filename
from src.service.ModelStore import ModelStore
from flask import request, abort, jsonify
from flask_restx import Resource, Namespace, fields
from src.model.entity.Content import Content
from src.manager.FolderManager import FolderManager
from src.model.enum.ContentType import ContentType
from src.model.enum.FolderEntity import FolderEntity, FOLDER_ROOT_PATH
from src.interface.ObController import ObController
from src.service.ExternalStorageServer import ExternalStorageServer
from src.util.utils import str_to_enum, get_optional_string
from src.util.UtilFile import randomize_filename
from src.util.utils import str_to_enum
from plugins.system.CoreApi.exception.ContentPathMissingException import ContentPathMissingException
from plugins.system.CoreApi.exception.ContentNotFoundException import ContentNotFoundException
from plugins.system.CoreApi.exception.FolderNotFoundException import FolderNotFoundException
from plugins.system.CoreApi.exception.FolderNotEmptyException import FolderNotEmptyException
# Namespace for content operations
content_ns = Namespace('content', description='Operations related to content management')
# Input model for adding/updating content
content_model = content_ns.model('Content', {
'name': fields.String(required=True, description='Name of the content'),
'type': fields.String(required=True, description='Type of the content'),
'location': fields.String(description='Location of the content (optional)'),
'path': fields.String(description='Path of the folder')
})
# Output model for content
content_output_model = content_ns.model('ContentOutput', {
'id': fields.Integer(readOnly=True, description='Unique identifier of the content'),
'name': fields.String(description='Name of the content'),
'type': fields.String(description='Type of the content'),
'location': fields.String(description='Location of the content'),
'folder_id': fields.Integer(description='Folder ID where the content is stored')
})
# Model for folder operations
folder_model = content_ns.model('Folder', {
'name': fields.String(required=True, description='Name of the folder'),
'path': fields.String(description='Path of the folder')
})
# Model for bulk operations
bulk_move_model = content_ns.model('BulkMove', {
'entity_ids': fields.List(fields.Integer, required=True, description='List of content IDs to move'),
'path': fields.String(description='Path of the folder')
})
class ContentApiController(ObController):
def register(self):
self._app.add_url_rule('/api/content', 'api_content_list', self.list_content, methods=['GET'])
self._app.add_url_rule('/api/content', 'api_content_add', self.add_content, methods=['POST'])
self._app.add_url_rule('/api/content/<int:content_id>', 'api_content_get', self.get_content, methods=['GET'])
self._app.add_url_rule('/api/content/<int:content_id>', 'api_content_update', self.update_content, methods=['PUT'])
self._app.add_url_rule('/api/content/<int:content_id>', 'api_content_delete', self.delete_content, methods=['DELETE'])
self._app.add_url_rule('/api/content/location/<int:content_id>', 'api_content_location', self.location_content, methods=['GET'])
self._app.add_url_rule('/api/content/upload-bulk', 'api_content_upload_bulk', self.upload_bulk_content, methods=['POST'])
self._app.add_url_rule('/api/content/folder/move-bulk', 'api_folder_content_bulk_move', self.move_bulk_content_folder, methods=['POST'])
self._app.add_url_rule('/api/content/folder', 'api_folder_add', self.add_folder, methods=['POST'])
self._app.add_url_rule('/api/content/folder', 'api_folder_delete', self.delete_folder, methods=['DELETE'])
self._app.add_url_rule('/api/content/folder', 'api_folder_update', self.update_folder, methods=['PUT'])
# - self._app.add_url_rule('/api/content', 'api_content_list', self.list_content, methods=['GET'])
# - self._app.add_url_rule('/api/content', 'api_content_add', self.add_content, methods=['POST'])
# - self._app.add_url_rule('/api/content/<int:content_id>', 'api_content_get', self.get_content, methods=['GET'])
# - self._app.add_url_rule('/api/content/<int:content_id>', 'api_content_update', self.update_content, methods=['PUT'])
# - self._app.add_url_rule('/api/content/<int:content_id>', 'api_content_delete', self.delete_content, methods=['DELETE'])
# - self._app.add_url_rule('/api/content/location/<int:content_id>', 'api_content_location', self.location_content, methods=['GET'])
# - self._app.add_url_rule('/api/content/upload-bulk', 'api_content_upload_bulk', self.upload_bulk_content, methods=['POST'])
# - self._app.add_url_rule('/api/content/folder/move-bulk', 'api_folder_content_bulk_move', self.move_bulk_content_folder, methods=['POST'])
# - self._app.add_url_rule('/api/content/folder', 'api_folder_add', self.add_folder, methods=['POST'])
# - self._app.add_url_rule('/api/content/folder', 'api_folder_delete', self.delete_folder, methods=['DELETE'])
# - self._app.add_url_rule('/api/content/folder', 'api_folder_update', self.update_folder, methods=['PUT'])
self.api().add_namespace(content_ns, path='/api/content')
content_ns.add_resource(self.create_resource(ContentListResource), '/')
content_ns.add_resource(self.create_resource(ContentResource), '/<int:content_id>')
content_ns.add_resource(self.create_resource(ContentLocationResource), '/location/<int:content_id>')
content_ns.add_resource(self.create_resource(ContentBulkUploadResource), '/upload-bulk')
content_ns.add_resource(self.create_resource(FolderBulkMoveResource), '/folder/move-bulk')
content_ns.add_resource(self.create_resource(FolderResource), '/folder')
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
})
def get_request_data(self):
data = {}
@ -67,14 +107,19 @@ class ContentApiController(ObController):
return FOLDER_ROOT_PATH if is_root_drive else path, folder
def list_content(self):
data = self.get_request_data()
class ContentListResource(Resource):
@content_ns.marshal_list_with(content_output_model)
def get(self):
"""List all contents"""
data = self._controller.get_request_data()
working_folder_path = None
working_folder = None
folder_id = None
try:
working_folder_path, working_folder = self.get_folder_context()
working_folder_path, working_folder = self._controller.get_folder_context()
folder_id = data.get('folder_id', 0 if not working_folder else working_folder.id)
except ContentPathMissingException:
pass
@ -85,15 +130,18 @@ class ContentApiController(ObController):
)
result = [content.to_dict() for content in contents]
return jsonify({
return {
'contents': result,
'working_folder_path': working_folder_path,
'working_folder': working_folder.to_dict() if working_folder else None
})
}
def add_content(self):
data = self.get_request_data()
working_folder_path, working_folder = self.get_folder_context()
@content_ns.expect(content_model)
@content_ns.marshal_with(content_output_model, code=201)
def post(self):
"""Add new content"""
data = self._controller.get_request_data()
working_folder_path, working_folder = self._controller.get_folder_context()
if 'name' not in data:
abort(400, description="Name is required")
@ -111,7 +159,7 @@ class ContentApiController(ObController):
name=data.get('name'),
type=content_type,
request_files=request.files,
upload_dir=self._app.config['UPLOAD_FOLDER'],
upload_dir=self._controller._app.config['UPLOAD_FOLDER'],
location=location,
folder_id=working_folder.id if working_folder else None
)
@ -119,17 +167,25 @@ class ContentApiController(ObController):
if not content:
abort(400, description="Failed to add content")
return jsonify(content.to_dict()), 201
return content.to_dict(), 201
def get_content(self, content_id: int):
class ContentResource(Resource):
@content_ns.marshal_with(content_output_model)
def get(self, content_id: int):
"""Get content by ID"""
content = self._model_store.content().get(content_id)
if not content:
raise ContentNotFoundException()
return jsonify(content.to_dict())
return content.to_dict()
def update_content(self, content_id: int):
data = self.get_request_data()
@content_ns.expect(content_model)
@content_ns.marshal_with(content_output_model)
def put(self, content_id: int):
"""Update existing content"""
data = self._controller.get_request_data()
content = self._model_store.content().get(content_id)
if not content:
@ -143,11 +199,12 @@ class ContentApiController(ObController):
name=data.get('name'),
)
self._post_update()
self._controller._post_update()
return jsonify(content.to_dict())
return content.to_dict()
def delete_content(self, content_id: int):
def delete(self, content_id: int):
"""Delete content"""
content = self._model_store.content().get(content_id)
if not content:
@ -157,11 +214,15 @@ class ContentApiController(ObController):
abort(400, description="Content is referenced in slides")
self._model_store.content().delete(content.id)
self._post_update()
self._controller._post_update()
return jsonify({'status': 'ok'}), 204
return {'status': 'ok'}, 204
def location_content(self, content_id: int):
class ContentLocationResource(Resource):
def get(self, content_id: int):
"""Get content location by ID"""
content = self._model_store.content().get(content_id)
if not content:
@ -169,10 +230,14 @@ class ContentApiController(ObController):
content_location = self._model_store.content().resolve_content_location(content)
return jsonify({'location': content_location})
return {'location': content_location}
def upload_bulk_content(self):
working_folder_path, working_folder = self.get_folder_context()
class ContentBulkUploadResource(Resource):
def post(self):
"""Upload multiple content files"""
working_folder_path, working_folder = self._controller.get_folder_context()
for key in request.files:
files = request.files.getlist(key)
@ -185,15 +250,20 @@ class ContentApiController(ObController):
name=name,
type=content_type,
request_files=file,
upload_dir=self._app.config['UPLOAD_FOLDER'],
upload_dir=self._controller._app.config['UPLOAD_FOLDER'],
folder_id=working_folder.id if working_folder else None
)
return jsonify({'status': 'ok'}), 201
return {'status': 'ok'}, 201
def move_bulk_content_folder(self):
data = self.get_request_data()
working_folder_path, working_folder = self.get_folder_context()
class FolderBulkMoveResource(Resource):
@content_ns.expect(bulk_move_model)
def post(self):
"""Move multiple content to another folder"""
data = self._controller.get_request_data()
working_folder_path, working_folder = self._controller.get_folder_context()
if 'entity_ids' not in data:
abort(400, description="Content IDs are required under 'entity_ids' field")
@ -208,11 +278,17 @@ class ContentApiController(ObController):
entity=FolderEntity.CONTENT
)
return jsonify({'status': 'ok'})
return {'status': 'ok'}
def add_folder(self):
data = self.get_request_data()
working_folder_path, working_folder = self.get_folder_context()
class FolderResource(Resource):
@content_ns.expect(folder_model)
@content_ns.marshal_with(folder_model, code=201)
def post(self):
"""Add a new folder"""
data = self._controller.get_request_data()
working_folder_path, working_folder = self._controller.get_folder_context()
if 'name' not in data:
abort(400, description="Name is required")
@ -223,13 +299,14 @@ class ContentApiController(ObController):
working_folder_path=working_folder_path
)
return jsonify(folder.to_dict()), 201
return folder.to_dict(), 201
def delete_folder(self):
working_folder_path, working_folder = self.get_folder_context()
def delete(self):
"""Delete a folder"""
working_folder_path, working_folder = self._controller.get_folder_context()
if not working_folder:
abort(400, description="You can't update this folder")
abort(400, description="You can't delete this folder")
content_counter = self._model_store.content().count_contents_for_folder(working_folder.id)
folder_counter = self._model_store.folder().count_subfolders_for_folder(working_folder.id)
@ -238,13 +315,15 @@ class ContentApiController(ObController):
raise FolderNotEmptyException()
self._model_store.folder().delete(id=working_folder.id)
self._post_update()
self._controller._post_update()
return jsonify({'status': 'ok'}), 204
return {'status': 'ok'}, 204
def update_folder(self):
data = self.get_request_data()
working_folder_path, working_folder = self.get_folder_context()
@content_ns.expect(folder_model)
def put(self):
"""Update a folder"""
data = self._controller.get_request_data()
working_folder_path, working_folder = self._controller.get_folder_context()
if 'name' not in data:
abort(400, description="Name is required")
@ -257,7 +336,5 @@ class ContentApiController(ObController):
name=data.get('name')
)
return jsonify({'status': 'ok'})
return {'status': 'ok'}
def _post_update(self):
self._model_store.variable().update_by_name("last_content_update", time.time())

View File

@ -1,32 +1,55 @@
from flask import Flask, render_template, jsonify, request, abort, make_response
from flask import request, abort, jsonify
from flask_restx import Resource, Namespace, fields
from src.model.entity.Playlist import Playlist
from src.interface.ObController import ObController
# Namespace pour les opérations sur les playlists
playlist_ns = Namespace('playlists', description='Playlist operations')
# Modèle d'entrée pour la playlist
playlist_model = playlist_ns.model('Playlist', {
'name': fields.String(required=True, description='The playlist name'),
'enabled': fields.Boolean(default=True, description='Is the playlist enabled?'),
'time_sync': fields.Boolean(default=False, description='Is time synchronization enabled?')
})
# Modèle de sortie pour la playlist
playlist_output_model = playlist_ns.model('PlaylistOutput', {
'id': fields.Integer(readOnly=True, description='The unique identifier of a playlist'),
'name': fields.String(required=True, description='The playlist name'),
'enabled': fields.Boolean(description='Is the playlist enabled?'),
'time_sync': fields.Boolean(description='Is time synchronization enabled?')
})
class PlaylistApiController(ObController):
def register(self):
self._app.add_url_rule('/api/playlist', 'api_playlist_list', self.get_playlists, methods=['GET'])
self._app.add_url_rule('/api/playlist', 'api_playlist_add', self.add_playlist, methods=['POST'])
self._app.add_url_rule('/api/playlist/<int:playlist_id>', 'api_playlist_get', self.get_playlist, methods=['GET'])
self._app.add_url_rule('/api/playlist/<int:playlist_id>', 'api_playlist_update', self.update_playlist, methods=['PUT'])
self._app.add_url_rule('/api/playlist/<int:playlist_id>', 'api_playlist_delete', self.delete_playlist, methods=['DELETE'])
self._app.add_url_rule('/api/playlist/<int:playlist_id>/slides', 'api_playlist_list_slides', self.get_playlists_slides, methods=['GET'])
self._app.add_url_rule('/api/playlist/<int:playlist_id>/notifications', 'api_playlist_list_notifications', self.get_playlists_notifications, methods=['GET'])
self.api().add_namespace(playlist_ns, path='/api/playlists')
playlist_ns.add_resource(self.create_resource(PlaylistResource), '/<int:playlist_id>')
playlist_ns.add_resource(self.create_resource(PlaylistListResource), '/')
playlist_ns.add_resource(self.create_resource(PlaylistSlidesResource), '/<int:playlist_id>/slides')
playlist_ns.add_resource(self.create_resource(PlaylistNotificationsResource), '/<int:playlist_id>/notifications')
def get_playlists(self):
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
})
class PlaylistListResource(Resource):
@playlist_ns.marshal_list_with(playlist_output_model)
def get(self):
"""List all playlists"""
playlists = self._model_store.playlist().get_all(sort="created_at", ascending=True)
result = [playlist.to_dict() for playlist in playlists]
return jsonify(result)
return result
def get_playlist(self, playlist_id: int):
playlist = self._model_store.playlist().get(playlist_id)
if not playlist:
abort(404, description="Playlist not found")
return jsonify(playlist.to_dict())
def add_playlist(self):
@playlist_ns.expect(playlist_model)
@playlist_ns.marshal_with(playlist_output_model, code=201)
def post(self):
"""Create a new playlist"""
data = request.get_json()
if not data or 'name' not in data:
abort(400, description="Invalid input")
@ -42,12 +65,24 @@ class PlaylistApiController(ObController):
except Exception as e:
abort(409, description=str(e))
return jsonify(playlist.to_dict()), 201
return playlist.to_dict(), 201
def update_playlist(self, playlist_id: int):
class PlaylistResource(Resource):
@playlist_ns.marshal_with(playlist_output_model)
def get(self, playlist_id):
"""Get a playlist by its ID"""
playlist = self._model_store.playlist().get(playlist_id)
if not playlist:
abort(404, description="Playlist not found")
return playlist.to_dict()
@playlist_ns.expect(playlist_model)
@playlist_ns.marshal_with(playlist_output_model)
def put(self, playlist_id):
"""Update an existing playlist"""
data = request.get_json()
if not data or 'name' not in data:
abort(400, description="Invalid input")
playlist = self._model_store.playlist().get(playlist_id)
if not playlist:
@ -60,9 +95,10 @@ class PlaylistApiController(ObController):
enabled=data.get('enabled', playlist.enabled)
)
updated_playlist = self._model_store.playlist().get(playlist_id)
return jsonify(updated_playlist.to_dict())
return updated_playlist.to_dict()
def delete_playlist(self, playlist_id: int):
def delete(self, playlist_id):
"""Delete a playlist"""
playlist = self._model_store.playlist().get(playlist_id)
if not playlist:
abort(404, description="Playlist not found")
@ -76,7 +112,11 @@ class PlaylistApiController(ObController):
self._model_store.playlist().delete(playlist_id)
return '', 204
def get_playlists_slides(self, playlist_id: int):
class PlaylistSlidesResource(Resource):
def get(self, playlist_id):
"""Get slides associated with a playlist"""
playlist = self._model_store.playlist().get(playlist_id)
if not playlist:
@ -87,7 +127,11 @@ class PlaylistApiController(ObController):
result = [slide.to_dict() for slide in slides]
return jsonify(result)
def get_playlists_notifications(self, playlist_id: int):
class PlaylistNotificationsResource(Resource):
def get(self, playlist_id):
"""Get notifications associated with a playlist"""
playlist = self._model_store.playlist().get(playlist_id)
if not playlist:

View File

@ -1,43 +1,69 @@
import json
import os
import time
from datetime import datetime
from flask import Flask, request, jsonify, abort, make_response
from werkzeug.utils import secure_filename
from src.service.ModelStore import ModelStore
from flask import request, abort, jsonify
from flask_restx import Resource, Namespace, fields
from src.model.entity.Slide import Slide
from src.model.enum.ContentType import ContentType
from src.interface.ObController import ObController
from src.util.utils import str_to_enum, get_optional_string, str_datetime_to_cron, str_weekdaytime_to_cron
from src.util.UtilFile import randomize_filename
from src.util.utils import str_datetime_to_cron, str_weekdaytime_to_cron
import time
# Namespace for slide operations
slide_ns = Namespace('slides', description='Operations on slides')
# Input model for adding/editing a slide
slide_model = slide_ns.model('Slide', {
'content_id': fields.Integer(required=True, description='The content ID for the slide'),
'playlist_id': fields.Integer(required=True, description='The playlist ID to which the slide belongs'),
'enabled': fields.Boolean(default=True, description='Is the slide enabled?'),
'delegate_duration': fields.Boolean(default=False, description='Should the duration be delegated?'),
'duration': fields.Integer(default=3, description='Duration of the slide'),
'position': fields.Integer(default=999, description='Position of the slide'),
'scheduling': fields.String(description='Scheduling type: loop, datetime, or inweek'),
'datetime_start': fields.String(description='Start datetime for scheduling'),
'datetime_end': fields.String(description='End datetime for scheduling'),
'day_start': fields.Integer(description='Start day for inweek scheduling'),
'time_start': fields.String(description='Start time for inweek scheduling'),
'day_end': fields.Integer(description='End day for inweek scheduling'),
'time_end': fields.String(description='End time for inweek scheduling'),
'cron_start': fields.String(description='Cron expression for scheduling start'),
'cron_end': fields.String(description='Cron expression for scheduling end'),
})
# 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')
})
class SlideApiController(ObController):
def register(self):
self._app.add_url_rule('/api/slide', 'api_slide_add', self.add_slide, methods=['POST'])
self._app.add_url_rule('/api/slide/notification', 'api_slide_notification_add', self.add_notification, methods=['POST'])
self._app.add_url_rule('/api/slide/<int:slide_id>', 'api_slide_get', self.get_slide, methods=['GET'])
self._app.add_url_rule('/api/slide/<int:slide_id>', 'api_slide_edit', self.edit_slide, methods=['PUT'])
self._app.add_url_rule('/api/slide/<int:slide_id>', 'api_slide_delete', self.delete_slide, methods=['DELETE'])
self._app.add_url_rule('/api/slide/positions', 'api_slide_positions', self.update_slide_positions, methods=['POST'])
self.api().add_namespace(slide_ns, path='/api/slides')
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 get_slide(self, slide_id: int):
slide = self._model_store.slide().get(slide_id)
if not slide:
abort(404, description="Slide not found")
return jsonify(slide.to_dict())
def add_slide(self):
return self.add_slide_or_notification(is_notification=False)
def add_notification(self):
return self.add_slide_or_notification(is_notification=True)
def add_slide_or_notification(self, is_notification=False):
data = request.get_json()
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
})
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")
@ -67,56 +93,7 @@ class SlideApiController(ObController):
slide = self._model_store.slide().add_form(slide)
self._post_update()
return jsonify(slide.to_dict()), 201
def edit_slide(self, slide_id: int):
data = request.get_json()
if not data or 'content_id' not in data:
abort(400, description="Content ID is required")
slide = self._model_store.slide().get(slide_id)
if not slide:
abort(404, description="Slide not found")
cron_schedule_start, cron_schedule_end = self._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 if 'scheduling' in data else slide.cron_schedule,
cron_schedule_end=cron_schedule_end if 'scheduling' in data else slide.cron_schedule_end,
)
self._post_update()
updated_slide = self._model_store.slide().get(slide_id)
return jsonify(updated_slide.to_dict())
def delete_slide(self, slide_id: int):
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._post_update()
return '', 204
def update_slide_positions(self):
data = request.get_json()
if not data:
abort(400, description="Positions data are required")
self._model_store.slide().update_positions(data)
self._post_update()
return jsonify({'status': 'ok'})
def _post_update(self):
self._model_store.variable().update_by_name("last_slide_update", time.time())
return slide.to_dict(), 201
def _resolve_scheduling(self, data, is_notification=False):
try:
@ -182,3 +159,100 @@ class SlideApiController(ObController):
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_model)
@slide_ns.marshal_with(slide_output_model, code=201)
def post(self):
"""Add a new slide"""
data = request.get_json()
return self._controller._add_slide_or_notification(data, is_notification=False)
class SlideAddNotificationResource(Resource):
@slide_ns.expect(slide_model)
@slide_ns.marshal_with(slide_output_model, code=201)
def post(self):
"""Add a new slide"""
data = request.get_json()
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"""
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_model)
@slide_ns.marshal_with(slide_output_model)
def put(self, slide_id):
"""Edit an existing slide"""
data = request.get_json()
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 'scheduling' in data:
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()
def delete(self, slide_id):
"""Delete a slide"""
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 SlidePositionResource(Resource):
@slide_ns.expect(positions_model)
def post(self):
"""Update positions of multiple slides"""
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'})

View File

@ -5,3 +5,4 @@ waitress
flask-login
pysqlite3
psutil
flask-restx

View File

@ -48,3 +48,6 @@ class ObController(abc.ABC):
def render_view(self, template_file: str, **parameters: dict) -> str:
return self._template_renderer.render_view(template_file, self.plugin(), **parameters)
def api(self):
return self._web_server.api

View File

@ -9,6 +9,7 @@ load_dotenv()
class ConfigManager:
APPLICATION_NAME = "Obscreen"
DEFAULT_PORT = 5000
DEFAULT_PORT_HTTP_EXTERNAL_STORAGE = 5001
VERSION_FILE = 'version.txt'
@ -16,6 +17,7 @@ class ConfigManager:
def __init__(self, replacers: Dict):
self._replacers = replacers
self._CONFIG = {
'application_name': self.APPLICATION_NAME,
'version': None,
'demo': False,
'port_http_external_storage': self.DEFAULT_PORT_HTTP_EXTERNAL_STORAGE,
@ -87,7 +89,7 @@ class ConfigManager:
if args.log_stdout:
self._CONFIG['log_stdout'] = args.log_stdout
if args.version:
print("Obscreen version v{} (https://github.com/jr-k/obscreen)".format(self._CONFIG['version']))
print("{} version v{} (https://github.com/jr-k/obscreen)".format(self.APPLICATION_NAME, self._CONFIG['version']))
sys.exit(0)
def load_from_env(self) -> None:

View File

@ -165,7 +165,7 @@ GROUP BY playlist_id;
return
form = {
"name": name,
"name": name if isinstance(name, str) else slide.name,
"time_sync": time_sync if isinstance(time_sync, bool) else slide.time_sync,
"enabled": enabled if isinstance(enabled, bool) else slide.enabled,
}

View File

@ -4,6 +4,7 @@ from waitress import serve
from flask import Flask, send_from_directory, redirect, url_for, request, jsonify, make_response
from flask_login import LoginManager, current_user
from flask_restx import Api
from src.manager.UserManager import UserManager
from src.service.ModelStore import ModelStore
@ -27,6 +28,7 @@ class WebServer:
def __init__(self, kernel, model_store: ModelStore, template_renderer: TemplateRenderer):
self._app = None
self._api = None
self._auth_enabled = False
self._login_manager = None
self._kernel = kernel
@ -35,6 +37,10 @@ class WebServer:
self._debug = self._model_store.config().map().get('debug')
self.setup()
@property
def api(self) -> Api:
return self._api
def run(self) -> None:
serve(
self._app,
@ -51,6 +57,7 @@ class WebServer:
self._setup_web_globals()
self._setup_web_errors()
self._setup_web_controllers()
self._setup_api()
def get_app(self):
return self._app
@ -76,6 +83,7 @@ class WebServer:
self._app.config['UPLOAD_FOLDER'] = "{}/{}".format(WebDirConstant.FOLDER_STATIC, WebDirConstant.FOLDER_STATIC_WEB_UPLOADS)
self._app.config['MAX_CONTENT_LENGTH'] = self._model_store.variable().map().get('slide_upload_limit').as_int() * 1024 * 1024
self._app.config['ERROR_404_HELP'] = False
self._setup_flask_login()
@ -115,6 +123,16 @@ class WebServer:
PlaylistController(self._kernel, self, self._app, self.auth_required, self._model_store, self._template_renderer)
AuthController(self._kernel, self, self._app, self.auth_required, self._model_store, self._template_renderer)
def _setup_api(self) -> None:
self._api = Api(
self._app,
version=self._model_store.config().map().get('version'),
title="{} {}".format(self._model_store.config().map().get('application_name'), "API"),
description='API Documentation with Swagger',
endpoint='api',
doc='/api'
)
def _setup_web_globals(self) -> None:
@self._app.context_processor
def inject_global_vars() -> dict: