Completa modulo Replay: S3, retention, sync YouTube e gestione società.

Streaming chunked da Garage, purge con delete YouTube, privacy sincronizzata,
archivio club migliorato, retention 30/90 separata da stato abbonamento.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-08 09:33:59 +02:00
parent ae36d17adb
commit 74eee24293
17 changed files with 385 additions and 34 deletions

View File

@@ -6,6 +6,7 @@ module Recordings
end
def call
delete_youtube_video
delete_storage_object if @recording.storage_key.present?
cleanup_local_artifacts
@@ -13,13 +14,23 @@ module Recordings
status: "expired",
deleted_at: Time.current,
storage_key: nil,
thumbnail_storage_key: nil
thumbnail_storage_key: nil,
youtube_video_id: nil,
youtube_published_at: nil
)
@recording
end
private
def delete_youtube_video
return if @recording.youtube_video_id.blank?
Youtube::VideoLifecycleService.new(@recording).delete!
rescue Youtube::VideoLifecycleService::Error => e
Rails.logger.warn("[Recordings::Delete] YouTube delete #{@recording.id}: #{e.message}")
end
def delete_storage_object
storage = Recordings::Storage.new
storage.delete(key: @recording.storage_key) if @recording.storage_key.present?

View File

@@ -6,7 +6,7 @@ module Recordings
def call
team = @session.match.team
return unless team.entitlements.can_access_recordings?
return unless team.entitlements.can_create_recordings?
retention_days = team.entitlements.recording_retention_days
expires_at = retention_days.positive? ? retention_days.days.from_now : nil

View File

@@ -5,6 +5,8 @@ require "aws-sdk-s3"
module Recordings
# Serve file replay/thumbnail al browser senza esporre URL S3 interni (es. http://garage:3900).
class ServeFromStorage
CHUNK_SIZE = 256 * 1024
def initialize(key:, content_type:, disposition: "inline", filename: nil)
@key = key
@content_type = content_type
@@ -14,7 +16,15 @@ module Recordings
def call(controller)
path = local_path
return controller.send_file(path, type: @content_type, disposition: @disposition, filename: @filename) if path
if path
return controller.send_file(
path,
type: @content_type,
disposition: @disposition,
filename: @filename,
stream: true
)
end
proxy_s3(controller)
end
@@ -41,22 +51,24 @@ module Recordings
controller.response.headers["Accept-Ranges"] = "bytes"
controller.response.headers["Content-Length"] = resp.content_length.to_s if resp.content_length
controller.response.headers["Content-Range"] = resp.content_range if resp.content_range.present?
controller.response.headers["Content-Disposition"] = content_disposition_header
data = resp.body.read
max = 1.gigabyte
raise ArgumentError, "File troppo grande per il proxy" if data.bytesize > max
if @disposition == "attachment" && @filename.present?
controller.send_data(
data,
type: @content_type,
disposition: "attachment",
filename: @filename
)
else
controller.send_data(data, type: @content_type, disposition: "inline")
end
controller.status = :partial_content if range.present? && resp.content_range.present?
body = resp.body
controller.response_body = Enumerator.new do |yielder|
while (chunk = body.read(CHUNK_SIZE))
yielder << chunk
end
end
end
def content_disposition_header
if @disposition == "attachment" && @filename.present?
%(attachment; filename="#{@filename.gsub('"', '\\"')}")
else
"inline"
end
end
def s3_client
@@ -66,7 +78,6 @@ module Recordings
endpoint: MatchLiveTv.replay_storage_endpoint,
region: MatchLiveTv.replay_storage_region,
force_path_style: MatchLiveTv.replay_storage_force_path_style?,
# Garage non sempre allinea i checksum AWS su Range/read parziali
response_checksum_validation: "WHEN_REQUIRED"
)
end