{queryStr && clearSearchButton}
-
+
+
+
+
+
-
+
}/>
diff --git a/modules/videos/api.py b/modules/videos/api.py
index dd629c39..99cd65d6 100644
--- a/modules/videos/api.py
+++ b/modules/videos/api.py
@@ -11,13 +11,13 @@ from wrolpi.common import create_websocket_feed, get_sanic_url, \
from wrolpi.common import logger
from wrolpi.root_api import add_blueprint, json_response
from wrolpi.schema import validate_doc, JSONErrorResponse
+from .channel import lib as channel_lib
from .channel.api import channel_bp
-from .channel.lib import download_channel, spread_channel_downloads
from .lib import _refresh_videos, get_statistics
from .schema import StreamResponse, \
- FavoriteRequest, FavoriteResponse, VideosStatisticsResponse
+ FavoriteRequest, FavoriteResponse, VideosStatisticsResponse, CensoredVideoResponse, CensoredVideoRequest
+from .video import lib as video_lib
from .video.api import video_bp
-from .video.lib import set_video_favorite
content_bp = Blueprint('VideoContent', '/api/videos')
bp = Blueprint('Videos', '/api/videos').group(
@@ -94,7 +94,7 @@ def download(_, link: str = None):
if refresh_event.is_set():
return response.json({'error': 'Refresh is running. Cannot download.'})
- download_channel(link)
+ channel_lib.download_channel(link)
download_logger.debug('do_download scheduled')
return response.json({'code': 'stream-started', 'stream_url': stream_url})
@@ -115,7 +115,7 @@ async def refresh_videos(channel_links: list = None):
]
)
async def favorite(_: Request, data: dict):
- _favorite = set_video_favorite(data['video_id'], data['favorite'])
+ _favorite = video_lib.set_video_favorite(data['video_id'], data['favorite'])
ret = {'video_id': data['video_id'], 'favorite': _favorite}
return json_response(ret, HTTPStatus.OK)
@@ -132,4 +132,20 @@ async def statistics(_: Request):
@before_startup
def startup_spread_channel_downloads():
- spread_channel_downloads()
+ channel_lib.spread_channel_downloads()
+
+
+@content_bp.post('/censored')
+@content_bp.post('/censored/')
+@validate_doc(
+ summary='Get all videos that were deleted on their source website',
+ consumes=CensoredVideoRequest,
+ produces=CensoredVideoResponse,
+)
+async def censored_videos(_: Request, data: dict, link: str = None):
+ limit = data.get('limit', 20)
+ offset = data.get('offset', 0)
+
+ videos = video_lib.censored_source_ids(link, limit, offset)
+ ret = {'videos': videos}
+ return json_response(ret)
diff --git a/modules/videos/channel/test/test_api.py b/modules/videos/channel/test/test_api.py
index 14beac8f..5fa5bd1d 100644
--- a/modules/videos/channel/test/test_api.py
+++ b/modules/videos/channel/test/test_api.py
@@ -453,65 +453,6 @@ class TestVideoAPI(TestAPI):
assert current_ids != last_ids, f'IDs are unchanged current_ids={current_ids}'
last_ids = current_ids
- @wrap_test_db
- def test_video_search(self):
- """
- Test that videos can be searched and that their order is by their textsearch rank.
- """
- # These captions have repeated letters so they will be higher in the ranking
- videos = [
- ('1', 'b b b b e d d'),
- ('2', '2 b b b d'),
- ('3', 'b b'),
- ('4', 'b e e'),
- ('5', ''),
- ]
- with get_db_session(commit=True) as session:
- for title, caption in videos:
- session.add(Video(title=title, caption=caption, video_path='foo'))
-
- def do_search(search_str, limit=20):
- d = json.dumps({'search_str': search_str, 'limit': limit})
- _, resp = api_app.test_client.post('/api/videos/search', content=d)
- return resp
-
- def search_is_as_expected(resp, expected):
- assert resp.status_code == HTTPStatus.OK
- response_ids = [i['id'] for i in resp.json['videos']]
- assert response_ids == expected
- assert resp.json['totals']['videos'] == len(expected)
-
- # Repeated runs should return the same result
- for _ in range(2):
- # Only videos with a b are returned, ordered by the amount of b's
- response = do_search('b')
- search_is_as_expected(response, [1, 2, 3, 4])
-
- # Only two captions have e
- response = do_search('e')
- search_is_as_expected(response, [4, 1])
-
- # Only two captions have d
- response = do_search('d')
- search_is_as_expected(response, [1, 2])
-
- # 5 can be gotten by it's title
- response = do_search('5')
- search_is_as_expected(response, [5])
-
- # only video 1 has e and d
- response = do_search('e d')
- search_is_as_expected(response, [1])
-
- # video 1 and 4 have b and e, but 1 has more
- response = do_search('b e')
- search_is_as_expected(response, [1, 4])
-
- # Check totals are correct even with a limit
- response = do_search('b', 2)
- assert [i['id'] for i in response.json['videos']] == [1, 2]
- assert response.json['totals']['videos'] == 4
-
@wrap_test_db
def test_channel_no_download_frequency(self):
"""
@@ -601,3 +542,72 @@ def test_delete_channel_delete_download(download_channel, test_session):
downloads = test_session.query(Download).all()
assert not list(downloads)
+
+
+def test_video_search(test_session):
+ """
+ Test that videos can be searched and that their order is by their textsearch rank.
+ """
+ # These captions have repeated letters, so they will be higher in the ranking.
+ videos = [
+ ('1', 'b b b b e d d'),
+ ('2', '2 b b b d'),
+ ('3', 'b b'),
+ ('4', 'b e e'),
+ ('5', ''),
+ ]
+ for title, caption in videos:
+ test_session.add(Video(title=title, caption=caption, video_path='foo'))
+ test_session.commit()
+
+ def do_search(search_str, limit=20):
+ d = json.dumps({'search_str': search_str, 'limit': limit})
+ _, resp = api_app.test_client.post('/api/videos/search', content=d)
+ return resp
+
+ def search_is_as_expected(resp, expected):
+ assert resp.status_code == HTTPStatus.OK
+ response_ids = [i['id'] for i in resp.json['videos']]
+ assert response_ids == expected
+ assert resp.json['totals']['videos'] == len(expected)
+
+ # Repeated runs should return the same result
+ for _ in range(2):
+ # Only videos with a b are returned, ordered by the amount of b's
+ response = do_search('b')
+ search_is_as_expected(response, [1, 2, 3, 4])
+
+ # Only two captions have e
+ response = do_search('e')
+ search_is_as_expected(response, [4, 1])
+
+ # Only two captions have d
+ response = do_search('d')
+ search_is_as_expected(response, [1, 2])
+
+ # 5 can be gotten by it's title
+ response = do_search('5')
+ search_is_as_expected(response, [5])
+
+ # only video 1 has e and d
+ response = do_search('e d')
+ search_is_as_expected(response, [1])
+
+ # video 1 and 4 have b and e, but 1 has more
+ response = do_search('b e')
+ search_is_as_expected(response, [1, 4])
+
+ # Check totals are correct even with a limit
+ response = do_search('b', 2)
+ assert [i['id'] for i in response.json['videos']] == [1, 2]
+ assert response.json['totals']['videos'] == 4
+
+
+def test_video_file_name(test_session, simple_video, test_client):
+ """
+ If a Video has no title, the front-end can use the file name as the title.
+ """
+ _, resp = test_client.get(f'/api/videos/video/{simple_video.id}')
+ assert resp.status_code == HTTPStatus.OK
+ assert resp.json['video']['video_path'] == 'simple_video.mp4'
+ assert resp.json['video'].get('stem') == 'simple_video'
diff --git a/modules/videos/conftest.py b/modules/videos/conftest.py
index f3e3f93a..b1e830bc 100644
--- a/modules/videos/conftest.py
+++ b/modules/videos/conftest.py
@@ -6,6 +6,7 @@ import pytest
from modules.videos.channel.lib import spread_channel_downloads
from modules.videos.models import Channel, Video
+from wrolpi.common import sanitize_link
from wrolpi.downloader import DownloadFrequency
from wrolpi.vars import PROJECT_DIR
@@ -27,6 +28,30 @@ def simple_channel(test_session, test_directory) -> Channel:
return channel
+@pytest.fixture
+def channel_factory(test_session, test_directory):
+ """
+ Create a random Channel with a directory, but no frequency.
+ """
+
+ def _():
+ name = str(uuid4())
+ directory = test_directory / name
+ directory.mkdir()
+ channel = Channel(
+ directory=directory, # noqa
+ name=name,
+ url=f'https://example.com/{name}',
+ download_frequency=None, # noqa
+ link=sanitize_link(name),
+ )
+ test_session.add(channel)
+ test_session.commit()
+ return channel
+
+ return _
+
+
@pytest.fixture
def download_channel(test_session, test_directory) -> Channel:
"""
diff --git a/modules/videos/downloader.py b/modules/videos/downloader.py
index 4663ff53..debbf25d 100755
--- a/modules/videos/downloader.py
+++ b/modules/videos/downloader.py
@@ -185,6 +185,7 @@ class VideoDownloader(Downloader, ABC):
config = load_downloader_config()
options = config.copy()
options['outtmpl'] = f'{out_dir}/{config["file_name_format"]}'
+ options['merge_output_format'] = PREFERRED_VIDEO_EXTENSION
# We don't need format when getting the filename.
del options['format']
diff --git a/modules/videos/models.py b/modules/videos/models.py
index 482e93ed..fb5f675a 100644
--- a/modules/videos/models.py
+++ b/modules/videos/models.py
@@ -48,7 +48,8 @@ class Video(ModelHelper, Base):
COALESCE(caption, ''::text)))'''))
def __repr__(self):
- return f'