diff --git a/Marechai.ApiClient/Models/CreateSoftwareVideoRequest.cs b/Marechai.ApiClient/Models/CreateSoftwareVideoRequest.cs new file mode 100644 index 00000000..68e5f638 --- /dev/null +++ b/Marechai.ApiClient/Models/CreateSoftwareVideoRequest.cs @@ -0,0 +1,85 @@ +// +#pragma warning disable CS0618 +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Serialization; +using System.Collections.Generic; +using System.IO; +using System; +namespace Marechai.ApiClient.Models +{ + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + #pragma warning disable CS1591 + public partial class CreateSoftwareVideoRequest : IAdditionalDataHolder, IParsable + #pragma warning restore CS1591 + { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData { get; set; } + /// The provider property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Provider { get; set; } +#nullable restore +#else + public string Provider { get; set; } +#endif + /// The title property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Title { get; set; } +#nullable restore +#else + public string Title { get; set; } +#endif + /// The video_id property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? VideoId { get; set; } +#nullable restore +#else + public string VideoId { get; set; } +#endif + /// + /// Instantiates a new and sets the default values. + /// + public CreateSoftwareVideoRequest() + { + AdditionalData = new Dictionary(); + } + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// + /// A + /// The parse node to use to read the discriminator value and create the object + public static global::Marechai.ApiClient.Models.CreateSoftwareVideoRequest CreateFromDiscriminatorValue(IParseNode parseNode) + { + if(ReferenceEquals(parseNode, null)) throw new ArgumentNullException(nameof(parseNode)); + return new global::Marechai.ApiClient.Models.CreateSoftwareVideoRequest(); + } + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "provider", n => { Provider = n.GetStringValue(); } }, + { "title", n => { Title = n.GetStringValue(); } }, + { "video_id", n => { VideoId = n.GetStringValue(); } }, + }; + } + /// + /// Serializes information the current object + /// + /// Serialization writer to use to serialize this model + public virtual void Serialize(ISerializationWriter writer) + { + if(ReferenceEquals(writer, null)) throw new ArgumentNullException(nameof(writer)); + writer.WriteStringValue("provider", Provider); + writer.WriteStringValue("title", Title); + writer.WriteStringValue("video_id", VideoId); + writer.WriteAdditionalData(AdditionalData); + } + } +} +#pragma warning restore CS0618 diff --git a/Marechai.ApiClient/Models/UpdateSoftwareVideoRequest.cs b/Marechai.ApiClient/Models/UpdateSoftwareVideoRequest.cs new file mode 100644 index 00000000..283ffe81 --- /dev/null +++ b/Marechai.ApiClient/Models/UpdateSoftwareVideoRequest.cs @@ -0,0 +1,65 @@ +// +#pragma warning disable CS0618 +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Serialization; +using System.Collections.Generic; +using System.IO; +using System; +namespace Marechai.ApiClient.Models +{ + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + #pragma warning disable CS1591 + public partial class UpdateSoftwareVideoRequest : IAdditionalDataHolder, IParsable + #pragma warning restore CS1591 + { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData { get; set; } + /// The title property +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public string? Title { get; set; } +#nullable restore +#else + public string Title { get; set; } +#endif + /// + /// Instantiates a new and sets the default values. + /// + public UpdateSoftwareVideoRequest() + { + AdditionalData = new Dictionary(); + } + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// + /// A + /// The parse node to use to read the discriminator value and create the object + public static global::Marechai.ApiClient.Models.UpdateSoftwareVideoRequest CreateFromDiscriminatorValue(IParseNode parseNode) + { + if(ReferenceEquals(parseNode, null)) throw new ArgumentNullException(nameof(parseNode)); + return new global::Marechai.ApiClient.Models.UpdateSoftwareVideoRequest(); + } + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "title", n => { Title = n.GetStringValue(); } }, + }; + } + /// + /// Serializes information the current object + /// + /// Serialization writer to use to serialize this model + public virtual void Serialize(ISerializationWriter writer) + { + if(ReferenceEquals(writer, null)) throw new ArgumentNullException(nameof(writer)); + writer.WriteStringValue("title", Title); + writer.WriteAdditionalData(AdditionalData); + } + } +} +#pragma warning restore CS0618 diff --git a/Marechai.ApiClient/Software/Item/Videos/VideosRequestBuilder.cs b/Marechai.ApiClient/Software/Item/Videos/VideosRequestBuilder.cs index 090a3fc3..6b4d8953 100644 --- a/Marechai.ApiClient/Software/Item/Videos/VideosRequestBuilder.cs +++ b/Marechai.ApiClient/Software/Item/Videos/VideosRequestBuilder.cs @@ -60,14 +60,16 @@ namespace Marechai.ApiClient.Software.Item.Videos /// Configuration for the request such as headers, query parameters, and middleware options. /// When receiving a 400 status code /// When receiving a 401 status code + /// When receiving a 403 status code + /// When receiving a 404 status code /// When receiving a 409 status code #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER #nullable enable - public async Task PostAsync(global::Marechai.ApiClient.Models.SoftwareVideoDto body, Action>? requestConfiguration = default, CancellationToken cancellationToken = default) + public async Task PostAsync(global::Marechai.ApiClient.Models.CreateSoftwareVideoRequest body, Action>? requestConfiguration = default, CancellationToken cancellationToken = default) { #nullable restore #else - public async Task PostAsync(global::Marechai.ApiClient.Models.SoftwareVideoDto body, Action> requestConfiguration = default, CancellationToken cancellationToken = default) + public async Task PostAsync(global::Marechai.ApiClient.Models.CreateSoftwareVideoRequest body, Action> requestConfiguration = default, CancellationToken cancellationToken = default) { #endif if(ReferenceEquals(body, null)) throw new ArgumentNullException(nameof(body)); @@ -76,6 +78,8 @@ namespace Marechai.ApiClient.Software.Item.Videos { { "400", global::Marechai.ApiClient.Models.ProblemDetails.CreateFromDiscriminatorValue }, { "401", global::Marechai.ApiClient.Models.ProblemDetails.CreateFromDiscriminatorValue }, + { "403", global::Marechai.ApiClient.Models.ProblemDetails.CreateFromDiscriminatorValue }, + { "404", global::Marechai.ApiClient.Models.ProblemDetails.CreateFromDiscriminatorValue }, { "409", global::Marechai.ApiClient.Models.ProblemDetails.CreateFromDiscriminatorValue }, }; return await RequestAdapter.SendAsync(requestInfo, global::Marechai.ApiClient.Models.SoftwareVideoDto.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false); @@ -101,11 +105,11 @@ namespace Marechai.ApiClient.Software.Item.Videos /// Configuration for the request such as headers, query parameters, and middleware options. #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER #nullable enable - public RequestInformation ToPostRequestInformation(global::Marechai.ApiClient.Models.SoftwareVideoDto body, Action>? requestConfiguration = default) + public RequestInformation ToPostRequestInformation(global::Marechai.ApiClient.Models.CreateSoftwareVideoRequest body, Action>? requestConfiguration = default) { #nullable restore #else - public RequestInformation ToPostRequestInformation(global::Marechai.ApiClient.Models.SoftwareVideoDto body, Action> requestConfiguration = default) + public RequestInformation ToPostRequestInformation(global::Marechai.ApiClient.Models.CreateSoftwareVideoRequest body, Action> requestConfiguration = default) { #endif if(ReferenceEquals(body, null)) throw new ArgumentNullException(nameof(body)); diff --git a/Marechai.ApiClient/Software/Videos/Item/VideosItemRequestBuilder.cs b/Marechai.ApiClient/Software/Videos/Item/VideosItemRequestBuilder.cs index 6b86020a..2ff7626e 100644 --- a/Marechai.ApiClient/Software/Videos/Item/VideosItemRequestBuilder.cs +++ b/Marechai.ApiClient/Software/Videos/Item/VideosItemRequestBuilder.cs @@ -36,6 +36,7 @@ namespace Marechai.ApiClient.Software.Videos.Item /// Cancellation token to use when cancelling requests /// Configuration for the request such as headers, query parameters, and middleware options. /// When receiving a 401 status code + /// When receiving a 403 status code /// When receiving a 404 status code #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER #nullable enable @@ -50,6 +51,7 @@ namespace Marechai.ApiClient.Software.Videos.Item var errorMapping = new Dictionary> { { "401", global::Marechai.ApiClient.Models.ProblemDetails.CreateFromDiscriminatorValue }, + { "403", global::Marechai.ApiClient.Models.ProblemDetails.CreateFromDiscriminatorValue }, { "404", global::Marechai.ApiClient.Models.ProblemDetails.CreateFromDiscriminatorValue }, }; await RequestAdapter.SendNoContentAsync(requestInfo, errorMapping, cancellationToken).ConfigureAwait(false); @@ -74,6 +76,33 @@ namespace Marechai.ApiClient.Software.Videos.Item }; return await RequestAdapter.SendAsync(requestInfo, global::Marechai.ApiClient.Models.SoftwareVideoDto.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false); } + /// The request body + /// Cancellation token to use when cancelling requests + /// Configuration for the request such as headers, query parameters, and middleware options. + /// When receiving a 400 status code + /// When receiving a 401 status code + /// When receiving a 403 status code + /// When receiving a 404 status code +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public async Task PutAsync(global::Marechai.ApiClient.Models.UpdateSoftwareVideoRequest body, Action>? requestConfiguration = default, CancellationToken cancellationToken = default) + { +#nullable restore +#else + public async Task PutAsync(global::Marechai.ApiClient.Models.UpdateSoftwareVideoRequest body, Action> requestConfiguration = default, CancellationToken cancellationToken = default) + { +#endif + if(ReferenceEquals(body, null)) throw new ArgumentNullException(nameof(body)); + var requestInfo = ToPutRequestInformation(body, requestConfiguration); + var errorMapping = new Dictionary> + { + { "400", global::Marechai.ApiClient.Models.ProblemDetails.CreateFromDiscriminatorValue }, + { "401", global::Marechai.ApiClient.Models.ProblemDetails.CreateFromDiscriminatorValue }, + { "403", global::Marechai.ApiClient.Models.ProblemDetails.CreateFromDiscriminatorValue }, + { "404", global::Marechai.ApiClient.Models.ProblemDetails.CreateFromDiscriminatorValue }, + }; + await RequestAdapter.SendNoContentAsync(requestInfo, errorMapping, cancellationToken).ConfigureAwait(false); + } /// A /// Configuration for the request such as headers, query parameters, and middleware options. #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER @@ -106,6 +135,25 @@ namespace Marechai.ApiClient.Software.Videos.Item requestInfo.Headers.TryAdd("Accept", "application/json, text/plain;q=0.9"); return requestInfo; } + /// A + /// The request body + /// Configuration for the request such as headers, query parameters, and middleware options. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public RequestInformation ToPutRequestInformation(global::Marechai.ApiClient.Models.UpdateSoftwareVideoRequest body, Action>? requestConfiguration = default) + { +#nullable restore +#else + public RequestInformation ToPutRequestInformation(global::Marechai.ApiClient.Models.UpdateSoftwareVideoRequest body, Action> requestConfiguration = default) + { +#endif + if(ReferenceEquals(body, null)) throw new ArgumentNullException(nameof(body)); + var requestInfo = new RequestInformation(Method.PUT, UrlTemplate, PathParameters); + requestInfo.Configure(requestConfiguration); + requestInfo.Headers.TryAdd("Accept", "application/json, text/plain;q=0.9"); + requestInfo.SetContentFromParsable(RequestAdapter, "application/json", body); + return requestInfo; + } /// /// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored. /// @@ -131,6 +179,14 @@ namespace Marechai.ApiClient.Software.Videos.Item public partial class VideosItemRequestBuilderGetRequestConfiguration : RequestConfiguration { } + /// + /// Configuration for the request such as headers, query parameters, and middleware options. + /// + [Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")] + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class VideosItemRequestBuilderPutRequestConfiguration : RequestConfiguration + { + } } } #pragma warning restore CS0618 diff --git a/Marechai.ApiClient/kiota-lock.json b/Marechai.ApiClient/kiota-lock.json index 4c43bffb..8580adb4 100644 --- a/Marechai.ApiClient/kiota-lock.json +++ b/Marechai.ApiClient/kiota-lock.json @@ -1,5 +1,5 @@ { - "descriptionHash": "9CD33AD0F35A1022CC152B32FF35423674776116946B4399403175D7110CDDD843BF49C2C55C3C8F5E7C2704DB6C7ED36E9EF850622E1F9C5507CBB2B71EAE2D", + "descriptionHash": "1D3DD10AC7F51E85403ABA2CCE8E12CF73B2B0FF44AE722917D1F577BD0CDA0E62E9E38E057F8CC57899B8390B95EA034BB3B25025C7EACA39C7A0BBAC24F1E6", "descriptionLocation": "../../../../../tmp/openapi.json", "lockFileVersion": "1.0.0", "kiotaVersion": "1.31.1", diff --git a/Marechai.Data/Dtos/CreateSoftwareVideoRequest.cs b/Marechai.Data/Dtos/CreateSoftwareVideoRequest.cs new file mode 100644 index 00000000..a2dcff71 --- /dev/null +++ b/Marechai.Data/Dtos/CreateSoftwareVideoRequest.cs @@ -0,0 +1,46 @@ +/****************************************************************************** +// MARECHAI: Master repository of computing history artifacts information +// ---------------------------------------------------------------------------- +// +// Author(s) : Natalia Portillo +// +// --[ License ] -------------------------------------------------------------- +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2003-2026 Natalia Portillo +*******************************************************************************/ + +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Marechai.Data.Dtos; + +public class CreateSoftwareVideoRequest +{ + [Required] + [StringLength(32)] + [JsonPropertyName("provider")] + public string Provider { get; set; } = null!; + + [Required] + [StringLength(64)] + [JsonPropertyName("video_id")] + public string VideoId { get; set; } = null!; + + [StringLength(512)] + [JsonPropertyName("title")] + public string? Title { get; set; } +} diff --git a/Marechai.Data/Dtos/UpdateSoftwareVideoRequest.cs b/Marechai.Data/Dtos/UpdateSoftwareVideoRequest.cs new file mode 100644 index 00000000..5c1b44fb --- /dev/null +++ b/Marechai.Data/Dtos/UpdateSoftwareVideoRequest.cs @@ -0,0 +1,36 @@ +/****************************************************************************** +// MARECHAI: Master repository of computing history artifacts information +// ---------------------------------------------------------------------------- +// +// Author(s) : Natalia Portillo +// +// --[ License ] -------------------------------------------------------------- +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2003-2026 Natalia Portillo +*******************************************************************************/ + +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Marechai.Data.Dtos; + +public class UpdateSoftwareVideoRequest +{ + [StringLength(512)] + [JsonPropertyName("title")] + public string? Title { get; set; } +} diff --git a/Marechai.Server/Controllers/SoftwareVideosController.cs b/Marechai.Server/Controllers/SoftwareVideosController.cs index 6bc7eb39..086a1336 100644 --- a/Marechai.Server/Controllers/SoftwareVideosController.cs +++ b/Marechai.Server/Controllers/SoftwareVideosController.cs @@ -87,40 +87,90 @@ public class SoftwareVideosController(MarechaiContext context) : ControllerBase [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task> CreateAsync(ulong softwareId, [FromBody] SoftwareVideoDto dto) + public async Task> CreateAsync(ulong softwareId, + [FromBody] CreateSoftwareVideoRequest request) { string userId = User.FindFirstValue(ClaimTypes.Sid); if(userId is null) return Unauthorized(); + if(string.IsNullOrWhiteSpace(request.Provider) || string.IsNullOrWhiteSpace(request.VideoId)) + return BadRequest("Provider and Video ID are required."); + + string provider = request.Provider.Trim(); + string videoId = request.VideoId.Trim(); + string title = string.IsNullOrWhiteSpace(request.Title) ? null : request.Title.Trim(); + + bool softwareExists = await context.Softwares.AnyAsync(s => s.Id == softwareId); + + if(!softwareExists) return NotFound(); + bool exists = await context.SoftwareVideos.AnyAsync(v => v.SoftwareId == softwareId && - v.Provider == dto.Provider && - v.VideoId == dto.VideoId); + v.Provider == provider && + v.VideoId == videoId); if(exists) return Conflict("This video is already linked to this software."); var model = new SoftwareVideo { SoftwareId = softwareId, - Provider = dto.Provider, - VideoId = dto.VideoId, - Title = dto.Title + Provider = provider, + VideoId = videoId, + Title = title }; context.SoftwareVideos.Add(model); await context.SaveChangesAsync(); - dto.Id = model.Id; - dto.SoftwareId = softwareId; + string softwareName = await context.Softwares.Where(s => s.Id == softwareId) + .Select(s => s.Name) + .FirstOrDefaultAsync(); + + var dto = new SoftwareVideoDto + { + Id = model.Id, + SoftwareId = softwareId, + SoftwareName = softwareName, + Provider = provider, + VideoId = videoId, + Title = title + }; return CreatedAtAction(nameof(GetAsync), new { id = model.Id }, dto); } + [HttpPut("{id:long}")] + [Authorize(Roles = "Admin,UberAdmin")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdateAsync(long id, [FromBody] UpdateSoftwareVideoRequest request) + { + string userId = User.FindFirstValue(ClaimTypes.Sid); + + if(userId is null) return Unauthorized(); + + SoftwareVideo video = await context.SoftwareVideos.FindAsync(id); + + if(video is null) return NotFound(); + + video.Title = string.IsNullOrWhiteSpace(request.Title) ? null : request.Title.Trim(); + + await context.SaveChangesAsync(); + + return NoContent(); + } + [HttpDelete("{id:long}")] [Authorize(Roles = "Admin,UberAdmin")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DeleteAsync(long id) { diff --git a/Marechai/Helpers/YouTubeUrlParser.cs b/Marechai/Helpers/YouTubeUrlParser.cs new file mode 100644 index 00000000..f3eae78b --- /dev/null +++ b/Marechai/Helpers/YouTubeUrlParser.cs @@ -0,0 +1,72 @@ +/****************************************************************************** +// MARECHAI: Master repository of computing history artifacts information +// ---------------------------------------------------------------------------- +// +// Author(s) : Natalia Portillo +// +// --[ License ] -------------------------------------------------------------- +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2003-2026 Natalia Portillo +*******************************************************************************/ + +using System; +using System.Text.RegularExpressions; + +namespace Marechai.Helpers; + +public static partial class YouTubeUrlParser +{ + [GeneratedRegex(@"^(?:https?://)?(?:www\.|m\.|music\.)?(?:youtube\.com|youtube-nocookie\.com)/(?:watch\?(?:[^&\s]*&)*v=|embed/|v/|shorts/|live/)([A-Za-z0-9_-]{11})", + RegexOptions.IgnoreCase)] + private static partial Regex YouTubeFullUrlRegex(); + + [GeneratedRegex(@"^(?:https?://)?youtu\.be/([A-Za-z0-9_-]{11})", RegexOptions.IgnoreCase)] + private static partial Regex YouTubeShortUrlRegex(); + + [GeneratedRegex(@"^[A-Za-z0-9_-]{11}$")] + private static partial Regex BareIdRegex(); + + /// + /// Tries to extract a YouTube video ID from a URL or a bare 11-character ID. Accepts youtube.com/watch?v=, + /// youtu.be/, youtube.com/embed/, youtube.com/shorts/, youtube.com/live/, and youtube-nocookie.com URLs. + /// + public static bool TryExtract(string input, out string videoId) + { + videoId = null; + + if(string.IsNullOrWhiteSpace(input)) return false; + + string trimmed = input.Trim(); + + Match match = YouTubeFullUrlRegex().Match(trimmed); + + if(!match.Success) match = YouTubeShortUrlRegex().Match(trimmed); + + if(match.Success) + { + videoId = match.Groups[1].Value; + + return true; + } + + if(!BareIdRegex().IsMatch(trimmed)) return false; + + videoId = trimmed; + + return true; + } +} diff --git a/Marechai/Pages/Admin/Software.razor b/Marechai/Pages/Admin/Software.razor index ae1862a9..161e1829 100644 --- a/Marechai/Pages/Admin/Software.razor +++ b/Marechai/Pages/Admin/Software.razor @@ -74,6 +74,8 @@ + + diff --git a/Marechai/Pages/Admin/Software.razor.cs b/Marechai/Pages/Admin/Software.razor.cs index c9d39cea..d708350d 100644 --- a/Marechai/Pages/Admin/Software.razor.cs +++ b/Marechai/Pages/Admin/Software.razor.cs @@ -64,6 +64,9 @@ public partial class Software void NavigateToPromoArt(SoftwareDto software) => NavigationManager.NavigateTo($"/admin/software/{software.Id}/promo-art"); + void NavigateToVideos(SoftwareDto software) => + NavigationManager.NavigateTo($"/admin/software/{software.Id}/videos"); + async Task OpenDescriptionsDialog(SoftwareDto software) { DialogParameters parameters = new() diff --git a/Marechai/Pages/Admin/SoftwareVideos.razor b/Marechai/Pages/Admin/SoftwareVideos.razor new file mode 100644 index 00000000..3297ee34 --- /dev/null +++ b/Marechai/Pages/Admin/SoftwareVideos.razor @@ -0,0 +1,164 @@ +@page "/admin/software/{SoftwareId:int}/videos" +@attribute [Authorize(Roles = "Admin, UberAdmin")] +@using Marechai.ApiClient.Models +@using Marechai.Services +@inject SoftwareService SoftwareService +@inject IStringLocalizer L +@inject IDialogService DialogService +@inject NavigationManager NavigationManager + +@L["Back to Software"] + + + + + @L["Video Management"] + + @(_softwareName ?? L["Manage software videos."]) + + +@if(!string.IsNullOrWhiteSpace(_errorMessage)) +{ + @_errorMessage +} + +@if(!string.IsNullOrWhiteSpace(_successMessage)) +{ + @_successMessage +} + +@if(_isLoading) +{ +
+ +
+ return; +} + +@* ── Add Video Section ── *@ + + @L["Link Video"] + + + + @if(!string.IsNullOrWhiteSpace(_detectedVideoId)) + { + + + @L["Detected Video ID"]: + @_detectedVideoId + + } + + + + + + + + + + + + + + @if(_isSaving) + { + + @L["Saving..."] + } + else + { + @L["Link Video"] + } + + + + +@* ── Videos Grid ── *@ + + + @L["Videos"] + @if(_videos is not null) + { + @_videos.Count + } + + + + + @if(_videos is null || _videos.Count == 0) + { + @L["No videos found for this software."] + } + else + { + + @foreach(SoftwareVideoDto video in _videos) + { + + + + @if(string.Equals(video.Provider, "YouTube", StringComparison.OrdinalIgnoreCase)) + { +
+ +
+ } + else + { + + @video.Provider: @video.VideoId + + } +
+ + + @video.Provider + @video.VideoId + + + + + @L["Delete"] + +
+
+ } +
+ } +
diff --git a/Marechai/Pages/Admin/SoftwareVideos.razor.cs b/Marechai/Pages/Admin/SoftwareVideos.razor.cs new file mode 100644 index 00000000..5db61139 --- /dev/null +++ b/Marechai/Pages/Admin/SoftwareVideos.razor.cs @@ -0,0 +1,168 @@ +/****************************************************************************** +// MARECHAI: Master repository of computing history artifacts information +// ---------------------------------------------------------------------------- +// +// Author(s) : Natalia Portillo +// +// --[ License ] -------------------------------------------------------------- +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2003-2026 Natalia Portillo +*******************************************************************************/ + +using System.Collections.Generic; +using System.Threading.Tasks; +using Marechai.ApiClient.Models; +using Marechai.Helpers; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace Marechai.Pages.Admin; + +public partial class SoftwareVideos +{ + string _detectedVideoId; + string _errorMessage; + bool _isLoading = true; + bool _isSaving; + string _manualVideoId; + string _provider = "YouTube"; + string _softwareName; + string _successMessage; + string _title; + string _urlInput; + List _videos; + + [Parameter] public int SoftwareId { get; set; } + + bool CanLink => !string.IsNullOrWhiteSpace(EffectiveProvider) && !string.IsNullOrWhiteSpace(EffectiveVideoId); + + string EffectiveProvider => string.IsNullOrWhiteSpace(_manualVideoId) + ? "YouTube" + : (_provider ?? string.Empty).Trim(); + + string EffectiveVideoId => !string.IsNullOrWhiteSpace(_manualVideoId) + ? _manualVideoId.Trim() + : _detectedVideoId; + + protected override async Task OnInitializedAsync() + { + SoftwareDto software = await SoftwareService.GetSoftwareByIdAsync(SoftwareId); + _softwareName = software?.Name; + await LoadDataAsync(); + } + + async Task LoadDataAsync() + { + _isLoading = true; + _videos = await SoftwareService.GetVideosBySoftwareAsync(SoftwareId); + _isLoading = false; + } + + void OnUrlInputChanged() + { + _detectedVideoId = YouTubeUrlParser.TryExtract(_urlInput, out string id) ? id : null; + } + + async Task LinkVideoAsync() + { + _errorMessage = null; + _successMessage = null; + + if(!CanLink) return; + + _isSaving = true; + + try + { + (SoftwareVideoDto dto, string error) = await SoftwareService.CreateVideoAsync(SoftwareId, + EffectiveProvider, EffectiveVideoId, string.IsNullOrWhiteSpace(_title) ? null : _title.Trim()); + + if(dto is not null) + { + _successMessage = L["Video linked successfully."]; + _urlInput = null; + _detectedVideoId = null; + _manualVideoId = null; + _title = null; + _provider = "YouTube"; + await LoadDataAsync(); + } + else + { + _errorMessage = string.IsNullOrWhiteSpace(error) ? L["Failed to link video."] : error; + } + } + finally + { + _isSaving = false; + } + } + + async Task OnTitleChangedAsync(SoftwareVideoDto video, string newTitle) + { + if(video.Title == newTitle) return; + + (bool succeeded, string error) = + await SoftwareService.UpdateVideoTitleAsync(video.Id!.Value, newTitle); + + if(succeeded) + { + video.Title = newTitle; + _successMessage = L["Video updated successfully."]; + } + else + { + _errorMessage = string.IsNullOrWhiteSpace(error) ? L["Failed to update video."] : error; + } + } + + async Task ConfirmDeleteAsync(SoftwareVideoDto video) + { + DialogParameters parameters = new() + { + { + x => x.ContentText, + L["Are you sure you want to delete this video? This action cannot be undone."] + } + }; + + IDialogReference dialog = + await DialogService.ShowAsync(L["Delete Video"], parameters, + new DialogOptions + { + MaxWidth = MaxWidth.ExtraSmall, + FullWidth = true + }); + + DialogResult result = await dialog.Result; + + if(result is not { Canceled: false }) return; + + (bool succeeded, string errorMessage) = + await SoftwareService.DeleteVideoAsync(video.Id ?? 0); + + if(succeeded) + { + _successMessage = L["Video deleted successfully."]; + await LoadDataAsync(); + } + else + { + _errorMessage = string.IsNullOrWhiteSpace(errorMessage) ? L["Failed to delete video."] : errorMessage; + } + } +} diff --git a/Marechai/Resources/Services/SoftwareService.de.resx b/Marechai/Resources/Services/SoftwareService.de.resx index 37806d50..c5ed08bd 100644 --- a/Marechai/Resources/Services/SoftwareService.de.resx +++ b/Marechai/Resources/Services/SoftwareService.de.resx @@ -1245,4 +1245,96 @@ Werbegrafik erfolgreich gelöscht. Promo art deleted successfully. + + Videos + Videos + + + Videoverwaltung + Video Management + + + Software-Videos verwalten. + Manage software videos. + + + Video verknüpfen + Link Video + + + YouTube-URL oder Video-ID + YouTube URL or Video ID + + + Erkannte Video-ID + Detected Video ID + + + Anbieter + Provider + + + Video-ID + Video ID + + + Videotitel + Video Title + + + Titel + Title + + + Erweitert + Advanced + + + Anbietername (z. B. YouTube). + Provider name (e.g. YouTube). + + + Verwenden Sie dies, wenn die obige URL nicht analysiert werden kann (Nicht-YouTube-Anbieter). + Use this when the URL above cannot be parsed (non-YouTube providers). + + + Speichern... + Saving... + + + Keine Videos für diese Software gefunden. + No videos found for this software. + + + Video löschen + Delete Video + + + Möchten Sie dieses Video wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden. + Are you sure you want to delete this video? This action cannot be undone. + + + Video erfolgreich verknüpft. + Video linked successfully. + + + Video erfolgreich aktualisiert. + Video updated successfully. + + + Video erfolgreich gelöscht. + Video deleted successfully. + + + Video konnte nicht verknüpft werden. + Failed to link video. + + + Video konnte nicht aktualisiert werden. + Failed to update video. + + + Video konnte nicht gelöscht werden. + Failed to delete video. + diff --git a/Marechai/Resources/Services/SoftwareService.en.resx b/Marechai/Resources/Services/SoftwareService.en.resx index 5478e4de..0ee003c3 100644 --- a/Marechai/Resources/Services/SoftwareService.en.resx +++ b/Marechai/Resources/Services/SoftwareService.en.resx @@ -1139,4 +1139,76 @@ Promo art deleted successfully. + + Videos + + + Video Management + + + Manage software videos. + + + Link Video + + + YouTube URL or Video ID + + + Detected Video ID + + + Provider + + + Video ID + + + Video Title + + + Title + + + Advanced + + + Provider name (e.g. YouTube). + + + Use this when the URL above cannot be parsed (non-YouTube providers). + + + Saving... + + + No videos found for this software. + + + Delete Video + + + Are you sure you want to delete this video? This action cannot be undone. + + + Video linked successfully. + + + Video updated successfully. + + + Video deleted successfully. + + + Failed to link video. + + + Failed to update video. + + + Failed to delete video. + + + Back to Software + diff --git a/Marechai/Resources/Services/SoftwareService.es.resx b/Marechai/Resources/Services/SoftwareService.es.resx index 8efd4bc8..9d58c89b 100644 --- a/Marechai/Resources/Services/SoftwareService.es.resx +++ b/Marechai/Resources/Services/SoftwareService.es.resx @@ -1187,4 +1187,96 @@ Arte promocional eliminado correctamente. Promo art deleted successfully. + + Vídeos + Videos + + + Gestión de vídeos + Video Management + + + Gestionar vídeos del software. + Manage software videos. + + + Vincular vídeo + Link Video + + + URL de YouTube o ID del vídeo + YouTube URL or Video ID + + + ID de vídeo detectado + Detected Video ID + + + Proveedor + Provider + + + ID del vídeo + Video ID + + + Título del vídeo + Video Title + + + Título + Title + + + Avanzado + Advanced + + + Nombre del proveedor (p. ej., YouTube). + Provider name (e.g. YouTube). + + + Úselo cuando la URL anterior no pueda analizarse (proveedores distintos de YouTube). + Use this when the URL above cannot be parsed (non-YouTube providers). + + + Guardando... + Saving... + + + No se encontraron vídeos para este software. + No videos found for this software. + + + Eliminar vídeo + Delete Video + + + ¿Está seguro de que desea eliminar este vídeo? Esta acción no se puede deshacer. + Are you sure you want to delete this video? This action cannot be undone. + + + Vídeo vinculado correctamente. + Video linked successfully. + + + Vídeo actualizado correctamente. + Video updated successfully. + + + Vídeo eliminado correctamente. + Video deleted successfully. + + + No se pudo vincular el vídeo. + Failed to link video. + + + No se pudo actualizar el vídeo. + Failed to update video. + + + No se pudo eliminar el vídeo. + Failed to delete video. + \ No newline at end of file diff --git a/Marechai/Resources/Services/SoftwareService.fr.resx b/Marechai/Resources/Services/SoftwareService.fr.resx index 773ec2c1..80ab39fb 100644 --- a/Marechai/Resources/Services/SoftwareService.fr.resx +++ b/Marechai/Resources/Services/SoftwareService.fr.resx @@ -1245,4 +1245,96 @@ Art promotionnel supprimé avec succès. Promo art deleted successfully. + + Vidéos + Videos + + + Gestion des vidéos + Video Management + + + Gérer les vidéos du logiciel. + Manage software videos. + + + Associer une vidéo + Link Video + + + URL YouTube ou identifiant de la vidéo + YouTube URL or Video ID + + + Identifiant de vidéo détecté + Detected Video ID + + + Fournisseur + Provider + + + Identifiant de la vidéo + Video ID + + + Titre de la vidéo + Video Title + + + Titre + Title + + + Avancé + Advanced + + + Nom du fournisseur (par ex. YouTube). + Provider name (e.g. YouTube). + + + Utilisez ceci lorsque l'URL ci-dessus ne peut pas être analysée (fournisseurs autres que YouTube). + Use this when the URL above cannot be parsed (non-YouTube providers). + + + Enregistrement... + Saving... + + + Aucune vidéo trouvée pour ce logiciel. + No videos found for this software. + + + Supprimer la vidéo + Delete Video + + + Êtes-vous sûr de vouloir supprimer cette vidéo ? Cette action ne peut pas être annulée. + Are you sure you want to delete this video? This action cannot be undone. + + + Vidéo associée avec succès. + Video linked successfully. + + + Vidéo mise à jour avec succès. + Video updated successfully. + + + Vidéo supprimée avec succès. + Video deleted successfully. + + + Échec de l'association de la vidéo. + Failed to link video. + + + Échec de la mise à jour de la vidéo. + Failed to update video. + + + Échec de la suppression de la vidéo. + Failed to delete video. + diff --git a/Marechai/Resources/Services/SoftwareService.it.resx b/Marechai/Resources/Services/SoftwareService.it.resx index 138a9b07..906a9876 100644 --- a/Marechai/Resources/Services/SoftwareService.it.resx +++ b/Marechai/Resources/Services/SoftwareService.it.resx @@ -1157,4 +1157,96 @@ Arte promozionale eliminata con successo. Promo art deleted successfully. + + Video + Videos + + + Gestione video + Video Management + + + Gestisci i video del software. + Manage software videos. + + + Collega video + Link Video + + + URL di YouTube o ID del video + YouTube URL or Video ID + + + ID del video rilevato + Detected Video ID + + + Fornitore + Provider + + + ID del video + Video ID + + + Titolo del video + Video Title + + + Titolo + Title + + + Avanzate + Advanced + + + Nome del fornitore (es. YouTube). + Provider name (e.g. YouTube). + + + Usalo quando l'URL sopra non può essere analizzato (fornitori diversi da YouTube). + Use this when the URL above cannot be parsed (non-YouTube providers). + + + Salvataggio in corso... + Saving... + + + Nessun video trovato per questo software. + No videos found for this software. + + + Elimina video + Delete Video + + + Sei sicuro di voler eliminare questo video? Questa azione non può essere annullata. + Are you sure you want to delete this video? This action cannot be undone. + + + Video collegato con successo. + Video linked successfully. + + + Video aggiornato con successo. + Video updated successfully. + + + Video eliminato con successo. + Video deleted successfully. + + + Impossibile collegare il video. + Failed to link video. + + + Impossibile aggiornare il video. + Failed to update video. + + + Impossibile eliminare il video. + Failed to delete video. + diff --git a/Marechai/Services/SoftwareService.cs b/Marechai/Services/SoftwareService.cs index 41a04b35..d6837ad0 100644 --- a/Marechai/Services/SoftwareService.cs +++ b/Marechai/Services/SoftwareService.cs @@ -1514,4 +1514,69 @@ public class SoftwareService(Marechai.ApiClient.Client client, IRequestAdapter r return []; } } + + public async Task<(SoftwareVideoDto dto, string error)> CreateVideoAsync(int softwareId, string provider, + string videoId, + string title) + { + try + { + var dto = await client.Software[softwareId] + .Videos.PostAsync(new CreateSoftwareVideoRequest + { + Provider = provider, + VideoId = videoId, + Title = title + }); + + return (dto, null); + } + catch(ApiException ex) + { + return (null, ExtractErrorMessage(ex)); + } + catch(Exception ex) + { + return (null, ex.Message); + } + } + + public async Task<(bool succeeded, string error)> UpdateVideoTitleAsync(long id, string title) + { + try + { + await client.Software.Videos[id].PutAsync(new UpdateSoftwareVideoRequest + { + Title = title + }); + + return (true, null); + } + catch(ApiException ex) + { + return (false, ExtractErrorMessage(ex)); + } + catch(Exception ex) + { + return (false, ex.Message); + } + } + + public async Task<(bool succeeded, string error)> DeleteVideoAsync(long id) + { + try + { + await client.Software.Videos[id].DeleteAsync(); + + return (true, null); + } + catch(ApiException ex) + { + return (false, ExtractErrorMessage(ex)); + } + catch(Exception ex) + { + return (false, ex.Message); + } + } }