Resolver/InnerTubeClients.cs
using System.Text.Json.Nodes;
namespace Bimp.Resolver;
/// <summary>
/// An InnerTube (YouTube's internal API) client identity. Different clients get different format lists;
/// we only use ones whose formats come with a plain url - no signature cipher, so no JavaScript is needed.
/// </summary>
public sealed class InnerTubeClient
{
/// <summary> Short name used in the bimp_yt_clients convar. </summary>
public string Key { get; init; }
/// <summary> context.client.clientName </summary>
public string Name { get; init; }
public string Version { get; init; }
/// <summary> X-YouTube-Client-Name </summary>
public int Id { get; init; }
/// <summary> Extra context.client fields (device, os, userAgent...). </summary>
public Dictionary<string, object> Extra { get; init; } = new();
public JsonObject BuildContext()
{
var client = new JsonObject
{
["clientName"] = Name,
["clientVersion"] = Version,
["hl"] = "en",
["gl"] = "US",
["timeZone"] = "UTC",
["utcOffsetMinutes"] = 0,
};
foreach ( var (k, v) in Extra )
{
client[k] = v switch
{
int i => JsonValue.Create( i ),
string s => JsonValue.Create( s ),
_ => JsonValue.Create( v.ToString() ),
};
}
return new JsonObject { ["client"] = client };
}
}
/// <summary>
/// The known clients, in the default order they're tried. YouTube changes which clients work every so often -
/// the order can be overridden with the bimp_yt_clients convar without touching code.
/// </summary>
public static class InnerTubeClients
{
public static readonly InnerTubeClient AndroidVr = new()
{
Key = "android_vr",
Name = "ANDROID_VR",
Version = "1.65.10",
Id = 28,
Extra = new()
{
["deviceMake"] = "Oculus",
["deviceModel"] = "Quest 3",
["androidSdkVersion"] = 32,
["osName"] = "Android",
["osVersion"] = "12L",
["userAgent"] = "com.google.android.apps.youtube.vr.oculus/1.65.10 (Linux; U; Android 12L; eureka-user Build/SQ3A.220605.009.A1) gzip",
},
};
public static readonly InnerTubeClient Ios = new()
{
Key = "ios",
Name = "IOS",
Version = "20.10.4",
Id = 5,
Extra = new()
{
["deviceMake"] = "Apple",
["deviceModel"] = "iPhone16,2",
["osName"] = "iPhone",
["osVersion"] = "18.3.2.22D82",
["userAgent"] = "com.google.ios.youtube/20.10.4 (iPhone16,2; U; CPU iOS 18_3_2 like Mac OS X;)",
},
};
public static readonly InnerTubeClient VisionOs = new()
{
Key = "visionos",
Name = "VISIONOS",
Version = "0.1",
Id = 101,
Extra = new()
{
["deviceMake"] = "Apple",
["deviceModel"] = "RealityDevice14,1",
["osName"] = "visionOS",
["osVersion"] = "1.3.21O771",
},
};
public static readonly InnerTubeClient Android = new()
{
Key = "android",
Name = "ANDROID",
Version = "20.10.38",
Id = 3,
Extra = new()
{
["androidSdkVersion"] = 30,
["osName"] = "Android",
["osVersion"] = "11",
["userAgent"] = "com.google.android.youtube/20.10.38 (Linux; U; Android 11) gzip",
},
};
public static readonly IReadOnlyList<InnerTubeClient> All = new[] { VisionOs, AndroidVr, Ios, Android };
public const string DefaultOrder = "visionos,android_vr,ios";
/// <summary>
/// The clients to try, in order, from a comma separated list of keys. Unknown keys are ignored.
/// </summary>
public static List<InnerTubeClient> Ordered( string keys )
{
if ( string.IsNullOrWhiteSpace( keys ) ) keys = DefaultOrder;
var list = keys.Split( ',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries )
.Select( k => All.FirstOrDefault( c => string.Equals( c.Key, k, StringComparison.OrdinalIgnoreCase ) ) )
.Where( c => c is not null )
.Distinct()
.ToList();
return list.Count > 0 ? list : Ordered( DefaultOrder );
}
}