2021-11-01 16:42:52 -06:00
|
|
|
import asyncio
|
2022-08-06 05:22:07 +00:00
|
|
|
import pathlib
|
2021-11-01 16:42:52 -06:00
|
|
|
from http import HTTPStatus
|
|
|
|
|
|
|
|
|
|
from sanic import response, Blueprint
|
|
|
|
|
from sanic.request import Request
|
2022-01-13 09:29:58 -07:00
|
|
|
from sanic_ext import validate
|
|
|
|
|
from sanic_ext.extensions.openapi import openapi
|
2021-11-01 16:42:52 -06:00
|
|
|
|
2024-06-12 16:47:31 -06:00
|
|
|
from wrolpi.api_utils import json_response
|
2024-09-15 15:33:07 -06:00
|
|
|
from wrolpi.common import logger, wrol_mode_check, get_media_directory, get_relative_to_media_directory
|
2023-07-17 17:32:29 -06:00
|
|
|
from wrolpi.events import Events
|
2022-01-13 09:29:58 -07:00
|
|
|
from wrolpi.schema import JSONErrorResponse
|
2022-04-08 09:57:04 -06:00
|
|
|
from wrolpi.vars import PYTEST
|
2022-02-03 12:39:14 -07:00
|
|
|
from . import lib
|
2024-06-25 22:12:13 -06:00
|
|
|
from .. import schema
|
2023-07-17 17:32:29 -06:00
|
|
|
from ..errors import UnknownChannel
|
2024-08-15 19:32:01 -06:00
|
|
|
from ..lib import save_channels_config
|
2024-06-25 22:12:13 -06:00
|
|
|
from ..models import Channel
|
2021-11-01 16:42:52 -06:00
|
|
|
|
|
|
|
|
channel_bp = Blueprint('Channel', url_prefix='/api/videos/channels')
|
|
|
|
|
|
|
|
|
|
logger = logger.getChild(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@channel_bp.get('/')
|
2022-01-13 09:29:58 -07:00
|
|
|
@openapi.description('Get a list of all Channels')
|
2022-02-03 12:39:14 -07:00
|
|
|
@openapi.response(HTTPStatus.OK, schema.ChannelsResponse)
|
2021-11-01 16:42:52 -06:00
|
|
|
async def get_channels(_: Request):
|
2022-02-03 12:39:14 -07:00
|
|
|
channels = await lib.get_minimal_channels()
|
2021-11-01 16:42:52 -06:00
|
|
|
return json_response({'channels': channels})
|
|
|
|
|
|
|
|
|
|
|
2022-04-08 09:57:04 -06:00
|
|
|
@channel_bp.get('/<channel_id:int>')
|
2022-01-13 09:29:58 -07:00
|
|
|
@openapi.description('Get a Channel')
|
2022-02-03 12:39:14 -07:00
|
|
|
@openapi.response(HTTPStatus.OK, schema.ChannelResponse)
|
2025-12-10 21:16:10 -07:00
|
|
|
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)
|
2021-11-01 16:42:52 -06:00
|
|
|
channel.pop('info_json')
|
|
|
|
|
return json_response({'channel': channel})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@channel_bp.post('/')
|
2022-02-03 11:36:26 -07:00
|
|
|
@openapi.definition(
|
|
|
|
|
description='Insert a Channel',
|
2022-02-03 12:39:14 -07:00
|
|
|
body=schema.ChannelPostRequest,
|
2022-02-03 11:36:26 -07:00
|
|
|
)
|
2022-02-03 12:39:14 -07:00
|
|
|
@openapi.response(HTTPStatus.OK, schema.ChannelPostResponse)
|
2022-01-13 09:29:58 -07:00
|
|
|
@openapi.response(HTTPStatus.BAD_REQUEST, JSONErrorResponse)
|
2022-02-03 12:39:14 -07:00
|
|
|
@validate(schema.ChannelPostRequest)
|
2021-11-01 16:42:52 -06:00
|
|
|
@wrol_mode_check
|
2025-12-10 21:16:10 -07:00
|
|
|
def channel_post(request: Request, body: schema.ChannelPostRequest):
|
2022-01-31 21:45:46 -07:00
|
|
|
body.directory = get_media_directory() / body.directory
|
2021-11-01 16:42:52 -06:00
|
|
|
|
2026-10-03 15:42:06 -06:00
|
|
|
# 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:
|
2026-09-16 19:25:36 -06:00
|
|
|
channel = lib.create_channel(session, data=body, return_dict=False)
|
|
|
|
|
channel_id, channel_name = channel.id, channel.name
|
2021-11-01 16:42:52 -06:00
|
|
|
|
|
|
|
|
# Refresh the videos asynchronously
|
2026-09-16 19:25:36 -06:00
|
|
|
Channel.refresh_files(channel_id)
|
2021-11-01 16:42:52 -06:00
|
|
|
|
2026-09-16 19:25:36 -06:00
|
|
|
Events.send_created(f'Created Channel: {channel_name}')
|
2023-07-17 17:32:29 -06:00
|
|
|
|
2021-11-01 16:42:52 -06:00
|
|
|
return response.json({'success': 'Channel created successfully'}, HTTPStatus.CREATED,
|
2026-09-16 19:25:36 -06:00
|
|
|
{'Location': f'/api/videos/channels/{channel_id}'})
|
2021-11-01 16:42:52 -06:00
|
|
|
|
|
|
|
|
|
2022-04-08 09:57:04 -06:00
|
|
|
@channel_bp.put('/<channel_id:int>')
|
2022-02-03 11:36:26 -07:00
|
|
|
@openapi.definition(
|
|
|
|
|
description='Update a Channel',
|
2022-02-03 12:39:14 -07:00
|
|
|
body=schema.ChannelPutRequest,
|
2022-02-03 11:36:26 -07:00
|
|
|
)
|
2022-01-13 09:29:58 -07:00
|
|
|
@openapi.response(HTTPStatus.NO_CONTENT)
|
|
|
|
|
@openapi.response(HTTPStatus.BAD_REQUEST, JSONErrorResponse)
|
2022-02-03 12:39:14 -07:00
|
|
|
@validate(schema.ChannelPutRequest)
|
2021-11-01 16:42:52 -06:00
|
|
|
@wrol_mode_check
|
2025-12-10 21:16:10 -07:00
|
|
|
async def channel_update(request: Request, channel_id: int, body: schema.ChannelPutRequest):
|
|
|
|
|
session = request.ctx.session
|
2022-08-06 05:22:07 +00:00
|
|
|
body.directory = pathlib.Path(body.directory) if body.directory else None
|
2025-12-10 21:16:10 -07:00
|
|
|
channel = await lib.update_channel(session, data=body, channel_id=channel_id)
|
2024-09-15 15:33:07 -06:00
|
|
|
save_channels_config.activate_switch()
|
2021-11-01 16:42:52 -06:00
|
|
|
return response.raw('', HTTPStatus.NO_CONTENT,
|
2022-04-08 09:57:04 -06:00
|
|
|
headers={'Location': f'/api/videos/channels/{channel.id}'})
|
2021-11-01 16:42:52 -06:00
|
|
|
|
|
|
|
|
|
2022-04-08 09:57:04 -06:00
|
|
|
@channel_bp.delete('/<channel_id:int>')
|
2022-01-13 09:29:58 -07:00
|
|
|
@openapi.description('Delete a Channel')
|
|
|
|
|
@openapi.response(HTTPStatus.NOT_FOUND, JSONErrorResponse)
|
2021-11-01 16:42:52 -06:00
|
|
|
@wrol_mode_check
|
2025-12-10 21:16:10 -07:00
|
|
|
def channel_delete(request: Request, channel_id: int):
|
|
|
|
|
session = request.ctx.session
|
|
|
|
|
channel = lib.delete_channel(session, channel_id=channel_id)
|
2023-07-17 17:32:29 -06:00
|
|
|
Events.send_deleted(f'Deleted Channel: {channel["name"]}')
|
2021-11-01 16:42:52 -06:00
|
|
|
return response.raw('', HTTPStatus.NO_CONTENT)
|
2022-08-06 05:22:07 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@channel_bp.post('/refresh/<channel_id:int>')
|
|
|
|
|
@openapi.description('Refresh all files in the Channel directory')
|
|
|
|
|
@openapi.response(HTTPStatus.NOT_FOUND, JSONErrorResponse)
|
|
|
|
|
@wrol_mode_check
|
2025-12-10 21:16:10 -07:00
|
|
|
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)
|
2022-08-06 05:22:07 +00:00
|
|
|
if not channel:
|
|
|
|
|
raise UnknownChannel()
|
|
|
|
|
|
2025-12-26 13:43:51 -07:00
|
|
|
channel.refresh_files(channel.id)
|
2023-07-17 17:32:29 -06:00
|
|
|
directory = get_relative_to_media_directory(channel.directory)
|
|
|
|
|
Events.send_directory_refresh(f'Refreshing: {directory}')
|
2022-08-06 05:22:07 +00:00
|
|
|
return response.empty()
|
|
|
|
|
|
|
|
|
|
|
2024-08-06 18:13:21 -06:00
|
|
|
@channel_bp.post('/<channel_id:int>/tag')
|
|
|
|
|
@openapi.definition(
|
2024-08-15 19:32:01 -06:00
|
|
|
description='Tag/untag a Channel with a single tag',
|
2024-08-06 18:13:21 -06:00
|
|
|
body=schema.ChannelTagRequest,
|
|
|
|
|
)
|
|
|
|
|
@openapi.response(HTTPStatus.NO_CONTENT)
|
|
|
|
|
@openapi.response(HTTPStatus.BAD_REQUEST, JSONErrorResponse)
|
|
|
|
|
@validate(schema.ChannelTagRequest)
|
|
|
|
|
@wrol_mode_check
|
2025-12-10 21:16:10 -07:00
|
|
|
async def channel_tag(request: Request, channel_id: int, body: schema.ChannelTagRequest):
|
|
|
|
|
session = request.ctx.session
|
2024-08-06 18:13:21 -06:00
|
|
|
directory = None
|
|
|
|
|
if body.directory:
|
|
|
|
|
directory = pathlib.Path(body.directory)
|
|
|
|
|
directory = get_media_directory() / directory
|
|
|
|
|
|
2025-12-10 21:16:10 -07:00
|
|
|
await lib.tag_channel(session, body.tag_name, directory, channel_id)
|
2024-08-06 18:13:21 -06:00
|
|
|
|
|
|
|
|
return response.raw('', HTTPStatus.NO_CONTENT)
|
2024-08-15 19:32:01 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@channel_bp.post('/search')
|
|
|
|
|
@openapi.definition(
|
|
|
|
|
description='Search Channels',
|
|
|
|
|
body=schema.ChannelSearchRequest,
|
|
|
|
|
)
|
|
|
|
|
@validate(schema.ChannelSearchRequest)
|
2025-12-10 21:16:10 -07:00
|
|
|
async def channel_search(request: Request, body: schema.ChannelSearchRequest):
|
|
|
|
|
session = request.ctx.session
|
|
|
|
|
channels = await lib.search_channels(session, body.tag_names)
|
2024-08-15 19:32:01 -06:00
|
|
|
ret = dict(
|
|
|
|
|
channels=channels,
|
|
|
|
|
)
|
2024-09-17 18:04:00 -07:00
|
|
|
return json_response(ret)
|