Add comprehensive deletion functionality and scheduled cleanup

Features:
- Delete entire channels with all videos and downloaded files
- Delete individual video files while keeping database entries
- Scheduled automatic cleanup of videos older than 7 days
- Proper cascading deletes with file cleanup

Channel Deletion:
- New DELETE endpoint at /api/channels/<id>
- Removes channel, all video entries, and downloaded files
- User ownership verification
- Returns count of deleted files
- UI button on channels page with detailed confirmation dialog

Video File Deletion:
- New DELETE endpoint at /api/videos/<id>/file
- Celery async task to remove file from disk
- Resets download status to pending (allows re-download)
- UI button on watch page for completed videos
- Confirmation dialog with clear warnings

Scheduled Cleanup:
- Celery beat configuration for periodic tasks
- cleanup_old_videos task runs daily at midnight
- Automatically deletes videos completed more than 7 days ago
- Removes files and resets database status
- scheduled_tasks.py for beat schedule configuration
- verify_schedule.py helper to check task scheduling

UI Improvements:
- Added .btn-danger CSS class (black/white theme)
- Delete buttons with loading states
- Detailed confirmation dialogs warning about permanent deletion
- Dashboard now filters to show only completed videos

Bug Fixes:
- Fixed navbar alignment issues
- Added proper error handling for file deletion

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-26 20:55:43 -05:00
parent 337d46cbb5
commit be76f0a610
7 changed files with 310 additions and 3 deletions

View File

@@ -163,3 +163,112 @@ def download_videos_batch(video_ids: list[int]) -> dict:
"total_queued": len(results),
"tasks": results
}
@celery_app.task(base=DatabaseTask, bind=True)
def delete_video_file(self, video_id: int) -> dict:
"""Delete a downloaded video file and reset its download status.
Args:
video_id: Database ID of the VideoEntry
Returns:
Dictionary with deletion result information
"""
session = self.session
# Get video entry from database
video = session.query(VideoEntry).filter_by(id=video_id).first()
if not video:
return {"error": f"Video ID {video_id} not found"}
# Check if video has a download path
if not video.download_path:
return {"error": "Video has no download path", "video_id": video_id}
# Delete the file if it exists
deleted = False
if os.path.exists(video.download_path):
try:
os.remove(video.download_path)
deleted = True
except OSError as e:
return {
"error": f"Failed to delete file: {str(e)}",
"video_id": video_id,
"path": video.download_path
}
# Reset download status and metadata
video.download_status = DownloadStatus.PENDING
video.download_path = None
video.download_completed_at = None
video.file_size = None
video.download_error = None
session.commit()
return {
"video_id": video_id,
"status": "deleted" if deleted else "reset",
"message": "File deleted and status reset" if deleted else "Status reset (file not found)"
}
@celery_app.task(base=DatabaseTask, bind=True)
def cleanup_old_videos(self) -> dict:
"""Clean up videos older than 7 days.
Returns:
Dictionary with cleanup results
"""
from datetime import timedelta
session = self.session
# Calculate cutoff date (7 days ago)
cutoff_date = datetime.utcnow() - timedelta(days=7)
# Query videos that are completed and older than 7 days
old_videos = session.query(VideoEntry).filter(
VideoEntry.download_status == DownloadStatus.COMPLETED,
VideoEntry.download_completed_at < cutoff_date
).all()
deleted_count = 0
failed_count = 0
results = []
for video in old_videos:
if video.download_path and os.path.exists(video.download_path):
try:
os.remove(video.download_path)
# Reset download status
video.download_status = DownloadStatus.PENDING
video.download_path = None
video.download_completed_at = None
video.file_size = None
video.download_error = None
deleted_count += 1
results.append({
"video_id": video.id,
"title": video.title,
"status": "deleted"
})
except OSError as e:
failed_count += 1
results.append({
"video_id": video.id,
"title": video.title,
"status": "failed",
"error": str(e)
})
session.commit()
return {
"total_processed": len(old_videos),
"deleted_count": deleted_count,
"failed_count": failed_count,
"cutoff_date": cutoff_date.isoformat(),
"results": results
}