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
53 changes: 53 additions & 0 deletions app/server/fireshare/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,59 @@ def reject_game_suggestion(video_id):

return Response(status=204)

@api.route('/api/videos/corrupt', methods=["GET"])
@login_required
def get_corrupt_videos():
"""Get a list of all videos marked as corrupt"""
from fireshare.cli import get_all_corrupt_videos

corrupt_video_ids = get_all_corrupt_videos()

# Get video details for all corrupt videos in a single query
video_info_map = {}
if corrupt_video_ids:
video_infos = VideoInfo.query.filter(VideoInfo.video_id.in_(corrupt_video_ids)).all()
video_info_map = {vi.video_id: vi for vi in video_infos}

corrupt_videos = []
for video_id in corrupt_video_ids:
vi = video_info_map.get(video_id)
if vi:
corrupt_videos.append({
'video_id': video_id,
'title': vi.title,
'path': vi.video.path if vi.video else None
})
else:
# Video may have been deleted but still in corrupt list
corrupt_videos.append({
'video_id': video_id,
'title': None,
'path': None
})
return jsonify(corrupt_videos)

@api.route('/api/videos/<video_id>/corrupt', methods=["DELETE"])
@login_required
def clear_corrupt_status(video_id):
"""Clear the corrupt status for a specific video so it can be retried"""
from fireshare.cli import clear_video_corrupt, is_video_corrupt

if not is_video_corrupt(video_id):
return Response(status=400, response="Video is not marked as corrupt")

clear_video_corrupt(video_id)
return Response(status=204)

@api.route('/api/videos/corrupt/clear-all', methods=["DELETE"])
@login_required
def clear_all_corrupt_status():
"""Clear the corrupt status for all videos so they can be retried"""
from fireshare.cli import clear_all_corrupt_videos

count = clear_all_corrupt_videos()
return jsonify({'cleared': count})

@api.after_request
def after_request(response):
response.headers.add('Accept-Ranges', 'bytes')
Expand Down
91 changes: 88 additions & 3 deletions app/server/fireshare/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,68 @@ def delete_game_suggestion(video_id):
return True
return False

# Helper functions for persistent corrupt video tracking
def _get_corrupt_videos_file():
"""Get path to the corrupt videos JSON file"""
from flask import current_app
data_dir = Path(current_app.config.get('DATA_DIRECTORY', '/data'))
return data_dir / 'corrupt_videos.json'

def _load_corrupt_videos():
"""Load corrupt videos list from JSON file"""
corrupt_file = _get_corrupt_videos_file()
if corrupt_file.exists():
try:
with open(corrupt_file, 'r') as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
return []
return []

def _save_corrupt_videos(corrupt_list):
"""Save corrupt videos list to JSON file"""
corrupt_file = _get_corrupt_videos_file()
corrupt_file.parent.mkdir(parents=True, exist_ok=True)
try:
with open(corrupt_file, 'w') as f:
json.dump(corrupt_list, f)
except IOError as e:
logger.error(f"Failed to save corrupt videos list: {e}")

def is_video_corrupt(video_id):
"""Check if a video is marked as corrupt"""
corrupt_list = _load_corrupt_videos()
return video_id in corrupt_list

def mark_video_corrupt(video_id):
"""Mark a video as corrupt"""
corrupt_list = _load_corrupt_videos()
if video_id not in corrupt_list:
corrupt_list.append(video_id)
_save_corrupt_videos(corrupt_list)
logger.info(f"Marked video {video_id} as corrupt")

def clear_video_corrupt(video_id):
"""Clear the corrupt status for a video"""
corrupt_list = _load_corrupt_videos()
if video_id in corrupt_list:
corrupt_list.remove(video_id)
_save_corrupt_videos(corrupt_list)
logger.info(f"Cleared corrupt status for video {video_id}")
return True
return False

def get_all_corrupt_videos():
"""Get list of all corrupt video IDs"""
return _load_corrupt_videos()

def clear_all_corrupt_videos():
"""Clear all corrupt video statuses"""
count = len(_load_corrupt_videos())
_save_corrupt_videos([])
logger.info(f"Cleared corrupt status for {count} video(s)")
return count

def send_discord_webhook(webhook_url=None, video_url=None):
payload = {
"content": video_url,
Expand Down Expand Up @@ -513,7 +575,8 @@ def create_boomerang_posters(regenerate):
@cli.command()
@click.option("--regenerate", "-r", help="Overwrite existing transcoded videos", is_flag=True)
@click.option("--video", "-v", help="Transcode a specific video by id", default=None)
def transcode_videos(regenerate, video):
@click.option("--include-corrupt", help="Include videos previously marked as corrupt", is_flag=True)
def transcode_videos(regenerate, video, include_corrupt):
"""Transcode videos to 1080p and 720p variants"""
with create_app().app_context():
if not current_app.config.get('ENABLE_TRANSCODING'):
Expand All @@ -526,6 +589,16 @@ def transcode_videos(regenerate, video):

# Get videos to transcode
vinfos = VideoInfo.query.filter(VideoInfo.video_id==video).all() if video else VideoInfo.query.all()

# Filter out corrupt videos unless explicitly included
corrupt_videos = set(get_all_corrupt_videos())
if not include_corrupt and not video:
original_count = len(vinfos)
vinfos = [vi for vi in vinfos if vi.video_id not in corrupt_videos]
skipped_count = original_count - len(vinfos)
if skipped_count > 0:
logger.info(f"Skipping {skipped_count} video(s) previously marked as corrupt. Use --include-corrupt to retry them.")

logger.info(f'Processing {len(vinfos):,} videos for transcoding (GPU: {use_gpu}, Base timeout: {base_timeout}s)')

for vi in vinfos:
Expand All @@ -541,20 +614,25 @@ def transcode_videos(regenerate, video):

# Determine which qualities to transcode
original_height = vi.height or 0
video_is_corrupt = False

# Transcode to 1080p if original is higher and 1080p doesn't exist
transcode_1080p_path = derived_path / f"{vi.video_id}-1080p.mp4"
if original_height > 1080 and (not transcode_1080p_path.exists() or regenerate):
logger.info(f"Transcoding {vi.video_id} to 1080p")
# Pass None for timeout to use smart calculation, or pass base_timeout if needed
timeout = None # Uses smart calculation based on video duration
success, failure_reason = util.transcode_video_quality(video_path, transcode_1080p_path, 1080, use_gpu, timeout)
if success:
vi.has_1080p = True
# Clear corrupt status if transcode succeeds (file may have been replaced)
if is_video_corrupt(vi.video_id):
clear_video_corrupt(vi.video_id)
db.session.add(vi)
db.session.commit()
elif failure_reason == 'corruption':
logger.warning(f"Skipping video {vi.video_id} 1080p transcode - source file appears corrupt")
mark_video_corrupt(vi.video_id)
video_is_corrupt = True
else:
logger.warning(f"Skipping video {vi.video_id} 1080p transcode - all encoders failed")
elif transcode_1080p_path.exists():
Expand All @@ -563,19 +641,26 @@ def transcode_videos(regenerate, video):
db.session.add(vi)
db.session.commit()

# Skip 720p transcode if video was marked as corrupt
if video_is_corrupt:
continue

# Transcode to 720p if original is higher than 720p and 720p doesn't exist
transcode_720p_path = derived_path / f"{vi.video_id}-720p.mp4"
if original_height > 720 and (not transcode_720p_path.exists() or regenerate):
logger.info(f"Transcoding {vi.video_id} to 720p")
# Pass None for timeout to use smart calculation, or pass base_timeout if needed
timeout = None # Uses smart calculation based on video duration
success, failure_reason = util.transcode_video_quality(video_path, transcode_720p_path, 720, use_gpu, timeout)
if success:
vi.has_720p = True
# Clear corrupt status if transcode succeeds (file may have been replaced)
if is_video_corrupt(vi.video_id):
clear_video_corrupt(vi.video_id)
db.session.add(vi)
db.session.commit()
elif failure_reason == 'corruption':
logger.warning(f"Skipping video {vi.video_id} 720p transcode - source file appears corrupt")
mark_video_corrupt(vi.video_id)
else:
logger.warning(f"Skipping video {vi.video_id} 720p transcode - all encoders failed")
elif transcode_720p_path.exists():
Expand Down