mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
279 lines
10 KiB
Python
279 lines
10 KiB
Python
import pathlib
|
|
import subprocess
|
|
import tempfile
|
|
from unittest import mock
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
from PIL import Image
|
|
|
|
from modules.videos.models import Channel, Video
|
|
from modules.videos.test.common import create_channel_structure
|
|
from wrolpi.common import get_absolute_media_path
|
|
from wrolpi.db import get_db_session
|
|
from wrolpi.test.common import ExtendedTestCase, build_test_directories, wrap_test_db
|
|
from wrolpi.vars import PROJECT_DIR
|
|
from ..common import get_matching_directories, convert_image, bulk_validate_posters, remove_duplicate_video_paths, \
|
|
update_view_count, get_video_duration
|
|
|
|
|
|
class TestCommon(ExtendedTestCase):
|
|
|
|
def test_get_absolute_media_path(self):
|
|
wrolpi = get_absolute_media_path('videos/wrolpi')
|
|
assert str(wrolpi).endswith('wrolpi')
|
|
|
|
def test_matching_directories(self):
|
|
structure = [
|
|
'foo/qux/',
|
|
'Bar/',
|
|
'baz/baz'
|
|
'barr',
|
|
'bazz',
|
|
]
|
|
|
|
with build_test_directories(structure) as temp_dir:
|
|
temp_dir = pathlib.Path(temp_dir)
|
|
|
|
# No directories have c
|
|
matches = get_matching_directories(temp_dir / 'c')
|
|
assert matches == []
|
|
|
|
# Get all directories starting with f
|
|
matches = get_matching_directories(temp_dir / 'f')
|
|
assert matches == [str(temp_dir / 'foo')]
|
|
|
|
# Get all directories starting with b, ignore case
|
|
matches = get_matching_directories(temp_dir / 'b')
|
|
assert matches == [str(temp_dir / 'Bar'), str(temp_dir / 'baz')]
|
|
|
|
# baz matches, but it has no subdirectories
|
|
matches = get_matching_directories(temp_dir / 'baz')
|
|
assert matches == [str(temp_dir / 'baz')]
|
|
|
|
# foo is an exact match, return subdirectories
|
|
matches = get_matching_directories(temp_dir / 'foo')
|
|
assert matches == [str(temp_dir / 'foo/qux')]
|
|
|
|
def test_get_video_duration(self):
|
|
"""
|
|
Video duration can be retrieved from the video file.
|
|
"""
|
|
video_path = PROJECT_DIR / 'test/videos/wrolpi/big_buck_bunny_720p_1mb.mp4'
|
|
self.assertEqual(get_video_duration(video_path), 5)
|
|
|
|
video_path = PROJECT_DIR / 'test/videos/wrolpi/does not exist.mp4'
|
|
self.assertRaises(FileNotFoundError, get_video_duration, video_path)
|
|
|
|
video_path = str(PROJECT_DIR / 'test/videos/wrolpi/does not exist.mp4')
|
|
self.assertRaises(FileNotFoundError, get_video_duration, video_path)
|
|
|
|
with tempfile.NamedTemporaryFile() as fh:
|
|
self.assertRaises(subprocess.CalledProcessError, get_video_duration, fh.name)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'paths,expected',
|
|
(
|
|
(['1.mp4', ], ['1.mp4', ]),
|
|
(['1.mp4', '2.mp4'], ['1.mp4', '2.mp4']),
|
|
(['1.mp4', '1.ogg'], ['1.mp4']),
|
|
(['1.bad_ext', '1.other_ext'], ['1.bad_ext']),
|
|
)
|
|
)
|
|
def test_remove_duplicate_video_paths(paths, expected):
|
|
result = remove_duplicate_video_paths(map(pathlib.Path, paths))
|
|
assert sorted(result) == sorted(map(pathlib.Path, expected))
|
|
|
|
|
|
def test_convert_image():
|
|
"""
|
|
An image's format can be changed. This tests that convert_image() converts from a WEBP format, to a JPEG format.
|
|
"""
|
|
foo = Image.new('RGB', (25, 25), color='grey')
|
|
|
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
tempdir = pathlib.Path(tempdir)
|
|
|
|
# Save the new image to "foo.webp".
|
|
existing_path = tempdir / 'foo.webp'
|
|
foo.save(existing_path)
|
|
assert Image.open(existing_path).format == 'WEBP'
|
|
|
|
destination_path = tempdir / 'foo.jpg'
|
|
assert not destination_path.is_file()
|
|
|
|
# Convert the WEBP to a JPEG. The WEBP image should be removed.
|
|
convert_image(existing_path, destination_path)
|
|
assert not existing_path.is_file()
|
|
assert destination_path.is_file()
|
|
assert Image.open(destination_path).format == 'JPEG'
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'_structure,paths',
|
|
(
|
|
(
|
|
{'channel1': ['vid1.mp4']},
|
|
[
|
|
'channel1/vid1.mp4',
|
|
],
|
|
),
|
|
(
|
|
{'channel1': ['vid1.mp4'], 'channel2': ['vid1.mp4']},
|
|
[
|
|
'channel1/vid1.mp4',
|
|
'channel2/vid1.mp4',
|
|
],
|
|
),
|
|
(
|
|
{'channel1': ['vid1.mp4'], 'channel2': ['vid1.mp4', 'vid2.mp4', 'vid2.en.vtt']},
|
|
[
|
|
'channel1/vid1.mp4',
|
|
'channel2/vid1.mp4',
|
|
'channel2/vid2.mp4',
|
|
'channel2/vid2.en.vtt',
|
|
],
|
|
),
|
|
)
|
|
)
|
|
def test_create_db_structure(_structure, paths):
|
|
@create_channel_structure(_structure)
|
|
def test_func(tempdir):
|
|
assert isinstance(tempdir, pathlib.Path)
|
|
for path in paths:
|
|
path = (tempdir / path)
|
|
assert path.exists()
|
|
assert path.is_file()
|
|
|
|
with get_db_session() as session:
|
|
for channel_name in _structure:
|
|
channel = session.query(Channel).filter_by(name=channel_name).one()
|
|
assert (tempdir / channel_name).is_dir()
|
|
assert channel
|
|
assert channel.directory == tempdir / channel_name
|
|
assert len(channel.videos) == len([i for i in _structure[channel_name] if i.endswith('mp4')])
|
|
|
|
test_func()
|
|
|
|
|
|
@wrap_test_db
|
|
@create_channel_structure(
|
|
{
|
|
'channel1': ['vid1.mp4', 'vid1.jpg'],
|
|
'channel2': ['vid2.flv', 'vid2.webp'],
|
|
}
|
|
)
|
|
def test_bulk_replace_invalid_posters(tempdir: pathlib.Path):
|
|
"""
|
|
Test that when a video has an invalid poster format, we convert it to JPEG.
|
|
"""
|
|
channel1, channel2 = sorted(tempdir.iterdir())
|
|
jpg, mp4 = sorted(channel1.iterdir())
|
|
flv, webp = sorted(channel2.iterdir())
|
|
|
|
Image.new('RGB', (25, 25)).save(jpg)
|
|
Image.new('RGB', (25, 25)).save(webp)
|
|
|
|
with open(jpg, 'rb') as jpg_fh, open(webp, 'rb') as webp_fh:
|
|
# Files are different formats.
|
|
jpg_fh_contents = jpg_fh.read()
|
|
webp_fh_contents = webp_fh.read()
|
|
assert jpg_fh_contents != webp_fh_contents
|
|
assert Image.open(jpg_fh).format == 'JPEG'
|
|
assert Image.open(webp_fh).format == 'WEBP'
|
|
|
|
with get_db_session() as session:
|
|
vid1 = session.query(Video).filter_by(poster_path='vid1.jpg').one()
|
|
assert vid1.validated_poster is False
|
|
|
|
vid2 = session.query(Video).filter_by(poster_path='vid2.webp').one()
|
|
assert vid2.validated_poster is False
|
|
|
|
# Convert the WEBP image. convert_image() should only be called once.
|
|
mocked_convert_image = Mock(wraps=convert_image)
|
|
with mock.patch('modules.videos.common.convert_image', mocked_convert_image):
|
|
video_ids = [vid1.id, vid2.id]
|
|
bulk_validate_posters(video_ids)
|
|
|
|
mocked_convert_image.assert_called_once_with(webp, tempdir / 'channel2/vid2.jpg')
|
|
|
|
with get_db_session() as session:
|
|
# Get the video by ID because it's poster is now a JPEG.
|
|
vid2 = session.query(Video).filter_by(id=vid2.id).one()
|
|
assert str(vid2.poster_path) == 'vid2.jpg'
|
|
assert all('webp' not in str(i.poster_path) for i in session.query(Video).all())
|
|
assert vid2.validated_poster is True
|
|
|
|
# Vid1's image was validated, but not converted.
|
|
vid1 = session.query(Video).filter_by(id=vid1.id).one()
|
|
assert str(vid1.poster_path) == 'vid1.jpg'
|
|
assert vid1.validated_poster is True
|
|
|
|
# Old webp was removed
|
|
assert not webp.is_file()
|
|
new_jpg = tempdir / 'channel2/vid2.jpg'
|
|
assert new_jpg.is_file()
|
|
# chmod 644
|
|
assert new_jpg.stat().st_mode == 0o100644
|
|
with open(new_jpg, 'rb') as new_jpg_fh:
|
|
# The converted image is the same as the other JPEG because both are black 25x25 pixel images.
|
|
assert jpg_fh_contents == new_jpg_fh.read()
|
|
assert Image.open(new_jpg_fh).format == 'JPEG'
|
|
|
|
# Calling convert again has no effect.
|
|
mocked_convert_image.reset_mock()
|
|
with mock.patch('modules.videos.common.convert_image', mocked_convert_image):
|
|
video_ids = [vid1.id, vid2.id]
|
|
bulk_validate_posters(video_ids)
|
|
|
|
mocked_convert_image.assert_not_called()
|
|
|
|
|
|
@wrap_test_db
|
|
@create_channel_structure(
|
|
{
|
|
'channel1': ['vid1.mp4', 'vid1.jpg'],
|
|
'channel2': ['vid2.mp4'],
|
|
'channel3': ['vid3.mp4', 'vid4.mp4'],
|
|
}
|
|
)
|
|
def test_update_view_count(tempdir: pathlib.Path):
|
|
def check_view_counts(view_counts):
|
|
with get_db_session(commit=True) as session:
|
|
for source_id, view_count in view_counts.items():
|
|
vid = session.query(Video).filter_by(source_id=source_id).one()
|
|
assert vid.view_count == view_count
|
|
|
|
with get_db_session(commit=True) as session:
|
|
channel1, channel2, channel3 = session.query(Channel).order_by(Channel.id).all()
|
|
channel1.info_json = {'entries': [{'id': 'vid1.mp4', 'view_count': 10}]}
|
|
channel2.info_json = {'entries': [{'id': 'vid2.mp4', 'view_count': 11}, {'id': 'bad_id', 'view_count': 12}]}
|
|
channel3.info_json = {'entries': [{'id': 'vid3.mp4', 'view_count': 13}, {'id': 'vid4.mp4', 'view_count': 14}]}
|
|
|
|
# Use the video_path as a unique source_id
|
|
for v in session.query(Video).all():
|
|
v.source_id = str(v.video_path)
|
|
|
|
# Check all videos are empty.
|
|
check_view_counts({'vid1.mp4': None, 'vid2.mp4': None, 'vid3.mp4': None, 'vid4.mp4': None})
|
|
|
|
# Channel 1 is updated, the other channels are left alone.
|
|
update_view_count(channel1.id)
|
|
check_view_counts({'vid1.mp4': 10, 'vid2.mp4': None, 'vid3.mp4': None, 'vid4.mp4': None})
|
|
|
|
# Channel 2 is updated, the other channels are left alone. The 'bad_id' video is ignored.
|
|
update_view_count(channel2.id)
|
|
check_view_counts({'vid1.mp4': 10, 'vid2.mp4': 11, 'vid3.mp4': None, 'vid4.mp4': None})
|
|
|
|
# All videos are updated.
|
|
update_view_count(channel3.id)
|
|
check_view_counts({'vid1.mp4': 10, 'vid2.mp4': 11, 'vid3.mp4': 13, 'vid4.mp4': 14})
|
|
|
|
# An outdated view count will be overwritten.
|
|
with get_db_session(commit=True) as session:
|
|
vid = session.query(Video).filter_by(id=1).one()
|
|
vid.view_count = 8
|
|
check_view_counts({'vid1.mp4': 8, 'vid2.mp4': 11, 'vid3.mp4': 13, 'vid4.mp4': 14})
|
|
update_view_count(channel1.id)
|
|
check_view_counts({'vid1.mp4': 10, 'vid2.mp4': 11, 'vid3.mp4': 13, 'vid4.mp4': 14})
|