Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions src/podcast/downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,13 @@ def __init__(
self.progress_callback = progress_callback

# Create download directory if it doesn't exist
os.makedirs(download_directory, exist_ok=True)
try:
os.makedirs(download_directory, exist_ok=True)
except PermissionError as e:
raise PermissionError(
f"Cannot create download directory '{download_directory}': {e}. "
"Check that the PODCAST_DOWNLOAD_DIRECTORY path is writable."
) from e

# Set up requests session with retry logic
self._session = self._create_session()
Expand Down Expand Up @@ -152,7 +158,18 @@ def download_episode(self, episode: Episode) -> DownloadResult:
self.download_directory,
self._sanitize_filename(podcast.title),
)
os.makedirs(podcast_dir, exist_ok=True)

try:
os.makedirs(podcast_dir, exist_ok=True)
except PermissionError as e:
error_msg = f"Permission denied creating directory {podcast_dir}: {e}"
logger.error(error_msg)
self.repository.mark_download_failed(episode.id, error_msg)
return DownloadResult(
episode_id=episode.id,
success=False,
error=error_msg,
)

filename = self._generate_filename(episode)
output_path = os.path.join(podcast_dir, filename)
Expand Down Expand Up @@ -406,7 +423,18 @@ async def _download_episode_async(self, episode: Episode) -> DownloadResult:
self.download_directory,
self._sanitize_filename(podcast.title),
)
os.makedirs(podcast_dir, exist_ok=True)

try:
os.makedirs(podcast_dir, exist_ok=True)
except PermissionError as e:
error_msg = f"Permission denied creating directory {podcast_dir}: {e}"
logger.error(error_msg)
self.repository.mark_download_failed(episode.id, error_msg)
return DownloadResult(
episode_id=episode.id,
success=False,
error=error_msg,
)

filename = self._generate_filename(episode)
output_path = os.path.join(podcast_dir, filename)
Expand Down
74 changes: 74 additions & 0 deletions tests/test_downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,3 +660,77 @@ def test_url_encoded_filename(self, downloader):
filename = downloader._generate_filename(mock_episode)
# Should get extension from URL
assert filename.endswith(".mp3")


class TestEpisodeDownloaderPermissionErrors:
"""Tests for PermissionError handling in downloader."""

@pytest.fixture
def mock_repository(self):
"""Create mock repository."""
return Mock()

def test_init_permission_error_raises_clear_message(self, mock_repository):
"""Test that PermissionError in init raises with helpful message."""
with patch("os.makedirs", side_effect=PermissionError("Permission denied")):
with pytest.raises(PermissionError) as exc_info:
EpisodeDownloader(
repository=mock_repository,
download_directory="/opt/podcasts",
)

assert "Cannot create download directory" in str(exc_info.value)
assert "PODCAST_DOWNLOAD_DIRECTORY" in str(exc_info.value)

def test_download_episode_permission_error_marks_failed(
self, mock_repository, tmp_path
):
"""Test that PermissionError when creating episode dir marks episode as failed."""
downloader = EpisodeDownloader(
repository=mock_repository,
download_directory=str(tmp_path),
)

mock_episode = Mock()
mock_episode.id = "ep-1"
mock_episode.podcast_id = "pod-1"
mock_episode.title = "Test Episode"

mock_podcast = Mock()
mock_podcast.title = "Test Podcast"
mock_podcast.local_directory = "/opt/podcasts/forbidden"
mock_repository.get_podcast.return_value = mock_podcast

with patch("os.makedirs", side_effect=PermissionError("Permission denied")):
result = downloader.download_episode(mock_episode)

assert result.success is False
assert "Permission denied" in result.error
mock_repository.mark_download_failed.assert_called_once()
assert "ep-1" in mock_repository.mark_download_failed.call_args[0]

def test_download_episode_permission_error_does_not_mark_started(
self, mock_repository, tmp_path
):
"""Test that PermissionError doesn't mark episode as download started."""
downloader = EpisodeDownloader(
repository=mock_repository,
download_directory=str(tmp_path),
)

mock_episode = Mock()
mock_episode.id = "ep-1"
mock_episode.podcast_id = "pod-1"
mock_episode.title = "Test Episode"

mock_podcast = Mock()
mock_podcast.title = "Test Podcast"
mock_podcast.local_directory = "/opt/podcasts/forbidden"
mock_repository.get_podcast.return_value = mock_podcast

with patch("os.makedirs", side_effect=PermissionError("Permission denied")):
result = downloader.download_episode(mock_episode)

assert result.success is False
# Should NOT have called mark_download_started
mock_repository.mark_download_started.assert_not_called()