Integrazione YouTube Live: OAuth squadra, canale piattaforma Light e UI.
Collegamento da Dettagli squadra e admin per il refresh token Match Live TV; API e app mobile allineate ai piani Light/Full. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
13
backend/app/controllers/admin/youtube_controller.rb
Normal file
13
backend/app/controllers/admin/youtube_controller.rb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
module Admin
|
||||||
|
class YoutubeController < Admin::BaseController
|
||||||
|
def platform
|
||||||
|
if ENV["YOUTUBE_CLIENT_ID"].blank?
|
||||||
|
redirect_to admin_root_path, alert: "Configura YOUTUBE_CLIENT_ID e YOUTUBE_CLIENT_SECRET in .env"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
state = Youtube::OauthState.for_platform(admin_id: current_admin_account.id)
|
||||||
|
redirect_to Youtube::OauthUrl.build(state: state), allow_other_host: true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -70,6 +70,7 @@ module Api
|
|||||||
|
|
||||||
def team_json(team, detail: false)
|
def team_json(team, detail: false)
|
||||||
ent = team.entitlements
|
ent = team.entitlements
|
||||||
|
yt = Youtube::TeamStatus.new(team)
|
||||||
data = {
|
data = {
|
||||||
id: team.id,
|
id: team.id,
|
||||||
name: team.name,
|
name: team.name,
|
||||||
@@ -79,7 +80,10 @@ module Api
|
|||||||
secondary_color: team.effective_secondary_color,
|
secondary_color: team.effective_secondary_color,
|
||||||
club_id: team.club_id,
|
club_id: team.club_id,
|
||||||
club_name: team.club.name,
|
club_name: team.club.name,
|
||||||
youtube_connected: team.youtube_credential.present?,
|
youtube_connected: yt.connected?,
|
||||||
|
youtube_selectable: yt.selectable?,
|
||||||
|
youtube_channel_title: yt.channel_title,
|
||||||
|
youtube_uses_platform_channel: yt.uses_platform_channel?,
|
||||||
plan_slug: ent.plan.slug,
|
plan_slug: ent.plan.slug,
|
||||||
plan_name: ent.plan.name,
|
plan_name: ent.plan.name,
|
||||||
premium_active: ent.premium_active?,
|
premium_active: ent.premium_active?,
|
||||||
|
|||||||
@@ -1,25 +1,70 @@
|
|||||||
module Api
|
module Api
|
||||||
module V1
|
module V1
|
||||||
class YoutubeController < BaseController
|
class YoutubeController < BaseController
|
||||||
|
include ActionController::Cookies
|
||||||
|
|
||||||
|
skip_before_action :authenticate_request!, only: :callback
|
||||||
|
|
||||||
def authorize
|
def authorize
|
||||||
team = current_user.manageable_teams.find(params[:team_id])
|
team = current_user.manageable_teams.find(params[:team_id])
|
||||||
team.entitlements.assert_can_connect_youtube!
|
team.entitlements.assert_can_connect_youtube!
|
||||||
url = Youtube::OauthUrl.build(team_id: team.id, redirect_uri: ENV.fetch("YOUTUBE_REDIRECT_URI"))
|
state = Youtube::OauthState.for_team(team_id: team.id, user_id: current_user.id)
|
||||||
|
url = Youtube::OauthUrl.build(state: state, redirect_uri: ENV.fetch("YOUTUBE_REDIRECT_URI"))
|
||||||
render json: { authorization_url: url }
|
render json: { authorization_url: url }
|
||||||
end
|
end
|
||||||
|
|
||||||
def callback
|
def callback
|
||||||
team = current_user.manageable_teams.find(params[:state])
|
return redirect_to_oauth_error("Codice OAuth mancante") if params[:code].blank?
|
||||||
|
|
||||||
|
ctx = Youtube::OauthState.verify!(params[:state])
|
||||||
tokens = Youtube::OauthExchange.call(params[:code])
|
tokens = Youtube::OauthExchange.call(params[:code])
|
||||||
|
|
||||||
|
if ctx[:kind] == :platform
|
||||||
|
unless platform_oauth_authorized?(ctx[:admin_id])
|
||||||
|
return redirect_to "#{admin_base_url}/login", alert: "Sessione admin richiesta", allow_other_host: true
|
||||||
|
end
|
||||||
|
|
||||||
|
return render_platform_token(tokens)
|
||||||
|
end
|
||||||
|
|
||||||
|
user = User.find(ctx[:user_id])
|
||||||
|
team = user.manageable_teams.find(ctx[:team_id])
|
||||||
cred = team.youtube_credential || team.build_youtube_credential
|
cred = team.youtube_credential || team.build_youtube_credential
|
||||||
cred.update!(
|
cred.update!(
|
||||||
access_token: tokens[:access_token],
|
access_token: tokens[:access_token],
|
||||||
refresh_token: tokens[:refresh_token],
|
refresh_token: tokens[:refresh_token] || cred.refresh_token,
|
||||||
expires_at: Time.current + tokens[:expires_in].seconds,
|
expires_at: Time.current + tokens[:expires_in].seconds,
|
||||||
channel_id: tokens[:channel_id],
|
channel_id: tokens[:channel_id],
|
||||||
channel_title: tokens[:channel_title]
|
channel_title: tokens[:channel_title]
|
||||||
)
|
)
|
||||||
redirect_to "/admin/teams/#{team.id}?youtube=connected"
|
redirect_to "#{public_base_url}/teams/#{team.id}/dettagli?youtube=connected", allow_other_host: true
|
||||||
|
rescue ArgumentError, Youtube::BroadcastService::Error => e
|
||||||
|
redirect_to_oauth_error(e.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def public_base_url
|
||||||
|
MatchLiveTv.app_public_url.chomp("/")
|
||||||
|
end
|
||||||
|
|
||||||
|
def redirect_to_oauth_error(message)
|
||||||
|
redirect_to "#{public_base_url}/prezzi?youtube_error=#{ERB::Util.url_encode(message)}",
|
||||||
|
allow_other_host: true
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_platform_token(tokens)
|
||||||
|
@refresh_token = tokens[:refresh_token]
|
||||||
|
@channel_title = tokens[:channel_title]
|
||||||
|
render "admin/youtube/platform_token", layout: "admin"
|
||||||
|
end
|
||||||
|
|
||||||
|
def platform_oauth_authorized?(admin_id)
|
||||||
|
session[:admin_account_id].present? && session[:admin_account_id].to_s == admin_id.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def admin_base_url
|
||||||
|
"#{public_base_url}/admin"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ module Public
|
|||||||
before_action :set_team, only: %i[
|
before_action :set_team, only: %i[
|
||||||
details dashboard roster edit update invite create_invitation
|
details dashboard roster edit update invite create_invitation
|
||||||
assign_self_staff clear_self_staff remove_member destroy_invitation
|
assign_self_staff clear_self_staff remove_member destroy_invitation
|
||||||
|
youtube_connect youtube_disconnect
|
||||||
]
|
]
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@@ -114,6 +115,27 @@ module Public
|
|||||||
redirect_to public_team_details_path(@team), notice: "Invito annullato"
|
redirect_to public_team_details_path(@team), notice: "Invito annullato"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def youtube_connect
|
||||||
|
require_club_owner_for_team!(@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
|
||||||
|
|
||||||
|
state = Youtube::OauthState.for_team(team_id: @team.id, user_id: current_user.id)
|
||||||
|
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"
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_team_details!
|
def load_team_details!
|
||||||
|
|||||||
@@ -25,9 +25,11 @@ class Plan < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def allows_platform?(platform)
|
def allows_platform?(platform)
|
||||||
return false if platform.to_s == "youtube" && !youtube_enabled?
|
platform = platform.to_s
|
||||||
|
return false if platform == "youtube" && !youtube_enabled?
|
||||||
|
return true if platform == "youtube" && youtube_mode == "matchlivetv_light"
|
||||||
|
|
||||||
Array(features["platforms"]).map(&:to_s).include?(platform.to_s)
|
Array(features["platforms"]).map(&:to_s).include?(platform)
|
||||||
end
|
end
|
||||||
|
|
||||||
def feature(key)
|
def feature(key)
|
||||||
|
|||||||
23
backend/app/models/youtube/platform_credential.rb
Normal file
23
backend/app/models/youtube/platform_credential.rb
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
module Youtube
|
||||||
|
# Credenziali del canale YouTube Match Live TV (Premium Light), da ENV.
|
||||||
|
class PlatformCredential
|
||||||
|
attr_accessor :access_token, :expires_at
|
||||||
|
|
||||||
|
def refresh_token
|
||||||
|
ENV["YOUTUBE_PLATFORM_REFRESH_TOKEN"].presence
|
||||||
|
end
|
||||||
|
|
||||||
|
def expired?
|
||||||
|
expires_at.present? && expires_at < Time.current
|
||||||
|
end
|
||||||
|
|
||||||
|
def update!(attrs)
|
||||||
|
self.access_token = attrs[:access_token] if attrs.key?(:access_token)
|
||||||
|
self.expires_at = attrs[:expires_at] if attrs.key?(:expires_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.configured?
|
||||||
|
ENV["YOUTUBE_PLATFORM_REFRESH_TOKEN"].present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,7 +4,8 @@ module Youtube
|
|||||||
|
|
||||||
def initialize(team)
|
def initialize(team)
|
||||||
@team = team
|
@team = team
|
||||||
@credential = team.youtube_credential
|
@resolver = CredentialResolver.new(team)
|
||||||
|
@credential = @resolver.resolve
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_broadcast!(title:, privacy_status: "unlisted", scheduled_at: nil)
|
def create_broadcast!(title:, privacy_status: "unlisted", scheduled_at: nil)
|
||||||
|
|||||||
18
backend/app/services/youtube/channel_info.rb
Normal file
18
backend/app/services/youtube/channel_info.rb
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
module Youtube
|
||||||
|
class ChannelInfo
|
||||||
|
def self.fetch(access_token:)
|
||||||
|
service = Google::Apis::YoutubeV3::YouTubeService.new
|
||||||
|
service.authorization = access_token
|
||||||
|
resp = service.list_channels("snippet", mine: true, max_results: 1)
|
||||||
|
item = resp.items&.first
|
||||||
|
return { channel_id: nil, channel_title: nil } unless item
|
||||||
|
|
||||||
|
{
|
||||||
|
channel_id: item.id,
|
||||||
|
channel_title: item.snippet&.title
|
||||||
|
}
|
||||||
|
rescue Google::Apis::Error
|
||||||
|
{ channel_id: nil, channel_title: nil }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
24
backend/app/services/youtube/credential_resolver.rb
Normal file
24
backend/app/services/youtube/credential_resolver.rb
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
module Youtube
|
||||||
|
class CredentialResolver
|
||||||
|
def self.for_team(team)
|
||||||
|
new(team).resolve
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(team)
|
||||||
|
@team = team
|
||||||
|
@mode = team.entitlements.plan.youtube_mode
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolve
|
||||||
|
if @mode == "matchlivetv_light" && PlatformCredential.configured?
|
||||||
|
PlatformCredential.new
|
||||||
|
else
|
||||||
|
@team.youtube_credential
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def uses_platform_channel?
|
||||||
|
@mode == "matchlivetv_light" && PlatformCredential.configured?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -13,12 +13,13 @@ module Youtube
|
|||||||
data = JSON.parse(response.body)
|
data = JSON.parse(response.body)
|
||||||
raise BroadcastService::Error, data["error_description"] if data["error"]
|
raise BroadcastService::Error, data["error_description"] if data["error"]
|
||||||
|
|
||||||
|
info = ChannelInfo.fetch(access_token: data["access_token"])
|
||||||
{
|
{
|
||||||
access_token: data["access_token"],
|
access_token: data["access_token"],
|
||||||
refresh_token: data["refresh_token"],
|
refresh_token: data["refresh_token"],
|
||||||
expires_in: data["expires_in"].to_i,
|
expires_in: data["expires_in"].to_i,
|
||||||
channel_id: nil,
|
channel_id: info[:channel_id],
|
||||||
channel_title: nil
|
channel_title: info[:channel_title]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
39
backend/app/services/youtube/oauth_state.rb
Normal file
39
backend/app/services/youtube/oauth_state.rb
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
module Youtube
|
||||||
|
class OauthState
|
||||||
|
PURPOSE = :youtube_oauth_connect
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def for_team(team_id:, user_id:)
|
||||||
|
verifier.generate([team_id.to_s, user_id.to_s], expires_in: 1.hour)
|
||||||
|
end
|
||||||
|
|
||||||
|
def for_platform(admin_id:)
|
||||||
|
verifier.generate(["platform", admin_id.to_s], expires_in: 1.hour)
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify!(token)
|
||||||
|
payload = verifier.verify(token)
|
||||||
|
case payload
|
||||||
|
when Array
|
||||||
|
if payload.first == "platform"
|
||||||
|
{ kind: :platform, admin_id: payload[1] }
|
||||||
|
elsif payload.size == 2
|
||||||
|
{ kind: :team, team_id: payload[0], user_id: payload[1] }
|
||||||
|
else
|
||||||
|
raise ArgumentError, "stato OAuth non valido"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise ArgumentError, "stato OAuth non valido"
|
||||||
|
end
|
||||||
|
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
||||||
|
raise ArgumentError, "stato OAuth scaduto o non valido"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def verifier
|
||||||
|
Rails.application.message_verifier(PURPOSE)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,7 +6,7 @@ module Youtube
|
|||||||
"https://www.googleapis.com/auth/youtube.force-ssl"
|
"https://www.googleapis.com/auth/youtube.force-ssl"
|
||||||
].join(" ").freeze
|
].join(" ").freeze
|
||||||
|
|
||||||
def self.build(team_id:, redirect_uri:)
|
def self.build(state:, redirect_uri: ENV["YOUTUBE_REDIRECT_URI"])
|
||||||
params = {
|
params = {
|
||||||
client_id: ENV.fetch("YOUTUBE_CLIENT_ID", "not_configured"),
|
client_id: ENV.fetch("YOUTUBE_CLIENT_ID", "not_configured"),
|
||||||
redirect_uri: redirect_uri,
|
redirect_uri: redirect_uri,
|
||||||
@@ -14,7 +14,7 @@ module Youtube
|
|||||||
scope: SCOPES,
|
scope: SCOPES,
|
||||||
access_type: "offline",
|
access_type: "offline",
|
||||||
prompt: "consent",
|
prompt: "consent",
|
||||||
state: team_id
|
state: state
|
||||||
}
|
}
|
||||||
"#{AUTH_URL}?#{URI.encode_www_form(params)}"
|
"#{AUTH_URL}?#{URI.encode_www_form(params)}"
|
||||||
end
|
end
|
||||||
|
|||||||
44
backend/app/services/youtube/team_status.rb
Normal file
44
backend/app/services/youtube/team_status.rb
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module Youtube
|
||||||
|
class TeamStatus
|
||||||
|
PLATFORM_LABEL = "Match Live TV".freeze
|
||||||
|
|
||||||
|
def initialize(team)
|
||||||
|
@team = team
|
||||||
|
@ent = team.entitlements
|
||||||
|
@mode = @ent.plan.youtube_mode
|
||||||
|
end
|
||||||
|
|
||||||
|
def selectable?
|
||||||
|
return false unless @ent.youtube_enabled?
|
||||||
|
|
||||||
|
case @mode
|
||||||
|
when "matchlivetv_light"
|
||||||
|
PlatformCredential.configured?
|
||||||
|
when "team"
|
||||||
|
@team.youtube_credential.present?
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def connected?
|
||||||
|
selectable?
|
||||||
|
end
|
||||||
|
|
||||||
|
def uses_platform_channel?
|
||||||
|
@mode == "matchlivetv_light" && PlatformCredential.configured?
|
||||||
|
end
|
||||||
|
|
||||||
|
def channel_title
|
||||||
|
if uses_platform_channel?
|
||||||
|
PLATFORM_LABEL
|
||||||
|
else
|
||||||
|
@team.youtube_credential&.channel_title
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def needs_team_oauth?
|
||||||
|
@mode == "team" && @ent.premium_full?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,7 +3,12 @@
|
|||||||
<% if @team.club %>
|
<% if @team.club %>
|
||||||
<p>Società: <%= @team.club.name %> · <%= link_to "Pagamenti e fatture", admin_billing_path(club_id: @team.club.id) %></p>
|
<p>Società: <%= @team.club.name %> · <%= link_to "Pagamenti e fatture", admin_billing_path(club_id: @team.club.id) %></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
<p>YouTube: <%= @team.youtube_credential ? "Connesso" : link_to("Connetti", "/api/v1/teams/#{@team.id}/youtube/authorize") %></p>
|
<% yt = Youtube::TeamStatus.new(@team) %>
|
||||||
|
<p>YouTube: <%= yt.channel_title || "—" %> · <%= yt.selectable? ? "pronto" : "non pronto" %></p>
|
||||||
|
<% if @team.entitlements.plan.youtube_mode == "team" && @team.youtube_credential.blank? %>
|
||||||
|
<p><%= link_to "Collega (da account titolare sul sito)", public_team_details_path(@team) %></p>
|
||||||
|
<% end %>
|
||||||
|
<p><%= link_to "Token canale Match Live TV (Light)", admin_youtube_platform_path %></p>
|
||||||
|
|
||||||
<h3>Partite</h3>
|
<h3>Partite</h3>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
15
backend/app/views/admin/youtube/platform_token.html.erb
Normal file
15
backend/app/views/admin/youtube/platform_token.html.erb
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<h1>Token canale Match Live TV</h1>
|
||||||
|
|
||||||
|
<% if @channel_title.present? %>
|
||||||
|
<p>Canale: <strong><%= @channel_title %></strong></p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if @refresh_token.present? %>
|
||||||
|
<p>Aggiungi in <code>/opt/matchlivetv/infra/.env</code> (o locale <code>infra/.env</code>):</p>
|
||||||
|
<pre style="background:#111;color:#eee;padding:12px;overflow:auto">YOUTUBE_PLATFORM_REFRESH_TOKEN=<%= @refresh_token %></pre>
|
||||||
|
<p class="muted">Copia ora: non verrà mostrato di nuovo. Poi riavvia il container Rails.</p>
|
||||||
|
<% else %>
|
||||||
|
<p class="muted">Google non ha restituito un refresh token. Ripeti il collegamento con <code>prompt=consent</code> (revoca l’accesso precedente in Google Account).</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<p><%= link_to "← Dashboard admin", admin_root_path %></p>
|
||||||
46
backend/app/views/public/teams/_youtube.html.erb
Normal file
46
backend/app/views/public/teams/_youtube.html.erb
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<% yt = Youtube::TeamStatus.new(team) %>
|
||||||
|
<% return unless entitlements.youtube_enabled? %>
|
||||||
|
|
||||||
|
<section class="card team-youtube" id="youtube">
|
||||||
|
<h2>YouTube Live</h2>
|
||||||
|
|
||||||
|
<% if params[:youtube] == "connected" %>
|
||||||
|
<p class="notice" style="color:#2e7d32;margin-bottom:12px">Canale YouTube collegato con successo.</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if entitlements.plan.youtube_mode == "matchlivetv_light" %>
|
||||||
|
<p>
|
||||||
|
Con il piano <strong>Premium Light</strong> la diretta va sul canale ufficiale
|
||||||
|
<strong>Match Live TV</strong>. Non serve collegare un canale della società.
|
||||||
|
</p>
|
||||||
|
<% if yt.selectable? %>
|
||||||
|
<p class="muted">Canale pronto · seleziona «YouTube Live» nell’app mobile.</p>
|
||||||
|
<% else %>
|
||||||
|
<p class="muted">Canale Match Live TV in configurazione lato server. Contatta il supporto se YouTube non compare in app.</p>
|
||||||
|
<% end %>
|
||||||
|
<% elsif entitlements.premium_full? %>
|
||||||
|
<% cred = team.youtube_credential %>
|
||||||
|
<% if cred %>
|
||||||
|
<p>
|
||||||
|
Canale collegato:
|
||||||
|
<strong><%= cred.channel_title.presence || cred.channel_id || "YouTube" %></strong>
|
||||||
|
</p>
|
||||||
|
<%= button_to "Scollega canale", public_team_youtube_disconnect_path(team),
|
||||||
|
method: :delete,
|
||||||
|
class: "btn btn-secondary",
|
||||||
|
form: { data: { turbo_confirm: "Scollegare il canale YouTube? Le dirette future richiederanno un nuovo collegamento." } } %>
|
||||||
|
<% else %>
|
||||||
|
<p class="muted">
|
||||||
|
Collega il canale YouTube della società per trasmettere dall’app con la piattaforma «YouTube Live».
|
||||||
|
</p>
|
||||||
|
<% if ENV["YOUTUBE_CLIENT_ID"].present? %>
|
||||||
|
<%= link_to "Collega canale YouTube", public_team_youtube_connect_path(team), class: "btn btn-primary" %>
|
||||||
|
<% else %>
|
||||||
|
<p class="muted">OAuth YouTube non ancora configurato sul server.</p>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<p class="muted">YouTube sul canale della società richiede il piano Premium Full.</p>
|
||||||
|
<%= link_to "Vedi piani", public_prezzi_path, class: "btn btn-secondary" %>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
@@ -92,6 +92,8 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<%= render "public/teams/youtube", team: @team, entitlements: @entitlements %>
|
||||||
|
|
||||||
<%= render "public/teams/streaming_staff", team: @team, club: @club, can_manage: @can_manage,
|
<%= render "public/teams/streaming_staff", team: @team, club: @club, can_manage: @can_manage,
|
||||||
entitlements: @entitlements, owner_membership: @owner_membership,
|
entitlements: @entitlements, owner_membership: @owner_membership,
|
||||||
staff_memberships: @staff_memberships, pending_invitations: @pending_invitations,
|
staff_memberships: @staff_memberships, pending_invitations: @pending_invitations,
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ Rails.application.routes.draw do
|
|||||||
resources :sessions, only: %i[index show] do
|
resources :sessions, only: %i[index show] do
|
||||||
member { post :stop }
|
member { post :stop }
|
||||||
end
|
end
|
||||||
|
get "youtube/platform", to: "youtube#platform", as: :youtube_platform
|
||||||
end
|
end
|
||||||
|
|
||||||
get "hls/*path", to: "hls_proxy#show", format: false, constraints: { path: /.+/ }
|
get "hls/*path", to: "hls_proxy#show", format: false, constraints: { path: /.+/ }
|
||||||
@@ -138,6 +139,8 @@ Rails.application.routes.draw do
|
|||||||
delete "teams/:id/staff/self", to: "teams#clear_self_staff", as: :team_clear_self_staff
|
delete "teams/:id/staff/self", to: "teams#clear_self_staff", as: :team_clear_self_staff
|
||||||
delete "teams/:id/members/:user_id", to: "teams#remove_member", as: :team_remove_member
|
delete "teams/:id/members/:user_id", to: "teams#remove_member", as: :team_remove_member
|
||||||
delete "teams/:id/invitations/:invitation_id", to: "teams#destroy_invitation", as: :team_destroy_invitation
|
delete "teams/:id/invitations/:invitation_id", to: "teams#destroy_invitation", as: :team_destroy_invitation
|
||||||
|
get "teams/:id/youtube/connect", to: "teams#youtube_connect", as: :team_youtube_connect
|
||||||
|
delete "teams/:id/youtube/disconnect", to: "teams#youtube_disconnect", as: :team_youtube_disconnect
|
||||||
get "join/:token", to: "invitations#show", as: :invitation
|
get "join/:token", to: "invitations#show", as: :invitation
|
||||||
post "join/:token", to: "invitations#accept"
|
post "join/:token", to: "invitations#accept"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
class EnableYoutubeOnPremiumLightPlan < ActiveRecord::Migration[7.2]
|
||||||
|
def up
|
||||||
|
plan = Plan.find_by(slug: "premium_light")
|
||||||
|
return unless plan
|
||||||
|
|
||||||
|
features = plan.features.deep_dup
|
||||||
|
platforms = Array(features["platforms"]).map(&:to_s)
|
||||||
|
features["platforms"] = (platforms + ["youtube"]).uniq
|
||||||
|
plan.update!(features: features)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
plan = Plan.find_by(slug: "premium_light")
|
||||||
|
return unless plan
|
||||||
|
|
||||||
|
features = plan.features.deep_dup
|
||||||
|
features["platforms"] = Array(features["platforms"]).map(&:to_s) - ["youtube"]
|
||||||
|
plan.update!(features: features)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -27,7 +27,7 @@ seed_plan!("premium_light", "Premium Light", {
|
|||||||
"max_staff_transmission" => 5,
|
"max_staff_transmission" => 5,
|
||||||
"max_staff_regia" => 5,
|
"max_staff_regia" => 5,
|
||||||
"concurrent_streams_limit" => 3,
|
"concurrent_streams_limit" => 3,
|
||||||
"platforms" => %w[matchlivetv],
|
"platforms" => %w[matchlivetv youtube],
|
||||||
"recordings_enabled" => true,
|
"recordings_enabled" => true,
|
||||||
"recording_days" => 30,
|
"recording_days" => 30,
|
||||||
"phone_download_enabled" => true,
|
"phone_download_enabled" => true,
|
||||||
|
|||||||
89
docs/YOUTUBE_SETUP.md
Normal file
89
docs/YOUTUBE_SETUP.md
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# Integrazione YouTube Live
|
||||||
|
|
||||||
|
Match Live TV supporta due modalità YouTube, legate al piano società:
|
||||||
|
|
||||||
|
| Piano | Modalità | Dove va la diretta |
|
||||||
|
|-------|----------|-------------------|
|
||||||
|
| **Premium Light** | `matchlivetv_light` | Canale YouTube **Match Live TV** (gestito da te) |
|
||||||
|
| **Premium Full** | `team` | Canale YouTube **della società** (OAuth del club) |
|
||||||
|
|
||||||
|
Flusso tecnico: telefono → RTMP MediaMTX → (relay ffmpeg) → YouTube Live.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Google Cloud Console
|
||||||
|
|
||||||
|
1. [Google Cloud Console](https://console.cloud.google.com/) → nuovo progetto o progetto esistente.
|
||||||
|
2. **API e servizi → Libreria** → abilita **YouTube Data API v3**.
|
||||||
|
3. **API e servizi → Credenziali** → **Crea credenziali → ID client OAuth**:
|
||||||
|
- Tipo: **Applicazione web**
|
||||||
|
- URI di reindirizzamento autorizzati:
|
||||||
|
- `https://www.matchlivetv.it/api/v1/youtube/callback`
|
||||||
|
- `http://localhost:3000/api/v1/youtube/callback` (solo sviluppo)
|
||||||
|
4. Copia **Client ID** e **Client Secret** in `infra/.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
YOUTUBE_CLIENT_ID=....apps.googleusercontent.com
|
||||||
|
YOUTUBE_CLIENT_SECRET=GOCSPX-...
|
||||||
|
YOUTUBE_REDIRECT_URI=https://www.matchlivetv.it/api/v1/youtube/callback
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Schermata di consenso OAuth**: stato di pubblicazione o utenti di test con gli account Google che collegheranno i canali.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Canale Match Live TV (Premium Light)
|
||||||
|
|
||||||
|
Serve un **refresh token** del canale YouTube ufficiale Match Live TV.
|
||||||
|
|
||||||
|
1. Una tantum: apri (da browser loggato sul canale Match Live TV):
|
||||||
|
|
||||||
|
`https://www.matchlivetv.it/admin/youtube/platform`
|
||||||
|
|
||||||
|
(solo admin tecnico) oppure usa lo script OAuth di Google con gli stessi scope dell’app.
|
||||||
|
|
||||||
|
2. Salva in produzione (`/opt/matchlivetv/infra/.env`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
YOUTUBE_PLATFORM_REFRESH_TOKEN=1//0e...
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Riavvia Rails. Le società **Premium Light** potranno scegliere «YouTube» in app senza collegare un canale proprio.
|
||||||
|
|
||||||
|
Se manca, l’app userà solo Match Live TV (HLS) e mostrerà YouTube disabilitato per Light.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Canale società (Premium Full)
|
||||||
|
|
||||||
|
1. Titolare società → login su matchlivetv.it
|
||||||
|
2. **Società → squadra → Dettagli** → sezione **YouTube**
|
||||||
|
3. **Collega canale YouTube** → consenso Google → ritorno al sito
|
||||||
|
4. Da app mobile: wizard trasmissione → piattaforma **YouTube Live**
|
||||||
|
|
||||||
|
Revoca: pulsante **Scollega canale** nella stessa sezione.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Variabili opzionali (sviluppo)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Se OAuth non configurato: broadcast finto + relay disattivato
|
||||||
|
YOUTUBE_MOCK_STREAM_KEY=mock-stream-key
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Verifica
|
||||||
|
|
||||||
|
- Premium Full: dopo collegamento, `GET /api/v1/teams/:id` → `youtube_connected: true`, `youtube_selectable: true`
|
||||||
|
- Premium Light: con `YOUTUBE_PLATFORM_REFRESH_TOKEN` → `youtube_selectable: true` anche senza OAuth squadra
|
||||||
|
- Avvia diretta YouTube da app → in admin session compare broadcast ID → link YouTube Studio
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Produzione
|
||||||
|
|
||||||
|
- `YOUTUBE_REDIRECT_URI` deve essere **esattamente** quello registrato in Google (HTTPS, dominio pubblico).
|
||||||
|
- Il relay ffmpeg gira nel container MediaMTX (`runOnReady`).
|
||||||
|
- Apri RTMP **1935** sul router verso il server.
|
||||||
@@ -5,6 +5,9 @@ JWT_SECRET=matchlivetv_jwt_dev_secret_change_in_prod
|
|||||||
YOUTUBE_CLIENT_ID=
|
YOUTUBE_CLIENT_ID=
|
||||||
YOUTUBE_CLIENT_SECRET=
|
YOUTUBE_CLIENT_SECRET=
|
||||||
YOUTUBE_REDIRECT_URI=http://localhost:3000/api/v1/youtube/callback
|
YOUTUBE_REDIRECT_URI=http://localhost:3000/api/v1/youtube/callback
|
||||||
|
# Refresh token canale Match Live TV (Premium Light) — vedi docs/YOUTUBE_SETUP.md
|
||||||
|
YOUTUBE_PLATFORM_REFRESH_TOKEN=
|
||||||
|
YOUTUBE_MOCK_STREAM_KEY=mock-stream-key
|
||||||
|
|
||||||
PRIVACY_CONTROLLER_NAME=Emiliano Frascaro
|
PRIVACY_CONTROLLER_NAME=Emiliano Frascaro
|
||||||
PRIVACY_CONTROLLER_ADDRESS=Via Guido De Ruggiero, 89 - 20142 - Milano (MI)
|
PRIVACY_CONTROLLER_ADDRESS=Via Guido De Ruggiero, 89 - 20142 - Milano (MI)
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ CORS_ORIGINS=https://www.matchlivetv.it,https://matchlivetv.it
|
|||||||
ALLOWED_HOSTS=www.matchlivetv.it,matchlivetv.it,192.168.1.146,localhost
|
ALLOWED_HOSTS=www.matchlivetv.it,matchlivetv.it,192.168.1.146,localhost
|
||||||
YOUTUBE_REDIRECT_URI=https://www.matchlivetv.it/api/v1/youtube/callback
|
YOUTUBE_REDIRECT_URI=https://www.matchlivetv.it/api/v1/youtube/callback
|
||||||
|
|
||||||
# YouTube OAuth (opzionale)
|
# YouTube OAuth — vedi docs/YOUTUBE_SETUP.md
|
||||||
YOUTUBE_CLIENT_ID=
|
YOUTUBE_CLIENT_ID=
|
||||||
YOUTUBE_CLIENT_SECRET=
|
YOUTUBE_CLIENT_SECRET=
|
||||||
|
YOUTUBE_PLATFORM_REFRESH_TOKEN=
|
||||||
|
|
||||||
# Stripe LIVE (Dashboard in modalità Live — non riusare price_ di Test)
|
# Stripe LIVE (Dashboard in modalità Live — non riusare price_ di Test)
|
||||||
# Match Live TV → Premium Light | Match Live TV PRO → Premium Full
|
# Match Live TV → Premium Light | Match Live TV PRO → Premium Full
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ services:
|
|||||||
YOUTUBE_CLIENT_ID: ${YOUTUBE_CLIENT_ID:-}
|
YOUTUBE_CLIENT_ID: ${YOUTUBE_CLIENT_ID:-}
|
||||||
YOUTUBE_CLIENT_SECRET: ${YOUTUBE_CLIENT_SECRET:-}
|
YOUTUBE_CLIENT_SECRET: ${YOUTUBE_CLIENT_SECRET:-}
|
||||||
YOUTUBE_REDIRECT_URI: ${YOUTUBE_REDIRECT_URI:-}
|
YOUTUBE_REDIRECT_URI: ${YOUTUBE_REDIRECT_URI:-}
|
||||||
|
YOUTUBE_PLATFORM_REFRESH_TOKEN: ${YOUTUBE_PLATFORM_REFRESH_TOKEN:-}
|
||||||
HLS_PUBLIC_URL: ${HLS_PUBLIC_URL:-http://localhost:8888}
|
HLS_PUBLIC_URL: ${HLS_PUBLIC_URL:-http://localhost:8888}
|
||||||
APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:3000}
|
APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:3000}
|
||||||
PRIVACY_CONTACT_EMAIL: ${PRIVACY_CONTACT_EMAIL:-privacy@matchlive.it}
|
PRIVACY_CONTACT_EMAIL: ${PRIVACY_CONTACT_EMAIL:-privacy@matchlive.it}
|
||||||
@@ -128,6 +129,8 @@ services:
|
|||||||
JWT_SECRET: ${JWT_SECRET}
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
YOUTUBE_CLIENT_ID: ${YOUTUBE_CLIENT_ID:-}
|
YOUTUBE_CLIENT_ID: ${YOUTUBE_CLIENT_ID:-}
|
||||||
YOUTUBE_CLIENT_SECRET: ${YOUTUBE_CLIENT_SECRET:-}
|
YOUTUBE_CLIENT_SECRET: ${YOUTUBE_CLIENT_SECRET:-}
|
||||||
|
YOUTUBE_REDIRECT_URI: ${YOUTUBE_REDIRECT_URI:-}
|
||||||
|
YOUTUBE_PLATFORM_REFRESH_TOKEN: ${YOUTUBE_PLATFORM_REFRESH_TOKEN:-}
|
||||||
HLS_PUBLIC_URL: ${HLS_PUBLIC_URL:-http://localhost:8888}
|
HLS_PUBLIC_URL: ${HLS_PUBLIC_URL:-http://localhost:8888}
|
||||||
APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:3000}
|
APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:3000}
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ services:
|
|||||||
YOUTUBE_CLIENT_ID: ${YOUTUBE_CLIENT_ID:-}
|
YOUTUBE_CLIENT_ID: ${YOUTUBE_CLIENT_ID:-}
|
||||||
YOUTUBE_CLIENT_SECRET: ${YOUTUBE_CLIENT_SECRET:-}
|
YOUTUBE_CLIENT_SECRET: ${YOUTUBE_CLIENT_SECRET:-}
|
||||||
YOUTUBE_REDIRECT_URI: ${YOUTUBE_REDIRECT_URI:-http://localhost:3000/api/v1/youtube/callback}
|
YOUTUBE_REDIRECT_URI: ${YOUTUBE_REDIRECT_URI:-http://localhost:3000/api/v1/youtube/callback}
|
||||||
|
YOUTUBE_PLATFORM_REFRESH_TOKEN: ${YOUTUBE_PLATFORM_REFRESH_TOKEN:-}
|
||||||
RAILS_LOG_TO_STDOUT: "true"
|
RAILS_LOG_TO_STDOUT: "true"
|
||||||
PRIVACY_CONTROLLER_NAME: ${PRIVACY_CONTROLLER_NAME:-Emiliano Frascaro}
|
PRIVACY_CONTROLLER_NAME: ${PRIVACY_CONTROLLER_NAME:-Emiliano Frascaro}
|
||||||
PRIVACY_CONTROLLER_ADDRESS: ${PRIVACY_CONTROLLER_ADDRESS:-Via Guido De Ruggiero, 89 - 20142 - Milano (MI)}
|
PRIVACY_CONTROLLER_ADDRESS: ${PRIVACY_CONTROLLER_ADDRESS:-Via Guido De Ruggiero, 89 - 20142 - Milano (MI)}
|
||||||
|
|||||||
@@ -76,25 +76,52 @@ class _StepTransmissionState extends ConsumerState<StepTransmission> {
|
|||||||
|
|
||||||
void _onYoutubeTap(Team? team) {
|
void _onYoutubeTap(Team? team) {
|
||||||
if (team == null) return;
|
if (team == null) return;
|
||||||
if (team.canUseYoutube && team.youtubeConnected) {
|
if (team.isYoutubeReady) {
|
||||||
setState(() => _platform = 'youtube');
|
setState(() => _platform = 'youtube');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (team.canUseYoutube && !team.youtubeConnected) {
|
if (team.canUseYoutube && team.youtubeMode == 'team') {
|
||||||
showPremiumRequiredDialog(
|
showPremiumRequiredDialog(
|
||||||
context,
|
context,
|
||||||
message: 'Collega il canale YouTube dalla dashboard sul sito, poi seleziona YouTube qui.',
|
message: 'Collega il canale YouTube dalla pagina Dettagli squadra sul sito, poi seleziona YouTube qui.',
|
||||||
billingUrl: team.billingUrl,
|
billingUrl: team.staffManageUrl ?? team.billingUrl,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (team.canUseYoutube && team.youtubeMode == 'matchlivetv_light') {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'YouTube sul canale Match Live TV non è ancora attivo. Riprova più tardi o usa Match Live TV.',
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showPremiumRequiredDialog(
|
showPremiumRequiredDialog(
|
||||||
context,
|
context,
|
||||||
message: 'YouTube Live è incluso nel piano Premium Full.',
|
message: 'YouTube Live richiede Premium Light o Full.',
|
||||||
billingUrl: team.billingUrl,
|
billingUrl: team.billingUrl,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _youtubeSubtitle(Team? team) {
|
||||||
|
if (team == null || !team.canUseYoutube) {
|
||||||
|
return 'Premium Light o Full';
|
||||||
|
}
|
||||||
|
if (team.isYoutubeReady) {
|
||||||
|
final label = team.youtubeChannelTitle;
|
||||||
|
if (team.youtubeUsesPlatformChannel) {
|
||||||
|
return label != null ? 'Canale $label' : 'Canale Match Live TV';
|
||||||
|
}
|
||||||
|
return label != null ? 'Canale $label' : 'Canale collegato';
|
||||||
|
}
|
||||||
|
if (team.youtubeMode == 'matchlivetv_light') {
|
||||||
|
return 'Canale Match Live TV (in attivazione)';
|
||||||
|
}
|
||||||
|
return 'Collega canale sul sito';
|
||||||
|
}
|
||||||
|
|
||||||
void _onExternalPremiumTap(String platform, Team? team) {
|
void _onExternalPremiumTap(String platform, Team? team) {
|
||||||
showPremiumRequiredDialog(
|
showPremiumRequiredDialog(
|
||||||
context,
|
context,
|
||||||
@@ -112,7 +139,7 @@ class _StepTransmissionState extends ConsumerState<StepTransmission> {
|
|||||||
loading: () => const Center(child: CircularProgressIndicator(color: AppTheme.primaryRed)),
|
loading: () => const Center(child: CircularProgressIndicator(color: AppTheme.primaryRed)),
|
||||||
error: (_, __) => const Center(child: Text('Errore caricamento squadra')),
|
error: (_, __) => const Center(child: Text('Errore caricamento squadra')),
|
||||||
data: (team) {
|
data: (team) {
|
||||||
final youtubeSelectable = team != null && team.canUseYoutube && team.youtubeConnected;
|
final youtubeSelectable = team != null && team.isYoutubeReady;
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
padding: ScreenInsets.contentPadding(context, horizontal: 20, vertical: 12),
|
padding: ScreenInsets.contentPadding(context, horizontal: 20, vertical: 12),
|
||||||
@@ -147,14 +174,10 @@ class _StepTransmissionState extends ConsumerState<StepTransmission> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_PlatformCard(
|
_PlatformCard(
|
||||||
title: 'YouTube Live',
|
title: 'YouTube Live',
|
||||||
subtitle: team?.canUseYoutube == true
|
subtitle: _youtubeSubtitle(team),
|
||||||
? (team!.youtubeConnected
|
|
||||||
? 'Canale collegato'
|
|
||||||
: 'Collega canale sul sito')
|
|
||||||
: 'Premium Full — collegamento canale',
|
|
||||||
selected: _platform == 'youtube',
|
selected: _platform == 'youtube',
|
||||||
enabled: youtubeSelectable,
|
enabled: youtubeSelectable,
|
||||||
badge: team?.canUseYoutube == true ? null : 'Full',
|
badge: team?.canUseYoutube == true ? null : 'Premium',
|
||||||
onTap: () => _onYoutubeTap(team),
|
onTap: () => _onYoutubeTap(team),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ class Team {
|
|||||||
required this.sport,
|
required this.sport,
|
||||||
this.logoUrl,
|
this.logoUrl,
|
||||||
this.youtubeConnected = false,
|
this.youtubeConnected = false,
|
||||||
|
this.youtubeSelectable = false,
|
||||||
|
this.youtubeChannelTitle,
|
||||||
|
this.youtubeUsesPlatformChannel = false,
|
||||||
this.planSlug = 'free',
|
this.planSlug = 'free',
|
||||||
this.planName = 'Free',
|
this.planName = 'Free',
|
||||||
this.premiumActive = false,
|
this.premiumActive = false,
|
||||||
@@ -36,6 +39,9 @@ class Team {
|
|||||||
final bool canStream;
|
final bool canStream;
|
||||||
final String? logoUrl;
|
final String? logoUrl;
|
||||||
final bool youtubeConnected;
|
final bool youtubeConnected;
|
||||||
|
final bool youtubeSelectable;
|
||||||
|
final String? youtubeChannelTitle;
|
||||||
|
final bool youtubeUsesPlatformChannel;
|
||||||
final String planSlug;
|
final String planSlug;
|
||||||
final String planName;
|
final String planName;
|
||||||
final bool premiumActive;
|
final bool premiumActive;
|
||||||
@@ -55,7 +61,9 @@ class Team {
|
|||||||
final bool youtubeEnabled;
|
final bool youtubeEnabled;
|
||||||
final String? youtubeMode;
|
final String? youtubeMode;
|
||||||
|
|
||||||
bool get canUseYoutube => premiumFull && youtubeEnabled;
|
bool get canUseYoutube => youtubeEnabled && (premiumFull || youtubeMode == 'matchlivetv_light');
|
||||||
|
|
||||||
|
bool get isYoutubeReady => youtubeSelectable;
|
||||||
bool get canUseRecordings => recordingsEnabled;
|
bool get canUseRecordings => recordingsEnabled;
|
||||||
bool get canDownloadOnPhone => phoneDownloadEnabled;
|
bool get canDownloadOnPhone => phoneDownloadEnabled;
|
||||||
|
|
||||||
@@ -76,6 +84,9 @@ class Team {
|
|||||||
canStream: json['can_stream'] as bool? ?? true,
|
canStream: json['can_stream'] as bool? ?? true,
|
||||||
logoUrl: json['logo_url'] as String?,
|
logoUrl: json['logo_url'] as String?,
|
||||||
youtubeConnected: json['youtube_connected'] as bool? ?? false,
|
youtubeConnected: json['youtube_connected'] as bool? ?? false,
|
||||||
|
youtubeSelectable: json['youtube_selectable'] as bool? ?? false,
|
||||||
|
youtubeChannelTitle: json['youtube_channel_title'] as String?,
|
||||||
|
youtubeUsesPlatformChannel: json['youtube_uses_platform_channel'] as bool? ?? false,
|
||||||
planSlug: json['plan_slug'] as String? ?? 'free',
|
planSlug: json['plan_slug'] as String? ?? 'free',
|
||||||
planName: json['plan_name'] as String? ?? 'Free',
|
planName: json['plan_name'] as String? ?? 'Free',
|
||||||
premiumActive: json['premium_active'] as bool? ?? false,
|
premiumActive: json['premium_active'] as bool? ?? false,
|
||||||
|
|||||||
71
scripts/deploy_youtube_production.sh
Executable file
71
scripts/deploy_youtube_production.sh
Executable file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deploy integrazione YouTube su produzione (sync + env + migrate + rebuild).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
ENV_FILE="${ENV_FILE:-$ROOT/infra/.env}"
|
||||||
|
SERVER="${DEPLOY_SERVER:-eminux@192.168.1.146}"
|
||||||
|
REMOTE_DIR="${DEPLOY_PATH:-/opt/matchlivetv}"
|
||||||
|
YOUTUBE_REDIRECT_URI="${YOUTUBE_REDIRECT_URI:-https://www.matchlivetv.it/api/v1/youtube/callback}"
|
||||||
|
|
||||||
|
if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
|
echo "File non trovato: $ENV_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
read_env() {
|
||||||
|
local key="$1"
|
||||||
|
grep -E "^${key}=" "$ENV_FILE" | head -1 | cut -d= -f2- | tr -d '\r'
|
||||||
|
}
|
||||||
|
|
||||||
|
YOUTUBE_CLIENT_ID="$(read_env YOUTUBE_CLIENT_ID)"
|
||||||
|
YOUTUBE_CLIENT_SECRET="$(read_env YOUTUBE_CLIENT_SECRET)"
|
||||||
|
YOUTUBE_PLATFORM_REFRESH_TOKEN="$(read_env YOUTUBE_PLATFORM_REFRESH_TOKEN)"
|
||||||
|
|
||||||
|
if [[ -z "$YOUTUBE_CLIENT_ID" || -z "$YOUTUBE_CLIENT_SECRET" ]]; then
|
||||||
|
echo "Manca YOUTUBE_CLIENT_ID o YOUTUBE_CLIENT_SECRET in $ENV_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Sync codice → $SERVER:$REMOTE_DIR ..."
|
||||||
|
bash "$ROOT/scripts/deploy/sync_to_server.sh"
|
||||||
|
|
||||||
|
echo "Aggiorno variabili YouTube in $REMOTE_DIR/infra/.env ..."
|
||||||
|
ssh "$SERVER" "bash -s" <<REMOTE
|
||||||
|
set -euo pipefail
|
||||||
|
ENV="$REMOTE_DIR/infra/.env"
|
||||||
|
touch "\$ENV"
|
||||||
|
upsert() {
|
||||||
|
local key="\$1" val="\$2"
|
||||||
|
if grep -q "^\${key}=" "\$ENV" 2>/dev/null; then
|
||||||
|
sed -i "s|^\${key}=.*|\${key}=\${val}|" "\$ENV"
|
||||||
|
else
|
||||||
|
echo "\${key}=\${val}" >> "\$ENV"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
upsert YOUTUBE_CLIENT_ID '${YOUTUBE_CLIENT_ID}'
|
||||||
|
upsert YOUTUBE_CLIENT_SECRET '${YOUTUBE_CLIENT_SECRET}'
|
||||||
|
upsert YOUTUBE_REDIRECT_URI '${YOUTUBE_REDIRECT_URI}'
|
||||||
|
REMOTE
|
||||||
|
|
||||||
|
if [[ -n "$YOUTUBE_PLATFORM_REFRESH_TOKEN" ]]; then
|
||||||
|
ssh "$SERVER" "grep -q '^YOUTUBE_PLATFORM_REFRESH_TOKEN=' $REMOTE_DIR/infra/.env && sed -i 's|^YOUTUBE_PLATFORM_REFRESH_TOKEN=.*|YOUTUBE_PLATFORM_REFRESH_TOKEN=${YOUTUBE_PLATFORM_REFRESH_TOKEN}|' $REMOTE_DIR/infra/.env || echo 'YOUTUBE_PLATFORM_REFRESH_TOKEN=${YOUTUBE_PLATFORM_REFRESH_TOKEN}' >> $REMOTE_DIR/infra/.env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Rebuild stack + migrate..."
|
||||||
|
ssh "$SERVER" "cd $REMOTE_DIR/infra && docker compose -f docker-compose.prod.yml --env-file .env up -d --build rails sidekiq"
|
||||||
|
sleep 25
|
||||||
|
ssh "$SERVER" "cd $REMOTE_DIR/infra && docker compose -f docker-compose.prod.yml exec -T rails bundle exec rails db:migrate"
|
||||||
|
ssh "$SERVER" "cd $REMOTE_DIR/infra && docker compose -f docker-compose.prod.yml exec -T rails bundle exec rails db:seed" || true
|
||||||
|
|
||||||
|
echo "Verifica route admin YouTube..."
|
||||||
|
ssh "$SERVER" "cd $REMOTE_DIR/infra && docker compose -f docker-compose.prod.yml exec -T rails bundle exec rails routes -g youtube_platform | head -5"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "OK. Prossimi passi manuali:"
|
||||||
|
echo " 1. Admin: https://www.matchlivetv.it/admin/login"
|
||||||
|
echo " 2. Token canale Light: https://www.matchlivetv.it/admin/youtube/platform"
|
||||||
|
if [[ -z "$YOUTUBE_PLATFORM_REFRESH_TOKEN" ]]; then
|
||||||
|
echo " (poi aggiungi YOUTUBE_PLATFORM_REFRESH_TOKEN al .env server e riavvia rails)"
|
||||||
|
fi
|
||||||
|
echo " 3. Full: Dettagli squadra → Collega canale YouTube"
|
||||||
Reference in New Issue
Block a user