mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Video tests pass with updated Sanic.
This commit is contained in:
parent
b1ed453466
commit
c277a75a1a
10 changed files with 72 additions and 98 deletions
|
|
@ -103,9 +103,9 @@ async def refresh_videos(channel_links: list = None):
|
|||
@validate(FavoriteRequest)
|
||||
@openapi.response(HTTPStatus.OK, FavoriteResponse)
|
||||
@openapi.response(HTTPStatus.BAD_REQUEST, JSONErrorResponse)
|
||||
async def favorite(_: Request, data: dict):
|
||||
_favorite = video_lib.set_video_favorite(data['video_id'], data['favorite'])
|
||||
ret = {'video_id': data['video_id'], 'favorite': _favorite}
|
||||
async def favorite(_: Request, body: FavoriteRequest):
|
||||
_favorite = video_lib.set_video_favorite(body.video_id, body.favorite)
|
||||
ret = {'video_id': body.video_id, 'favorite': _favorite}
|
||||
return json_response(ret, HTTPStatus.OK)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from wrolpi.root_api import json_response
|
|||
from wrolpi.schema import JSONErrorResponse
|
||||
from .lib import get_minimal_channels, delete_channel, update_channel, get_channel, create_channel
|
||||
from ..schema import ChannelsResponse, ChannelResponse, ChannelPostRequest, \
|
||||
ChannelPostResponse, ChannelPutRequest, SuccessResponse
|
||||
ChannelPostResponse, ChannelPutRequest
|
||||
|
||||
channel_bp = Blueprint('Channel', url_prefix='/api/videos/channels')
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ async def get_channels(_: Request):
|
|||
return json_response({'channels': channels})
|
||||
|
||||
|
||||
@channel_bp.route('/<link:str>', methods=['GET', 'OPTIONS'])
|
||||
@channel_bp.get('/<link:str>')
|
||||
@openapi.description('Get a Channel')
|
||||
@openapi.response(HTTPStatus.OK, ChannelResponse)
|
||||
def channel_get(_: Request, link: str):
|
||||
|
|
@ -42,14 +42,14 @@ def channel_get(_: Request, link: str):
|
|||
@openapi.response(HTTPStatus.BAD_REQUEST, JSONErrorResponse)
|
||||
@validate(ChannelPostRequest)
|
||||
@wrol_mode_check
|
||||
def channel_post(_: Request, data: dict):
|
||||
def channel_post(_: Request, body: ChannelPostRequest):
|
||||
try:
|
||||
# Channel directory is relative to the media directory. Channel directory may not be in "videos" directory!
|
||||
data['directory'] = get_media_directory() / data['directory']
|
||||
body.directory = get_media_directory() / body.directory
|
||||
except UnknownDirectory:
|
||||
if data['mkdir']:
|
||||
make_media_directory(data['directory'])
|
||||
data['directory'] = get_relative_to_media_directory(data['directory'])
|
||||
if body.mkdir:
|
||||
make_media_directory(body.directory)
|
||||
body.directory = get_relative_to_media_directory(body.directory)
|
||||
else:
|
||||
raise
|
||||
# Channel directory is relative to the media directory. Channel directory may not be in "videos" directory!
|
||||
|
|
@ -57,16 +57,7 @@ def channel_post(_: Request, data: dict):
|
|||
if data.get('mkdir') is True:
|
||||
make_media_directory(data['directory'])
|
||||
|
||||
if download_frequency := data.get('download_frequency'):
|
||||
try:
|
||||
download_frequency = int(download_frequency)
|
||||
except ValueError:
|
||||
if download_frequency in ('null', 'None'):
|
||||
download_frequency = None
|
||||
|
||||
data['download_frequency'] = download_frequency
|
||||
|
||||
channel = create_channel(data)
|
||||
channel = create_channel(body.__dict__) # TODO don't use the dataclass as a dict.
|
||||
|
||||
# Refresh the videos asynchronously
|
||||
from ..api import refresh_videos
|
||||
|
|
@ -78,14 +69,13 @@ def channel_post(_: Request, data: dict):
|
|||
|
||||
|
||||
@channel_bp.put('/<link:str>')
|
||||
@channel_bp.patch('/<link:str>')
|
||||
@openapi.description('Update a Channel')
|
||||
@openapi.response(HTTPStatus.NO_CONTENT)
|
||||
@openapi.response(HTTPStatus.BAD_REQUEST, JSONErrorResponse)
|
||||
@validate(ChannelPutRequest)
|
||||
@wrol_mode_check
|
||||
def channel_update(_: Request, link: str, data: dict):
|
||||
channel = update_channel(data, link)
|
||||
def channel_update(_: Request, link: str, body: ChannelPutRequest):
|
||||
channel = update_channel(body.__dict__, link)
|
||||
return response.raw('', HTTPStatus.NO_CONTENT,
|
||||
headers={'Location': f'/api/videos/channels/{channel.link}'})
|
||||
|
||||
|
|
@ -93,7 +83,6 @@ def channel_update(_: Request, link: str, data: dict):
|
|||
@channel_bp.delete('/<link:str>')
|
||||
@openapi.description('Delete a Channel')
|
||||
@openapi.response(HTTPStatus.NOT_FOUND, JSONErrorResponse)
|
||||
@validate(SuccessResponse)
|
||||
@wrol_mode_check
|
||||
def channel_delete(_: Request, link: str):
|
||||
delete_channel(link)
|
||||
|
|
|
|||
|
|
@ -139,25 +139,18 @@ def update_channel(data: dict, link: str) -> Channel:
|
|||
if 'directory' in data:
|
||||
data['directory'] = str(data['directory'])
|
||||
|
||||
if 'download_frequency' in data:
|
||||
try:
|
||||
data['download_frequency'] = int(data['download_frequency'])
|
||||
except ValueError:
|
||||
if data['download_frequency'] in ('null', 'None', ''):
|
||||
data['download_frequency'] = None
|
||||
else:
|
||||
raise APIError(f'Invalid download frequency {data["download_frequency"]}')
|
||||
|
||||
if data.get('match_regex') in ('None', 'null'):
|
||||
data['match_regex'] = None
|
||||
|
||||
data['link'] = data.get('link') or link
|
||||
|
||||
# Verify that the URL/Name/Link aren't taken
|
||||
check_for_channel_conflicts(
|
||||
session,
|
||||
id_=channel.id,
|
||||
url=data.get('url'),
|
||||
name=data.get('name'),
|
||||
link=data.get('link'),
|
||||
link=data['link'],
|
||||
directory=data.get('directory'),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ class TestVideoAPI(TestAPI):
|
|||
|
||||
# Create it
|
||||
request, response = api_app.test_client.post('/api/videos/channels', content=json.dumps(new_channel))
|
||||
assert response.status_code == HTTPStatus.CREATED
|
||||
assert response.status_code == HTTPStatus.CREATED, response.json
|
||||
location = response.headers['Location']
|
||||
request, response = api_app.test_client.get(location)
|
||||
created = response.json['channel']
|
||||
|
|
@ -94,20 +94,6 @@ class TestVideoAPI(TestAPI):
|
|||
'url': 'https://example.com/channel1',
|
||||
})
|
||||
|
||||
# Patch it
|
||||
patch = {'name': 'new name'}
|
||||
request, response = api_app.test_client.patch(location, content=json.dumps(patch))
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, response.status_code
|
||||
request, response = api_app.test_client.get(location)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
self.assertDictContains(response.json['channel'], {
|
||||
'id': 1,
|
||||
'name': 'new name',
|
||||
'directory': channel_directory_name,
|
||||
'match_regex': 'asdf',
|
||||
'url': 'https://example.com/channel1',
|
||||
})
|
||||
|
||||
# Can't update channel that doesn't exist
|
||||
request, response = api_app.test_client.put('/api/videos/channels/doesnt_exist',
|
||||
content=json.dumps(new_channel))
|
||||
|
|
@ -361,9 +347,7 @@ class TestVideoAPI(TestAPI):
|
|||
|
||||
@wrap_test_db
|
||||
def test_channel_no_download_frequency(self):
|
||||
"""
|
||||
A channel does not require a download frequency.
|
||||
"""
|
||||
"""A channel does not require a download frequency."""
|
||||
channel_directory = tempfile.TemporaryDirectory(dir=self.tmp_dir.name).name
|
||||
os.mkdir(channel_directory)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,30 +2,33 @@ from dataclasses import dataclass, field
|
|||
from datetime import date, datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from modules.videos.lib import DEFAULT_DOWNLOAD_FREQUENCY
|
||||
from modules.videos.video.lib import DEFAULT_VIDEO_ORDER, VIDEO_QUERY_LIMIT
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChannelPostRequest:
|
||||
url: str
|
||||
name: str
|
||||
match_regex: str
|
||||
directory: str
|
||||
mkdir: bool
|
||||
generate_posters: bool
|
||||
calculate_duration: bool
|
||||
download_frequency: str
|
||||
calculate_duration: Optional[bool] = None
|
||||
download_frequency: Optional[int] = DEFAULT_DOWNLOAD_FREQUENCY
|
||||
generate_posters: Optional[bool] = None
|
||||
match_regex: Optional[str] = None
|
||||
mkdir: Optional[bool] = None
|
||||
url: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChannelPutRequest:
|
||||
url: str
|
||||
name: str
|
||||
match_regex: str
|
||||
link: str
|
||||
directory: str
|
||||
generate_posters: bool
|
||||
calculate_duration: bool
|
||||
mkdir: bool
|
||||
download_frequency: str
|
||||
calculate_duration: Optional[bool] = None
|
||||
directory: Optional[str] = None
|
||||
download_frequency: Optional[int] = DEFAULT_DOWNLOAD_FREQUENCY
|
||||
generate_posters: Optional[bool] = None
|
||||
link: Optional[str] = None
|
||||
match_regex: Optional[str] = None
|
||||
mkdir: Optional[bool] = None
|
||||
name: Optional[str] = None
|
||||
url: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -33,11 +36,6 @@ class RefreshRequest:
|
|||
channel_name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class SuccessResponse:
|
||||
success: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChannelModel:
|
||||
id: int
|
||||
|
|
@ -129,11 +127,11 @@ class VideoResponse:
|
|||
|
||||
@dataclass
|
||||
class VideoSearchRequest:
|
||||
search_str: str
|
||||
filters: Optional[List[str]] = field(default_factory=list)
|
||||
search_str: Optional[str] = None
|
||||
filters: List[str] = field(default_factory=list)
|
||||
offset: Optional[int] = None
|
||||
limit: Optional[int] = None
|
||||
order_by: Optional[str] = None
|
||||
limit: Optional[int] = VIDEO_QUERY_LIMIT
|
||||
order_by: Optional[str] = DEFAULT_VIDEO_ORDER
|
||||
channel_link: Optional[str] = None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ from sanic_ext.extensions.openapi import openapi
|
|||
from wrolpi.common import logger, wrol_mode_check, run_after, get_media_directory, \
|
||||
get_relative_to_media_directory
|
||||
from wrolpi.db import get_db_session
|
||||
from wrolpi.errors import ValidationError, InvalidOrderBy
|
||||
from wrolpi.errors import InvalidOrderBy
|
||||
from wrolpi.root_api import json_response
|
||||
from wrolpi.schema import JSONErrorResponse
|
||||
from .lib import get_video, VIDEO_ORDERS, DEFAULT_VIDEO_ORDER, video_search, get_video_for_app
|
||||
from ..common import get_matching_directories, video_limiter
|
||||
from .lib import get_video, VIDEO_ORDERS, video_search, get_video_for_app
|
||||
from ..common import get_matching_directories
|
||||
from ..lib import save_channels_config
|
||||
from ..schema import VideoResponse, VideoSearchRequest, VideoSearchResponse, \
|
||||
DirectoriesResponse, DirectoriesRequest
|
||||
|
|
@ -36,21 +36,18 @@ def video_get(_: Request, video_id: int):
|
|||
@openapi.response(HTTPStatus.OK, VideoSearchResponse)
|
||||
@openapi.response(HTTPStatus.NOT_FOUND, JSONErrorResponse)
|
||||
@validate(VideoSearchRequest)
|
||||
async def search(_: Request, data: dict):
|
||||
try:
|
||||
search_str = data.get('search_str')
|
||||
channel_link = data.get('channel_link')
|
||||
order_by = data.get('order_by', DEFAULT_VIDEO_ORDER)
|
||||
offset = int(data.get('offset', 0))
|
||||
limit = video_limiter(data.get('limit'))
|
||||
filters = data.get('filters', None)
|
||||
except Exception as e:
|
||||
raise ValidationError('Unable to validate search queries') from e
|
||||
|
||||
if order_by not in VIDEO_ORDERS:
|
||||
async def search(_: Request, body: VideoSearchRequest):
|
||||
if body.order_by not in VIDEO_ORDERS:
|
||||
raise InvalidOrderBy('Invalid order by')
|
||||
|
||||
videos, videos_total = video_search(search_str, offset, limit, channel_link, order_by, filters)
|
||||
videos, videos_total = video_search(
|
||||
body.search_str,
|
||||
body.offset,
|
||||
body.limit,
|
||||
body.channel_link,
|
||||
body.order_by,
|
||||
body.filters,
|
||||
)
|
||||
|
||||
ret = {'videos': list(videos), 'totals': {'videos': videos_total}}
|
||||
return json_response(ret)
|
||||
|
|
|
|||
|
|
@ -91,8 +91,6 @@ class TestVideoFunctions(TestAPI):
|
|||
self.assertError(resp, HTTPStatus.FORBIDDEN, API_ERRORS[WROLModeEnabled]['code'])
|
||||
_, resp = api_app.test_client.put('/api/videos/channels/channel1', content=channel)
|
||||
self.assertError(resp, HTTPStatus.FORBIDDEN, API_ERRORS[WROLModeEnabled]['code'])
|
||||
_, resp = api_app.test_client.patch('/api/videos/channels/channel1', content=channel)
|
||||
self.assertError(resp, HTTPStatus.FORBIDDEN, API_ERRORS[WROLModeEnabled]['code'])
|
||||
_, resp = api_app.test_client.delete('/api/videos/channels/channel1')
|
||||
self.assertError(resp, HTTPStatus.FORBIDDEN, API_ERRORS[WROLModeEnabled]['code'])
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ pytest==6.2.5
|
|||
pytz==2021.3
|
||||
PyYAML==5.3.1
|
||||
requests==2.27.1
|
||||
sanic-ext==21.12.1
|
||||
sanic-ext==22.1.2
|
||||
sanic-testing==0.8.2
|
||||
sanic==21.12.1
|
||||
selenium==4.1.0
|
||||
|
|
|
|||
|
|
@ -458,7 +458,7 @@ def get_absolute_media_path(path: str) -> Path:
|
|||
return path
|
||||
|
||||
|
||||
def get_relative_to_media_directory(path: str) -> Path:
|
||||
def get_relative_to_media_directory(path: Union[str, Path]) -> Path:
|
||||
"""
|
||||
Get the path for a file/directory relative to the config media directory.
|
||||
|
||||
|
|
@ -482,7 +482,7 @@ def minimize_dict(d: dict, keys: Iterable) -> Optional[dict]:
|
|||
return {k: d[k] for k in set(keys) & d.keys()}
|
||||
|
||||
|
||||
def make_media_directory(path: str):
|
||||
def make_media_directory(path: Union[str, Path]):
|
||||
"""
|
||||
Make a directory relative within the media directory.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ from wrolpi.common import set_sanic_url_parts, logger, get_config, wrol_mode_ena
|
|||
Base, get_media_directory, wrol_mode_check
|
||||
from wrolpi.dates import set_timezone
|
||||
from wrolpi.downloader import download_manager
|
||||
from wrolpi.errors import WROLModeEnabled, InvalidTimezone
|
||||
from wrolpi.errors import WROLModeEnabled, InvalidTimezone, API_ERRORS, APIError, ValidationError
|
||||
from wrolpi.media_path import MediaPath
|
||||
from wrolpi.schema import RegexRequest, RegexResponse, SettingsRequest, SettingsResponse, DownloadRequest, EchoResponse
|
||||
from wrolpi.vars import DOCKERIZED
|
||||
|
|
@ -250,3 +250,18 @@ def json_response(*a, **kwargs) -> HTTPResponse:
|
|||
"""
|
||||
resp = response.json(*a, **kwargs, cls=CustomJSONEncoder, dumps=json.dumps)
|
||||
return resp
|
||||
|
||||
|
||||
def json_error_handler(request, exception: Exception):
|
||||
error = API_ERRORS[type(exception)]
|
||||
if isinstance(exception, ValidationError):
|
||||
body = dict(error='Could not validate the contents of the request', code=error['code'])
|
||||
else:
|
||||
body = dict(message=str(exception), api_error=error['message'], code=error['code'])
|
||||
if cause := exception.__cause__:
|
||||
cause = API_ERRORS[type(cause)]
|
||||
body['cause'] = dict(error=cause['message'], code=cause['code'])
|
||||
return json_response(body, error['status'])
|
||||
|
||||
|
||||
api_app.error_handler.add(APIError, json_error_handler)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue