mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-25 18:40:06 -04:00
Adding toggle to Channel page to stop downloading comments for certain channels.
This commit is contained in:
parent
47d4d72bf8
commit
581d5eaf6e
20 changed files with 150 additions and 77 deletions
|
|
@ -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')
|
||||
|
|
@ -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}) {
|
|||
</Grid.Row>;
|
||||
}
|
||||
|
||||
const downloadMissingDataInfo = 'Automatically download missing comments, etc, in the background.';
|
||||
const downloadMissingDataLabel = <>Download Missing Data<InfoPopup content={downloadMissingDataInfo}/></>;
|
||||
const downloadMissingDataRow = <Grid.Row>
|
||||
<Grid.Column>
|
||||
<ToggleForm
|
||||
form={form}
|
||||
label={downloadMissingDataLabel}
|
||||
path='download_missing_data'
|
||||
/>
|
||||
</Grid.Column>
|
||||
</Grid.Row>;
|
||||
|
||||
let messageRow;
|
||||
if (error || success) {
|
||||
messageRow = <Grid.Row columns={1}>
|
||||
|
|
@ -384,6 +398,7 @@ export function ChannelPage({create, header}) {
|
|||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
{channelTagRow}
|
||||
{downloadMissingDataRow}
|
||||
{!create && (channel.url || channel.rss_url) &&
|
||||
<SimpleAccordion title='Details'>
|
||||
<Grid>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</FormInput>
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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__)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue