changeValue('directory', value)}
+ placeholder='videos/channel directory'
/>
diff --git a/app/src/components/Common.js b/app/src/components/Common.js
index 04481271..c97994ad 100644
--- a/app/src/components/Common.js
+++ b/app/src/components/Common.js
@@ -20,7 +20,6 @@ import {Link, NavLink, useNavigate, useSearchParams} from "react-router-dom";
import Message from "semantic-ui-react/dist/commonjs/collections/Message";
import {
useCPUTemperature,
- useDirectories,
useHotspot,
useLoad,
useSearchDirectories,
@@ -53,7 +52,6 @@ import {
import {FilePreviewContext} from "./FilePreview";
import _ from "lodash";
import {killDownloads, startDownloads} from "../api";
-import Grid from "semantic-ui-react/dist/commonjs/collections/Grid";
export const API_URI = process.env && process.env.REACT_APP_API_URI ? process.env.REACT_APP_API_URI : `https://${window.location.host}/api`;
export const VIDEOS_API = `${API_URI}/videos`;
@@ -1213,16 +1211,21 @@ export function DirectorySearch({onSelect, value, ...props}) {
directories,
channelDirectories,
domainDirectories,
+ isDir,
loading,
} = useSearchDirectories(value);
const [results, setResults] = useState();
useEffect(() => {
- if (
- (directories && directories.length >= 0)
- || (channelDirectories && channelDirectories.length >= 0)
- || (domainDirectories && domainDirectories.length >= 0)) {
- setResults({
+ if (directories && directories.length >= 0) {
+ const newDirectory = isDir ? {} : {
+ newDirectory: {
+ name: 'New Directory',
+ results: [{title: directoryName}],
+ }
+ };
+ const newResults = {
+ ...newDirectory,
directories: {
name: 'Directories',
results: directories.map(i => {
@@ -1240,8 +1243,9 @@ export function DirectorySearch({onSelect, value, ...props}) {
results: domainDirectories.map(i => {
return {title: i['path'], description: i['domain']};
}),
- }
- });
+ },
+ };
+ setResults(newResults);
}
}, [
JSON.stringify(directories),
@@ -1254,9 +1258,6 @@ export function DirectorySearch({onSelect, value, ...props}) {
if (e) {
e.preventDefault();
}
- if (onSelect) {
- onSelect('');
- }
setDirectoryName(data.value);
}
@@ -1264,77 +1265,22 @@ export function DirectorySearch({onSelect, value, ...props}) {
if (e) {
e.preventDefault();
}
+ setDirectoryName(data.result.title);
// title is the relative path.
if (onSelect) {
onSelect(data.result.title);
}
- setDirectoryName(data.result.title);
}
- const handleClear = (e) => {
- if (e) {
- e.preventDefault();
- }
- onSelect('');
- setDirectoryName('');
- }
-
- return
-
-
-
-
-
-
-
-
-}
-
-export function DirectoryInput({disabled, error, placeholder, setInput, value, required, isDirectory}) {
- const {directory, directories, setDirectory, isDir} = useDirectories(value);
- const {settings} = React.useContext(SettingsContext);
-
- if (!directories || !settings) {
- return <>>;
- }
-
- const localSetInput = (e, {value}) => {
- setInput(value);
- setDirectory(value);
- }
-
- let {media_directory} = settings;
- if (media_directory && !media_directory.endsWith('/')) {
- media_directory = `${media_directory}/`;
- }
- error = error || isDirectory && !isDir;
-
- return
-
-
-
;
+ return
}
export const BackButton = ({...props}) => {
diff --git a/app/src/components/Upload.js b/app/src/components/Upload.js
index 9423c559..af098002 100644
--- a/app/src/components/Upload.js
+++ b/app/src/components/Upload.js
@@ -70,7 +70,11 @@ export function Upload({disabled}) {
diff --git a/app/src/components/VideoPlayer.js b/app/src/components/VideoPlayer.js
index 1b015265..29e53726 100644
--- a/app/src/components/VideoPlayer.js
+++ b/app/src/components/VideoPlayer.js
@@ -8,7 +8,7 @@ import {
encodeMediaPath,
getParentDirectory,
humanFileSize,
- humanNumber,
+ humanNumber, isoDatetimeToElapsedPopup,
isoDatetimeToString,
MultilineText,
PageContainer,
@@ -367,7 +367,7 @@ function VideoPage({videoFile, prevFile, nextFile, fetchVideo, ...props}) {
- {videoFile.published_datetime && {isoDatetimeToString(videoFile.published_datetime)}
}
+ {videoFile.published_datetime && {isoDatetimeToElapsedPopup(videoFile.published_datetime)}
}
{channel &&
{channel.name}
diff --git a/app/src/hooks/customHooks.js b/app/src/hooks/customHooks.js
index 4172c855..8b8959c0 100644
--- a/app/src/hooks/customHooks.js
+++ b/app/src/hooks/customHooks.js
@@ -8,7 +8,6 @@ import {
getChannel,
getChannels,
getConfigs,
- getDirectories,
getDownloads,
getFiles,
getInventory,
@@ -745,58 +744,22 @@ export const useThrottle = () => {
return {on, setOn, setThrottle: localSetThrottle};
}
-export const useDirectories = (defaultDirectory) => {
- const [directories, setDirectories] = useState();
- const [directory, setDirectory] = useState(defaultDirectory);
- const [exists, setExists] = useState();
- const [isDir, setIsDir] = useState();
- const [isFile, setIsFile] = useState();
-
- const fetchDirectories = async () => {
- if (defaultDirectory && directory === '') {
- setDirectory(defaultDirectory);
- }
- try {
- const {directories, is_dir, exists_, is_file} = await getDirectories(directory);
- setDirectories(directories);
- setIsDir(is_dir);
- setExists(exists_);
- setIsFile(is_file);
- } catch (e) {
- toast({
- type: 'error',
- title: 'Unexpected server response',
- description: 'Could not get directories',
- time: 5000,
- });
- }
- }
-
- useEffect(() => {
- fetchDirectories();
- }, [directory, defaultDirectory]);
-
- useEffect(() => {
- setDirectory(defaultDirectory);
- }, [defaultDirectory])
-
- return {directory, directories, setDirectory, exists, isDir, isFile};
-}
-
export const useSearchDirectories = (value) => {
const [directoryName, setDirectoryName] = useState(value ?? '');
const [directories, setDirectories] = useState(null);
const [channelDirectories, setChannelDirectories] = useState(null);
const [domainDirectories, setDomainDirectories] = useState(null);
+ const [isDir, setIsDir] = useState(false);
const [loading, setLoading] = useState(false);
const localSearchDirectories = async () => {
setLoading(true);
try {
- const {directories: dirs, channel_directories, domain_directories} = await searchDirectories(directoryName);
- setDirectories(dirs);
- setChannelDirectories(channel_directories);
- setDomainDirectories(domain_directories);
+ const result = await searchDirectories(directoryName);
+ setDirectories(result.directories);
+ setChannelDirectories(result.channel_directories);
+ setDomainDirectories(result.domain_directories);
+ setIsDir(result.is_dir);
} finally {
setLoading(false);
}
@@ -806,13 +769,19 @@ export const useSearchDirectories = (value) => {
localSearchDirectories();
}, [directoryName]);
+ useEffect(() => {
+ // Directory value was changed above.
+ setDirectoryName(value);
+ }, [value]);
+
return {
directoryName,
setDirectoryName,
directories,
loading,
channelDirectories,
- domainDirectories
+ domainDirectories,
+ isDir,
}
}
diff --git a/etc/raspberrypios/renderd.service b/etc/raspberrypios/renderd.service
index 54549d09..e191a1d9 100644
--- a/etc/raspberrypios/renderd.service
+++ b/etc/raspberrypios/renderd.service
@@ -5,7 +5,7 @@ After=network.target auditd.service
[Service]
# Delay startup so WROLPi API has time to start.
-ExecStartPre=/bin/sleep 30
+ExecStartPre=/bin/sleep 60
# Map is intense, but not a high priority.
Nice=19
ExecStart=/usr/bin/renderd -f
diff --git a/wrolpi/files/api.py b/wrolpi/files/api.py
index 6b01c0cb..47b86c4a 100644
--- a/wrolpi/files/api.py
+++ b/wrolpi/files/api.py
@@ -1,6 +1,5 @@
import pathlib
from http import HTTPStatus
-from typing import List
import sanic.request
from sanic import response, Request, Blueprint
@@ -127,31 +126,51 @@ def post_directories(_, body: schema.DirectoriesRequest):
@files_bp.post('/search_directories')
@openapi.definition(
- summary='Get all directories whose name matches the provided name.',
+ summary='Get all directories whose name matches the provided name or path.',
body=schema.DirectoriesSearchRequest,
)
@validate(schema.DirectoriesSearchRequest)
async def post_search_directories(_, body: schema.DirectoriesSearchRequest):
- if len(body.name) <= 1:
- return response.empty()
+ if len(body.path) <= 1:
+ # Need more data before searching.
+ return json_response({
+ 'is_dir': False,
+ 'directories': [],
+ 'channel_directories': [],
+ 'domain_directories': [],
+ })
- from modules.videos.channel import lib as channels_lib
- channels = await channels_lib.search_channels_by_name(name=body.name)
+ path = get_media_directory() / body.path
+
+ try:
+ matching_directories = lib.get_matching_directories(str(path))
+ matching_directories = list(map(pathlib.Path, matching_directories))
+ except FileNotFoundError:
+ matching_directories = []
+
+ # Search Channels by name.
+ from modules.videos.channel.lib import search_channels_by_name
+ channels = await search_channels_by_name(name=body.path)
channel_directories = [dict(path=i.directory, name=i.name) for i in channels]
channel_paths = [i['path'] for i in channel_directories]
- from modules.archive import lib as archives_lib
- domains = await archives_lib.search_domains_by_name(name=body.name)
+ # Search Domains by name.
+ from modules.archive.lib import search_domains_by_name
+ domains = await search_domains_by_name(name=body.path)
domain_directories = [dict(path=i.directory, domain=i.domain) for i in domains]
domain_paths = [i['path'] for i in domain_directories]
- # Get all directories that match but do not contain the above directories.
- from wrolpi.files.models import Directory
- directories: List[Directory] = await lib.search_directories_by_name(
- name=body.name,
- excluded=list(map(str, channel_paths + domain_paths)))
+ # Get all Directory that match but do not contain the above directories.
+ excluded = [str(i) for i in channel_paths + domain_paths + matching_directories]
+ directories = await lib.search_directories_by_name(name=body.path, excluded=excluded)
+
+ # Return only the top 20 directories.
+ directories = [i.__json__() for i in directories]
+ directories.extend([{'path': i, 'name': i.name} for i in matching_directories])
+ directories = list(sorted(directories, key=lambda i: i['path']))[:20]
body = {
+ 'is_dir': path.is_dir(),
'directories': directories,
'channel_directories': channel_directories,
'domain_directories': domain_directories
diff --git a/wrolpi/files/schema.py b/wrolpi/files/schema.py
index 466232f0..b303cbd9 100644
--- a/wrolpi/files/schema.py
+++ b/wrolpi/files/schema.py
@@ -50,7 +50,7 @@ class DirectoriesRequest:
@dataclass
class DirectoriesSearchRequest:
- name: str
+ path: str
@dataclass
diff --git a/wrolpi/files/test/test_api.py b/wrolpi/files/test/test_api.py
index 44fb513c..4d4b82df 100644
--- a/wrolpi/files/test/test_api.py
+++ b/wrolpi/files/test/test_api.py
@@ -342,32 +342,54 @@ def test_file_group_tag(test_client):
assert 'tag_id' in response.json['error']
-def test_search_directories(test_client, test_session, make_files_structure, assert_directories):
+@pytest.mark.asyncio
+async def test_search_directories(async_client, test_session, test_directory, make_files_structure,
+ assert_directories):
"""Directories can be searched by name."""
make_files_structure(['foo/one.txt', 'foo/two.txt', 'bar/one.txt'])
- request, response = test_client.post('/api/files/refresh')
- assert response.status_code == HTTPStatus.NO_CONTENT
+ await lib.refresh_files()
assert_directories({'foo', 'bar'})
+ # Create directory that was not refreshed.
+ (test_directory / 'baz').mkdir()
# More than one character required.
- content = dict(name='f')
- request, response = test_client.post('/api/files/search_directories', content=json.dumps(content))
- assert response.status_code == HTTPStatus.NO_CONTENT
+ content = dict(path='f')
+ request, response = await async_client.post('/api/files/search_directories', content=json.dumps(content))
+ assert response.status_code == HTTPStatus.OK, response.content
+ assert response.json['directories'] == []
+ assert response.json['is_dir'] is False
- content = dict(name='fo')
- request, response = test_client.post('/api/files/search_directories', content=json.dumps(content))
+ # Can search using partial directory name.
+ content = dict(path='fo')
+ request, response = await async_client.post('/api/files/search_directories', content=json.dumps(content))
assert response.status_code == HTTPStatus.OK
assert [i['path'] for i in response.json['directories']] == ['foo', ]
+ assert response.json['is_dir'] is False
+
+ # Searching directory exactly.
+ content = dict(path='foo')
+ request, response = await async_client.post('/api/files/search_directories', content=json.dumps(content))
+ assert response.status_code == HTTPStatus.OK
+ assert [i['path'] for i in response.json['directories']] == ['foo', ]
+ assert response.json['is_dir'] is True
# Case is ignored.
- content = dict(name='BAR')
- request, response = test_client.post('/api/files/search_directories', content=json.dumps(content))
+ content = dict(path='BAR')
+ request, response = await async_client.post('/api/files/search_directories', content=json.dumps(content))
assert response.status_code == HTTPStatus.OK
assert [i['path'] for i in response.json['directories']] == ['bar', ]
+ assert response.json['is_dir'] is False
+
+ # Can search directories not yet in DB.
+ content = dict(path='ba')
+ request, response = await async_client.post('/api/files/search_directories', content=json.dumps(content))
+ assert response.status_code == HTTPStatus.OK
+ assert [i['path'] for i in response.json['directories']] == ['bar', 'baz']
+ assert response.json['is_dir'] is False
# Searching for something that does not exist.
- content = dict(name='does not exist')
- request, response = test_client.post('/api/files/search_directories', content=json.dumps(content))
+ content = dict(path='does not exist')
+ request, response = await async_client.post('/api/files/search_directories', content=json.dumps(content))
assert response.status_code == HTTPStatus.OK
assert [i['path'] for i in response.json['directories']] == []
@@ -395,55 +417,69 @@ def test_search_directories(test_client, test_session, make_files_structure, ass
'foooooooooooooooooooooo/',
'fooooooooooooooooooooooo/',
])
- test_client.post('/api/files/refresh')
- content = dict(name='fo')
- request, response = test_client.post('/api/files/search_directories', content=json.dumps(content))
+ await async_client.post('/api/files/refresh')
+ content = dict(path='fo')
+ request, response = await async_client.post('/api/files/search_directories', content=json.dumps(content))
assert response.status_code == HTTPStatus.OK
assert [i['path'] for i in response.json['directories']] == [f'f{"o" * i}' for i in range(2, 22)]
-def test_post_search_directories(test_session, test_client, make_files_structure):
+@pytest.mark.asyncio
+async def test_post_search_directories(test_session, async_client, make_files_structure):
"""Directory names can be searched. This endpoint also returns Channel and Domain directories."""
- channel_dir, domain_dir, _, _ = make_files_structure([
+ channel1_dir, channel2_dir, domain_dir, _ = make_files_structure([
'dir1/',
'dir2/',
'dir3/',
'dir4/',
])
- request, response = test_client.post('/api/files/refresh')
+ request, response = await async_client.post('/api/files/refresh')
assert response.status_code == HTTPStatus.NO_CONTENT
from modules.videos.models import Channel
- channel = Channel(directory=channel_dir, name='Channel Name')
+ channel1 = Channel(directory=channel1_dir, name='Channel Name')
+ channel2 = Channel(directory=channel2_dir, name='OtherChannel')
from modules.archive.models import Domain
domain = Domain(directory=domain_dir, domain='example.com')
- test_session.add_all([channel, domain])
+ test_session.add_all([channel1, channel2, domain])
test_session.commit()
- content = {'name': 'di'}
- request, response = test_client.post('/api/files/search_directories', content=json.dumps(content))
- assert response.status_code == HTTPStatus.OK
# All directories contain "di". The names of the Channel and Directory do not match.
+ content = {'path': 'di'}
+ request, response = await async_client.post('/api/files/search_directories', content=json.dumps(content))
+ assert response.status_code == HTTPStatus.OK
assert response.json['directories'] == [{'name': 'dir1', 'path': 'dir1'}, {'name': 'dir2', 'path': 'dir2'},
{'name': 'dir3', 'path': 'dir3'}, {'name': 'dir4', 'path': 'dir4'}]
assert response.json['channel_directories'] == []
assert response.json['domain_directories'] == []
- content = {'name': 'Chan'}
- request, response = test_client.post('/api/files/search_directories', content=json.dumps(content))
+ # Channel name matches.
+ content = {'path': 'Chan'}
+ request, response = await async_client.post('/api/files/search_directories', content=json.dumps(content))
+ assert response.status_code == HTTPStatus.OK
+ assert response.json['directories'] == []
+ assert response.json['channel_directories'] == [
+ {'name': 'Channel Name', 'path': 'dir1'},
+ {'name': 'OtherChannel', 'path': 'dir2'},
+ ]
+ assert response.json['domain_directories'] == []
+
+ # "OtherChannel" matches even though the case is wrong, and it has a space.
+ content = {'path': 'other channel'}
+ request, response = await async_client.post('/api/files/search_directories', content=json.dumps(content))
assert response.status_code == HTTPStatus.OK
# Channel name matches.
assert response.json['directories'] == []
- assert response.json['channel_directories'] == [{'name': 'Channel Name', 'path': 'dir1'}]
+ assert response.json['channel_directories'] == [{'name': 'OtherChannel', 'path': 'dir2'}]
assert response.json['domain_directories'] == []
- content = {'name': 'exam'}
- request, response = test_client.post('/api/files/search_directories', content=json.dumps(content))
+ content = {'path': 'exam'}
+ request, response = await async_client.post('/api/files/search_directories', content=json.dumps(content))
assert response.status_code == HTTPStatus.OK
# Domain name matches.
assert response.json['directories'] == []
assert response.json['channel_directories'] == []
- assert response.json['domain_directories'] == [{'domain': 'example.com', 'path': 'dir2'}]
+ assert response.json['domain_directories'] == [{'domain': 'example.com', 'path': 'dir3'}]
def test_post_upload_directory(test_session, test_client, test_directory, make_files_structure, make_multipart_form):
diff --git a/wrolpi/scrape_downloader.py b/wrolpi/scrape_downloader.py
index f234b0c1..f9493ecf 100644
--- a/wrolpi/scrape_downloader.py
+++ b/wrolpi/scrape_downloader.py
@@ -61,7 +61,7 @@ class ScrapeHTMLDownloader(Downloader):
raise UnrecoverableDownloadError('Destination must be defined')
destination = pathlib.Path(destination)
if not destination.is_dir():
- raise UnrecoverableDownloadError(f'Destination does not exist: {destination}')
+ destination.mkdir(parents=True)
suffix = suffix.lower()