Clips
This commit is contained in:
@ -8,11 +8,12 @@ using Nostr.Client.Json;
|
|||||||
using NostrStreamer.ApiModel;
|
using NostrStreamer.ApiModel;
|
||||||
using NostrStreamer.Database;
|
using NostrStreamer.Database;
|
||||||
using NostrStreamer.Services;
|
using NostrStreamer.Services;
|
||||||
|
using NostrStreamer.Services.Clips;
|
||||||
using NostrStreamer.Services.StreamManager;
|
using NostrStreamer.Services.StreamManager;
|
||||||
|
|
||||||
namespace NostrStreamer.Controllers;
|
namespace NostrStreamer.Controllers;
|
||||||
|
|
||||||
[Authorize]
|
[Authorize(AuthenticationSchemes = NostrAuth.Scheme)]
|
||||||
[EnableCors]
|
[EnableCors]
|
||||||
[Route("/api/nostr")]
|
[Route("/api/nostr")]
|
||||||
public class NostrController : Controller
|
public class NostrController : Controller
|
||||||
@ -21,13 +22,16 @@ public class NostrController : Controller
|
|||||||
private readonly Config _config;
|
private readonly Config _config;
|
||||||
private readonly StreamManagerFactory _streamManagerFactory;
|
private readonly StreamManagerFactory _streamManagerFactory;
|
||||||
private readonly UserService _userService;
|
private readonly UserService _userService;
|
||||||
|
private readonly IClipService _clipService;
|
||||||
|
|
||||||
public NostrController(StreamerContext db, Config config, StreamManagerFactory streamManager, UserService userService)
|
public NostrController(StreamerContext db, Config config, StreamManagerFactory streamManager, UserService userService,
|
||||||
|
IClipService clipService)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
_config = config;
|
_config = config;
|
||||||
_streamManagerFactory = streamManager;
|
_streamManagerFactory = streamManager;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
|
_clipService = clipService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("account")]
|
[HttpGet("account")]
|
||||||
@ -161,6 +165,19 @@ public class NostrController : Controller
|
|||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("clip/{id:guid}")]
|
||||||
|
public async Task<IActionResult> CreateClip([FromRoute] Guid id)
|
||||||
|
{
|
||||||
|
var pk = GetPubKey();
|
||||||
|
var clip = await _clipService.CreateClip(id, pk);
|
||||||
|
if (clip == default) return StatusCode(500);
|
||||||
|
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
url = clip.Url
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<User?> GetUser()
|
private async Task<User?> GetUser()
|
||||||
{
|
{
|
||||||
var pk = GetPubKey();
|
var pk = GetPubKey();
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace NostrStreamer.Database.Configuration;
|
||||||
|
|
||||||
|
public class UserStreamClipConfiguration : IEntityTypeConfiguration<UserStreamClip>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<UserStreamClip> builder)
|
||||||
|
{
|
||||||
|
builder.HasKey(a => a.Id);
|
||||||
|
builder.Property(a => a.Created)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.Property(a => a.TakenByPubkey)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.Property(a => a.Url)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.HasOne(a => a.UserStream)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(a => a.UserStreamId)
|
||||||
|
.HasPrincipalKey(a => a.Id);
|
||||||
|
}
|
||||||
|
}
|
@ -31,4 +31,6 @@ public class StreamerContext : DbContext
|
|||||||
public DbSet<UserStreamRecording> Recordings => Set<UserStreamRecording>();
|
public DbSet<UserStreamRecording> Recordings => Set<UserStreamRecording>();
|
||||||
|
|
||||||
public DbSet<UserStreamForwards> Forwards => Set<UserStreamForwards>();
|
public DbSet<UserStreamForwards> Forwards => Set<UserStreamForwards>();
|
||||||
|
|
||||||
|
public DbSet<UserStreamClip> Clips => Set<UserStreamClip>();
|
||||||
}
|
}
|
||||||
|
15
NostrStreamer/Database/UserStreamClip.cs
Normal file
15
NostrStreamer/Database/UserStreamClip.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
namespace NostrStreamer.Database;
|
||||||
|
|
||||||
|
public class UserStreamClip
|
||||||
|
{
|
||||||
|
public Guid Id { get; init; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public Guid UserStreamId { get; init; }
|
||||||
|
public UserStream UserStream { get; init; } = null!;
|
||||||
|
|
||||||
|
public DateTime Created { get; init; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
public string TakenByPubkey { get; init; } = null!;
|
||||||
|
|
||||||
|
public string Url { get; init; } = null!;
|
||||||
|
}
|
394
NostrStreamer/Migrations/20231208131841_Clips.Designer.cs
generated
Normal file
394
NostrStreamer/Migrations/20231208131841_Clips.Designer.cs
generated
Normal file
@ -0,0 +1,394 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using NostrStreamer.Database;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NostrStreamer.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(StreamerContext))]
|
||||||
|
[Migration("20231208131841_Clips")]
|
||||||
|
partial class Clips
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "7.0.8")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("NostrStreamer.Database.IngestEndpoint", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("App")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<List<string>>("Capabilities")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text[]");
|
||||||
|
|
||||||
|
b.Property<int>("Cost")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Forward")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("App")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Endpoints");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NostrStreamer.Database.Payment", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("PaymentHash")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasColumnType("numeric(20,0)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Invoice")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("IsPaid")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Nostr")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PubKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("PaymentHash");
|
||||||
|
|
||||||
|
b.HasIndex("PubKey");
|
||||||
|
|
||||||
|
b.ToTable("Payments");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NostrStreamer.Database.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("PubKey")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<long>("Balance")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("ContentWarning")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Goal")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Image")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("StreamKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Tags")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("TosAccepted")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<uint>("Version")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("xid")
|
||||||
|
.HasColumnName("xmin");
|
||||||
|
|
||||||
|
b.HasKey("PubKey");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NostrStreamer.Database.UserStream", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("EdgeIp")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("EndpointId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Ends")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Event")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ForwardClientId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastSegment")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("PubKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Starts")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("State")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("StreamId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Thumbnail")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("EndpointId");
|
||||||
|
|
||||||
|
b.HasIndex("PubKey");
|
||||||
|
|
||||||
|
b.ToTable("Streams");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NostrStreamer.Database.UserStreamClip", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("TakenByPubkey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Url")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserStreamId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserStreamId");
|
||||||
|
|
||||||
|
b.ToTable("Clips");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NostrStreamer.Database.UserStreamForwards", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Target")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("UserPubkey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserPubkey");
|
||||||
|
|
||||||
|
b.ToTable("Forwards");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NostrStreamer.Database.UserStreamGuest", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("PubKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Relay")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Role")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Sig")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("StreamId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal>("ZapSplit")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StreamId", "PubKey")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Guests");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NostrStreamer.Database.UserStreamRecording", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<double>("Duration")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Timestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Url")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserStreamId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserStreamId");
|
||||||
|
|
||||||
|
b.ToTable("Recordings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NostrStreamer.Database.Payment", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("NostrStreamer.Database.User", "User")
|
||||||
|
.WithMany("Payments")
|
||||||
|
.HasForeignKey("PubKey")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NostrStreamer.Database.UserStream", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("NostrStreamer.Database.IngestEndpoint", "Endpoint")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("EndpointId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("NostrStreamer.Database.User", "User")
|
||||||
|
.WithMany("Streams")
|
||||||
|
.HasForeignKey("PubKey")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Endpoint");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NostrStreamer.Database.UserStreamClip", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("NostrStreamer.Database.UserStream", "UserStream")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserStreamId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("UserStream");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NostrStreamer.Database.UserStreamForwards", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("NostrStreamer.Database.User", "User")
|
||||||
|
.WithMany("Forwards")
|
||||||
|
.HasForeignKey("UserPubkey")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NostrStreamer.Database.UserStreamGuest", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("NostrStreamer.Database.UserStream", "Stream")
|
||||||
|
.WithMany("Guests")
|
||||||
|
.HasForeignKey("StreamId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Stream");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NostrStreamer.Database.UserStreamRecording", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("NostrStreamer.Database.UserStream", "Stream")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserStreamId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Stream");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NostrStreamer.Database.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Forwards");
|
||||||
|
|
||||||
|
b.Navigation("Payments");
|
||||||
|
|
||||||
|
b.Navigation("Streams");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NostrStreamer.Database.UserStream", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Guests");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
48
NostrStreamer/Migrations/20231208131841_Clips.cs
Normal file
48
NostrStreamer/Migrations/20231208131841_Clips.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NostrStreamer.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Clips : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Clips",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
UserStreamId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
TakenByPubkey = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Url = table.Column<string>(type: "text", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Clips", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Clips_Streams_UserStreamId",
|
||||||
|
column: x => x.UserStreamId,
|
||||||
|
principalTable: "Streams",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Clips_UserStreamId",
|
||||||
|
table: "Clips",
|
||||||
|
column: "UserStreamId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Clips");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -187,6 +187,33 @@ namespace NostrStreamer.Migrations
|
|||||||
b.ToTable("Streams");
|
b.ToTable("Streams");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NostrStreamer.Database.UserStreamClip", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("TakenByPubkey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Url")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserStreamId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserStreamId");
|
||||||
|
|
||||||
|
b.ToTable("Clips");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NostrStreamer.Database.UserStreamForwards", b =>
|
modelBuilder.Entity("NostrStreamer.Database.UserStreamForwards", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@ -301,6 +328,17 @@ namespace NostrStreamer.Migrations
|
|||||||
b.Navigation("User");
|
b.Navigation("User");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NostrStreamer.Database.UserStreamClip", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("NostrStreamer.Database.UserStream", "UserStream")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserStreamId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("UserStream");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NostrStreamer.Database.UserStreamForwards", b =>
|
modelBuilder.Entity("NostrStreamer.Database.UserStreamForwards", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("NostrStreamer.Database.User", "User")
|
b.HasOne("NostrStreamer.Database.User", "User")
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="LNURL" Version="0.0.30" />
|
<PackageReference Include="LNURL" Version="0.0.34" />
|
||||||
<PackageReference Include="MaxMind.GeoIP2" Version="5.1.0" />
|
<PackageReference Include="MaxMind.GeoIP2" Version="5.1.0" />
|
||||||
<PackageReference Include="MediaFormatLibrary.Lib" Version="1.0.0" />
|
<PackageReference Include="MediaFormatLibrary.Lib" Version="1.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.0" />
|
||||||
@ -48,7 +48,7 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Nostr.Client" Version="1.4.2" />
|
<PackageReference Include="Nostr.Client" Version="2.0.0" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" />
|
||||||
<PackageReference Include="prometheus-net.AspNetCore" Version="8.0.1" />
|
<PackageReference Include="prometheus-net.AspNetCore" Version="8.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -8,6 +8,7 @@ using Nostr.Client.Client;
|
|||||||
using NostrStreamer.Database;
|
using NostrStreamer.Database;
|
||||||
using NostrStreamer.Services;
|
using NostrStreamer.Services;
|
||||||
using NostrStreamer.Services.Background;
|
using NostrStreamer.Services.Background;
|
||||||
|
using NostrStreamer.Services.Clips;
|
||||||
using NostrStreamer.Services.Dvr;
|
using NostrStreamer.Services.Dvr;
|
||||||
using NostrStreamer.Services.StreamManager;
|
using NostrStreamer.Services.StreamManager;
|
||||||
using NostrStreamer.Services.Thumbnail;
|
using NostrStreamer.Services.Thumbnail;
|
||||||
@ -71,9 +72,12 @@ internal static class Program
|
|||||||
services.AddTransient<StreamManagerFactory>();
|
services.AddTransient<StreamManagerFactory>();
|
||||||
services.AddTransient<UserService>();
|
services.AddTransient<UserService>();
|
||||||
|
|
||||||
|
// dvr services
|
||||||
|
services.AddTransient<IDvrStore, S3DvrStore>();
|
||||||
|
|
||||||
|
// thumbnail services
|
||||||
services.AddTransient<IThumbnailService, S3ThumbnailService>();
|
services.AddTransient<IThumbnailService, S3ThumbnailService>();
|
||||||
services.AddHostedService<ThumbnailGenerator>();
|
services.AddHostedService<ThumbnailGenerator>();
|
||||||
services.AddTransient<IDvrStore, S3DvrStore>();
|
|
||||||
|
|
||||||
// lnd services
|
// lnd services
|
||||||
services.AddSingleton<LndNode>();
|
services.AddSingleton<LndNode>();
|
||||||
@ -82,6 +86,10 @@ internal static class Program
|
|||||||
// game services
|
// game services
|
||||||
services.AddSingleton<GameDb>();
|
services.AddSingleton<GameDb>();
|
||||||
|
|
||||||
|
// clip services
|
||||||
|
services.AddTransient<ClipGenerator>();
|
||||||
|
services.AddTransient<IClipService, S3ClipService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
|
34
NostrStreamer/Services/Clips/ClipGenerator.cs
Normal file
34
NostrStreamer/Services/Clips/ClipGenerator.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using FFMpegCore;
|
||||||
|
using NostrStreamer.Database;
|
||||||
|
|
||||||
|
namespace NostrStreamer.Services.Clips;
|
||||||
|
|
||||||
|
public class ClipGenerator
|
||||||
|
{
|
||||||
|
private readonly ILogger<ClipGenerator> _logger;
|
||||||
|
private readonly Config _config;
|
||||||
|
|
||||||
|
public ClipGenerator(ILogger<ClipGenerator> logger, Config config)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GenerateClip(UserStream stream)
|
||||||
|
{
|
||||||
|
const int clipLength = 20;
|
||||||
|
var path = Path.ChangeExtension(Path.GetTempFileName(), ".mp4");
|
||||||
|
var cmd = FFMpegArguments
|
||||||
|
.FromUrlInput(new Uri(_config.DataHost, $"stream/{stream.Id}.m3u8"),
|
||||||
|
inOpt =>
|
||||||
|
{
|
||||||
|
inOpt.WithCustomArgument($"-ss -{clipLength}");
|
||||||
|
})
|
||||||
|
.OutputToFile(path, true, o => { o.WithDuration(TimeSpan.FromSeconds(clipLength)); })
|
||||||
|
.CancellableThrough(new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token);
|
||||||
|
|
||||||
|
_logger.LogInformation("Running command {cmd}", cmd.Arguments);
|
||||||
|
await cmd.ProcessAsynchronously();
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
8
NostrStreamer/Services/Clips/IClipService.cs
Normal file
8
NostrStreamer/Services/Clips/IClipService.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace NostrStreamer.Services.Clips;
|
||||||
|
|
||||||
|
public interface IClipService
|
||||||
|
{
|
||||||
|
Task<ClipResult?> CreateClip(Guid streamId, string takenBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ClipResult(Uri Url);
|
80
NostrStreamer/Services/Clips/S3ClipService.cs
Normal file
80
NostrStreamer/Services/Clips/S3ClipService.cs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
using Amazon.S3;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NostrStreamer.Database;
|
||||||
|
|
||||||
|
namespace NostrStreamer.Services.Clips;
|
||||||
|
|
||||||
|
public class S3ClipService : IClipService
|
||||||
|
{
|
||||||
|
private readonly ClipGenerator _generator;
|
||||||
|
private readonly AmazonS3Client _client;
|
||||||
|
private readonly Config _config;
|
||||||
|
private readonly StreamerContext _context;
|
||||||
|
|
||||||
|
public S3ClipService(ClipGenerator generator, Config config, StreamerContext context)
|
||||||
|
{
|
||||||
|
_generator = generator;
|
||||||
|
_client = config.S3Store.CreateClient();
|
||||||
|
;
|
||||||
|
_config = config;
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ClipResult?> CreateClip(Guid streamId, string takenBy)
|
||||||
|
{
|
||||||
|
var stream = await _context.Streams
|
||||||
|
.Include(a => a.User)
|
||||||
|
.Include(a => a.Endpoint)
|
||||||
|
.FirstOrDefaultAsync(a => a.Id == streamId);
|
||||||
|
|
||||||
|
if (stream == default)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tmpClip = await _generator.GenerateClip(stream);
|
||||||
|
|
||||||
|
var clipId = Guid.NewGuid();
|
||||||
|
var s3Path = $"{stream.Id}/clips/{clipId}.mp4";
|
||||||
|
|
||||||
|
await using var fs = new FileStream(tmpClip, FileMode.Open, FileAccess.Read);
|
||||||
|
await _client.PutObjectAsync(new()
|
||||||
|
{
|
||||||
|
BucketName = _config.S3Store.BucketName,
|
||||||
|
Key = s3Path,
|
||||||
|
InputStream = fs,
|
||||||
|
AutoCloseStream = false,
|
||||||
|
AutoResetStreamPosition = false,
|
||||||
|
ContentType = "video/mp4",
|
||||||
|
DisablePayloadSigning = _config.S3Store.DisablePayloadSigning
|
||||||
|
});
|
||||||
|
|
||||||
|
var uri = _client.GetPreSignedURL(new()
|
||||||
|
{
|
||||||
|
BucketName = _config.S3Store.BucketName,
|
||||||
|
Key = s3Path,
|
||||||
|
Expires = DateTime.UtcNow.AddYears(1000)
|
||||||
|
});
|
||||||
|
|
||||||
|
var ub = new UriBuilder(uri)
|
||||||
|
{
|
||||||
|
Scheme = _config.S3Store.PublicHost.Scheme,
|
||||||
|
Host = _config.S3Store.PublicHost.Host,
|
||||||
|
Port = _config.S3Store.PublicHost.Port
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var clipObj = new UserStreamClip()
|
||||||
|
{
|
||||||
|
Id = clipId,
|
||||||
|
UserStreamId = stream.Id,
|
||||||
|
TakenByPubkey = takenBy,
|
||||||
|
Url = ub.Uri.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.Clips.Add(clipObj);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return new(ub.Uri);
|
||||||
|
}
|
||||||
|
}
|
@ -41,7 +41,8 @@ public class StreamEventBuilder
|
|||||||
new("status", status),
|
new("status", status),
|
||||||
new("p", user.PubKey, "", "host"),
|
new("p", user.PubKey, "", "host"),
|
||||||
new("relays", _config.Relays),
|
new("relays", _config.Relays),
|
||||||
new("starts", new DateTimeOffset(stream.Starts).ToUnixTimeSeconds().ToString())
|
new("starts", new DateTimeOffset(stream.Starts).ToUnixTimeSeconds().ToString()),
|
||||||
|
new("service", new Uri(_config.ApiHost, "/api/nostr").ToString())
|
||||||
};
|
};
|
||||||
|
|
||||||
if (status == "live")
|
if (status == "live")
|
||||||
|
Reference in New Issue
Block a user