WPF + YouTube Data API v3: como listar e cancelar todas as suas inscrições com OAuth (Visual Studio Community)

Este artigo é um guia end-to-end, no estilo “roteiro de laboratório”, para você criar um app WPF (.NET) que:

  • autentica com OAuth 2.0 (conta do YouTube),
  • lista todas as suas inscrições em um DataGrid,
  • faz backup em CSV,
  • e (só com confirmação explícita) remove as inscrições via API.

Além do passo a passo prático, eu também explico o ecossistema de APIs do Google / YouTube, como quota funciona (e por que isso normalmente não tem custo em dinheiro), e os pontos de atenção que mais travam esse tipo de projeto.


1) Visão geral: que APIs estamos usando e por quê

Google APIs (visão macro)

O Google expõe serviços via APIs (YouTube, Drive, Gmail, etc.). A maioria segue o mesmo padrão:

  • Você cria um projeto no Google Cloud.
  • Ativa a API desejada.
  • Configura OAuth 2.0 (tela de consentimento + credenciais).
  • Sua aplicação obtém um access token e chama endpoints. (Google for Developers)

YouTube Data API v3 (o que resolve aqui)

Para “inscrições” você vai usar dois métodos:

  • subscriptions.list com mine=true → lista inscrições do usuário autenticado. (Google for Developers)
  • subscriptions.delete com id=<subscriptionId> → remove uma inscrição pelo ID da inscrição (não é o channelId). (Google for Developers)

Detalhe crítico: o parâmetro do delete é o subscriptionId (o id do recurso “subscription”). (Google for Developers)

Outras APIs do YouTube (contexto)

Além da Data API (metadados, playlists, canais, inscrições), existem APIs voltadas para:

  • YouTube Analytics API (métricas/relatórios de canal),
  • YouTube Reporting API (relatórios em lote),
  • YouTube Live Streaming API (eventos e lives).

Neste roteiro, o foco é a YouTube Data API v3.


2) Custo: isso é pago?

Na prática, para esse caso, você esbarra em quota, não em cobrança financeira.

A YouTube Data API usa um sistema de quotas (unidades) para manter a qualidade do serviço. Projetos geralmente têm uma alocação padrão (comumente citada como 10.000 unidades/dia). (Google for Developers)
Você consegue estimar consumo pelo Quota Calculator, que lista o “custo” por método. (Google for Developers)

O resultado: listar e apagar inscrições normalmente não chega perto do limite em uso pessoal.


3) Pré-requisitos

  • Visual Studio Community (com workload de Desktop .NET)
  • WPF App (.NET) (recomendo .NET 8)
  • Uma conta Google/YouTube para autenticar
  • Acesso ao Google Cloud Console para criar projeto e credenciais

4) Google Cloud Console: projeto, API e OAuth

4.1) Crie/seleciona um projeto

No topo do Console, selecione ou crie um projeto (ex.: Projeto01).

4.2) Ative a YouTube Data API v3

Menu ☰ → APIs e serviçosBiblioteca → procure YouTube Data API v3Ativar

Se a API não estiver ativada, você autentica, mas as chamadas falham.

4.3) Configure a tela de consentimento (OAuth consent screen)

Essa etapa “autoriza seu usuário” quando o app está em modo de teste.

O Google explica que configurar a tela de consentimento define o que o usuário vê e registra seu app para uso/validação. (Google for Developers)

Fluxo típico:

  1. Menu ☰ → APIs e serviçosTela de consentimento OAuth
  2. Tipo: geralmente Externo
  3. Preencha o mínimo (nome do app, e-mail de suporte)
  4. Em Usuários de teste, adicione seu e-mail do Google (o mesmo que vai logar).
    Isso evita o erro “usuário não autorizado” quando seu app está em “Testing”.

O próprio material oficial do Google reforça que você pode adicionar a si mesmo como test user para continuar o fluxo. (Google AI for Developers)

4.4) Crie as credenciais OAuth (Desktop app) e baixe o JSON

Menu ☰ → APIs e serviçosCredenciaisCriar credenciaisID do cliente OAuth → Tipo: Aplicativo para computador

Depois:

  • Baixe o JSON e renomeie para client_secret.json

5) Visual Studio: criar o app WPF

5.1) Criar o projeto

File → New → Project → WPF App (.NET)
Nome sugerido: YouTubeUnsubscriberWpf

5.2) NuGet packages

Instale:

Install-Package Google.Apis.YouTube.v3
Install-Package Google.Apis.Auth
Install-Package CsvHelper

O Google mantém exemplos em .NET para a YouTube Data API e o padrão geral de autenticação/serviço segue a mesma linha desses samples. (Google for Developers)

5.3) Adicionar client_secret.json ao projeto

  • Add → Existing Item… → client_secret.json
  • Propriedades do arquivo:
    • Build Action: Content
    • Copy to Output Directory: Copy if newer

Assim, ele vai parar em bin\Debug\netX\client_secret.json, e o app consegue ler pelo AppDomain.CurrentDomain.BaseDirectory.


6) Código: arquitetura simples e segura

Objetivo de segurança

Cancelar tudo é irreversível na prática. Então o app:

  • sempre gera backup CSV,
  • exige checkbox I understand,
  • exige confirmação final.

6.1) Model

Models/SubscriptionItem.cs

namespace YouTubeUnsubscriberWpf.Models;
public sealed class SubscriptionItem
{
public string SubscriptionId { get; set; } = "";
public string ChannelId { get; set; } = "";
public string Title { get; set; } = "";
}

6.2) Cliente da YouTube API (OAuth + list/delete)

Services/YouTubeApiClient.cs

using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;
using Google.Apis.YouTube.v3;
using Google.Apis.YouTube.v3.Data;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using YouTubeUnsubscriberWpf.Models;
namespace YouTubeUnsubscriberWpf.Services;
public sealed class YouTubeApiClient
{
private const string AppName = "YouTubeUnsubscriberWpf";
private static readonly string[] Scopes = { YouTubeService.Scope.Youtube }; // manage account
private YouTubeService _service;
public async Task EnsureAuthenticatedAsync(string clientSecretPath, CancellationToken ct)
{
if (_service != null) return;
using var stream = new FileStream(clientSecretPath, FileMode.Open, FileAccess.Read);
var credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
GoogleClientSecrets.FromStream(stream).Secrets,
Scopes,
"user",
ct
);
_service = new YouTubeService(new BaseClientService.Initializer
{
HttpClientInitializer = credential,
ApplicationName = AppName
});
}
public async Task<List<SubscriptionItem>> GetMySubscriptionsAsync(CancellationToken ct)
{
if (_service == null) throw new InvalidOperationException("Not authenticated.");
var items = new List<SubscriptionItem>();
string pageToken = null;
do
{
var req = _service.Subscriptions.List("snippet");
req.Mine = true; // mine=true
req.MaxResults = 50;
req.PageToken = pageToken;
SubscriptionListResponse resp = await req.ExecuteAsync(ct);
foreach (var it in resp.Items)
{
items.Add(new SubscriptionItem
{
SubscriptionId = it.Id, // <- isso é o que o delete precisa
ChannelId = it.Snippet?.ResourceId?.ChannelId ?? "",
Title = it.Snippet?.Title ?? ""
});
}
pageToken = resp.NextPageToken;
}
while (!string.IsNullOrEmpty(pageToken));
return items;
}
public async Task DeleteSubscriptionAsync(string subscriptionId, CancellationToken ct)
{
if (_service == null) throw new InvalidOperationException("Not authenticated.");
await _service.Subscriptions.Delete(subscriptionId).ExecuteAsync(ct);
}
}

Por que isso funciona


7) Interface WPF (DataGrid + botões + proteção)

7.1) MainWindow.xaml

<Window x:Class="YouTubeUnsubscriberWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="YouTube Unsubscriber" Height="720" Width="1050">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row="0">
<Button x:Name="BtnAuth" Content="1) Authenticate" Width="150" Margin="0,0,8,0" Click="BtnAuth_Click"/>
<Button x:Name="BtnLoad" Content="2) Load Subscriptions" Width="180" Margin="0,0,8,0" Click="BtnLoad_Click" IsEnabled="False"/>
<Button x:Name="BtnExport" Content="Export CSV" Width="120" Margin="0,0,8,0" Click="BtnExport_Click" IsEnabled="False"/>
<Button x:Name="BtnUnsub" Content="Unsubscribe ALL" Width="160" Margin="0,0,8,0" Click="BtnUnsub_Click" IsEnabled="False"/>
<CheckBox x:Name="ChkConfirm" Content="I understand (enable delete)" Margin="12,0,0,0" VerticalAlignment="Center"/>
</StackPanel>
<TextBlock x:Name="TxtStatus" Grid.Row="1" Margin="0,10,0,10" Text="Ready." />
<DataGrid x:Name="GridSubs" Grid.Row="2" AutoGenerateColumns="False" IsReadOnly="True" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Title" Binding="{Binding Title}" Width="*"/>
<DataGridTextColumn Header="ChannelId" Binding="{Binding ChannelId}" Width="320"/>
<DataGridTextColumn Header="SubscriptionId" Binding="{Binding SubscriptionId}" Width="320"/>
</DataGrid.Columns>
</DataGrid>
<ProgressBar x:Name="Progress" Grid.Row="3" Height="18" Margin="0,12,0,0" Minimum="0" Maximum="100" Value="0"/>
</Grid>
</Window>

7.2) MainWindow.xaml.cs

using CsvHelper;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Windows;
using YouTubeUnsubscriberWpf.Models;
using YouTubeUnsubscriberWpf.Services;
namespace YouTubeUnsubscriberWpf;
public partial class MainWindow : Window
{
private readonly YouTubeApiClient _yt = new();
private List<SubscriptionItem> _subs = new();
private readonly CancellationTokenSource _cts = new();
public MainWindow()
{
InitializeComponent();
Closing += (_, __) => _cts.Cancel();
}
private string ClientSecretPath =>
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "client_secret.json");
private async void BtnAuth_Click(object sender, RoutedEventArgs e)
{
try
{
SetUiBusy(true, "Authenticating...");
await _yt.EnsureAuthenticatedAsync(ClientSecretPath, _cts.Token);
TxtStatus.Text = "Authenticated. Now load subscriptions.";
BtnLoad.IsEnabled = true;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Auth error", MessageBoxButton.OK, MessageBoxImage.Error);
TxtStatus.Text = "Auth failed.";
}
finally
{
SetUiBusy(false);
}
}
private async void BtnLoad_Click(object sender, RoutedEventArgs e)
{
try
{
SetUiBusy(true, "Loading subscriptions...");
_subs = await _yt.GetMySubscriptionsAsync(_cts.Token);
GridSubs.ItemsSource = _subs;
TxtStatus.Text = $"Loaded: {_subs.Count} subscriptions.";
BtnExport.IsEnabled = _subs.Count > 0;
BtnUnsub.IsEnabled = _subs.Count > 0;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Load error", MessageBoxButton.OK, MessageBoxImage.Error);
TxtStatus.Text = "Load failed.";
}
finally
{
SetUiBusy(false);
}
}
private void BtnExport_Click(object sender, RoutedEventArgs e)
{
if (_subs.Count == 0)
{
MessageBox.Show("No subscriptions loaded.", "Info");
return;
}
var file = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "subscriptions_backup.csv");
using var writer = new StreamWriter(file);
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
csv.WriteRecords(_subs);
TxtStatus.Text = $"CSV exported: {file}";
}
private async void BtnUnsub_Click(object sender, RoutedEventArgs e)
{
if (_subs.Count == 0)
{
MessageBox.Show("No subscriptions loaded.", "Info");
return;
}
if (ChkConfirm.IsChecked != true)
{
MessageBox.Show("Check 'I understand' to enable deletion.", "Safety");
return;
}
// Always export before delete
BtnExport_Click(sender, e);
var confirm = MessageBox.Show(
$"This will unsubscribe from {_subs.Count} channels.\n\nProceed?",
"Confirm",
MessageBoxButton.YesNo,
MessageBoxImage.Warning);
if (confirm != MessageBoxResult.Yes) return;
try
{
SetUiBusy(true, "Unsubscribing...");
Progress.Value = 0;
int total = _subs.Count;
int ok = 0, fail = 0;
var list = _subs.ToList();
for (int i = 0; i < total; i++)
{
var s = list[i];
TxtStatus.Text = $"[{i + 1}/{total}] Unsubscribing: {s.Title}";
try { await _yt.DeleteSubscriptionAsync(s.SubscriptionId, _cts.Token); ok++; }
catch { fail++; }
Progress.Value = (i + 1) * 100.0 / total;
await System.Threading.Tasks.Task.Delay(150, _cts.Token);
}
TxtStatus.Text = $"Done. Success: {ok}, Failed: {fail}. Reload to confirm.";
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Delete error", MessageBoxButton.OK, MessageBoxImage.Error);
TxtStatus.Text = "Unsubscribe failed.";
}
finally
{
SetUiBusy(false);
}
}
private void SetUiBusy(bool busy, string status = null)
{
BtnAuth.IsEnabled = !busy;
if (!string.IsNullOrEmpty(status)) TxtStatus.Text = status;
}
}

8) O bug clássico: “meu usuário não está autorizado”

Quando seu app está em Testing, você precisa adicionar seu e-mail em Test users (Usuários de teste) na tela de consentimento. O Google descreve o fluxo de consentimento e a importância de configurar corretamente (incluindo scopes e público). (Google for Developers)

Se você ver mensagens como “usuário não autorizado”, volte para:

  • APIs e serviços → Tela de consentimento OAuth → Usuários de teste

9) Quota na prática (para não se surpreender)

  • O YouTube Data API aplica quota por método e request (até request inválido consome algo). (Google for Developers)
  • Existe uma alocação padrão diária (o doc destaca 10.000 unidades/dia como default típico). (Google for Developers)

Para esse app, o consumo é pequeno, porque você está só listando e deletando inscrições, não fazendo operações “caras” como upload de vídeo.


10) Evoluções naturais do app (próximos upgrades)

Depois que o baseline estiver ok, os upgrades mais úteis são:

  1. Remover apenas selecionados no DataGrid
  2. Filtro por texto (ex.: remover só canais com “news” no título)
  3. Dry-run real: simular e só mostrar o que seria removido
  4. Log detalhado (sucesso/falha por item com export)
  5. Paginação / retry (se pegar rate limit ou instabilidade)

11) Resumo do roteiro (checklist final)

  • Projeto Google Cloud criado
  • YouTube Data API v3 ativada
  • Tela de consentimento configurada + seu e-mail em Test users (Google for Developers)
  • OAuth Client Desktop app criado
  • JSON baixado e renomeado para client_secret.json
  • WPF com NuGets instalados
  • App autentica, lista inscrições (mine=true) (Google for Developers)
  • App remove inscrições via subscriptions.delete(id=subscriptionId) (Google for Developers)
Edvaldo Guimrães Filho Avatar

Published by