Cleanup recordings
This commit is contained in:
@ -56,6 +56,8 @@ public class Config
|
|||||||
public Uri SnortApi { get; init; } = null!;
|
public Uri SnortApi { get; init; } = null!;
|
||||||
|
|
||||||
public Uri? DiscordLiveWebhook { get; init; }
|
public Uri? DiscordLiveWebhook { get; init; }
|
||||||
|
|
||||||
|
public int RetainRecordingsDays { get; init; } = 90;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class VapidKeyDetails
|
public class VapidKeyDetails
|
||||||
|
@ -18,7 +18,7 @@ public class UserStreamRecordingConfiguration : IEntityTypeConfiguration<UserStr
|
|||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
builder.HasOne(a => a.Stream)
|
builder.HasOne(a => a.Stream)
|
||||||
.WithMany()
|
.WithMany(a => a.Recordings)
|
||||||
.HasForeignKey(a => a.UserStreamId);
|
.HasForeignKey(a => a.UserStreamId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,8 @@ public class UserStream
|
|||||||
public decimal Length { get; set; }
|
public decimal Length { get; set; }
|
||||||
|
|
||||||
public List<UserStreamGuest> Guests { get; init; } = new();
|
public List<UserStreamGuest> Guests { get; init; } = new();
|
||||||
|
|
||||||
|
public List<UserStreamRecording> Recordings { get; init; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum UserStreamState
|
public enum UserStreamState
|
||||||
|
@ -113,6 +113,12 @@ public static class Extensions
|
|||||||
|
|
||||||
return new NostrEventIdentifier(ev.Id!, ev.Pubkey, null, ev.Kind);
|
return new NostrEventIdentifier(ev.Id!, ev.Pubkey, null, ev.Kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static NostrIdentifier ToIdentifier(this UserStream stream)
|
||||||
|
{
|
||||||
|
var ev = NostrJson.Deserialize<NostrEvent>(stream.Event);
|
||||||
|
return ev!.ToIdentifier();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Variant
|
public class Variant
|
||||||
|
@ -53,6 +53,9 @@ namespace NostrStreamer.Migrations
|
|||||||
principalTable: "Endpoints",
|
principalTable: "Endpoints",
|
||||||
principalColumn: "Id",
|
principalColumn: "Id",
|
||||||
onDelete: ReferentialAction.Cascade);
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.Sql(
|
||||||
|
"INSERT INTO public.\"Endpoints\"(\"Id\", \"Name\", \"App\", \"Forward\", \"Cost\", \"Capabilities\") VALUES(gen_random_uuid(), 'basic', 'basic', 'base.in.zap.stream', 1000, '{variant:source,dvr:source}');");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
466
NostrStreamer/Migrations/20240226120459_StreamRecordings.Designer.cs
generated
Normal file
466
NostrStreamer/Migrations/20240226120459_StreamRecordings.Designer.cs
generated
Normal file
@ -0,0 +1,466 @@
|
|||||||
|
// <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("20240226120459_StreamRecordings")]
|
||||||
|
partial class StreamRecordings
|
||||||
|
{
|
||||||
|
/// <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.PushSubscription", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Auth")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Endpoint")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUsed")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Pubkey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("Scope")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("PushSubscriptions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NostrStreamer.Database.PushSubscriptionTarget", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("SubscriberPubkey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("TargetPubkey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TargetPubkey");
|
||||||
|
|
||||||
|
b.HasIndex("SubscriberPubkey", "TargetPubkey")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("PushSubscriptionTargets");
|
||||||
|
});
|
||||||
|
|
||||||
|
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<decimal>("Length")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal>("MilliSatsCollected")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
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("Recordings")
|
||||||
|
.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");
|
||||||
|
|
||||||
|
b.Navigation("Recordings");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
NostrStreamer/Migrations/20240226120459_StreamRecordings.cs
Normal file
22
NostrStreamer/Migrations/20240226120459_StreamRecordings.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NostrStreamer.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class StreamRecordings : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -434,7 +434,7 @@ namespace NostrStreamer.Migrations
|
|||||||
modelBuilder.Entity("NostrStreamer.Database.UserStreamRecording", b =>
|
modelBuilder.Entity("NostrStreamer.Database.UserStreamRecording", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("NostrStreamer.Database.UserStream", "Stream")
|
b.HasOne("NostrStreamer.Database.UserStream", "Stream")
|
||||||
.WithMany()
|
.WithMany("Recordings")
|
||||||
.HasForeignKey("UserStreamId")
|
.HasForeignKey("UserStreamId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
@ -454,6 +454,8 @@ namespace NostrStreamer.Migrations
|
|||||||
modelBuilder.Entity("NostrStreamer.Database.UserStream", b =>
|
modelBuilder.Entity("NostrStreamer.Database.UserStream", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Guests");
|
b.Navigation("Guests");
|
||||||
|
|
||||||
|
b.Navigation("Recordings");
|
||||||
});
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
|
@ -105,6 +105,7 @@ internal static class Program
|
|||||||
|
|
||||||
// dvr services
|
// dvr services
|
||||||
services.AddTransient<IDvrStore, S3DvrStore>();
|
services.AddTransient<IDvrStore, S3DvrStore>();
|
||||||
|
services.AddHostedService<RecordingDeleter>();
|
||||||
|
|
||||||
// thumbnail services
|
// thumbnail services
|
||||||
services.AddTransient<IThumbnailService, S3ThumbnailService>();
|
services.AddTransient<IThumbnailService, S3ThumbnailService>();
|
||||||
|
70
NostrStreamer/Services/Background/RecordingDeleter.cs
Normal file
70
NostrStreamer/Services/Background/RecordingDeleter.cs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NostrStreamer.Database;
|
||||||
|
using NostrStreamer.Services.Dvr;
|
||||||
|
|
||||||
|
namespace NostrStreamer.Services.Background;
|
||||||
|
|
||||||
|
public class RecordingDeleter : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly ILogger<RecordingDeleter> _logger;
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly Config _config;
|
||||||
|
|
||||||
|
public RecordingDeleter(ILogger<RecordingDeleter> logger, IServiceScopeFactory scopeFactory, Config config)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
await using var db = scope.ServiceProvider.GetRequiredService<StreamerContext>();
|
||||||
|
var dvrStore = scope.ServiceProvider.GetRequiredService<IDvrStore>();
|
||||||
|
|
||||||
|
var olderThan = DateTime.UtcNow.Subtract(TimeSpan.FromDays(_config.RetainRecordingsDays));
|
||||||
|
var toDelete = await db.Streams
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(a => a.Starts < olderThan)
|
||||||
|
.Where(a => a.Recordings.Count > 0)
|
||||||
|
.ToListAsync(cancellationToken: stoppingToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("Starting delete of {n:###0} stream recordings", toDelete.Count);
|
||||||
|
|
||||||
|
foreach (var stream in toDelete)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var streamRecordings = await db.Streams
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(a => a.Recordings)
|
||||||
|
.SingleAsync(a => a.Id == stream.Id, cancellationToken: stoppingToken);
|
||||||
|
var deleted = await dvrStore.DeleteRecordings(streamRecordings);
|
||||||
|
|
||||||
|
await db.Recordings
|
||||||
|
.Where(a => deleted.Contains(a.Id))
|
||||||
|
.ExecuteDeleteAsync(cancellationToken: stoppingToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("Deleted {n}/{m} recordings from stream {id}", deleted.Count,
|
||||||
|
streamRecordings.Recordings.Count, stream.Id);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to delete stream recordings {id} {msg}", stream.Id, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to run background deleter");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromMinutes(30), stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,13 @@ public interface IDvrStore
|
|||||||
/// <param name="source"></param>
|
/// <param name="source"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<UploadResult> UploadRecording(UserStream stream, Uri source);
|
Task<UploadResult> UploadRecording(UserStream stream, Uri source);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delete all recordings from the storage by stream
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stream"></param>
|
||||||
|
/// <returns>List of deleted recordings</returns>
|
||||||
|
Task<List<Guid>> DeleteRecordings(UserStream stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
public record UploadResult(Uri Result, double Duration);
|
public record UploadResult(Uri Result, double Duration);
|
||||||
|
@ -87,9 +87,29 @@ public class S3DvrStore : IDvrStore
|
|||||||
fs.Close();
|
fs.Close();
|
||||||
File.Delete(tmpFile);
|
File.Delete(tmpFile);
|
||||||
|
|
||||||
_logger.LogInformation("download={tc:#,##0}ms, probe={pc:#,##0}ms, upload={uc:#,##0}ms", tsDownload.TotalMilliseconds,
|
_logger.LogInformation("download={tc:#,##0}ms, probe={pc:#,##0}ms, upload={uc:#,##0}ms",
|
||||||
|
tsDownload.TotalMilliseconds,
|
||||||
tsProbe.TotalMilliseconds, tsUpload.TotalMilliseconds);
|
tsProbe.TotalMilliseconds, tsUpload.TotalMilliseconds);
|
||||||
|
|
||||||
return new(ub.Uri, probe.Duration.TotalSeconds);
|
return new(ub.Uri, probe.Duration.TotalSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<Guid>> DeleteRecordings(UserStream stream)
|
||||||
|
{
|
||||||
|
var deleted = new List<Guid>();
|
||||||
|
foreach (var batch in stream.Recordings.Select((a, i) => (Batch: i / 1000, Item: a)).GroupBy(a => a.Batch))
|
||||||
|
{
|
||||||
|
var res = await _client.DeleteObjectsAsync(new()
|
||||||
|
{
|
||||||
|
BucketName = _config.BucketName,
|
||||||
|
Objects = batch.Select(a => new KeyVersion()
|
||||||
|
{
|
||||||
|
Key = $"{stream.Id}/{a.Item.Id}.ts"
|
||||||
|
}).ToList()
|
||||||
|
});
|
||||||
|
deleted.AddRange(res.DeletedObjects.Select(a => Guid.Parse(Path.GetFileNameWithoutExtension(a.Key))));
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
}
|
}
|
@ -102,6 +102,22 @@ public class StreamEventBuilder
|
|||||||
return ev.Sign(pk);
|
return ev.Sign(pk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public NostrEvent CreateDm(UserStream stream, string message)
|
||||||
|
{
|
||||||
|
var pk = NostrPrivateKey.FromBech32(_config.PrivateKey);
|
||||||
|
var ev = new NostrEvent
|
||||||
|
{
|
||||||
|
Kind = NostrKind.EncryptedDm,
|
||||||
|
Content = message,
|
||||||
|
CreatedAt = DateTime.Now,
|
||||||
|
Tags = new NostrEventTags(
|
||||||
|
new NostrEventTag("p", stream.PubKey)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
return ev.EncryptDirect(pk, NostrPublicKey.FromHex(stream.PubKey)).Sign(pk);
|
||||||
|
}
|
||||||
|
|
||||||
public void BroadcastEvent(NostrEvent ev)
|
public void BroadcastEvent(NostrEvent ev)
|
||||||
{
|
{
|
||||||
_nostrClient.Send(new NostrEventRequest(ev));
|
_nostrClient.Send(new NostrEventRequest(ev));
|
||||||
|
@ -107,6 +107,16 @@ public class NostrStreamManager : IStreamManager
|
|||||||
_logger.LogInformation("Stream stopped for: {pubkey}", _context.User.PubKey);
|
_logger.LogInformation("Stream stopped for: {pubkey}", _context.User.PubKey);
|
||||||
|
|
||||||
await UpdateStreamState(UserStreamState.Ended);
|
await UpdateStreamState(UserStreamState.Ended);
|
||||||
|
|
||||||
|
// send DM with link to summary
|
||||||
|
var msg =
|
||||||
|
(_context.UserStream.Thumbnail != default ? $"{_context.UserStream.Thumbnail}\n" : "") +
|
||||||
|
$"Your stream summary is available here: https://zap.stream/summary/{_context.UserStream.ToIdentifier().ToBech32()}\n\n" +
|
||||||
|
$"You paid {_context.UserStream.MilliSatsCollected / 1000:#,##0.###} sats for this stream!\n\n" +
|
||||||
|
$"You streamed for {_context.UserStream.Length / 60:#,##0} mins!";
|
||||||
|
|
||||||
|
var chat = _eventBuilder.CreateDm(_context.UserStream, msg);
|
||||||
|
_eventBuilder.BroadcastEvent(chat);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ConsumeQuota(double duration)
|
public async Task ConsumeQuota(double duration)
|
||||||
|
@ -29,13 +29,13 @@
|
|||||||
"MacaroonPath": "/Users/kieran/.polar/networks/1/volumes/lnd/bob/data/chain/bitcoin/regtest/admin.macaroon"
|
"MacaroonPath": "/Users/kieran/.polar/networks/1/volumes/lnd/bob/data/chain/bitcoin/regtest/admin.macaroon"
|
||||||
},
|
},
|
||||||
"S3Store": {
|
"S3Store": {
|
||||||
"ServiceUrl": "http://localhost:9010",
|
"ServiceUrl": "http://localhost:9000",
|
||||||
"AccessKey": "TQcxug1ZAXfnZ5bvc9n5",
|
"AccessKey": "GfvDg9FoarDnNZj5axOq",
|
||||||
"SecretKey": "p7EK4qew6DBkBPqrpRPuJgTOc6ChUlfIcEdAwE7K",
|
"SecretKey": "1XtAmUcJDPHWEarFLmOx75z6Ok3FyMEdP7cvCkQP",
|
||||||
"PublicHost": "http://localhost:9010"
|
"PublicHost": "http://localhost:9000"
|
||||||
},
|
},
|
||||||
"SnortApi": "https://api.snort.social",
|
"SnortApi": "https://api.snort.social",
|
||||||
"GeoIpDatabase": "/Users/kieran/Downloads/GeoLite2-City_20230801/GeoLite2-City.mmdb",
|
"GeoIpDatabase": "C:\\Users\\Kieran\\Downloads\\GeoLite2-City.mmdb",
|
||||||
"Edges": [
|
"Edges": [
|
||||||
{
|
{
|
||||||
"Name": "US0",
|
"Name": "US0",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
volumes:
|
volumes:
|
||||||
minio-dvr:
|
minio-dvr:
|
||||||
|
postgres:
|
||||||
services:
|
services:
|
||||||
srs-origin:
|
srs-origin:
|
||||||
image: ossrs/srs:5
|
image: ossrs/srs:5
|
||||||
@ -29,18 +30,20 @@ services:
|
|||||||
- "POSTGRES_HOST_AUTH_METHOD=trust"
|
- "POSTGRES_HOST_AUTH_METHOD=trust"
|
||||||
ports:
|
ports:
|
||||||
- "5431:5432"
|
- "5431:5432"
|
||||||
|
volumes:
|
||||||
|
- "postgres:/var/lib/postgresql/data"
|
||||||
minio:
|
minio:
|
||||||
image: quay.io/minio/minio
|
image: quay.io/minio/minio
|
||||||
command:
|
command:
|
||||||
- "server"
|
- "server"
|
||||||
- "/data"
|
- "/data"
|
||||||
- "--console-address"
|
- "--console-address"
|
||||||
- ":9001"
|
- ":9011"
|
||||||
environment:
|
environment:
|
||||||
- "MINIO_SERVER_URL=http://localhost:9010"
|
- "MINIO_SERVER_URL=http://localhost:9000"
|
||||||
ports:
|
ports:
|
||||||
- "9010:9000"
|
- "9000:9000"
|
||||||
- "9011:9001"
|
- "9011:9011"
|
||||||
volumes:
|
volumes:
|
||||||
- "minio-dvr:/data"
|
- "minio-dvr:/data"
|
||||||
redis:
|
redis:
|
||||||
|
Reference in New Issue
Block a user