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);
+ }
+ }
}