I’m not a big fan of Youtube’s web application. I’d like to categorise my subscriptions, but YouTube doesn’t allow this. I have multiple Google accounts, and I use them to group certain videos. When I’m in the mode of watching software development videos, I switch to one account. If I’m in the LEGO mood, I switch to another. The problem is I’m not watching YouTube on Kodi on Raspberry Pi (about which I wrote a tutorial), and managing multiple accounts is harder, so I decided to combine all my subscriptions in one account. This article shows I managed to do it.

Authentication

First, Google needs to know you have permission to fetch the subscriptions from your YouTube account. To achieve this, go to Google Cloud Console.

Create a new project:

New project screen

Then, click Library on the left menu and search for YouTube. Select YouTube Data API v3 and click Enable.

enable youtube API screen

Click the OAuth consent screen link. Select External user type and click the Create button.

OAuth consent screen select user type screen

Give your app a name and select your account’s email as “User support email”. The app name appears on your confirmation screen so I’d recommend giving it a meaningful name such as YouTube-Migration-Source (there will be a destination too).

Enter your email again as “Developer contact information”.

Click Save and Continue.

In the Scopes screen, click Add or Remove Scopes and select youtube:

Add or remove scope screen showing youtube selected

Click Save and Continue.

Adding Test users is not mandatory but is helpful in the next steps so I’d recommend adding your email address as a test user. This way you can still use the application without having to publish it.

Click Save and Continue after you’ve added your email address as a test user.

Then, click the Credentials link on the menu.

Click Create Credentials and select OAuth client ID.

Select Desktop app as your application type, give it a name and click the Create button.

create OAuth client application type selection screen

Click Download JSON in the confirmation dialog box:

OAuth client created confirmation dialog with Download JSON button

Now, the good news is that you have the credentials to access your source YouTube account. The bad news is you have to repeat the same steps for the destination account. In the end, rename your credential files to client_secrets_source.json and client_secrets_destination.json and move on to the next section to implement the application.

Implement the Application

Now that the boring part is over let’s write some code and have fun.

Create a new dotnet console project by running

mkdir YouTubeMigrationClient
cd YouTubeMigrationClient
dotnet new console

Then add Google YouTube SDK to the project:

dotnet add package Google.Apis.YouTube.v3

Copy client_secrets_source.json and client_secrets_destination.json files under this project’s folder.

Add a new C# file named YouTubeServiceFactory.cs and replace its contents with the code below:

using System.Reflection;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Util.Store;
using Google.Apis.YouTube.v3;

namespace YouTubeMigrationClient;

public class YouTubeServiceFactory
{
    public static async Task<UserCredential> CreateCredential(string credentialFileName)
    {
        UserCredential credential;
        using (var stream = new FileStream(credentialFileName, FileMode.Open, FileAccess.Read))
        {
            credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
                (await GoogleClientSecrets.FromStreamAsync(stream)).Secrets,
                new[] { YouTubeService.Scope.Youtube },
                "user",
                CancellationToken.None,
                new FileDataStore(Assembly.GetExecutingAssembly().GetType().ToString())
            );
        }

        return credential;
    }
}

To test the credentials, edit the Program.cs and replace the code with this:

using System.Reflection;
using Google.Apis.Services;
using Google.Apis.YouTube.v3;
using YouTubeMigrationClient;

var sourceYouTubeCredential = await YouTubeServiceFactory.CreateCredential("client_secrets_source.json");
var sourceYouTubeService = new YouTubeService(new BaseClientService.Initializer()
{
    HttpClientInitializer = sourceYouTubeCredential,
    ApplicationName = Assembly.GetExecutingAssembly().GetType().ToString()
});

var sourceSubscriptionListRequest = sourceYouTubeService.Subscriptions.List("id,snippet");
sourceSubscriptionListRequest.Mine = true;

var sourceSubscriptions = await sourceSubscriptionListRequest.ExecuteAsync();
foreach (var subscription in sourceSubscriptions.Items)
{
    Console.WriteLine($"ChannelId: {subscription.Snippet.ResourceId.ChannelId}\t\tTitle: {subscription.Snippet.Title}");
}

await sourceYouTubeCredential.RevokeTokenAsync(new CancellationToken());

The reason we’re revoking the token is to be able to authenticate to both source and destination accounts. If we don’t revoke it now, it still tries to use the source account’s access token when we try to access the destination account. It will be more obvious when you’ve finished implementing the application.

Now run the application in your terminal:

dotnet run

It should launch your default browser and show a Google account selection screen:

Google account selection screen

As this is an application in testing on Google’s side, it shows a warning:

Application not verified warning

Click Continue.

Then it asks for your permission to allow the app to access your Google Account.

Google asking for permission to access your account by your application

Click Allow.

After the authorization is complete, you will see a message that you can close the tab. Do so and go back to your terminal window. You should now see up to 5 (default page size) results like this:

List subscription results

I’d recommend running the application again with client_secrets_destination.json to confirm they both work. This is where revoking the token helps because otherwise, you wouldn’t be asked to select an account.

Refactor Getting Source Subscriptions

The default page size is 5. You can adjust this by setting MaxResults as shown below:

sourceSubscriptionListRequest.MaxResults = 10;

Unfortunately, the maximum value allowed is 50. If you have more than 50 subscriptions, your implementation won’t be able to migrate all of them, which is something you need to fix.

Google uses paging in their results, and you can access the previous and next pages by setting the PageToken property to one of those tokens. The refactored version below shows how it works:

string nextPageToken = null;
var sourceSubscriptionList = new List<Subscription>();

do
{
    var sourceSubscriptionListRequest = sourceYouTubeService.Subscriptions.List("id,snippet");
    sourceSubscriptionListRequest.Mine = true;
    sourceSubscriptionListRequest.MaxResults = 10;
    sourceSubscriptionListRequest.Order = SubscriptionsResource.ListRequest.OrderEnum.Alphabetical;
    sourceSubscriptionListRequest.PageToken = nextPageToken;

    var sourceSubscriptions = await sourceSubscriptionListRequest.ExecuteAsync();
    nextPageToken = sourceSubscriptions.NextPageToken;

    sourceSubscriptionList.AddRange(sourceSubscriptions.Items);
          
    Console.WriteLine(sourceSubscriptions.Items.Count);
} while (nextPageToken != null);

Console.WriteLine(sourceSubscriptionList.Count);

The example gets results 10 at a time and keeps doing it as long as NextPageToken is not null.

Terminal window showing paged result example

So now you have access to your entire subscription list, let’s talk about migrating them into the destination account.

Import Subscriptions into the Destination Account

The next step is to iterate over the subscription list and add them to the destination account. Add the following code block to Program.cs:

var targetYouTubeCredential = await YouTubeServiceFactory.CreateCredential("client_secrets_destination.json");
var targetYouTubeService = new YouTubeService(new BaseClientService.Initializer()
{
    HttpClientInitializer = targetYouTubeCredential,
    ApplicationName = Assembly.GetExecutingAssembly().GetType().ToString()
});

foreach (var subscription in sourceSubscriptionList)
{
    Console.WriteLine($"ChannelId: {subscription.Snippet.ResourceId.ChannelId}\t\tTitle: {subscription.Snippet.Title}");
            
    var targetSubscription = new Subscription
    {
        Snippet = new SubscriptionSnippet
        {
            ResourceId = new ResourceId
            {
                Kind = "youtube#subscription",
                ChannelId = subscription.Snippet.ResourceId.ChannelId
            }
        }
    };
    
    try
    {
        await targetYouTubeService.Subscriptions.Insert(targetSubscription, "id,snippet").ExecuteAsync();
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }
}

await targetYouTubeCredential.RevokeTokenAsync(new CancellationToken());

The exception handling is to ensure the program keeps running if you already have the same subscription in the destination account.

Run the application again, and you should see it adding the subscriptions one by one. After you’re done, refresh your destination account and confirm the results.

Here’s the final version of the Program.cs in case you didn’t follow along:

using System.Reflection;
using Google.Apis.Services;
using Google.Apis.YouTube.v3;
using Google.Apis.YouTube.v3.Data;
using YouTubeMigrationClient;

// Get the subscriptions from the Source Account
var sourceYouTubeCredential = await YouTubeServiceFactory.CreateCredential("client_secrets_source.json");
var sourceYouTubeService = new YouTubeService(new BaseClientService.Initializer()
{
    HttpClientInitializer = sourceYouTubeCredential,
    ApplicationName = Assembly.GetExecutingAssembly().GetType().ToString()
});

string nextPageToken = null;
var sourceSubscriptionList = new List<Subscription>();

do
{
    var sourceSubscriptionListRequest = sourceYouTubeService.Subscriptions.List("id,snippet");
    sourceSubscriptionListRequest.Mine = true;
    sourceSubscriptionListRequest.MaxResults = 50;
    sourceSubscriptionListRequest.Order = SubscriptionsResource.ListRequest.OrderEnum.Alphabetical;
    sourceSubscriptionListRequest.PageToken = nextPageToken;

    var sourceSubscriptions = await sourceSubscriptionListRequest.ExecuteAsync();
    nextPageToken = sourceSubscriptions.NextPageToken;

    sourceSubscriptionList.AddRange(sourceSubscriptions.Items);
} while (nextPageToken != null);

Console.WriteLine($"Retrieved {sourceSubscriptionList.Count} subscriptions from the source account");

await sourceYouTubeCredential.RevokeTokenAsync(new CancellationToken());

// Import subscriptions into the Destination Account
var targetYouTubeCredential = await YouTubeServiceFactory.CreateCredential("client_secrets_destination.json");
var targetYouTubeService = new YouTubeService(new BaseClientService.Initializer()
{
    HttpClientInitializer = targetYouTubeCredential,
    ApplicationName = Assembly.GetExecutingAssembly().GetType().ToString()
});

foreach (var subscription in sourceSubscriptionList)
{
    Console.WriteLine($"ChannelId: {subscription.Snippet.ResourceId.ChannelId}\t\tTitle: {subscription.Snippet.Title}");
            
    var targetSubscription = new Subscription
    {
        Snippet = new SubscriptionSnippet
        {
            ResourceId = new ResourceId
            {
                Kind = "youtube#subscription",
                ChannelId = subscription.Snippet.ResourceId.ChannelId
            }
        }
    };
    
    try
    {
        await targetYouTubeService.Subscriptions.Insert(targetSubscription, "id,snippet").ExecuteAsync();
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }
}

await targetYouTubeCredential.RevokeTokenAsync(new CancellationToken());

Conclusion

This is a simple tutorial about managing your Youtube account by using your own code. Other tools do the same job, but nothing beats the experience and satisfaction of achieving something by software that you built yourself. I hope you enjoyed it too.

Categories: csharpdotnet

Volkan Paksoy

Volkan Paksoy is a software developer with more than 15 years of experience, focusing mostly on C# and AWS. He’s a home lab and self-hosting fan who loves to spend his personal time developing hobby projects with Raspberry Pi, Arduino, LEGO and everything in-between.