Resolver/Extractors/DailymotionExtractor.cs
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using System.Threading;
namespace Bimp.Resolver.Extractors;
/// <summary>
/// dailymotion.com/video/{id}, dailymotion.com/embed/video/{id} and dai.ly/{id}: the player metadata's HLS playlist
/// (a finished video is remuxed from a start time, a live one is followed at its edge).
/// </summary>
public sealed class DailymotionExtractor : SiteExtractor
{
public override string Key => "dailymotion";
protected override string[] Hosts => new[] { "dailymotion.com", "dai.ly" };
// ids are an "x" and a few base 36 digits; page urls can carry a slug after them ("x93tj4e_dragon-ball-daima")
static readonly Regex PagePattern = new( "(?:^|/)video/(x[a-z0-9]{4,10})(?:[_/?]|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled );
static readonly Regex ShortPattern = new( "^/?(x[a-z0-9]{4,10})/?$", RegexOptions.IgnoreCase | RegexOptions.Compiled );
protected override string ParseId( Uri url )
{
var path = url.AbsolutePath;
var m = url.Host.EndsWith( "dai.ly", StringComparison.OrdinalIgnoreCase ) ? ShortPattern.Match( path ) : PagePattern.Match( path );
return m.Success ? m.Groups[1].Value : null;
}
protected override async Task<SiteMedia> FetchAsync( string id, CancellationToken ct )
{
var j = await GetJson( $"https://www.dailymotion.com/player/metadata/video/{id}", ct );
var hls = j?["qualities"]?["auto"].Items().Select( q => q["url"].Str() ).FirstOrDefault( u => !string.IsNullOrEmpty( u ) );
if ( hls is null )
{
// the player's own explanation ("Channel offline.", "This video has been deleted", a country block...)
var why = j?["error"]?["title"].Str() ?? j?["error"]?["raw_message"].Str();
throw new ResolveException( why is null ? "Dailymotion didn't offer a stream for that video." : $"Dailymotion: {why}" );
}
if ( j["protected_delivery"].Bool() )
throw new ResolveException( "That Dailymotion video is DRM protected." );
// Dailymotion's CDN (Cloudflare) answers 403 to the s&box HTTP client whatever headers it sends, while curl and
// browsers get 200 (measured 2026-10-01; Referer/Origin can't be set from the sandbox). Say so instead of failing
// later with a bare 403 - and play it if that ever changes.
if ( !await CanFetch( hls, ct ) )
throw new ResolveException( "Dailymotion's servers refuse connections from s&box, so this video can't be played." );
return new SiteMedia
{
Title = j["title"].Str(),
Duration = j["duration"].Float(),
IsLive = j["mode"].Str() == "live" || j["stream_type"].Str() == "live",
Hls = hls,
};
}
static async Task<bool> CanFetch( string url, CancellationToken ct )
{
try
{
using var r = await Http.RequestAsync( url, cancellationToken: ct );
return (int)r.StatusCode is >= 200 and < 300;
}
catch ( Exception e ) when ( e is not OperationCanceledException )
{
return false;
}
}
}