diff --git a/TwitchLib.Api.Core/ApiBase.cs b/TwitchLib.Api.Core/ApiBase.cs index d4f208a7..a4114ef1 100644 --- a/TwitchLib.Api.Core/ApiBase.cs +++ b/TwitchLib.Api.Core/ApiBase.cs @@ -140,7 +140,7 @@ protected async Task> TwitchDeleteAsync(string resourc return await _rateLimiter.Perform(async () => (await _http.GeneralRequestAsync(url, "DELETE", null, api, clientId, accessToken).ConfigureAwait(false))).ConfigureAwait(false); } - protected async Task TwitchPostGenericAsync(string resource, ApiVersion api, string payload, List>? getParams = null, string? accessToken = null, string? clientId = null, string? customBase = null) + protected async Task TwitchPostGenericAsync(string resource, ApiVersion api, string? payload, List>? getParams = null, string? accessToken = null, string? clientId = null, string? customBase = null) { var url = ConstructResourceUrl(resource, getParams, api, customBase); diff --git a/TwitchLib.Api.Core/Exceptions/BadParameterException.cs b/TwitchLib.Api.Core/Exceptions/BadParameterException.cs index 8b8c3947..4698e3b1 100644 --- a/TwitchLib.Api.Core/Exceptions/BadParameterException.cs +++ b/TwitchLib.Api.Core/Exceptions/BadParameterException.cs @@ -38,6 +38,13 @@ public static void ThrowIfNotBetween(int value, int min, int max, [CallerArgumen throw new BadParameterException($"Parameter '{paramName}' cannot be less than {min}(inclusive) or greater than {max}(inclusive)."); } } + public static void ThrowIfNotBetween(float value, float min, float max, [CallerArgumentExpression(nameof(value))] string? paramName = null) + { + if (value < min || value > max) + { + throw new BadParameterException($"Parameter '{paramName}' cannot be less than {min}(inclusive) or greater than {max}(inclusive)."); + } + } public static void ThrowIfCollectionNullOrEmptyOrGreaterThan(List value, int max, [CallerArgumentExpression(nameof(value))] string? paramName = null) { diff --git a/TwitchLib.Api.Helix.Models/Clips/CreateClip/CreatedClip.cs b/TwitchLib.Api.Helix.Models/Clips/CreateClip/CreatedClip.cs index f8a0a24a..e927c2fe 100644 --- a/TwitchLib.Api.Helix.Models/Clips/CreateClip/CreatedClip.cs +++ b/TwitchLib.Api.Helix.Models/Clips/CreateClip/CreatedClip.cs @@ -1,5 +1,4 @@ -#nullable disable -using Newtonsoft.Json; +using Newtonsoft.Json; namespace TwitchLib.Api.Helix.Models.Clips.CreateClip; @@ -13,11 +12,11 @@ public class CreatedClip /// The URL is valid for up to 24 hours or until the clip is published, whichever comes first. /// [JsonProperty(PropertyName = "edit_url")] - public string EditUrl { get; protected set; } + public string EditUrl { get; protected set; } = null!; /// /// An ID that uniquely identifies the clip. /// [JsonProperty(PropertyName = "id")] - public string Id { get; protected set; } + public string Id { get; protected set; } = null!; } diff --git a/TwitchLib.Api.Helix.Models/Clips/CreateClip/CreatedClipRequest.cs b/TwitchLib.Api.Helix.Models/Clips/CreateClip/CreatedClipRequest.cs new file mode 100644 index 00000000..dbe9f85a --- /dev/null +++ b/TwitchLib.Api.Helix.Models/Clips/CreateClip/CreatedClipRequest.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace TwitchLib.Api.Helix.Models.Clips.CreateClip; + +public class CreatedClipRequest +{ + /// + /// The ID of the broadcaster whose stream you want to create a clip from. + /// + public string BroadcasterId { get; set; } = null!; + + /// + /// The title of the clip. + /// + public string? Title { get; set; } + + /// + /// The length of the clip in seconds. Possible values range from 5 to 60 inclusively with a precision of 0.1. The default is 30. + /// + public float? Duration { get; set; } + + /// + public virtual List> ToParams() + { + var getParams = new List> + { + new("broadcaster_id", BroadcasterId), + }; + + if (Title is not null) + getParams.Add(new("title", Title)); + + if (Duration is not null) + getParams.Add(new("duration", Duration.Value.ToString())); + + return getParams; + } +} diff --git a/TwitchLib.Api.Helix.Models/Clips/CreateClip/CreatedClipResponse.cs b/TwitchLib.Api.Helix.Models/Clips/CreateClip/CreatedClipResponse.cs index 94e89e19..b2ad237d 100644 --- a/TwitchLib.Api.Helix.Models/Clips/CreateClip/CreatedClipResponse.cs +++ b/TwitchLib.Api.Helix.Models/Clips/CreateClip/CreatedClipResponse.cs @@ -1,5 +1,4 @@ -#nullable disable -using Newtonsoft.Json; +using Newtonsoft.Json; namespace TwitchLib.Api.Helix.Models.Clips.CreateClip; @@ -12,5 +11,5 @@ public class CreatedClipResponse /// Contains clip's ID and edit_URL that can be used to edit the clip's title, identify the part of the clip to publish, and publish the clip. /// [JsonProperty(PropertyName = "data")] - public CreatedClip[] CreatedClips { get; protected set; } + public CreatedClip[] CreatedClips { get; protected set; } = null!; } diff --git a/TwitchLib.Api.Helix.Models/Clips/CreateClipFromVod/CreatedClipFromVod.cs b/TwitchLib.Api.Helix.Models/Clips/CreateClipFromVod/CreatedClipFromVod.cs new file mode 100644 index 00000000..3e5b8b74 --- /dev/null +++ b/TwitchLib.Api.Helix.Models/Clips/CreateClipFromVod/CreatedClipFromVod.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace TwitchLib.Api.Helix.Models.Clips.CreateClipFromVod; + +/// +/// A Twitch clip created from CreateClipFromVod +/// +public class CreatedClipFromVod +{ + /// + /// An ID that uniquely identifies the clip. + /// + [JsonProperty(PropertyName = "id")] + public string Id { get; protected set; } = null!; + + /// + /// A URL that you can use to edit the clip’s title, identify the part of the clip to publish, and publish the clip. + /// The URL is valid for up to 24 hours or until the clip is published, whichever comes first. + /// + [JsonProperty(PropertyName = "edit_url")] + public string EditUrl { get; protected set; } = null!; +} diff --git a/TwitchLib.Api.Helix.Models/Clips/CreateClipFromVod/CreatedClipFromVodRequest.cs b/TwitchLib.Api.Helix.Models/Clips/CreateClipFromVod/CreatedClipFromVodRequest.cs new file mode 100644 index 00000000..b5748750 --- /dev/null +++ b/TwitchLib.Api.Helix.Models/Clips/CreateClipFromVod/CreatedClipFromVodRequest.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; + +namespace TwitchLib.Api.Helix.Models.Clips.CreateClipFromVod; + +public class CreatedClipFromVodRequest +{ + /// + /// The user ID of the editor for the channel you want to create a clip for. If using the broadcaster’s auth token, this is the same as broadcaster_id. This must match the user_id in the user access token. + /// + public string EditorId { get; set; } = null!; + + /// + /// The user ID for the channel you want to create a clip for. + /// + public string BroadcasterId { get; set; } = null!; + + /// + /// ID of the VOD the user wants to clip. + /// + public string VodId { get; set; } = null!; + + /// + /// Offset in the VOD to create the clip. + /// + public int VodOffset { get; set; } + + /// + /// The length of the clip, in seconds. Precision is 0.1. Defaults to 30. Min: 5 seconds, Max: 60 seconds. + /// + public float? Duration { get; set; } + + /// + /// The title of the clip. + /// + public string Title { get; set; } = null!; + + /// + public virtual List> ToParams() + { + var getParams = new List> + { + new("editor_id", EditorId), + new("broadcaster_id", BroadcasterId), + new("vod_offset", VodId), + new("duration", VodOffset.ToString()), + new("title", Title), + }; + + if (Duration is not null) + { + getParams.Add(new("user_id", Duration.Value.ToString())); + } + + return getParams; + } +} diff --git a/TwitchLib.Api.Helix.Models/Clips/CreateClipFromVod/CreatedClipFromVodResponse.cs b/TwitchLib.Api.Helix.Models/Clips/CreateClipFromVod/CreatedClipFromVodResponse.cs new file mode 100644 index 00000000..d8b45c27 --- /dev/null +++ b/TwitchLib.Api.Helix.Models/Clips/CreateClipFromVod/CreatedClipFromVodResponse.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; +using TwitchLib.Api.Helix.Models.Clips.CreateClip; + +namespace TwitchLib.Api.Helix.Models.Clips.CreateClipFromVod; + +/// +/// Response for CreateClipFromVod which creates a clip from the broadcaster's stream. +/// +public class CreatedClipFromVodResponse +{ + /// + /// A list containing the created clip. + /// + [JsonProperty(PropertyName = "data")] + public CreatedClip[] Data { get; protected set; } = null!; +} diff --git a/TwitchLib.Api.Helix.Models/Clips/GetClips/Clip.cs b/TwitchLib.Api.Helix.Models/Clips/GetClips/Clip.cs index d677e40d..282b56fb 100644 --- a/TwitchLib.Api.Helix.Models/Clips/GetClips/Clip.cs +++ b/TwitchLib.Api.Helix.Models/Clips/GetClips/Clip.cs @@ -1,5 +1,4 @@ -#nullable disable -using Newtonsoft.Json; +using Newtonsoft.Json; namespace TwitchLib.Api.Helix.Models.Clips.GetClips; @@ -12,69 +11,69 @@ public class Clip /// An ID that uniquely identifies the clip. /// [JsonProperty(PropertyName = "id")] - public string Id { get; protected set; } + public string Id { get; protected set; } = null!; /// /// A URL to the clip. /// [JsonProperty(PropertyName = "url")] - public string Url { get; protected set; } + public string Url { get; protected set; } = null!; /// /// A URL that you can use in an iframe to embed the clip. /// [JsonProperty(PropertyName = "embed_url")] - public string EmbedUrl { get; protected set; } + public string EmbedUrl { get; protected set; } = null!; /// /// An ID that identifies the broadcaster that the video was clipped from. /// [JsonProperty(PropertyName = "broadcaster_id")] - public string BroadcasterId { get; protected set; } + public string BroadcasterId { get; protected set; } = null!; /// /// The broadcaster’s display name. /// [JsonProperty(PropertyName = "broadcaster_name")] - public string BroadcasterName { get; protected set; } + public string BroadcasterName { get; protected set; } = null!; /// /// An ID that identifies the user that created the clip. /// [JsonProperty(PropertyName = "creator_id")] - public string CreatorId { get; protected set; } + public string CreatorId { get; protected set; } = null!; /// /// The display name of the user that created the clip. /// [JsonProperty(PropertyName = "creator_name")] - public string CreatorName { get; protected set; } + public string CreatorName { get; protected set; } = null!; /// /// An ID that identifies the video that the clip came from. /// This field contains an empty string if the video is not available. /// [JsonProperty(PropertyName = "video_id")] - public string VideoId { get; protected set; } + public string VideoId { get; protected set; } = null!; /// /// The ID of the game that was being played when the clip was created. /// [JsonProperty(PropertyName = "game_id")] - public string GameId { get; protected set; } + public string GameId { get; protected set; } = null!; /// /// The ISO 639-1 two-letter language code that the broadcaster broadcasts in. For example, en for English. /// The value is other if the broadcaster uses a language that Twitch doesn’t support. /// [JsonProperty(PropertyName = "language")] - public string Language { get; protected set; } + public string Language { get; protected set; } = null!; /// /// The title of the clip. /// [JsonProperty(PropertyName = "title")] - public string Title { get; protected set; } + public string Title { get; protected set; } = null!; /// /// The number of times the clip has been viewed. @@ -86,13 +85,13 @@ public class Clip /// The date and time of when the clip was created. The date and time is in RFC3339 format. /// [JsonProperty(PropertyName = "created_at")] - public string CreatedAt { get; protected set; } + public string CreatedAt { get; protected set; } = null!; /// /// A URL to a thumbnail image of the clip. /// [JsonProperty(PropertyName = "thumbnail_url")] - public string ThumbnailUrl { get; protected set; } + public string ThumbnailUrl { get; protected set; } = null!; /// /// The length of the clip, in seconds. Precision is 0.1. @@ -105,6 +104,7 @@ public class Clip /// Is null if the video is not available or hasn’t been created yet from the live stream (see video_id). /// Note that there’s a delay between when a clip is created during a broadcast and when the offset is set. During the delay period, vod_offset is null. The delay is indeterminant but is typically minutes long. /// + // TODO: breaking change: make nullable [JsonProperty(PropertyName = "vod_offset")] public int VodOffset { get; protected set; } diff --git a/TwitchLib.Api.Helix.Models/Clips/GetClips/GetClipsResponse.cs b/TwitchLib.Api.Helix.Models/Clips/GetClips/GetClipsResponse.cs index 36e4e487..b4d12286 100644 --- a/TwitchLib.Api.Helix.Models/Clips/GetClips/GetClipsResponse.cs +++ b/TwitchLib.Api.Helix.Models/Clips/GetClips/GetClipsResponse.cs @@ -1,5 +1,4 @@ -#nullable disable -using Newtonsoft.Json; +using Newtonsoft.Json; using TwitchLib.Api.Helix.Models.Common; namespace TwitchLib.Api.Helix.Models.Clips.GetClips; @@ -15,12 +14,12 @@ public class GetClipsResponse /// For lists returned by id, the list is in the same order as the input IDs. /// [JsonProperty(PropertyName = "data")] - public Clip[] Clips { get; protected set; } + public Clip[] Clips { get; protected set; } = null!; /// /// The information used to page through the list of results.
/// The object is empty if there are no more pages left to page through.
///
[JsonProperty(PropertyName = "pagination")] - public Pagination Pagination { get; protected set; } + public Pagination Pagination { get; protected set; } = null!; } diff --git a/TwitchLib.Api.Helix.Models/Clips/GetClipsDownload/GetClipsDownloadResponse.cs b/TwitchLib.Api.Helix.Models/Clips/GetClipsDownload/GetClipsDownloadResponse.cs index b6199e60..c268ccbe 100644 --- a/TwitchLib.Api.Helix.Models/Clips/GetClipsDownload/GetClipsDownloadResponse.cs +++ b/TwitchLib.Api.Helix.Models/Clips/GetClipsDownload/GetClipsDownloadResponse.cs @@ -1,5 +1,4 @@ -#nullable disable -using Newtonsoft.Json; +using Newtonsoft.Json; namespace TwitchLib.Api.Helix.Models.Clips.GetClipsDownload; @@ -12,5 +11,5 @@ public class GetClipsDownloadResponse /// List of clips and their download URLs. /// [JsonProperty(PropertyName = "data")] - public ClipDownload[] Clips { get; protected set; } + public ClipDownload[] Clips { get; protected set; } = null!; } diff --git a/TwitchLib.Api.Helix/Clips.cs b/TwitchLib.Api.Helix/Clips.cs index 4b894d1c..8f4bcb93 100644 --- a/TwitchLib.Api.Helix/Clips.cs +++ b/TwitchLib.Api.Helix/Clips.cs @@ -1,5 +1,4 @@ -#nullable disable -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -9,6 +8,7 @@ using TwitchLib.Api.Core.Extensions.System; using TwitchLib.Api.Core.Interfaces; using TwitchLib.Api.Helix.Models.Clips.CreateClip; +using TwitchLib.Api.Helix.Models.Clips.CreateClipFromVod; using TwitchLib.Api.Helix.Models.Clips.GetClips; using TwitchLib.Api.Helix.Models.Clips.GetClipsDownload; @@ -19,6 +19,9 @@ namespace TwitchLib.Api.Helix; /// public class Clips : ApiBase { + /// + /// Initializes a new instance of the class + /// public Clips(IApiSettings settings, IRateLimiter rateLimiter, IHttpCallHandler http) : base(settings, rateLimiter, http) { } @@ -63,7 +66,7 @@ public Clips(IApiSettings settings, IRateLimiter rateLimiter, IHttpCallHandler h /// optional access token to override the use of the stored one in the TwitchAPI instance /// /// - public Task GetClipsAsync(List clipIds = null, string gameId = null, string broadcasterId = null, string before = null, string after = null, DateTime? startedAt = null, DateTime? endedAt = null, bool? isFeatured = null, int first = 20, string accessToken = null) + public Task GetClipsAsync(List? clipIds = null, string? gameId = null, string? broadcasterId = null, string? before = null, string? after = null, DateTime? startedAt = null, DateTime? endedAt = null, bool? isFeatured = null, int first = 20, string? accessToken = null) { BadParameterException.ThrowIfNotBetween(first, 1, 100); @@ -121,8 +124,10 @@ public Task GetClipsAsync(List clipIds = null, string /// ID of the stream from which the clip will be made. /// optional access token to override the use of the stored one in the TwitchAPI instance /// - public Task CreateClipAsync(string broadcasterId, string accessToken = null) + public Task CreateClipAsync(string broadcasterId, string? accessToken = null) { + BadParameterException.ThrowIfNullOrEmpty(broadcasterId); + var getParams = new List> { new("broadcaster_id", broadcasterId) @@ -131,6 +136,27 @@ public Task CreateClipAsync(string broadcasterId, string ac return TwitchPostGenericAsync("/clips", ApiVersion.Helix, null, getParams, accessToken); } + /// + /// Creates a clip programmatically. This returns both an ID and an edit URL for the new clip. + /// Clip creation takes time. We recommend that you query Get Clips, with the clip ID that is returned here. + /// If Get Clips returns a valid clip, your clip creation was successful. + /// If, after 15 seconds, you still have not gotten back a valid clip from Get Clips, assume that the clip was not created and retry Create Clip. + /// This endpoint has a global rate limit, across all callers. + /// Required scope: clips:edit + /// + /// Request parameters for the call. + /// optional access token to override the use of the stored one in the TwitchAPI instance + /// + public Task CreateClipAsync(CreatedClipRequest request, string? accessToken = null) + { + BadParameterException.ThrowIfNull(request); + BadParameterException.ThrowIfNullOrEmpty(request.BroadcasterId); + + var getParams = request.ToParams(); + + return TwitchPostGenericAsync("/clips", ApiVersion.Helix, null, getParams, accessToken); + } + #endregion #region GetClipsDownload @@ -145,7 +171,7 @@ public Task CreateClipAsync(string broadcasterId, string ac /// The ID that identifies the clip you want to download. Include this parameter for each clip you want to download, up to a maximum of 10 clips. /// optional access token to override the use of the stored one in the TwitchAPI instance /// - public Task GetClipsDownloadAsync(string editorId, string broadcasterId, List clipIds, string accessToken = null) + public Task GetClipsDownloadAsync(string editorId, string broadcasterId, List clipIds, string? accessToken = null) { BadParameterException.ThrowIfNullOrEmpty(editorId); BadParameterException.ThrowIfNullOrEmpty(broadcasterId); @@ -163,4 +189,29 @@ public Task GetClipsDownloadAsync(string editorId, str #endregion + #region CreateClipFromVod + + /// + /// Creates a clip programmatically. This returns both an ID and an edit URL for the new. + /// + /// + /// Request parameters for the call. + /// optional access token to override the use of the stored one in the TwitchAPI instance + /// + public Task CreateClipFromVodAsync(CreatedClipFromVodRequest request, string? accessToken = null) + { + BadParameterException.ThrowIfNull(request); + BadParameterException.ThrowIfNull(request.EditorId); + BadParameterException.ThrowIfNull(request.BroadcasterId); + BadParameterException.ThrowIfNull(request.VodId); + BadParameterException.ThrowIfNull(request.Title); + if (request.Duration is not null) + BadParameterException.ThrowIfNotBetween(request.Duration.Value, 5, 60); + + var getParams = request.ToParams(); + + return TwitchPostGenericAsync("/videos/clips", ApiVersion.Helix, null, getParams, accessToken); + } + + #endregion } \ No newline at end of file