Aggiunge replay YouTube temporaneo, pull registrazioni dai CPX e snapshot ingest in admin.

Così overflow Hetzner e VOD YouTube restano in archivio dopo lo spegnimento del nodo, e la colonna ingest non si svuota.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-28 13:01:41 +02:00
co-authored by Cursor
parent 35dfa923e3
commit 05ef56c56d
54 changed files with 1843 additions and 201 deletions
@@ -0,0 +1,60 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe "Public replay show youtube-only", type: :request do
let!(:club) { Club.create!(name: "Club", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
let!(:team) { club.teams.create!(name: "Team", sport: "volleyball") }
let!(:user) { User.create!(email: "pub-replay@test.com", name: "Coach", password: "Password123", role: "coach") }
let!(:match) { team.matches.create!(opponent_name: "Rival", scheduled_at: 1.day.ago) }
let!(:session) do
StreamSession.create!(
match: match, user: user, platform: "youtube", status: "ended", ended_at: 1.hour.ago,
youtube_broadcast_id: "bcast"
)
end
it "mostra embed YouTube senza storage_key" do
Recording.create!(
stream_session: session,
team: team,
status: "ready",
storage_policy: "temporary",
storage_key: nil,
youtube_video_id: "publicYtVid01",
youtube_verified_at: Time.current,
local_media_purged_at: Time.current,
privacy_status: "public",
title: "Derby",
expires_at: nil,
metadata: { "source_platform" => "youtube" }
)
get "/replay/#{session.id}"
expect(response).to have_http_status(:ok)
expect(response.body).to include("youtube.com/embed/publicYtVid01")
expect(response.body).not_to include("id=\"replay-player\"")
end
it "mostra player MP4 per retained con storage_key" do
mltv = StreamSession.create!(
match: match, user: user, platform: "matchlivetv", status: "ended", ended_at: 1.hour.ago
)
Recording.create!(
stream_session: mltv,
team: team,
status: "ready",
storage_policy: "retained",
storage_key: "teams/#{team.id}/sessions/#{mltv.id}/replay.mp4",
privacy_status: "public",
title: "Casa",
expires_at: 30.days.from_now,
metadata: { "source_platform" => "matchlivetv" }
)
get "/replay/#{mltv.id}"
expect(response).to have_http_status(:ok)
expect(response.body).to include("id=\"replay-player\"")
expect(response.body).not_to include("youtube.com/embed/")
end
end
@@ -0,0 +1,94 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe "Api::V1 team recordings replay_source", type: :request do
let!(:user) { User.create!(email: "rec-api@test.it", name: "U", password: "Password123", role: "coach") }
let!(:club) { Club.create!(name: "C", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
let!(:owner) { club.club_memberships.create!(user: user, role: "owner") }
let!(:team) { club.teams.create!(name: "T", sport: "volleyball") }
let!(:match) { team.matches.create!(opponent_name: "Opp", sport: "volleyball", scheduled_at: 1.day.ago) }
def auth_headers
post "/api/v1/auth/login", params: { email: user.email, password: "Password123" }
token = response.parsed_body["access_token"]
{ "Authorization" => "Bearer #{token}" }
end
before do
load Rails.root.join("db/seeds/plans.rb")
Billing::AssignPlan.call(club: club, plan_slug: "premium_full")
end
it "youtube-only: replay_source youtube, no playback/download" do
session = StreamSession.create!(
match: match, user: user, platform: "youtube", status: "ended", ended_at: 1.hour.ago,
youtube_broadcast_id: "bcast1"
)
Recording.create!(
stream_session: session,
team: team,
status: "ready",
storage_policy: "temporary",
storage_key: nil,
youtube_video_id: "ytVideoId123",
youtube_verified_at: Time.current,
local_media_purged_at: Time.current,
privacy_status: "public",
expires_at: nil,
metadata: { "source_platform" => "youtube" }
)
get "/api/v1/teams/#{team.id}/recordings", headers: auth_headers
expect(response).to have_http_status(:ok)
item = response.parsed_body.find { |r| r["session_id"] == session.id } ||
response.parsed_body.find { |r| r["youtube_video_id"] == "ytVideoId123" }
expect(item).to be_present
expect(item["replay_source"]).to eq("youtube")
expect(item["youtube_watch_url"]).to include("ytVideoId123")
expect(item["playback_url"]).to be_nil
expect(item["download_enabled"]).to eq(false)
end
it "matchlivetv retained: replay_source matchlivetv with playback" do
session = StreamSession.create!(
match: match, user: user, platform: "matchlivetv", status: "ended", ended_at: 1.hour.ago
)
Recording.create!(
stream_session: session,
team: team,
status: "ready",
storage_policy: "retained",
storage_key: "teams/#{team.id}/sessions/#{session.id}/replay.mp4",
privacy_status: "public",
expires_at: 30.days.from_now,
metadata: { "source_platform" => "matchlivetv" }
)
get "/api/v1/teams/#{team.id}/recordings", headers: auth_headers
expect(response).to have_http_status(:ok)
item = response.parsed_body.find { |r| r["session_id"] == session.id }
expect(item["replay_source"]).to eq("matchlivetv")
expect(item["playback_url"]).to be_present
expect(item["download_enabled"]).to eq(true)
end
it "download API rejects youtube-only recording" do
session = StreamSession.create!(
match: match, user: user, platform: "youtube", status: "ended", ended_at: 1.hour.ago
)
recording = Recording.create!(
stream_session: session,
team: team,
status: "ready",
storage_policy: "temporary",
storage_key: nil,
youtube_video_id: "ytVideoId999",
youtube_verified_at: Time.current,
privacy_status: "public"
)
get "/api/v1/recordings/#{recording.id}/download", headers: auth_headers
expect(response).to have_http_status(:unprocessable_entity)
end
end