From 581d5eaf6e3cee85c5f09ae298f308fec697c5cc Mon Sep 17 00:00:00 2001 From: Roland Date: Wed, 19 Feb 2025 14:52:50 -0700 Subject: [PATCH] Adding toggle to Channel page to stop downloading comments for certain channels. --- ...29a67e333_channel_download_missing_data.py | 31 ++++++++++ app/src/components/Channels.js | 19 +++++- app/src/hooks/customHooks.js | 3 + app/src/hooks/useForm.js | 1 + modules/archive/errors.py | 2 +- modules/inventory/errors.py | 4 +- modules/otp/errors.py | 6 +- modules/videos/channel/test/test_api.py | 26 +++----- modules/videos/errors.py | 16 ++--- modules/videos/lib.py | 2 +- modules/videos/models.py | 2 + modules/videos/schema.py | 2 + modules/videos/test/test_common.py | 19 ++++++ modules/videos/video/lib.py | 8 ++- modules/zim/errors.py | 8 +-- scripts/map_download_tiles.py | 4 +- wrolpi/api_utils.py | 4 +- wrolpi/errors.py | 62 +++++++++---------- wrolpi/files/lib.py | 2 +- wrolpi/test/test_root_api.py | 6 +- 20 files changed, 150 insertions(+), 77 deletions(-) create mode 100644 alembic/versions/f7229a67e333_channel_download_missing_data.py diff --git a/alembic/versions/f7229a67e333_channel_download_missing_data.py b/alembic/versions/f7229a67e333_channel_download_missing_data.py new file mode 100644 index 00000000..7a461b37 --- /dev/null +++ b/alembic/versions/f7229a67e333_channel_download_missing_data.py @@ -0,0 +1,31 @@ +"""Channel.download_missing_data + +Revision ID: f7229a67e333 +Revises: dc6637ced17b +Create Date: 2025-02-19 14:02:57.952791 + +""" +import os + +from alembic import op +from sqlalchemy.orm import Session + +# revision identifiers, used by Alembic. +revision = 'f7229a67e333' +down_revision = 'dc6637ced17b' +branch_labels = None +depends_on = None + +DOCKERIZED = True if os.environ.get('DOCKER', '').lower().startswith('t') else False + + +def upgrade(): + bind = op.get_bind() + session = Session(bind=bind) + session.execute('ALTER TABLE channel ADD COLUMN IF NOT EXISTS download_missing_data BOOLEAN DEFAULT True') + + +def downgrade(): + bind = op.get_bind() + session = Session(bind=bind) + session.execute('ALTER TABLE channel DROP COLUMN IF EXISTS download_missing_data') diff --git a/app/src/components/Channels.js b/app/src/components/Channels.js index 06154465..2032ffb1 100644 --- a/app/src/components/Channels.js +++ b/app/src/components/Channels.js @@ -7,6 +7,7 @@ import { ErrorMessage, humanFileSize, humanNumber, + InfoPopup, SearchInput, secondsToFrequency, secondsToFullDuration, @@ -36,7 +37,7 @@ import {SortableTable} from "./SortableTable"; import {toast} from "react-semantic-toasts-2"; import {RecurringDownloadsTable} from "./admin/Downloads"; import {TagsContext, TagsSelector} from "../Tags"; -import {InputForm} from "../hooks/useForm"; +import {InputForm, ToggleForm} from "../hooks/useForm"; import {ChannelDownloadForm, DestinationForm, DownloadTagsSelector} from "./Download"; @@ -123,6 +124,7 @@ export function ChannelPage({create, header}) { name: channel.name, directory: channel.directory, url: channel.url, + download_missing_data: channel.download_missing_data, }; let response = null; @@ -184,7 +186,7 @@ export function ChannelPage({create, header}) { 'name', ); } else { - setErrorMessage('Invalid channel', 'Unable to save channel. See logs.'); + setErrorMessage('Invalid channel', error.message || error.error || 'Unable to save channel. See logs.'); } } else { console.error('Did not get a response for channel!'); @@ -327,6 +329,18 @@ export function ChannelPage({create, header}) { ; } + const downloadMissingDataInfo = 'Automatically download missing comments, etc, in the background.'; + const downloadMissingDataLabel = <>Download Missing Data; + const downloadMissingDataRow = + + + + ; + let messageRow; if (error || success) { messageRow = @@ -384,6 +398,7 @@ export function ChannelPage({create, header}) { {channelTagRow} + {downloadMissingDataRow} {!create && (channel.url || channel.rss_url) && diff --git a/app/src/hooks/customHooks.js b/app/src/hooks/customHooks.js index af1d579a..3dd009fa 100644 --- a/app/src/hooks/customHooks.js +++ b/app/src/hooks/customHooks.js @@ -467,6 +467,7 @@ export const useChannel = (channel_id) => { directory: '', url: '', tag_name: null, + download_missing_data: null, }; const fetchChannel = async () => { @@ -479,6 +480,7 @@ export const useChannel = (channel_id) => { c['url'] = c['url'] || ''; c['download_frequency'] = c['download_frequency'] || ''; c['match_regex'] = c['match_regex'] || ''; + c['download_missing_data'] = c['download_missing_data'] ?? true; return c; } @@ -487,6 +489,7 @@ export const useChannel = (channel_id) => { name: form.formData.name, directory: form.formData.directory, url: form.formData.url, + download_missing_data: form.formData.download_missing_data, }; if (channel_id) { diff --git a/app/src/hooks/useForm.js b/app/src/hooks/useForm.js index d26c8eb2..dac21c8f 100644 --- a/app/src/hooks/useForm.js +++ b/app/src/hooks/useForm.js @@ -437,6 +437,7 @@ export function ToggleForm({form, name, label, path, icon = null, iconSize = 'bi disabled={form.disabled} name={name} checked={inputProps.value} + onChange={inputProps.onChange} /> } \ No newline at end of file diff --git a/modules/archive/errors.py b/modules/archive/errors.py index 3551190d..8ed9eb5c 100644 --- a/modules/archive/errors.py +++ b/modules/archive/errors.py @@ -5,5 +5,5 @@ from wrolpi.errors import APIError class InvalidArchive(APIError): code = 'INVALID_ARCHIVE' - message = 'The archive is invalid. See server logs.' + summary = 'The archive is invalid. See server logs.' status_code = HTTPStatus.BAD_REQUEST diff --git a/modules/inventory/errors.py b/modules/inventory/errors.py index 3506d381..5da50e71 100644 --- a/modules/inventory/errors.py +++ b/modules/inventory/errors.py @@ -5,11 +5,11 @@ from wrolpi.errors import APIError class NoInventories(APIError): code = 'NO_INVENTORIES' - message = 'No Inventories' + summary = 'No Inventories' status_code = HTTPStatus.BAD_REQUEST class InventoriesVersionMismatch(APIError): code = 'INVENTORIES_VERSION_MISMATCH' - message = 'Inventories version in the DB does not match the inventories config' + summary = 'Inventories version in the DB does not match the inventories config' status_code = HTTPStatus.BAD_REQUEST diff --git a/modules/otp/errors.py b/modules/otp/errors.py index a27a2cd5..865a24df 100644 --- a/modules/otp/errors.py +++ b/modules/otp/errors.py @@ -5,17 +5,17 @@ from wrolpi.errors import APIError class InvalidOTP(APIError): code = 'INVALID_OTP' - message = 'OTP has invalid characters' + summary = 'OTP has invalid characters' status_code = HTTPStatus.BAD_REQUEST class InvalidPlaintext(APIError): code = 'INVALID_PLAINTEXT' - message = 'Plaintext has invalid characters' + summary = 'Plaintext has invalid characters' status_code = HTTPStatus.BAD_REQUEST class InvalidCiphertext(APIError): code = 'INVALID_CIPHERTEXT' - message = 'Ciphertext has invalid characters' + summary = 'Ciphertext has invalid characters' status_code = HTTPStatus.BAD_REQUEST diff --git a/modules/videos/channel/test/test_api.py b/modules/videos/channel/test/test_api.py index 5301bbbc..ec4d206c 100644 --- a/modules/videos/channel/test/test_api.py +++ b/modules/videos/channel/test/test_api.py @@ -127,10 +127,10 @@ async def test_channel_conflicts(async_client, test_session, test_directory): request, response = await _post_channel(new_channel) assert response.status_code == HTTPStatus.BAD_REQUEST assert response.json == {'cause': {'code': 'CHANNEL_NAME_CONFLICT', - 'error': 'The channel name is already taken.', + 'error': 'Bad Request', 'message': 'The channel name is already taken.'}, 'code': 'VALIDATION_ERROR', - 'error': 'Could not validate the contents of the request', + 'error': 'Bad Request', 'message': 'Could not validate the contents of the request'} # Directory was already used @@ -141,10 +141,10 @@ async def test_channel_conflicts(async_client, test_session, test_directory): request, response = await _post_channel(new_channel) assert response.status_code == HTTPStatus.BAD_REQUEST assert response.json == {'cause': {'code': 'CHANNEL_DIRECTORY_CONFLICT', - 'error': 'The directory is already used by another channel.', + 'error': 'Bad Request', 'message': 'The directory is already used by another channel.'}, 'code': 'VALIDATION_ERROR', - 'error': 'Could not validate the contents of the request', + 'error': 'Bad Request', 'message': 'Could not validate the contents of the request'} # URL is already used @@ -158,10 +158,10 @@ async def test_channel_conflicts(async_client, test_session, test_directory): request, response = await _post_channel(new_channel) assert response.status_code == HTTPStatus.BAD_REQUEST assert response.json == {'cause': {'code': 'CHANNEL_URL_CONFLICT', - 'error': 'The URL is already used by another channel.', + 'error': 'Bad Request', 'message': 'The URL is already used by another channel.'}, 'code': 'VALIDATION_ERROR', - 'error': 'Could not validate the contents of the request', + 'error': 'Bad Request', 'message': 'Could not validate the contents of the request'} @@ -170,21 +170,15 @@ def test_channel_empty_url_doesnt_conflict(test_client, test_session, test_direc channel_directory = tempfile.TemporaryDirectory(dir=test_directory).name pathlib.Path(channel_directory).mkdir() - new_channel = { - 'name': 'Fooz', - 'directory': channel_directory, - } - request, response = test_client.post('/api/videos/channels', content=json.dumps(new_channel)) + new_channel = dict(name='Fooz', directory=channel_directory) + request, response = test_client.post('/api/videos/channels', json=new_channel) assert response.status_code == HTTPStatus.CREATED, response.json location = response.headers['Location'] channel_directory2 = tempfile.TemporaryDirectory(dir=test_directory).name pathlib.Path(channel_directory2).mkdir() - new_channel = { - 'name': 'Barz', - 'directory': channel_directory2, - } - request, response = test_client.post('/api/videos/channels', content=json.dumps(new_channel)) + new_channel = dict(name='Barz', directory=channel_directory2) + request, response = test_client.post('/api/videos/channels', json=new_channel) assert response.status_code == HTTPStatus.CREATED, response.json assert location != response.headers['Location'] diff --git a/modules/videos/errors.py b/modules/videos/errors.py index a42d741f..258fdfe0 100644 --- a/modules/videos/errors.py +++ b/modules/videos/errors.py @@ -5,47 +5,47 @@ from wrolpi.errors import APIError class UnknownVideo(APIError): code = 'UNKNOWN_VIDEO' - message = 'The video could not be found.' + summary = 'The video could not be found.' status_code = HTTPStatus.NOT_FOUND class UnknownChannel(APIError): code = 'UNKNOWN_CHANNEL' - message = 'The channel could not be found.' + summary = 'The channel could not be found.' status_code = HTTPStatus.NOT_FOUND class InvalidChannel(APIError): code = 'INVALID_CHANNEL' - message = 'The channel is not supported' + summary = 'The channel is not supported' status_code = HTTPStatus.BAD_REQUEST class ChannelNameConflict(APIError): code = 'CHANNEL_NAME_CONFLICT' - message = 'The channel name is already taken.' + summary = 'The channel name is already taken.' status_code = HTTPStatus.BAD_REQUEST class ChannelURLConflict(APIError): code = 'CHANNEL_URL_CONFLICT' - message = 'The URL is already used by another channel.' + summary = 'The URL is already used by another channel.' status_code = HTTPStatus.BAD_REQUEST class ChannelDirectoryConflict(APIError): code = 'CHANNEL_DIRECTORY_CONFLICT' - message = 'The directory is already used by another channel.' + summary = 'The directory is already used by another channel.' status_code = HTTPStatus.BAD_REQUEST class ChannelSourceIdConflict(APIError): code = 'CHANNEL_SOURCE_ID_CONFLICT' - message = 'Search is empty, search_str must have content.' + summary = 'Search is empty, search_str must have content.' status_code = HTTPStatus.BAD_REQUEST class ChannelURLEmpty(APIError): code = 'CHANNEL_URL_EMPTY' - message = 'This channel has no URL' + summary = 'This channel has no URL' status_code = HTTPStatus.BAD_REQUEST diff --git a/modules/videos/lib.py b/modules/videos/lib.py index 5c917f89..e5d7756b 100644 --- a/modules/videos/lib.py +++ b/modules/videos/lib.py @@ -297,7 +297,7 @@ class ChannelsConfig(ConfigFile): if not channel.source_id and channel.url and flags.have_internet.is_set(): # If we can download from a channel, we must have its source_id. - if download_manager.can_download: + if download_manager.can_download and channel.download_missing_data: logger.info(f'Fetching channel source id for {channel}') background_task(fetch_channel_source_id(channel.id)) diff --git a/modules/videos/models.py b/modules/videos/models.py index 040f12f2..ed72b3bc 100644 --- a/modules/videos/models.py +++ b/modules/videos/models.py @@ -528,6 +528,7 @@ class Channel(ModelHelper, Base): directory: pathlib.Path = Column(MediaPathType) generate_posters = Column(Boolean, default=False) # generating posters may delete files, and can be slow. calculate_duration = Column(Boolean, default=True) # use ffmpeg to extract duration (slower than info json). + download_missing_data = Column(Boolean, default=True) # fetch missing data like `source_id` and video comments. source_id = Column(String) # the ID from the source website. refreshed = Column(Boolean, default=False) # The files in the Channel have been refreshed. @@ -633,6 +634,7 @@ class Channel(ModelHelper, Base): config = dict( calculate_duration=self.calculate_duration, directory=str(self.directory), + download_missing_data=self.download_missing_data, downloads=[{'url': i.url, 'frequency': i.frequency} for i in self.downloads], generate_posters=self.generate_posters, name=self.name, diff --git a/modules/videos/schema.py b/modules/videos/schema.py index de0aef45..daefd381 100644 --- a/modules/videos/schema.py +++ b/modules/videos/schema.py @@ -10,6 +10,7 @@ class ChannelPostRequest: name: str directory: str calculate_duration: Optional[bool] = None + download_missing_data: Optional[bool] = True generate_posters: Optional[bool] = None source_id: Optional[str] = None tag_name: Optional[str] = None @@ -26,6 +27,7 @@ class ChannelPostRequest: class ChannelPutRequest: calculate_duration: Optional[bool] = None directory: Optional[str] = None + download_missing_data: Optional[bool] = True generate_posters: Optional[bool] = None mkdir: Optional[bool] = None name: Optional[str] = None diff --git a/modules/videos/test/test_common.py b/modules/videos/test/test_common.py index 3b7772c1..93b72086 100644 --- a/modules/videos/test/test_common.py +++ b/modules/videos/test/test_common.py @@ -288,6 +288,25 @@ async def test_import_channel_delete_missing_channels(await_switches, test_sessi assert str(channel2.directory) not in test_channels_config.read_text() +@pytest.mark.asyncio +async def test_import_channel_download_comments(await_switches, test_session, channel_factory, + test_channels_config): + channel = channel_factory() + assert channel.download_missing_data is True + + # download_missing_data is saved to the config. + save_channels_config() + await await_switches() + assert 'download_missing_data: true' in test_channels_config.read_text() + + # change download_missing_data to False, channel should be updated on import. + contents = test_channels_config.read_text() + contents = contents.replace('download_missing_data: true', 'download_missing_data: false') + test_channels_config.write_text(contents) + import_channels_config() + assert channel.download_missing_data is False + + @pytest.mark.asyncio async def test_ffprobe_json(async_client, video_file, corrupted_video_file): content = await common.ffprobe_json(video_file) diff --git a/modules/videos/video/lib.py b/modules/videos/video/lib.py index 71e955ce..86d7ce82 100644 --- a/modules/videos/video/lib.py +++ b/modules/videos/video/lib.py @@ -7,7 +7,7 @@ import yt_dlp from sqlalchemy import or_ from sqlalchemy.orm import Session -from modules.videos.models import Video +from modules.videos.models import Video, Channel from wrolpi.common import logger, limit_concurrent, wrol_mode_check from wrolpi.dates import now from wrolpi.db import get_db_session, optional_session @@ -209,7 +209,13 @@ async def get_missing_videos_comments(limit: int = VIDEO_COMMENTS_FETCH_COUNT): one_month_ago > FileGroup.published_datetime, FileGroup.published_datetime == None, ), + # Do not download a Videos with a Channel and Channel.download_missing_data is False. + or_( + Channel.download_missing_data == True, + Channel.id == None, + ) ).join(FileGroup) \ + .outerjoin(Channel) \ .order_by(FileGroup.published_datetime.nullsfirst()) \ .limit(limit) video_urls = [i.file_group.url for i in videos] diff --git a/modules/zim/errors.py b/modules/zim/errors.py index 6afa149f..d43d1169 100644 --- a/modules/zim/errors.py +++ b/modules/zim/errors.py @@ -5,23 +5,23 @@ from wrolpi.errors import APIError class UnknownZim(APIError): code = 'UNKNOWN_ZIM' - message = 'Failed to find the Zim' + summary = 'Failed to find the Zim' status_code = HTTPStatus.NOT_FOUND class UnknownZimEntry(APIError): code = 'UNKNOWN_ZIM_ENTRY' - message = 'Failed to find the Zim entry at that path' + summary = 'Failed to find the Zim entry at that path' status_code = HTTPStatus.NOT_FOUND class UnknownZimTagEntry(APIError): code = 'UNKNOWN_ZIM_TAG_ENTRY' - message = 'Failed to find the TagZimEntry' + summary = 'Failed to find the TagZimEntry' status_code = HTTPStatus.NOT_FOUND class UnknownZimSubscription(APIError): code = 'UNKNOWN_ZIM_SUBSCRIPTION' - message = 'Failed to find the ZimSubscription' + summary = 'Failed to find the ZimSubscription' status_code = HTTPStatus.NOT_FOUND diff --git a/scripts/map_download_tiles.py b/scripts/map_download_tiles.py index b294f555..06472575 100755 --- a/scripts/map_download_tiles.py +++ b/scripts/map_download_tiles.py @@ -30,7 +30,7 @@ def longitude_to_tile_x(lon: float, zoom: int) -> int: def get_tile(host: str, zoom: int, x: int, y: int) -> bytes: - response = requests.get(f"{host}/tile/{zoom}/{x}/{y}.png", verify=False) + response = requests.get(f"{host}/{zoom}/{x}/{y}.png", verify=False, timeout=5 * 60) if response.status_code != 200: raise RuntimeError(f"Request failed with status code {response.status_code}") return response.content @@ -75,7 +75,7 @@ def main( if __name__ == "__main__": parser = argparse.ArgumentParser(description="Download map tiles from WROLPi") parser.add_argument('-H', '--host', type=str, - default='https://localhost:8084/', + default='https://localhost:8084/hot', help='The URL of your map tiles server', ) parser.add_argument('-a', '--zoom-start', type=int, default=0, diff --git a/wrolpi/api_utils.py b/wrolpi/api_utils.py index 539caee8..f6623d8a 100644 --- a/wrolpi/api_utils.py +++ b/wrolpi/api_utils.py @@ -1,13 +1,13 @@ import asyncio import json import logging -from time import time from asyncio import CancelledError from datetime import datetime, timezone, date from decimal import Decimal from functools import wraps from http import HTTPStatus from pathlib import Path +from time import time from sanic import response, HTTPResponse, Request, Sanic, SanicException @@ -74,7 +74,7 @@ def get_error_json(exception: BaseException): """Return a JSON representation of the Exception instance.""" if isinstance(exception, APIError): # An exception from WROLPi. - body = dict(error=str(exception), message=exception.message, code=exception.code) + body = dict(error=str(exception), message=exception.summary, code=exception.code) elif isinstance(exception, SanicException): # An exception from Sanic. body = dict(error=str(exception), message=exception.message, code=type(exception).__name__) diff --git a/wrolpi/errors.py b/wrolpi/errors.py index 2c4650d3..735a93e5 100644 --- a/wrolpi/errors.py +++ b/wrolpi/errors.py @@ -5,185 +5,185 @@ from sanic import SanicException class APIError(SanicException): code = 'APIError' - message = 'No summary defined' + summary = 'No summary defined' status_code = HTTPStatus.INTERNAL_SERVER_ERROR class UnknownDirectory(APIError): code = 'UNKNOWN_DIRECTORY' - message = 'The directory does not exist.' + summary = 'The directory does not exist.' status_code = HTTPStatus.NOT_FOUND class UnknownFile(APIError): code = 'UNKNOWN_FILE' - message = 'The file does not exist.' + summary = 'The file does not exist.' status_code = HTTPStatus.NOT_FOUND class SearchEmpty(APIError): code = 'SEARCH_EMPTY' - message = 'Search is empty, search_str must have content.' + summary = 'Search is empty, search_str must have content.' status_code = HTTPStatus.BAD_REQUEST class ValidationError(APIError): code = 'VALIDATION_ERROR' - message = 'Could not validate the contents of the request' + summary = 'Could not validate the contents of the request' status_code = HTTPStatus.BAD_REQUEST class InvalidOrderBy(ValidationError): code = 'INVALID_ORDER_BY' - message = 'Invalid order_by' + summary = 'Invalid order_by' status_code = HTTPStatus.BAD_REQUEST class WROLModeEnabled(APIError): code = 'WROL_MODE_ENABLED' - message = 'This method is disabled while WROL Mode is enabled.' + summary = 'This method is disabled while WROL Mode is enabled.' status_code = HTTPStatus.FORBIDDEN class InvalidDownload(APIError): code = 'INVALID_DOWNLOAD' - message = 'The URL cannot be downloaded.' + summary = 'The URL cannot be downloaded.' status_code = HTTPStatus.BAD_REQUEST class UnknownDownload(APIError): code = 'UNKNOWN_DOWNLOAD' - message = 'The Download cannot be found.' + summary = 'The Download cannot be found.' status_code = HTTPStatus.NOT_FOUND class UnrecoverableDownloadError(APIError): code = 'UNRECOVERABLE_DOWNLOAD_ERROR' - message = 'Download experienced an error which cannot be recovered. Download will be deleted.' + summary = 'Download experienced an error which cannot be recovered. Download will be deleted.' status_code = HTTPStatus.BAD_REQUEST class InvalidFile(APIError): code = 'INVALID_FILE' - message = 'File does not exist or is a directory' + summary = 'File does not exist or is a directory' status_code = HTTPStatus.BAD_REQUEST class NativeOnly(APIError): code = 'NATIVE_ONLY' - message = 'This functionality is only supported outside a docker container' + summary = 'This functionality is only supported outside a docker container' status_code = HTTPStatus.BAD_REQUEST class HotspotError(APIError): code = 'HOTSPOT_ERROR' - message = 'Updating/accessing Hotspot encountered an error' + summary = 'Updating/accessing Hotspot encountered an error' status_code = HTTPStatus.INTERNAL_SERVER_ERROR class LogLevelError(APIError): code = 'LOG_LEVEL_ERROR' - message = 'Log level is invalid' + summary = 'Log level is invalid' status_code = HTTPStatus.BAD_REQUEST class UnknownArchive(APIError): code = 'UNKNOWN_ARCHIVE' - message = 'Unable to find the archive' + summary = 'Unable to find the archive' status_code = HTTPStatus.NOT_FOUND class UnknownTag(APIError): code = 'UNKNOWN_TAG' - message = 'Unable to find the tag' + summary = 'Unable to find the tag' status_code = HTTPStatus.NOT_FOUND class UsedTag(APIError): code = 'USED_TAG' - message = 'Tag is being used' + summary = 'Tag is being used' status_code = HTTPStatus.BAD_REQUEST class InvalidTag(APIError): code = 'INVALID_TAG' - message = 'Tag is not allowed' + summary = 'Tag is not allowed' status_code = HTTPStatus.BAD_REQUEST class FileGroupIsTagged(APIError): code = 'FILE_GROUP_IS_TAGGED' - message = 'FileGroup is tagged' + summary = 'FileGroup is tagged' status_code = HTTPStatus.CONFLICT class FileGroupAlreadyTagged(APIError): code = 'FILE_GROUP_ALREADY_TAGGED' - message = 'FileGroup is already tagged with this Tag' + summary = 'FileGroup is already tagged with this Tag' status_code = HTTPStatus.CONFLICT class FileUploadFailed(APIError): code = 'FILE_UPLOAD_FAILED' - message = 'Failed to upload file' + summary = 'Failed to upload file' status_code = HTTPStatus.BAD_REQUEST class FileConflict(APIError): code = 'FILE_CONFLICT' - message = 'File/directory already exists' + summary = 'File/directory already exists' status_code = HTTPStatus.CONFLICT class HotspotPasswordTooShort(APIError): code = 'HOTSPOT_PASSWORD_TOO_SHORT' - message = 'Hotspot password must be at least 8 characters' + summary = 'Hotspot password must be at least 8 characters' status_code = HTTPStatus.BAD_REQUEST class ShutdownFailed(APIError): code = 'SHUTDOWN_FAILED' - message = 'Unable to shutdown the WROLPi' + summary = 'Unable to shutdown the WROLPi' status_code = HTTPStatus.INTERNAL_SERVER_ERROR class InvalidConfig(APIError): code = 'INVALID_CONFIG' - message = 'Config is invalid' + summary = 'Config is invalid' status_code = HTTPStatus.BAD_REQUEST class NoPrimaryFile(APIError): code = 'NO_PRIMARY_FILE' - message = 'Could not find a primary file in the provided group' + summary = 'Could not find a primary file in the provided group' status_code = HTTPStatus.INTERNAL_SERVER_ERROR class InvalidDatetime(APIError): code = 'INVALID_DATETIME' - message = 'Unable to parse a datetime string' + summary = 'Unable to parse a datetime string' status_code = HTTPStatus.INTERNAL_SERVER_ERROR class InvalidDirectory(APIError): code = 'INVALID_DIRECTORY' - message = 'Directory is not valid or impossible' + summary = 'Directory is not valid or impossible' status_code = HTTPStatus.BAD_REQUEST class RefreshConflict(APIError): code = 'REFRESH_CONFLICT' - message = 'Not possible during file refresh' + summary = 'Not possible during file refresh' status_code = HTTPStatus.CONFLICT class IgnoredDirectoryError(APIError): code = 'IGNORED_DIRECTORY_ERROR' - message = 'Directory is being ignored' + summary = 'Directory is being ignored' status_code = HTTPStatus.BAD_REQUEST class DownloadError(APIError): code = 'DOWNLOAD_ERROR' - message = 'Unable to complete download' + summary = 'Unable to complete download' status_code = HTTPStatus.INTERNAL_SERVER_ERROR diff --git a/wrolpi/files/lib.py b/wrolpi/files/lib.py index 06984445..6fee96b2 100644 --- a/wrolpi/files/lib.py +++ b/wrolpi/files/lib.py @@ -1615,7 +1615,7 @@ async def upsert_file(file: pathlib.Path | str, tag_names: List[str] = None) -> if url and download_manager.is_skipped(url): download_manager.remove_from_skip_list(url) - upsert_directories(file.parents, []) + upsert_directories([], file.parents) session.commit() diff --git a/wrolpi/test/test_root_api.py b/wrolpi/test/test_root_api.py index cf5b67f6..7422a32a 100644 --- a/wrolpi/test/test_root_api.py +++ b/wrolpi/test/test_root_api.py @@ -261,7 +261,7 @@ def test_hotspot_settings(test_session, test_client, test_wrolpi_config): request, response = test_client.patch('/api/settings', content=json.dumps(content)) assert response.status_code == HTTPStatus.BAD_REQUEST assert response.json == {'code': 'HOTSPOT_PASSWORD_TOO_SHORT', - 'error': 'Hotspot password must be at least 8 characters', + 'error': 'Bad Request', 'message': 'Hotspot password must be at least 8 characters' } @@ -714,7 +714,7 @@ def test_recursive_errors(): try: two() except Exception as e2: - raise ValidationError('A broad error') from e2 + raise ValidationError() from e2 try: three() @@ -722,7 +722,7 @@ def test_recursive_errors(): response = json_error_handler(None, e) assert json.loads(response.body.decode()) == { 'code': 'VALIDATION_ERROR', - 'error': 'A broad error', + 'error': 'Bad Request', # Fallback when no message is passed on exception creation. 'message': 'Could not validate the contents of the request', 'cause': { 'code': 'SEARCH_EMPTY',