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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user