Initial commit: monorepo Match Live TV.

Rails API, app Flutter, infrastruttura Docker/MediaMTX, sito marketing e documentazione di deploy.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-26 17:45:37 +02:00
commit bba6df52c0
381 changed files with 20599 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
module Youtube
class BroadcastService
class Error < StandardError; end
def initialize(team)
@team = team
@credential = team.youtube_credential
end
def create_broadcast!(title:, privacy_status: "unlisted", scheduled_at: nil)
return mock_broadcast if @credential.blank? || missing_oauth_config?
client = authorized_client
broadcast = Google::Apis::YoutubeV3::LiveBroadcast.new(
snippet: Google::Apis::YoutubeV3::LiveBroadcastSnippet.new(
title: title,
scheduled_start_time: scheduled_at&.iso8601
),
status: Google::Apis::YoutubeV3::LiveBroadcastStatus.new(
privacy_status: privacy_status,
self_declared_made_for_kids: false
),
content_details: Google::Apis::YoutubeV3::LiveBroadcastContentDetails.new(
enable_auto_start: true,
enable_auto_stop: true
)
)
result = client.insert_live_broadcast("snippet,status,contentDetails", broadcast)
stream = Google::Apis::YoutubeV3::LiveStream.new(
snippet: Google::Apis::YoutubeV3::LiveStreamSnippet.new(title: "#{title} stream"),
cdn: Google::Apis::YoutubeV3::CdnSettings.new(
frame_rate: "30fps",
ingestion_type: "rtmp",
resolution: "720p"
)
)
stream_result = client.insert_live_stream("snippet,cdn", stream)
client.bind_live_broadcast(result.id, "id,contentDetails", stream_result.id)
{
broadcast_id: result.id,
stream_id: stream_result.id,
stream_key: stream_result.cdn.ingestion_info.stream_name,
rtmp_url: stream_result.cdn.ingestion_info.ingestion_address
}
rescue Google::Apis::Error => e
raise Error, e.message
end
def complete_broadcast!(broadcast_id)
return if @credential.blank? || missing_oauth_config?
client = authorized_client
client.transition_live_broadcast(broadcast_id, "complete")
rescue Google::Apis::Error
nil
end
def viewer_count(broadcast_id)
return 0 if @credential.blank? || missing_oauth_config?
client = authorized_client
resp = client.list_live_broadcasts("statistics", id: broadcast_id)
resp.items&.first&.statistics&.concurrent_viewers.to_i
rescue Google::Apis::Error
0
end
private
def mock_broadcast
{
broadcast_id: "mock_#{SecureRandom.hex(8)}",
stream_id: "mock_stream",
stream_key: ENV.fetch("YOUTUBE_MOCK_STREAM_KEY", "mock-stream-key"),
rtmp_url: "rtmp://a.rtmp.youtube.com/live2"
}
end
def missing_oauth_config?
ENV["YOUTUBE_CLIENT_ID"].blank?
end
def authorized_client
OauthRefresh.new(@credential).apply!(Google::Apis::YoutubeV3::YouTubeService.new)
end
end
end

View File

@@ -0,0 +1,25 @@
module Youtube
class OauthExchange
TOKEN_URL = "https://oauth2.googleapis.com/token".freeze
def self.call(code)
response = Faraday.post(TOKEN_URL, {
code: code,
client_id: ENV["YOUTUBE_CLIENT_ID"],
client_secret: ENV["YOUTUBE_CLIENT_SECRET"],
redirect_uri: ENV["YOUTUBE_REDIRECT_URI"],
grant_type: "authorization_code"
})
data = JSON.parse(response.body)
raise BroadcastService::Error, data["error_description"] if data["error"]
{
access_token: data["access_token"],
refresh_token: data["refresh_token"],
expires_in: data["expires_in"].to_i,
channel_id: nil,
channel_title: nil
}
end
end
end

View File

@@ -0,0 +1,31 @@
module Youtube
class OauthRefresh
TOKEN_URL = "https://oauth2.googleapis.com/token".freeze
def initialize(credential)
@credential = credential
end
def apply!(service)
refresh! if @credential.expired?
service.authorization = @credential.access_token
service
end
def refresh!
response = Faraday.post(TOKEN_URL, {
client_id: ENV["YOUTUBE_CLIENT_ID"],
client_secret: ENV["YOUTUBE_CLIENT_SECRET"],
refresh_token: @credential.refresh_token,
grant_type: "refresh_token"
})
data = JSON.parse(response.body)
raise Youtube::BroadcastService::Error, data["error"] if data["error"]
@credential.update!(
access_token: data["access_token"],
expires_at: Time.current + data["expires_in"].to_i.seconds
)
end
end
end

View File

@@ -0,0 +1,22 @@
module Youtube
class OauthUrl
AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth".freeze
SCOPES = [
"https://www.googleapis.com/auth/youtube",
"https://www.googleapis.com/auth/youtube.force-ssl"
].join(" ").freeze
def self.build(team_id:, redirect_uri:)
params = {
client_id: ENV.fetch("YOUTUBE_CLIENT_ID", "not_configured"),
redirect_uri: redirect_uri,
response_type: "code",
scope: SCOPES,
access_type: "offline",
prompt: "consent",
state: team_id
}
"#{AUTH_URL}?#{URI.encode_www_form(params)}"
end
end
end