import asyncio import pathlib from http import HTTPStatus from sanic import response, Blueprint from sanic.request import Request from sanic_ext import validate from sanic_ext.extensions.openapi import openapi from wrolpi.api_utils import json_response from wrolpi.common import logger, wrol_mode_check, get_media_directory, get_relative_to_media_directory from wrolpi.events import Events from wrolpi.schema import JSONErrorResponse from wrolpi.vars import PYTEST from . import lib from .. import schema from ..errors import UnknownChannel from ..lib import save_channels_config from ..models import Channel channel_bp = Blueprint('Channel', url_prefix='/api/videos/channels') logger = logger.getChild(__name__) @channel_bp.get('/') @openapi.description('Get a list of all Channels') @openapi.response(HTTPStatus.OK, schema.ChannelsResponse) async def get_channels(request: Request): channels = await lib.get_minimal_channels(request.ctx.session) return json_response({'channels': channels}) @channel_bp.get('/') @openapi.description('Get a Channel') @openapi.response(HTTPStatus.OK, schema.ChannelResponse) def channel_get(request: Request, channel_id: int = None): session = request.ctx.session channel = lib.get_channel(session, channel_id=channel_id, return_dict=True) channel.pop('info_json') return json_response({'channel': channel}) @channel_bp.post('/') @openapi.definition( description='Insert a Channel', body=schema.ChannelPostRequest, ) @openapi.response(HTTPStatus.OK, schema.ChannelPostResponse) @openapi.response(HTTPStatus.BAD_REQUEST, JSONErrorResponse) @validate(schema.ChannelPostRequest) @wrol_mode_check def channel_post(request: Request, body: schema.ChannelPostRequest): body.directory = get_media_directory() / body.directory # A write block: creating a Channel reads (the URL/name/directory conflict check) before it # INSERTs, and a deferred transaction fails that lock upgrade *instantly* with "database is # locked" if anything else wrote in between -- which is what a user creating a Channel while # background workers saved configs hit. The block also commits before the refresh below is # fired, so the refresh does not wait on a lock this request still holds. with request.ctx.db.write() as session: channel = lib.create_channel(session, data=body, return_dict=False) channel_id, channel_name = channel.id, channel.name # Refresh the videos asynchronously Channel.refresh_files(channel_id) Events.send_created(f'Created Channel: {channel_name}') return response.json({'success': 'Channel created successfully'}, HTTPStatus.CREATED, {'Location': f'/api/videos/channels/{channel_id}'}) @channel_bp.put('/') @openapi.definition( description='Update a Channel', body=schema.ChannelPutRequest, ) @openapi.response(HTTPStatus.NO_CONTENT) @openapi.response(HTTPStatus.BAD_REQUEST, JSONErrorResponse) @validate(schema.ChannelPutRequest) @wrol_mode_check async def channel_update(request: Request, channel_id: int, body: schema.ChannelPutRequest): session = request.ctx.session body.directory = pathlib.Path(body.directory) if body.directory else None channel = await lib.update_channel(session, data=body, channel_id=channel_id) save_channels_config.activate_switch() return response.raw('', HTTPStatus.NO_CONTENT, headers={'Location': f'/api/videos/channels/{channel.id}'}) @channel_bp.delete('/') @openapi.description('Delete a Channel') @openapi.response(HTTPStatus.NOT_FOUND, JSONErrorResponse) @wrol_mode_check def channel_delete(request: Request, channel_id: int): session = request.ctx.session channel = lib.delete_channel(session, channel_id=channel_id) Events.send_deleted(f'Deleted Channel: {channel["name"]}') return response.raw('', HTTPStatus.NO_CONTENT) @channel_bp.post('/refresh/') @openapi.description('Refresh all files in the Channel directory') @openapi.response(HTTPStatus.NOT_FOUND, JSONErrorResponse) @wrol_mode_check def channel_refresh(request: Request, channel_id: int): session = request.ctx.session channel: Channel = lib.get_channel(session, channel_id=channel_id, return_dict=False) if not channel: raise UnknownChannel() channel.refresh_files(channel.id) directory = get_relative_to_media_directory(channel.directory) Events.send_directory_refresh(f'Refreshing: {directory}') return response.empty() @channel_bp.post('//tag') @openapi.definition( description='Tag/untag a Channel with a single tag', body=schema.ChannelTagRequest, ) @openapi.response(HTTPStatus.NO_CONTENT) @openapi.response(HTTPStatus.BAD_REQUEST, JSONErrorResponse) @validate(schema.ChannelTagRequest) @wrol_mode_check async def channel_tag(request: Request, channel_id: int, body: schema.ChannelTagRequest): session = request.ctx.session directory = None if body.directory: directory = pathlib.Path(body.directory) directory = get_media_directory() / directory await lib.tag_channel(session, body.tag_name, directory, channel_id) return response.raw('', HTTPStatus.NO_CONTENT) @channel_bp.post('/search') @openapi.definition( description='Search Channels', body=schema.ChannelSearchRequest, ) @validate(schema.ChannelSearchRequest) async def channel_search(request: Request, body: schema.ChannelSearchRequest): session = request.ctx.session channels = await lib.search_channels(session, body.tag_names) ret = dict( channels=channels, ) return json_response(ret)