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.listcommine=true→ lista inscrições do usuário autenticado. (Google for Developers)subscriptions.deletecomid=<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ços → Biblioteca → procure YouTube Data API v3 → Ativar
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:
- Menu ☰ → APIs e serviços → Tela de consentimento OAuth
- Tipo: geralmente Externo
- Preencha o mínimo (nome do app, e-mail de suporte)
- 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ços → Credenciais → Criar credenciais → ID 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.v3Install-Package Google.Apis.AuthInstall-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
- Build Action:
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
mine=trueemsubscriptions.listretorna as inscrições do usuário autenticado. (Google for Developers)subscriptions.deleterecebe oid(subscriptionId) do recurso. (Google for Developers)
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:
- Remover apenas selecionados no DataGrid
- Filtro por texto (ex.: remover só canais com “news” no título)
- Dry-run real: simular e só mostrar o que seria removido
- Log detalhado (sucesso/falha por item com export)
- 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)
