Video download deletes yt-dlp format leftovers of any extension (#816)

A merge that never finished leaves `<stem>.f313.webm` (video-only) next to
where the merged file belongs.  A later download of the same video globs
every `<stem>.*` file into its FileGroup, but only `.part` files and format
files with the *same* extension as the merged video were deleted.  A `.webm`
leftover beside a new `.mp4` survived, joined the new FileGroup, and could
become the Video's file.

Delete every `<stem>.fNNN.*` file in the group, whatever its extension, which
also covers WROLPi's poster/ffprobe sidecars of an indexed leftover and
yt-dlp's `.ytdl` state files.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
lrnselfreliance 2026-10-11 16:20:25 -06:00 • committed by GitHub
parent 4adc4e6a15
commit fc7db34864
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 46 additions and 12 deletions

View file

@ -1406,23 +1406,20 @@ class VideoDownloader(Downloader, ABC):
"""Delete temporary "part" files that are used by yt-dlp when downloading video (and related) files."""
files = list(files)
video_suffix = video_file.suffix.lstrip('.')
# Delete any files in this group that end with .part.
for part_file in [i for i in files if i.name.endswith('.part')]:
logger.warning(f'Deleting leftover .part file: {part_file}')
part_file.unlink()
files.remove(part_file)
# Delete any video files that are a temporary file that contain the "format" number.
# Example: The Video Title.f616.mp4
video_files = [i for i in files if i != video_file and i.name.endswith(video_suffix)]
for part_video in video_files:
if VIDEO_FORMAT_PARSER.match(part_video.stem):
# Extra yt-dlp format video file that is not the video file, delete it.
logger.warning(f'Deleting leftover incomplete video file: {part_video}')
part_video.unlink()
files.remove(part_video)
# Delete files of an unmerged yt-dlp "format" download, whatever their extension, and anything made from them
# (an earlier refresh may have indexed one and made a poster). Example: The Video Title.f313.webm
video_stem, _ = split_path_stem_and_suffix(video_file)
format_file = re.compile(rf'^{re.escape(video_stem)}\.f\d{{2,4}}\.')
for part_file in [i for i in files if i != video_file and format_file.match(i.name)]:
logger.warning(f'Deleting leftover yt-dlp format file: {part_file}')
part_file.unlink()
files.remove(part_file)
return files
@ -1451,7 +1448,6 @@ class VideoDownloader(Downloader, ABC):
VTT_SUFFIX_PARSER = re.compile(r'^\.([a-z]{2,3})-(.+?)\.(vtt|srt)$', re.IGNORECASE)
VIDEO_FORMAT_PARSER = re.compile(r'^.+?\.f\d{2,4}')
channel_downloader = ChannelDownloader()
# Videos may match the ChannelDownloader, give it a higher priority.

View file

@ -563,6 +563,44 @@ async def test_video_download(test_session, test_directory, mock_video_extract_i
assert 'formats' not in video.get_info_json(), 'formats should be deleted from info json'
@pytest.mark.asyncio
async def test_video_download_deletes_format_leftovers(test_session, test_directory, mock_video_extract_info,
simple_channel, await_switches, video_download_manager,
mock_video_process_runner, image_file):
"""yt-dlp's per-format files (`<stem>.f313.webm`) left by an earlier, unmerged download are deleted, whatever
their extension, so they do not join the new Video's FileGroup."""
simple_channel.source_id = example_video_json['channel_id']
simple_channel.directory = test_directory / 'videos/channel name'
simple_channel.directory.mkdir(parents=True)
video_path = simple_channel.directory / 'a video.mp4'
shutil.copy(PROJECT_DIR / 'test/big_buck_bunny_720p_1mb.mp4', video_path)
# An earlier download's video-only format file was never merged; WROLPi indexed it and made a poster for it.
leftover_video = simple_channel.directory / 'a video.f313.webm'
shutil.copy(PROJECT_DIR / 'test/big_buck_bunny_720p_1mb.mp4', leftover_video)
leftover_poster = simple_channel.directory / 'a video.f313.jpg'
image_file.rename(leftover_poster)
leftover_ytdl = simple_channel.directory / 'a video.f251.webm.ytdl'
leftover_ytdl.write_text('{}')
# Not a format file of this video.
other = simple_channel.directory / 'a video.f313 notes.txt'
other.write_text('keep me')
url = 'https://www.youtube.com/watch?v=31jPEBiAC3c'
with mock.patch('modules.videos.downloader.prepare_video_filename') as mock_prepare_filename:
mock_video_extract_info.return_value = example_video_json
mock_prepare_filename.return_value = (video_path, {'id': 'foo'})
video_download_manager.create_download(test_session, url, video_downloader.name)
await video_download_manager.wait_for_all_downloads()
video: Video = test_session.query(Video).one()
assert video.video_path == video_path
assert not leftover_video.exists() and not leftover_poster.exists() and not leftover_ytdl.exists()
assert not any('.f313.' in i.name or '.f251.' in i.name for i in video.file_group.my_paths())
assert other.exists()
@pytest.mark.asyncio
async def test_download_info_json_finds_existing_video(test_session, test_directory, video_factory):
"""`download_info_json` looks up the existing Video by its URL.