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>
183 lines
4.7 KiB
Ruby
183 lines
4.7 KiB
Ruby
class Recording < ApplicationRecord
|
|
STATUSES = %w[processing ready expired failed].freeze
|
|
PRIVACY_STATUSES = %w[public unlisted].freeze
|
|
STORAGE_BACKENDS = %w[local s3].freeze
|
|
|
|
belongs_to :stream_session
|
|
belongs_to :team
|
|
|
|
validates :status, inclusion: { in: STATUSES }
|
|
validates :privacy_status, inclusion: { in: PRIVACY_STATUSES }
|
|
validates :storage_backend, inclusion: { in: STORAGE_BACKENDS }
|
|
|
|
scope :not_deleted, -> { where(deleted_at: nil) }
|
|
scope :ready, lambda {
|
|
not_deleted.where(status: "ready").where("expires_at IS NULL OR expires_at > ?", Time.current)
|
|
}
|
|
scope :publicly_listed, -> { where(privacy_status: "public") }
|
|
scope :for_team, ->(team) { where(team: team) }
|
|
scope :for_club, lambda { |club|
|
|
joins(:team).where(teams: { club_id: club.id })
|
|
}
|
|
scope :expiring_within, lambda { |days|
|
|
ready.where(expires_at: ..days.days.from_now)
|
|
}
|
|
scope :expired_pending_purge, lambda {
|
|
not_deleted.where(status: %w[ready failed]).where("expires_at IS NOT NULL AND expires_at <= ?", Time.current)
|
|
}
|
|
scope :search_replays, lambda { |query|
|
|
q = query.to_s.strip
|
|
return all if q.blank?
|
|
|
|
term = "%#{sanitize_sql_like(q)}%"
|
|
joins(stream_session: { match: { team: :club } }).where(
|
|
"recordings.title ILIKE :term OR teams.name ILIKE :term OR matches.opponent_name ILIKE :term OR clubs.name ILIKE :term OR matches.location ILIKE :term",
|
|
term: term
|
|
)
|
|
}
|
|
|
|
before_validation :normalize_privacy_status
|
|
|
|
def replay_url
|
|
return nil unless stream_session_id.present?
|
|
return nil unless ready? || status == "processing"
|
|
|
|
"#{MatchLiveTv.app_public_url.chomp('/')}/replay/#{stream_session_id}"
|
|
end
|
|
|
|
def playback_stream_url
|
|
return nil unless ready? && stream_session_id.present?
|
|
|
|
"#{MatchLiveTv.app_public_url.chomp('/')}/replay/#{stream_session_id}/stream"
|
|
end
|
|
|
|
def thumbnail_url
|
|
return nil unless thumbnail_storage_key.present? && stream_session_id.present?
|
|
|
|
"#{MatchLiveTv.app_public_url.chomp('/')}/replay/#{stream_session_id}/thumbnail"
|
|
end
|
|
|
|
def download_api_path
|
|
return nil unless ready?
|
|
|
|
"/api/v1/recordings/#{id}/download"
|
|
end
|
|
|
|
def youtube_watch_url
|
|
return nil if youtube_video_id.blank?
|
|
return nil if youtube_video_id.to_s.start_with?("mock_")
|
|
|
|
"https://www.youtube.com/watch?v=#{youtube_video_id}"
|
|
end
|
|
|
|
def ready?
|
|
status == "ready" && !deleted? && (expires_at.nil? || expires_at.future?)
|
|
end
|
|
|
|
def deleted?
|
|
deleted_at.present?
|
|
end
|
|
|
|
def publicly_listed?
|
|
privacy_status == "public"
|
|
end
|
|
|
|
def unlisted?
|
|
privacy_status == "unlisted"
|
|
end
|
|
|
|
def auto_publish_youtube?
|
|
metadata.is_a?(Hash) && metadata["auto_publish_youtube"] == true
|
|
end
|
|
|
|
def ai_metadata
|
|
metadata.fetch("ai", {})
|
|
end
|
|
|
|
def merge_metadata!(attrs)
|
|
update!(metadata: metadata.merge(attrs.stringify_keys))
|
|
end
|
|
|
|
def title_or_default
|
|
title.presence || default_title
|
|
end
|
|
|
|
def default_title
|
|
match = stream_session&.match
|
|
return "Replay" unless match
|
|
|
|
"#{match.team.name} vs #{match.opponent_name}"
|
|
end
|
|
|
|
def recorded_at_or_fallback
|
|
recorded_at || stream_session&.ended_at || stream_session&.started_at || created_at
|
|
end
|
|
|
|
def duration_label
|
|
return "—" if duration_secs.to_i <= 0
|
|
|
|
mins = duration_secs / 60
|
|
secs = duration_secs % 60
|
|
format("%d:%02d", mins, secs)
|
|
end
|
|
|
|
def byte_size_label
|
|
bytes = byte_size.to_i
|
|
return "—" if bytes <= 0
|
|
|
|
if bytes >= 1.gigabyte
|
|
format("%.1f GB", bytes / 1.gigabyte.to_f)
|
|
elsif bytes >= 1.megabyte
|
|
format("%.1f MB", bytes / 1.megabyte.to_f)
|
|
else
|
|
format("%.0f KB", bytes / 1.kilobyte.to_f)
|
|
end
|
|
end
|
|
|
|
def views_label
|
|
count = view_count.to_i
|
|
return "0 visualizzazioni" if count.zero?
|
|
|
|
count == 1 ? "1 visualizzazione" : "#{count} visualizzazioni"
|
|
end
|
|
|
|
def source_platform
|
|
metadata.is_a?(Hash) ? metadata["source_platform"].presence : nil
|
|
end
|
|
|
|
def source_platform_label
|
|
case source_platform
|
|
when "youtube" then "YouTube"
|
|
when "matchlivetv" then "Match Live TV"
|
|
else "—"
|
|
end
|
|
end
|
|
|
|
def playable_on_site?
|
|
ready? && storage_key.present?
|
|
end
|
|
|
|
def days_until_expiry
|
|
return nil unless expires_at
|
|
|
|
((expires_at - Time.current) / 1.day).ceil
|
|
end
|
|
|
|
def status_label
|
|
return "Eliminato" if deleted?
|
|
return "Scaduto" if status == "expired"
|
|
return "Errore" if status == "failed"
|
|
return "In elaborazione" if status == "processing"
|
|
return "Scade presto" if expires_at.present? && expires_at <= 7.days.from_now
|
|
|
|
"Disponibile"
|
|
end
|
|
|
|
private
|
|
|
|
def normalize_privacy_status
|
|
self.privacy_status = "public" if privacy_status == "private"
|
|
self.privacy_status = "unlisted" if privacy_status.blank?
|
|
end
|
|
end
|