Downloading only the first video when the VideoDownloader gets a playlist by accident.
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Controller Tests / controller-tests (push) Waiting to run
Jest Tests / jest-tests (push) Waiting to run

This commit is contained in:
Roland 2026-03-08 11:55:59 -06:00
parent b2185ad7c3
commit 8c294dad39
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
2 changed files with 42 additions and 0 deletions

View file

@ -639,6 +639,13 @@ class VideoDownloader(Downloader, ABC):
video_path = preferred_path
logger.info(f'Using preferred video file which exists: {preferred_path}')
if not video_path.is_file():
# yt-dlp may have appended the real extension to a bogus one (e.g. .NA.mp4 from a playlist entry).
appended_path = video_path.parent / f'{video_path.name}.{video_format}'
if appended_path.is_file():
video_path = appended_path
logger.info(f'Using video file with appended extension: {appended_path}')
if video_path.suffix == '.part':
return DownloadResult(
success=False,
@ -864,6 +871,12 @@ class VideoDownloader(Downloader, ABC):
# Get the path where the video will be saved.
try:
entry = extract_info(url, ydl=ydl, process=True)
# If the entry is a playlist (e.g. Twitter/X post with multiple videos),
# use the first video entry for filename preparation.
if entry.get('entries'):
entries = list(entry['entries']) if not isinstance(entry['entries'], list) else entry['entries']
if entries:
entry = entries[0]
final_filename = pathlib.Path(prepare_filename(entry, ydl=ydl)).absolute()
except DownloadError as e:
if ' Cannot write ' in str(e):

View file

@ -880,3 +880,32 @@ def test_skip_download_excludes_bot_blocked():
# Other unrecoverable errors should still be skipped.
assert _skip_download("404: Not Found") is True
assert _skip_download("requires payment") is True
@pytest.mark.asyncio
async def test_video_download_playlist_appended_extension(test_session, test_directory, mock_video_extract_info,
video_download_manager, mock_video_process_runner,
simple_channel, image_file):
"""A video download from a playlist (e.g. Twitter/X) where yt-dlp appends the real extension to a bogus .NA
extension should still find the file."""
simple_channel.source_id = example_video_json['channel_id']
simple_channel.directory = test_directory / 'videos/channel name'
simple_channel.directory.mkdir(parents=True)
# Simulate yt-dlp saving a file with .NA.mp4 (appended extension from playlist entry with ext=NA).
video_path_na = simple_channel.directory / 'uploader name_20190707_some long id_The video title.NA.mp4'
shutil.copy(PROJECT_DIR / 'test/big_buck_bunny_720p_1mb.mp4', video_path_na)
url = 'https://x.com/example/status/123'
mock_video_extract_info.return_value = example_video_json
with mock.patch('modules.videos.downloader.VideoDownloader.prepare_filename') as mock_prepare_filename:
# prepare_filename returns the path without the real extension (ending in .NA).
bogus_path = video_path_na.parent / video_path_na.stem # Removes .mp4, leaving .NA as "extension"
mock_prepare_filename.return_value = (bogus_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_na