diff --git a/modules/videos/test/test_transcode_job.py b/modules/videos/test/test_transcode_job.py index d773f9a5..2d5f7bed 100644 --- a/modules/videos/test/test_transcode_job.py +++ b/modules/videos/test/test_transcode_job.py @@ -7,7 +7,7 @@ from unittest import mock import pytest from modules.videos.models import Video -from modules.videos.transcode import make_ffmpeg_progress_parser, transcode_video_job +from modules.videos.transcode import make_ffmpeg_progress_parser, transcode_video_job, transcode_tmp_path from wrolpi import jobs from wrolpi.cmd import CommandResult from wrolpi.files.models import FileGroup @@ -293,3 +293,28 @@ async def test_video_transcode_api_fragmented(async_client, test_session, video_ request, response = await async_client.post(f'/api/videos/{video.file_group_id}/transcode', content=json.dumps({'container': 'mkv', 'fragmented': True})) assert response.status == 400 + + +@pytest.mark.asyncio +async def test_transcode_temp_file_is_not_discovered_by_refresh(test_session, test_directory, async_client, + video_factory, refresh_files): + """A refresh that runs while ffmpeg is still writing the transcoder's output must not model it. + + The temporary file is renamed over the final path moments later; a FileGroup for it would point + at a file that no longer exists, and probing a half-written file logs errors. + """ + video_path = test_directory / 'videos/NO CHANNEL/movie.mp4' + video_path.parent.mkdir(parents=True, exist_ok=True) + video = video_factory(with_video_file=video_path) + test_session.commit() + + tmp_path = transcode_tmp_path(video_path, 'mp4') + shutil.copy(PROJECT_DIR / 'test/big_buck_bunny_720p_1mb.mp4', tmp_path) + assert tmp_path.is_file() + + await refresh_files() + + primary_paths = {str(i.primary_path) for i in test_session.query(FileGroup)} + assert str(video_path) in primary_paths + assert str(tmp_path) not in primary_paths, 'the transcoder temporary file was indexed' + assert not any(str(tmp_path) in str(p) for p in primary_paths) diff --git a/modules/videos/transcode.py b/modules/videos/transcode.py index 701c2440..3a9a970d 100644 --- a/modules/videos/transcode.py +++ b/modules/videos/transcode.py @@ -149,6 +149,15 @@ async def _acquire_transcode_lock(): return lock +def transcode_tmp_path(video_path: pathlib.Path, container: str) -> pathlib.Path: + """Where ffmpeg writes its output before it is renamed over the final path. + + Hidden (dot-prefixed) in the video's own directory: a refresh skips hidden files, so a refresh that + overlaps the transcode neither probes the half-written output nor creates a FileGroup for a file + that the rename removes moments later; and same-directory keeps the final rename atomic.""" + return video_path.with_name(f'.{video_path.stem}.transcode.{container}') + + async def transcode_video_file(video_path: pathlib.Path, target_vcodec: Optional[str] = None, target_acodec: Optional[str] = None, @@ -363,7 +372,7 @@ async def _transcode_video_file(video_path: pathlib.Path, target_vcodec: Optiona mux_args = () final_path = video_path.with_suffix(f'.{container}') - tmp_path = video_path.with_suffix(f'.transcode.{container}') + tmp_path = transcode_tmp_path(video_path, container) cmd = (FFMPEG_BIN, '-y', '-i', video_path, *map_args,