Aggiunge modulo Replay, Garage dev e YouTube a livello società.

Pipeline registrazione/upload con storage S3, archivio web e app, proxy replay
per il browser, OAuth YouTube sulla pagina club (Premium Full) e footer legale.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-03 07:53:11 +02:00
parent a87cda156b
commit 1f273f849d
87 changed files with 2952 additions and 195 deletions

View File

@@ -0,0 +1,55 @@
module Public
class ClubRecordingsController < WebBaseController
before_action :require_login!
before_action :set_club
before_action -> { require_club_owner!(@club) }
before_action :set_recording, only: %i[update destroy publish_youtube]
def index
@stats = Recordings::ClubStats.new(@club).call
@recordings = Recording.for_club(@club)
.not_deleted
.includes(stream_session: { match: :team })
.order(recorded_at: :desc, created_at: :desc)
@teams = @club.teams.order(:name)
end
def update
privacy = params.require(:recording).permit(:privacy_status, :title)[:privacy_status]
attrs = {}
attrs[:privacy_status] = privacy if privacy.in?(Recording::PRIVACY_STATUSES)
title = params.dig(:recording, :title)
attrs[:title] = title if title.present?
@recording.update!(attrs)
redirect_to public_club_recordings_path(@club), notice: "Replay aggiornato"
rescue ActiveRecord::RecordInvalid => e
redirect_to public_club_recordings_path(@club), alert: e.record.errors.full_messages.join(", ")
end
def destroy
Recordings::Delete.new(@recording).call
redirect_to public_club_recordings_path(@club), notice: "Replay eliminato"
end
def publish_youtube
ent = @recording.team.entitlements
unless ent.premium_full? && ent.youtube_enabled?
redirect_to public_club_recordings_path(@club), alert: "Republicazione YouTube con Premium Full"
return
end
Recordings::PublishToYoutubeJob.perform_async(@recording.id)
redirect_to public_club_recordings_path(@club), notice: "Pubblicazione su YouTube avviata"
end
private
def set_club
@club = current_user.owned_clubs.find(params[:club_id])
end
def set_recording
@recording = Recording.for_club(@club).not_deleted.find(params[:id])
end
end
end

View File

@@ -3,7 +3,7 @@ module Public
include BrandingAttachments
before_action :require_login!
before_action :set_club, only: %i[show edit update billing checkout portal]
before_action :set_club, only: %i[show edit update billing checkout portal youtube_connect youtube_disconnect]
before_action :require_billing_profile_for_premium_checkout!, only: :checkout
def new
@@ -54,6 +54,7 @@ module Public
@entitlements_team = @club.teams.first
@entitlements = @entitlements_team&.entitlements
@teams = @club.teams.order(:name)
@replay_stats = Recordings::ClubStats.new(@club).call if @entitlements&.can_access_recordings?
end
def edit
@@ -136,6 +137,33 @@ module Public
redirect_to public_club_billing_path(@club), alert: "Errore Stripe: #{e.message}"
end
def youtube_connect
require_club_owner!(@club)
entitlements = @club.teams.first!.entitlements
entitlements.assert_can_connect_youtube!
if ENV["YOUTUBE_CLIENT_ID"].blank?
redirect_to public_club_path(@club), alert: "YouTube OAuth non configurato sul server"
return
end
return_app = params[:return_app] == "1"
state = Youtube::OauthState.for_club(
club_id: @club.id,
user_id: current_user.id,
return_app: return_app
)
redirect_to Youtube::OauthUrl.build(state: state), allow_other_host: true
rescue Teams::EntitlementError => e
redirect_to public_club_path(@club), alert: e.message
end
def youtube_disconnect
require_club_owner!(@club)
@club.youtube_credential&.destroy!
redirect_to public_club_path(@club), notice: "Canale YouTube della società scollegato"
end
def portal
require_club_owner!(@club)
unless MatchLiveTv.stripe_enabled?

View File

@@ -2,14 +2,102 @@ module Public
class ReplayController < SiteBaseController
layout "marketing_live"
def show
@session = StreamSession.includes(match: :team).find(params[:id])
@recording = Recording.ready.find_by(stream_session: @session)
@match = @session.match
before_action :set_session_and_recording, only: %i[show stream thumbnail download]
before_action :authorize_replay_access!, only: %i[show stream thumbnail download]
unless @recording
redirect_to public_live_path(@session), alert: "Replay non disponibile"
def index
@query = params[:q].to_s.strip
@recordings = Recording.ready.publicly_listed
.includes(stream_session: { match: { team: :club } })
.search_replays(@query)
.order(recorded_at: :desc, created_at: :desc)
.limit(100)
if params[:club_id].present?
@recordings = @recordings.joins(:team).where(teams: { club_id: params[:club_id] })
end
if params[:team_id].present?
@recordings = @recordings.where(team_id: params[:team_id])
end
@clubs = Club.joins(teams: :recordings)
.merge(Recording.ready.publicly_listed)
.distinct
.order(:name)
@filter_club = @clubs.find { |c| c.id.to_s == params[:club_id].to_s } if params[:club_id].present?
end
def show
@match = @session.match
end
def stream
Recordings::IncrementViews.new(@recording).call
serve_video(disposition: "inline")
end
def thumbnail
serve_thumbnail
end
def download
unless download_allowed?
redirect_to public_replay_path(@session), alert: "Download non disponibile"
return
end
serve_video(disposition: "attachment")
end
private
def serve_video(disposition:)
Recordings::ServeFromStorage.new(
key: @recording.storage_key,
content_type: "video/mp4",
disposition: disposition,
filename: disposition == "attachment" ? download_filename : nil
).call(self)
rescue ArgumentError
head :not_found
end
def serve_thumbnail
key = @recording.thumbnail_storage_key
return head :not_found if key.blank?
Recordings::ServeFromStorage.new(
key: key,
content_type: "image/jpeg",
disposition: "inline"
).call(self)
rescue ArgumentError
head :not_found
end
def download_allowed?
ent = @recording.team.entitlements
return false unless ent.phone_download_enabled?
Recordings::AccessPolicy.new(@recording, viewer: current_user).allowed? ||
Recordings::AccessPolicy.new(@recording, viewer: current_user).manage_allowed?
end
def download_filename
slug = @recording.title_or_default.parameterize.presence || "replay"
"#{slug}.mp4"
end
def set_session_and_recording
@session = StreamSession.includes(match: { team: :club }).find(params[:id])
@recording = Recording.not_deleted.find_by(stream_session: @session)
return if @recording
redirect_to public_live_path(@session), alert: "Replay non disponibile"
end
def authorize_replay_access!
return if Recordings::AccessPolicy.new(@recording, viewer: current_user).allowed?
redirect_to public_live_index_path, alert: "Replay non disponibile"
end
end
end

View File

@@ -116,40 +116,20 @@ module Public
end
def youtube_connect
require_youtube_connect_access!(@team)
@team.entitlements.assert_can_connect_youtube!
if ENV["YOUTUBE_CLIENT_ID"].blank?
redirect_to public_team_details_path(@team), alert: "YouTube OAuth non configurato sul server"
return
end
return_app = params[:return_app] == "1"
state = Youtube::OauthState.for_team(
team_id: @team.id,
user_id: current_user.id,
return_app: return_app
redirect_to public_club_youtube_connect_path(
@team.club_id,
return_app: params[:return_app]
)
redirect_to Youtube::OauthUrl.build(state: state), allow_other_host: true
rescue Teams::EntitlementError => e
redirect_to public_team_details_path(@team), alert: e.message
end
def youtube_disconnect
require_club_owner_for_team!(@team)
@team.youtube_credential&.destroy!
redirect_to public_team_details_path(@team), notice: "Canale YouTube scollegato"
@team.club.youtube_credential&.destroy!
redirect_to public_club_path(@team.club), notice: "Canale YouTube della società scollegato"
end
private
def require_youtube_connect_access!(team)
return if team.club&.owned_by?(current_user)
return if current_user.can_stream_for?(team)
redirect_to public_team_details_path(team), alert: "Accesso non consentito"
end
def load_team_details!
@club = @team.club
@can_manage = @club.owned_by?(current_user)
@@ -160,7 +140,11 @@ module Public
category: params[:category].presence_in(TeamRosterMember::CATEGORIES) || "player"
)
end
@recordings = @team.recordings.ready.includes(stream_session: :match).order(created_at: :desc).limit(10)
@recordings = @team.recordings.not_deleted
.where(status: %w[ready processing])
.includes(stream_session: :match)
.order(recorded_at: :desc, created_at: :desc)
.limit(10)
@owner_membership = current_user.user_teams.find_by(team: @team)
@staff_memberships = @team.user_teams.includes(:user)
.where("user_teams.role = 'member' OR (user_teams.staff_kind IS NOT NULL)")