feat: stream-keys

This commit is contained in:
2024-08-28 10:02:49 +01:00
parent 015b75f894
commit 97e43c8cbd
27 changed files with 1723 additions and 87 deletions

View File

@ -4,9 +4,6 @@ namespace NostrStreamer.ApiModel;
public class Account public class Account
{ {
[JsonProperty("event")]
public PatchEvent? Event { get; init; }
[JsonProperty("endpoints")] [JsonProperty("endpoints")]
public List<AccountEndpoint> Endpoints { get; init; } = new(); public List<AccountEndpoint> Endpoints { get; init; } = new();

View File

@ -0,0 +1,8 @@
namespace NostrStreamer.ApiModel;
public class CreateStreamKeyRequest
{
public PatchEvent Event { get; init; } = null!;
public DateTime? Expires { get; init; }
}

View File

@ -4,6 +4,9 @@ namespace NostrStreamer.ApiModel;
public class PatchEvent public class PatchEvent
{ {
[JsonProperty("id")]
public Guid Id { get; init; }
[JsonProperty("title")] [JsonProperty("title")]
public string Title { get; init; } = null!; public string Title { get; init; } = null!;

View File

@ -27,10 +27,12 @@ public class NostrController : Controller
private readonly IClipService _clipService; private readonly IClipService _clipService;
private readonly ILogger<NostrController> _logger; private readonly ILogger<NostrController> _logger;
private readonly PushSender _pushSender; private readonly PushSender _pushSender;
private readonly StreamEventBuilder _eventBuilder;
public NostrController(StreamerContext db, Config config, StreamManagerFactory streamManager, public NostrController(StreamerContext db, Config config, StreamManagerFactory streamManager,
UserService userService, UserService userService,
IClipService clipService, ILogger<NostrController> logger, PushSender pushSender) IClipService clipService, ILogger<NostrController> logger, PushSender pushSender,
StreamEventBuilder eventBuilder)
{ {
_db = db; _db = db;
_config = config; _config = config;
@ -39,6 +41,7 @@ public class NostrController : Controller
_clipService = clipService; _clipService = clipService;
_logger = logger; _logger = logger;
_pushSender = pushSender; _pushSender = pushSender;
_eventBuilder = eventBuilder;
} }
[HttpGet("account")] [HttpGet("account")]
@ -57,15 +60,6 @@ public class NostrController : Controller
var account = new Account var account = new Account
{ {
Event = new PatchEvent()
{
Title = user.Title ?? "",
Summary = user.Summary ?? "",
Image = user.Image ?? "",
ContentWarning = user.ContentWarning,
Tags = user.SplitTags(),
Goal = user.Goal
},
Endpoints = endpoints.Select(a => new AccountEndpoint Endpoints = endpoints.Select(a => new AccountEndpoint
{ {
Name = a.Name, Name = a.Name,
@ -100,9 +94,9 @@ public class NostrController : Controller
var pubkey = GetPubKey(); var pubkey = GetPubKey();
if (string.IsNullOrEmpty(pubkey)) return Unauthorized(); if (string.IsNullOrEmpty(pubkey)) return Unauthorized();
await _userService.UpdateStreamInfo(pubkey, req);
try try
{ {
await _userService.UpdateStreamInfo(pubkey, req);
var streamManager = await _streamManagerFactory.ForCurrentStream(pubkey); var streamManager = await _streamManagerFactory.ForCurrentStream(pubkey);
await streamManager.UpdateEvent(); await streamManager.UpdateEvent();
} }
@ -404,6 +398,91 @@ public class NostrController : Controller
} }
} }
[HttpGet("keys")]
public async Task<IActionResult> ListStreamKeys([FromQuery] int page = 0, [FromQuery] int pageSize = 100)
{
var userPubkey = GetPubKey();
if (string.IsNullOrEmpty(userPubkey))
return BadRequest();
try
{
var keys = await _db.StreamKeys
.AsNoTracking()
.Include(a => a.UserStream)
.Where(a => a.UserPubkey == userPubkey)
.Skip(page * pageSize)
.Take(pageSize)
.Select(a =>
new
{
a.Id,
a.Created,
a.Key,
a.Expires,
Stream = a.UserStream.Event
})
.ToListAsync();
return Json(new
{
items = keys,
page, pageSize
});
}
catch (Exception e)
{
return Json(new
{
error = e.Message
});
}
}
[HttpPost("keys")]
public async Task<IActionResult> CreateStreamKey([FromBody] CreateStreamKeyRequest req)
{
var userPubkey = GetPubKey();
if (string.IsNullOrEmpty(userPubkey))
return BadRequest();
try
{
var newStream = new UserStream()
{
PubKey = userPubkey,
State = UserStreamState.Planned,
};
newStream.PatchStream(req.Event);
var ev = _eventBuilder.CreateStreamEvent(newStream);
newStream.Event = NostrJson.Serialize(ev) ?? "";
var newKey = new UserStreamKey()
{
Expires = req.Expires,
Key = Guid.NewGuid().ToString(),
StreamId = newStream.Id,
UserPubkey = userPubkey
};
_db.Streams.Add(newStream);
_db.StreamKeys.Add(newKey);
await _db.SaveChangesAsync();
return Json(new
{
newKey.Key,
newStream.Event
});
}
catch (Exception e)
{
return Json(new
{
error = e.Message
});
}
}
private async Task<User?> GetUser() private async Task<User?> GetUser()
{ {
var pk = GetPubKey(); var pk = GetPubKey();

View File

@ -41,7 +41,7 @@ public class PlaylistController : Controller
var streamManager = await _streamManagerFactory.ForStream(id); var streamManager = await _streamManagerFactory.ForStream(id);
var userStream = streamManager.GetStream(); var userStream = streamManager.GetStream();
var path = $"/{userStream.Endpoint.App}/{variant}/{userStream.User.StreamKey}.m3u8"; var path = $"/{userStream.Endpoint.App}/{variant}/{userStream.Key}.m3u8";
var ub = new UriBuilder(_config.SrsHttpHost) var ub = new UriBuilder(_config.SrsHttpHost)
{ {
Path = path, Path = path,
@ -130,7 +130,7 @@ public class PlaylistController : Controller
foreach (var variant in userStream.Endpoint.GetVariants().OrderBy(a => a.Bandwidth)) foreach (var variant in userStream.Endpoint.GetVariants().OrderBy(a => a.Bandwidth))
{ {
var stream = streams.FirstOrDefault(a => var stream = streams.FirstOrDefault(a =>
a.Name == userStream.User.StreamKey && a.App == $"{userStream.Endpoint.App}/{variant.SourceName}"); a.Name == userStream.Key && a.App == $"{userStream.Endpoint.App}/{variant.SourceName}");
var resArg = stream?.Video != default ? $"RESOLUTION={stream.Video?.Width}x{stream.Video?.Height}" : var resArg = stream?.Video != default ? $"RESOLUTION={stream.Video?.Width}x{stream.Video?.Height}" :
variant.ToResolutionArg(); variant.ToResolutionArg();
@ -171,7 +171,7 @@ public class PlaylistController : Controller
var streamManager = await _streamManagerFactory.ForStream(id); var streamManager = await _streamManagerFactory.ForStream(id);
var userStream = streamManager.GetStream(); var userStream = streamManager.GetStream();
var path = $"/{userStream.Endpoint.App}/{variant}/{userStream.User.StreamKey}-{segment}"; var path = $"/{userStream.Endpoint.App}/{variant}/{userStream.Key}-{segment}";
await ProxyRequest(path); await ProxyRequest(path);
} }
catch catch
@ -253,7 +253,7 @@ public class PlaylistController : Controller
private async Task<string?> GetHlsCtx(UserStream stream) private async Task<string?> GetHlsCtx(UserStream stream)
{ {
var path = $"/{stream.Endpoint.App}/source/{stream.User.StreamKey}.m3u8"; var path = $"/{stream.Endpoint.App}/source/{stream.Key}.m3u8";
var ub = new Uri(_config.SrsHttpHost, path); var ub = new Uri(_config.SrsHttpHost, path);
var req = CreateProxyRequest(ub); var req = CreateProxyRequest(ub);
using var rsp = await _client.SendAsync(req); using var rsp = await _client.SendAsync(req);

View File

@ -29,8 +29,8 @@ public class PodcastController(StreamerContext db, Config config) : Controller
pod.LiveItem = new() pod.LiveItem = new()
{ {
Guid = stream.Id, Guid = stream.Id,
Title = stream.User.Title ?? "", Title = stream.Title ?? "",
Description = stream.User.Summary, Description = stream.Summary,
Status = stream.State.ToString().ToLower(), Status = stream.State.ToString().ToLower(),
Start = stream.Starts, Start = stream.Starts,
End = stream.Ends ?? new DateTime(), End = stream.Ends ?? new DateTime(),

View File

@ -8,8 +8,6 @@ public class UserStreamConfiguration : IEntityTypeConfiguration<UserStream>
public void Configure(EntityTypeBuilder<UserStream> builder) public void Configure(EntityTypeBuilder<UserStream> builder)
{ {
builder.HasKey(a => a.Id); builder.HasKey(a => a.Id);
builder.Property(a => a.StreamId)
.IsRequired();
builder.Property(a => a.Starts) builder.Property(a => a.Starts)
.IsRequired(); .IsRequired();
@ -35,6 +33,13 @@ public class UserStreamConfiguration : IEntityTypeConfiguration<UserStream>
builder.Property(a => a.AdmissionCost); builder.Property(a => a.AdmissionCost);
builder.Property(a => a.Title);
builder.Property(a => a.Image);
builder.Property(a => a.Summary);
builder.Property(a => a.ContentWarning);
builder.Property(a => a.Tags);
builder.Property(a => a.Goal);
builder.HasOne(a => a.Endpoint) builder.HasOne(a => a.Endpoint)
.WithMany() .WithMany()
.HasForeignKey(a => a.EndpointId); .HasForeignKey(a => a.EndpointId);

View File

@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace NostrStreamer.Database.Configuration;
public class UserStreamKeyConfiguration : IEntityTypeConfiguration<UserStreamKey>
{
public void Configure(EntityTypeBuilder<UserStreamKey> builder)
{
builder.HasKey(a => a.Id);
builder.Property(a => a.Key)
.IsRequired();
builder.Property(a => a.Created)
.IsRequired();
builder.Property(a => a.Expires)
.IsRequired(false);
builder.HasOne(a => a.UserStream)
.WithOne(a => a.StreamKey)
.HasPrincipalKey<UserStream>(a => a.Id)
.HasForeignKey<UserStreamKey>(a => a.StreamId);
builder.HasOne(a => a.User)
.WithMany(a => a.StreamKeys)
.HasForeignKey(a => a.UserPubkey)
.HasPrincipalKey(a => a.PubKey);
}
}

View File

@ -37,4 +37,6 @@ public class StreamerContext : DbContext
public DbSet<PushSubscription> PushSubscriptions => Set<PushSubscription>(); public DbSet<PushSubscription> PushSubscriptions => Set<PushSubscription>();
public DbSet<PushSubscriptionTarget> PushSubscriptionTargets => Set<PushSubscriptionTarget>(); public DbSet<PushSubscriptionTarget> PushSubscriptionTargets => Set<PushSubscriptionTarget>();
public DbSet<UserStreamKey> StreamKeys => Set<UserStreamKey>();
} }

View File

@ -67,4 +67,5 @@ public class User
public List<Payment> Payments { get; init; } = new(); public List<Payment> Payments { get; init; } = new();
public List<UserStream> Streams { get; init; } = new(); public List<UserStream> Streams { get; init; } = new();
public List<UserStreamForwards> Forwards { get; init; } = new(); public List<UserStreamForwards> Forwards { get; init; } = new();
} public List<UserStreamKey> StreamKeys { get; init; } = new();
}

View File

@ -7,14 +7,42 @@ public class UserStream
public string PubKey { get; init; } = null!; public string PubKey { get; init; } = null!;
public User User { get; init; } = null!; public User User { get; init; } = null!;
public string StreamId { get; init; } = null!;
public DateTime Starts { get; init; } = DateTime.UtcNow; public DateTime Starts { get; init; } = DateTime.UtcNow;
public DateTime? Ends { get; set; } public DateTime? Ends { get; set; }
public UserStreamState State { get; set; } public UserStreamState State { get; set; }
/// <summary>
/// Stream title
/// </summary>
public string? Title { get; set; }
/// <summary>
/// Stream summary
/// </summary>
public string? Summary { get; set; }
/// <summary>
/// Stream cover image
/// </summary>
public string? Image { get; set; }
/// <summary>
/// Comma seperated tags
/// </summary>
public string? Tags { get; set; }
/// <summary>
/// Any content warning tag (NIP-36)
/// </summary>
public string? ContentWarning { get; set; }
/// <summary>
/// Stream goal
/// </summary>
public string? Goal { get; set; }
/// <summary> /// <summary>
/// Nostr Event for this stream /// Nostr Event for this stream
/// </summary> /// </summary>
@ -25,7 +53,7 @@ public class UserStream
/// </summary> /// </summary>
public string? Thumbnail { get; set; } public string? Thumbnail { get; set; }
public Guid EndpointId { get; init; } public Guid EndpointId { get; set; }
public IngestEndpoint Endpoint { get; init; } = null!; public IngestEndpoint Endpoint { get; init; } = null!;
/// <summary> /// <summary>
@ -58,10 +86,15 @@ public class UserStream
public List<UserStreamGuest> Guests { get; init; } = new(); public List<UserStreamGuest> Guests { get; init; } = new();
public List<UserStreamRecording> Recordings { get; init; } = new(); public List<UserStreamRecording> Recordings { get; init; } = new();
public UserStreamKey? StreamKey { get; init; }
public string Key => StreamKey?.Key ?? User.StreamKey;
} }
public enum UserStreamState public enum UserStreamState
{ {
Unknown = 0,
Planned = 1, Planned = 1,
Live = 2, Live = 2,
Ended = 3 Ended = 3

View File

@ -0,0 +1,25 @@
namespace NostrStreamer.Database;
/// <summary>
/// Single use stream keys
/// </summary>
public class UserStreamKey
{
public Guid Id { get; init; } = Guid.NewGuid();
public string UserPubkey { get; init; } = null!;
public User User { get; init; } = null!;
public string Key { get; init; } = null!;
public DateTime Created { get; init; } = DateTime.UtcNow;
/// <summary>
/// Expiry of the key when it can no longer be used
/// </summary>
public DateTime? Expires { get; init; }
public Guid StreamId { get; init; }
public UserStream UserStream { get; init; } = null!;
}

View File

@ -3,6 +3,7 @@ using Amazon.Runtime;
using Amazon.S3; using Amazon.S3;
using Igdb; using Igdb;
using MaxMind.GeoIP2; using MaxMind.GeoIP2;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json; using Newtonsoft.Json;
using Nostr.Client.Identifiers; using Nostr.Client.Identifiers;
using Nostr.Client.Json; using Nostr.Client.Json;
@ -49,10 +50,11 @@ public static class Extensions
}); });
} }
public static string[] SplitTags(this User user) public static string[] SplitTags(this UserStream stream)
{ {
return !string.IsNullOrEmpty(user.Tags) ? return !string.IsNullOrEmpty(stream.Tags)
user.Tags.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) : Array.Empty<string>(); ? stream.Tags.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
: Array.Empty<string>();
} }
public static (double lat, double lon)? GetLocation(this HttpContext ctx, IGeoIP2DatabaseReader db) public static (double lat, double lon)? GetLocation(this HttpContext ctx, IGeoIP2DatabaseReader db)
@ -90,14 +92,16 @@ public static class Extensions
var num1 = longitude * (Math.PI / 180.0); var num1 = longitude * (Math.PI / 180.0);
var d2 = otherLatitude * (Math.PI / 180.0); var d2 = otherLatitude * (Math.PI / 180.0);
var num2 = otherLongitude * (Math.PI / 180.0) - num1; var num2 = otherLongitude * (Math.PI / 180.0) - num1;
var d3 = Math.Pow(Math.Sin((d2 - d1) / 2.0), 2.0) + Math.Cos(d1) * Math.Cos(d2) * Math.Pow(Math.Sin(num2 / 2.0), 2.0); var d3 = Math.Pow(Math.Sin((d2 - d1) / 2.0), 2.0) +
Math.Cos(d1) * Math.Cos(d2) * Math.Pow(Math.Sin(num2 / 2.0), 2.0);
return 6376500.0 * (2.0 * Math.Atan2(Math.Sqrt(d3), Math.Sqrt(1.0 - d3))); return 6376500.0 * (2.0 * Math.Atan2(Math.Sqrt(d3), Math.Sqrt(1.0 - d3)));
} }
public static string GetHost(this NostrEvent ev) public static string GetHost(this NostrEvent ev)
{ {
var hostTag = ev.Tags!.FirstOrDefault(a => a.TagIdentifier == "p" && a.AdditionalData[2] == "host")?.AdditionalData[0]; var hostTag = ev.Tags!.FirstOrDefault(a => a.TagIdentifier == "p" && a.AdditionalData[2] == "host")
?.AdditionalData[0];
if (!string.IsNullOrEmpty(hostTag)) if (!string.IsNullOrEmpty(hostTag))
{ {
return hostTag; return hostTag;
@ -105,7 +109,7 @@ public static class Extensions
return ev.Pubkey!; return ev.Pubkey!;
} }
public static NostrIdentifier ToIdentifier(this NostrEvent ev) public static NostrIdentifier ToIdentifier(this NostrEvent ev)
{ {
if ((long)ev.Kind is >= 30_000 and < 40_000) if ((long)ev.Kind is >= 30_000 and < 40_000)
@ -132,6 +136,33 @@ public static class Extensions
Genres = a.Genres.Select(b => b.Name).ToList() Genres = a.Genres.Select(b => b.Name).ToList()
}; };
} }
public static async Task CopyLastStreamDetails(this UserStream stream, StreamerContext db)
{
var lastStream = await db.Streams
.AsNoTracking()
.Where(a => a.PubKey == stream.PubKey)
.OrderByDescending(a => a.Starts)
.FirstOrDefaultAsync();
stream.Title = lastStream?.Title;
stream.Summary = lastStream?.Summary;
stream.Image = lastStream?.Image;
stream.ContentWarning = lastStream?.ContentWarning;
stream.Tags = lastStream?.Tags;
stream.Goal = lastStream?.Goal;
}
public static void PatchStream(this UserStream stream, PatchEvent ev)
{
stream.Title = ev.Title;
stream.Summary = ev.Summary;
stream.Image = ev.Image;
stream.ContentWarning = ev.ContentWarning;
stream.Tags = ev.Tags.Length > 0 ? string.Join(',', ev.Tags) : null;
stream.Goal = ev.Goal;
}
} }
public class Variant public class Variant
@ -161,7 +192,8 @@ public class Variant
} }
var strSplit = str.Split(":"); var strSplit = str.Split(":");
if (strSplit.Length != 3 || !int.TryParse(strSplit[1][..^1], out var h) || !int.TryParse(strSplit[2], out var b)) if (strSplit.Length != 3 || !int.TryParse(strSplit[1][..^1], out var h) ||
!int.TryParse(strSplit[2], out var b))
{ {
throw new FormatException(); throw new FormatException();
} }
@ -203,4 +235,4 @@ public class Variant
return $"BANDWIDTH={Bandwidth * 1000}"; return $"BANDWIDTH={Bandwidth * 1000}";
} }
} }

View File

@ -0,0 +1,558 @@
// <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("20240821112332_StreamKeys")]
partial class StreamKeys
{
/// <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<decimal>("Fee")
.HasColumnType("numeric(20,0)");
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.StreamTickets", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("Token")
.HasColumnType("uuid");
b.Property<Guid>("UserStreamId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserStreamId");
b.ToTable("StreamTickets");
});
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<decimal?>("AdmissionCost")
.HasColumnType("numeric");
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.UserStreamKey", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("Expires")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("Key")
.HasColumnType("text");
b.Property<Guid>("StreamId")
.HasColumnType("uuid");
b.Property<string>("UserPubkey")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("StreamId")
.IsUnique();
b.HasIndex("UserPubkey");
b.ToTable("StreamKeys");
});
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.StreamTickets", b =>
{
b.HasOne("NostrStreamer.Database.UserStream", "UserStream")
.WithMany()
.HasForeignKey("UserStreamId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("UserStream");
});
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.UserStreamKey", b =>
{
b.HasOne("NostrStreamer.Database.UserStream", "UserStream")
.WithOne()
.HasForeignKey("NostrStreamer.Database.UserStreamKey", "StreamId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("NostrStreamer.Database.User", "User")
.WithMany("StreamKeys")
.HasForeignKey("UserPubkey")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
b.Navigation("UserStream");
});
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("StreamKeys");
b.Navigation("Streams");
});
modelBuilder.Entity("NostrStreamer.Database.UserStream", b =>
{
b.Navigation("Guests");
b.Navigation("Recordings");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,61 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NostrStreamer.Migrations
{
/// <inheritdoc />
public partial class StreamKeys : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "StreamKeys",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
UserPubkey = table.Column<string>(type: "text", nullable: false),
Key = table.Column<string>(type: "text", nullable: false),
Created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Expires = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
StreamId = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_StreamKeys", x => x.Id);
table.ForeignKey(
name: "FK_StreamKeys_Streams_StreamId",
column: x => x.StreamId,
principalTable: "Streams",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_StreamKeys_Users_UserPubkey",
column: x => x.UserPubkey,
principalTable: "Users",
principalColumn: "PubKey",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_StreamKeys_StreamId",
table: "StreamKeys",
column: "StreamId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_StreamKeys_UserPubkey",
table: "StreamKeys",
column: "UserPubkey");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "StreamKeys");
}
}
}

View File

@ -0,0 +1,557 @@
// <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("20240821132917_MigrateStreamInfo")]
partial class MigrateStreamInfo
{
/// <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<decimal>("Fee")
.HasColumnType("numeric(20,0)");
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.StreamTickets", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("Token")
.HasColumnType("uuid");
b.Property<Guid>("UserStreamId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserStreamId");
b.ToTable("StreamTickets");
});
modelBuilder.Entity("NostrStreamer.Database.User", b =>
{
b.Property<string>("PubKey")
.HasColumnType("text");
b.Property<long>("Balance")
.HasColumnType("bigint");
b.Property<string>("StreamKey")
.IsRequired()
.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<decimal?>("AdmissionCost")
.HasColumnType("numeric");
b.Property<string>("ContentWarning")
.HasColumnType("text");
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<string>("Goal")
.HasColumnType("text");
b.Property<string>("Image")
.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>("Summary")
.HasColumnType("text");
b.Property<string>("Tags")
.HasColumnType("text");
b.Property<string>("Thumbnail")
.HasColumnType("text");
b.Property<string>("Title")
.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.UserStreamKey", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("Expires")
.HasColumnType("timestamp with time zone");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("StreamId")
.HasColumnType("uuid");
b.Property<string>("UserPubkey")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("StreamId")
.IsUnique();
b.HasIndex("UserPubkey");
b.ToTable("StreamKeys");
});
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.StreamTickets", b =>
{
b.HasOne("NostrStreamer.Database.UserStream", "UserStream")
.WithMany()
.HasForeignKey("UserStreamId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("UserStream");
});
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.UserStreamKey", b =>
{
b.HasOne("NostrStreamer.Database.UserStream", "UserStream")
.WithOne("StreamKey")
.HasForeignKey("NostrStreamer.Database.UserStreamKey", "StreamId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("NostrStreamer.Database.User", "User")
.WithMany("StreamKeys")
.HasForeignKey("UserPubkey")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
b.Navigation("UserStream");
});
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("StreamKeys");
b.Navigation("Streams");
});
modelBuilder.Entity("NostrStreamer.Database.UserStream", b =>
{
b.Navigation("Guests");
b.Navigation("Recordings");
b.Navigation("StreamKey");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,149 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NostrStreamer.Migrations
{
/// <inheritdoc />
public partial class MigrateStreamInfo : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ContentWarning",
table: "Users");
migrationBuilder.DropColumn(
name: "Goal",
table: "Users");
migrationBuilder.DropColumn(
name: "Image",
table: "Users");
migrationBuilder.DropColumn(
name: "Summary",
table: "Users");
migrationBuilder.DropColumn(
name: "Tags",
table: "Users");
migrationBuilder.DropColumn(
name: "Title",
table: "Users");
migrationBuilder.DropColumn(
name: "StreamId",
table: "Streams");
migrationBuilder.AddColumn<string>(
name: "ContentWarning",
table: "Streams",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Goal",
table: "Streams",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Image",
table: "Streams",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Summary",
table: "Streams",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Tags",
table: "Streams",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Title",
table: "Streams",
type: "text",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ContentWarning",
table: "Streams");
migrationBuilder.DropColumn(
name: "Goal",
table: "Streams");
migrationBuilder.DropColumn(
name: "Image",
table: "Streams");
migrationBuilder.DropColumn(
name: "Summary",
table: "Streams");
migrationBuilder.DropColumn(
name: "Tags",
table: "Streams");
migrationBuilder.DropColumn(
name: "Title",
table: "Streams");
migrationBuilder.AddColumn<string>(
name: "ContentWarning",
table: "Users",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Goal",
table: "Users",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Image",
table: "Users",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Summary",
table: "Users",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Tags",
table: "Users",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Title",
table: "Users",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "StreamId",
table: "Streams",
type: "text",
nullable: false,
defaultValue: "");
}
}
}

View File

@ -238,6 +238,9 @@ namespace NostrStreamer.Migrations
b.Property<decimal?>("AdmissionCost") b.Property<decimal?>("AdmissionCost")
.HasColumnType("numeric"); .HasColumnType("numeric");
b.Property<string>("ContentWarning")
.HasColumnType("text");
b.Property<string>("EdgeIp") b.Property<string>("EdgeIp")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
@ -256,6 +259,12 @@ namespace NostrStreamer.Migrations
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.Property<string>("Goal")
.HasColumnType("text");
b.Property<string>("Image")
.HasColumnType("text");
b.Property<DateTime>("LastSegment") b.Property<DateTime>("LastSegment")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
@ -275,13 +284,18 @@ namespace NostrStreamer.Migrations
b.Property<int>("State") b.Property<int>("State")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<string>("StreamId") b.Property<string>("Summary")
.IsRequired() .HasColumnType("text");
b.Property<string>("Tags")
.HasColumnType("text"); .HasColumnType("text");
b.Property<string>("Thumbnail") b.Property<string>("Thumbnail")
.HasColumnType("text"); .HasColumnType("text");
b.Property<string>("Title")
.HasColumnType("text");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("EndpointId"); b.HasIndex("EndpointId");
@ -376,6 +390,39 @@ namespace NostrStreamer.Migrations
b.ToTable("Guests"); b.ToTable("Guests");
}); });
modelBuilder.Entity("NostrStreamer.Database.UserStreamKey", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("Expires")
.HasColumnType("timestamp with time zone");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("StreamId")
.HasColumnType("uuid");
b.Property<string>("UserPubkey")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("StreamId")
.IsUnique();
b.HasIndex("UserPubkey");
b.ToTable("StreamKeys");
});
modelBuilder.Entity("NostrStreamer.Database.UserStreamRecording", b => modelBuilder.Entity("NostrStreamer.Database.UserStreamRecording", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -476,6 +523,25 @@ namespace NostrStreamer.Migrations
b.Navigation("Stream"); b.Navigation("Stream");
}); });
modelBuilder.Entity("NostrStreamer.Database.UserStreamKey", b =>
{
b.HasOne("NostrStreamer.Database.UserStream", "UserStream")
.WithOne("StreamKey")
.HasForeignKey("NostrStreamer.Database.UserStreamKey", "StreamId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("NostrStreamer.Database.User", "User")
.WithMany("StreamKeys")
.HasForeignKey("UserPubkey")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
b.Navigation("UserStream");
});
modelBuilder.Entity("NostrStreamer.Database.UserStreamRecording", b => modelBuilder.Entity("NostrStreamer.Database.UserStreamRecording", b =>
{ {
b.HasOne("NostrStreamer.Database.UserStream", "Stream") b.HasOne("NostrStreamer.Database.UserStream", "Stream")
@ -493,6 +559,8 @@ namespace NostrStreamer.Migrations
b.Navigation("Payments"); b.Navigation("Payments");
b.Navigation("StreamKeys");
b.Navigation("Streams"); b.Navigation("Streams");
}); });
@ -501,6 +569,8 @@ namespace NostrStreamer.Migrations
b.Navigation("Guests"); b.Navigation("Guests");
b.Navigation("Recordings"); b.Navigation("Recordings");
b.Navigation("StreamKey");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }

View File

@ -72,7 +72,7 @@ public class LndInvoicesStream : BackgroundService
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Subscribe invoices failed"); //_logger.LogError(ex, "Subscribe invoices failed");
} }
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);

View File

@ -22,7 +22,7 @@ public class StreamEventBuilder
_nostrClient = nostrClient; _nostrClient = nostrClient;
} }
public NostrEvent CreateStreamEvent(User user, UserStream stream) public NostrEvent CreateStreamEvent(UserStream stream)
{ {
var status = stream.State switch var status = stream.State switch
{ {
@ -35,11 +35,11 @@ public class StreamEventBuilder
var tags = new List<NostrEventTag> var tags = new List<NostrEventTag>
{ {
new("d", stream.Id.ToString()), new("d", stream.Id.ToString()),
new("title", user.Title ?? ""), new("title", stream.Title ?? ""),
new("summary", user.Summary ?? ""), new("summary", stream.Summary ?? ""),
new("image", stream.Thumbnail ?? user.Image ?? ""), new("image", stream.Thumbnail ?? stream.Image ?? ""),
new("status", status), new("status", status),
new("p", user.PubKey, "", "host"), new("p", stream.PubKey, "wss://relay.zap.stream", "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()) new("service", new Uri(_config.ApiHost, "/api/nostr").ToString())
@ -51,9 +51,9 @@ public class StreamEventBuilder
tags.Add(new("streaming", new Uri(_config.DataHost, $"stream/{stream.Id}.m3u8").ToString())); tags.Add(new("streaming", new Uri(_config.DataHost, $"stream/{stream.Id}.m3u8").ToString()));
tags.Add(new("current_participants", viewers.ToString())); tags.Add(new("current_participants", viewers.ToString()));
if (!string.IsNullOrEmpty(user.ContentWarning)) if (!string.IsNullOrEmpty(stream.ContentWarning))
{ {
tags.Add(new("content-warning", user.ContentWarning)); tags.Add(new("content-warning", stream.ContentWarning));
} }
} }
else if (status == "ended") else if (status == "ended")
@ -70,14 +70,14 @@ public class StreamEventBuilder
} }
} }
foreach (var tag in user.SplitTags()) foreach (var tag in stream.SplitTags())
{ {
tags.Add(new("t", tag)); tags.Add(new("t", tag));
} }
if (!string.IsNullOrEmpty(user.Goal)) if (!string.IsNullOrEmpty(stream.Goal))
{ {
tags.Add(new("goal", user.Goal)); tags.Add(new("goal", stream.Goal));
} }
var ev = new NostrEvent var ev = new NostrEvent

View File

@ -57,7 +57,7 @@ public class NostrStreamManager : IStreamManager
TestCanStream(); TestCanStream();
var fwds = new List<string> var fwds = new List<string>
{ {
$"rtmp://127.0.0.1:1935/{_context.UserStream.Endpoint.App}/{_context.User.StreamKey}?vhost={_context.UserStream.Endpoint.Forward}" $"rtmp://127.0.0.1:1935/{_context.UserStream.Endpoint.App}/{_context.StreamKey}?vhost={_context.UserStream.Endpoint.Forward}"
}; };
var dataProtector = _dataProtectionProvider.CreateProtector("forward-targets"); var dataProtector = _dataProtectionProvider.CreateProtector("forward-targets");
@ -103,7 +103,6 @@ public class NostrStreamManager : IStreamManager
} }
} }
}); });
} }
public async Task StreamStopped() public async Task StreamStopped()
@ -194,20 +193,27 @@ public class NostrStreamManager : IStreamManager
{ {
//var matches = new Regex("\\.(\\d+)\\.[\\w]{2,4}$").Match(segment.AbsolutePath); //var matches = new Regex("\\.(\\d+)\\.[\\w]{2,4}$").Match(segment.AbsolutePath);
if (_context.UserStream.Endpoint.Capabilities.Contains("dvr:source")) try
{ {
var result = await _dvrStore.UploadRecording(_context.UserStream, segment); if (_context.UserStream.Endpoint.Capabilities.Contains("dvr:source"))
_context.Db.Recordings.Add(new()
{ {
Id = result.Id, var result = await _dvrStore.UploadRecording(_context.UserStream, segment);
UserStreamId = _context.UserStream.Id, _context.Db.Recordings.Add(new()
Url = result.Result.ToString(), {
Duration = result.Duration, Id = result.Id,
Timestamp = DateTime UserStreamId = _context.UserStream.Id,
.UtcNow //DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(matches.Groups[1].Value)).UtcDateTime Url = result.Result.ToString(),
}); Duration = result.Duration,
Timestamp = DateTime
.UtcNow //DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(matches.Groups[1].Value)).UtcDateTime
});
await _context.Db.SaveChangesAsync(); await _context.Db.SaveChangesAsync();
}
}
catch (Exception ex)
{
_logger.LogWarning("Failed to save recording segment {}, {}", segment, ex.Message);
} }
await _context.Db.Streams await _context.Db.Streams
@ -242,7 +248,7 @@ public class NostrStreamManager : IStreamManager
var existingEvent = _context.UserStream.GetEvent(); var existingEvent = _context.UserStream.GetEvent();
var oldViewers = existingEvent?.Tags?.FindFirstTagValue("current_participants"); var oldViewers = existingEvent?.Tags?.FindFirstTagValue("current_participants");
var newEvent = _eventBuilder.CreateStreamEvent(_context.User, _context.UserStream); var newEvent = _eventBuilder.CreateStreamEvent(_context.UserStream);
var newViewers = newEvent?.Tags?.FindFirstTagValue("current_participants"); var newViewers = newEvent?.Tags?.FindFirstTagValue("current_participants");
if (newEvent != default && int.TryParse(oldViewers, out var a) && int.TryParse(newViewers, out var b) && a != b) if (newEvent != default && int.TryParse(oldViewers, out var a) && int.TryParse(newViewers, out var b) && a != b)
@ -260,7 +266,7 @@ public class NostrStreamManager : IStreamManager
DateTime? ends = state == UserStreamState.Ended ? DateTime.UtcNow : null; DateTime? ends = state == UserStreamState.Ended ? DateTime.UtcNow : null;
_context.UserStream.State = state; _context.UserStream.State = state;
_context.UserStream.Ends = ends; _context.UserStream.Ends = ends;
var ev = _eventBuilder.CreateStreamEvent(_context.User, _context.UserStream); var ev = _eventBuilder.CreateStreamEvent(_context.UserStream);
await _context.Db.Streams.Where(a => a.Id == _context.UserStream.Id) await _context.Db.Streams.Where(a => a.Id == _context.UserStream.Id)
.ExecuteUpdateAsync(o => o.SetProperty(v => v.State, state) .ExecuteUpdateAsync(o => o.SetProperty(v => v.State, state)

View File

@ -9,4 +9,5 @@ public class StreamManagerContext
public User User => UserStream.User; public User User => UserStream.User;
public StreamInfo? StreamInfo { get; init; } public StreamInfo? StreamInfo { get; init; }
public SrsApi EdgeApi { get; init; } = null!; public SrsApi EdgeApi { get; init; } = null!;
public string StreamKey { get; init; } = null!;
} }

View File

@ -28,7 +28,9 @@ public class StreamManagerFactory
var user = await _db.Users var user = await _db.Users
.AsNoTracking() .AsNoTracking()
.Include(a => a.Forwards) .Include(a => a.Forwards)
.SingleOrDefaultAsync(a => a.StreamKey.Equals(info.StreamKey)); .Include(user => user.StreamKeys)
.SingleOrDefaultAsync(a =>
a.StreamKey.Equals(info.StreamKey) || a.StreamKeys.Any(b => b.Key == info.StreamKey));
if (user == default) throw new Exception("No user found"); if (user == default) throw new Exception("No user found");
@ -53,24 +55,28 @@ public class StreamManagerFactory
throw new Exception("User account blocked"); throw new Exception("User account blocked");
} }
var existingLive = await _db.Streams var singleUseKey = user.StreamKeys.FirstOrDefault(a => a.Key == info.StreamKey);
.SingleOrDefaultAsync(a => a.State == UserStreamState.Live && a.PubKey == user.PubKey);
var existingLive = singleUseKey != default
? await _db.Streams.SingleOrDefaultAsync(a => a.Id == singleUseKey.StreamId)
: await _db.Streams
.SingleOrDefaultAsync(a => a.State == UserStreamState.Live && a.PubKey == user.PubKey);
var stream = existingLive ?? new UserStream var stream = existingLive ?? new UserStream
{ {
EndpointId = ep.Id, EndpointId = ep.Id,
PubKey = user.PubKey, PubKey = user.PubKey,
StreamId = "",
State = UserStreamState.Live, State = UserStreamState.Live,
EdgeIp = info.EdgeIp, EdgeIp = info.EdgeIp,
ForwardClientId = info.ClientId ForwardClientId = info.ClientId,
}; };
// add new stream // add new stream
if (existingLive == default) if (existingLive == default)
{ {
var ev = _eventBuilder.CreateStreamEvent(user, stream); await stream.CopyLastStreamDetails(_db);
stream.Event = JsonConvert.SerializeObject(ev, NostrSerializer.Settings); var ev = _eventBuilder.CreateStreamEvent(stream);
stream.Event = NostrJson.Serialize(ev) ?? "";
_db.Streams.Add(stream); _db.Streams.Add(stream);
await _db.SaveChangesAsync(); await _db.SaveChangesAsync();
} }
@ -85,18 +91,19 @@ public class StreamManagerFactory
var ctx = new StreamManagerContext var ctx = new StreamManagerContext
{ {
Db = _db, Db = _db,
StreamKey = info.StreamKey,
UserStream = new() UserStream = new()
{ {
Id = stream.Id, Id = stream.Id,
PubKey = stream.PubKey, PubKey = stream.PubKey,
StreamId = stream.StreamId,
State = stream.State, State = stream.State,
EdgeIp = stream.EdgeIp, EdgeIp = stream.EdgeIp,
ForwardClientId = stream.ForwardClientId, ForwardClientId = stream.ForwardClientId,
Endpoint = ep, Endpoint = ep,
User = user User = user
}, },
EdgeApi = new SrsApi(_serviceProvider.GetRequiredService<HttpClient>(), new Uri($"http://{stream.EdgeIp}:1985")) EdgeApi = new SrsApi(_serviceProvider.GetRequiredService<HttpClient>(),
new Uri($"http://{stream.EdgeIp}:1985"))
}; };
return new NostrStreamManager(_loggerFactory.CreateLogger<NostrStreamManager>(), ctx, _serviceProvider); return new NostrStreamManager(_loggerFactory.CreateLogger<NostrStreamManager>(), ctx, _serviceProvider);
@ -108,6 +115,7 @@ public class StreamManagerFactory
.AsNoTracking() .AsNoTracking()
.Include(a => a.User) .Include(a => a.User)
.Include(a => a.Endpoint) .Include(a => a.Endpoint)
.Include(a => a.StreamKey)
.FirstOrDefaultAsync(a => a.Id == id); .FirstOrDefaultAsync(a => a.Id == id);
if (stream == default) throw new Exception("No live stream"); if (stream == default) throw new Exception("No live stream");
@ -115,8 +123,10 @@ public class StreamManagerFactory
var ctx = new StreamManagerContext var ctx = new StreamManagerContext
{ {
Db = _db, Db = _db,
StreamKey = stream.StreamKey?.Key ?? stream.User.StreamKey,
UserStream = stream, UserStream = stream,
EdgeApi = new SrsApi(_serviceProvider.GetRequiredService<HttpClient>(), new Uri($"http://{stream.EdgeIp}:1985")) EdgeApi = new SrsApi(_serviceProvider.GetRequiredService<HttpClient>(),
new Uri($"http://{stream.EdgeIp}:1985"))
}; };
return new NostrStreamManager(_loggerFactory.CreateLogger<NostrStreamManager>(), ctx, _serviceProvider); return new NostrStreamManager(_loggerFactory.CreateLogger<NostrStreamManager>(), ctx, _serviceProvider);
@ -128,6 +138,7 @@ public class StreamManagerFactory
.AsNoTracking() .AsNoTracking()
.Include(a => a.User) .Include(a => a.User)
.Include(a => a.Endpoint) .Include(a => a.Endpoint)
.Include(a => a.StreamKey)
.FirstOrDefaultAsync(a => a.PubKey.Equals(pubkey) && a.State == UserStreamState.Live); .FirstOrDefaultAsync(a => a.PubKey.Equals(pubkey) && a.State == UserStreamState.Live);
if (stream == default) throw new Exception("No live stream"); if (stream == default) throw new Exception("No live stream");
@ -135,8 +146,10 @@ public class StreamManagerFactory
var ctx = new StreamManagerContext var ctx = new StreamManagerContext
{ {
Db = _db, Db = _db,
StreamKey = stream.StreamKey?.Key ?? stream.User.StreamKey,
UserStream = stream, UserStream = stream,
EdgeApi = new SrsApi(_serviceProvider.GetRequiredService<HttpClient>(), new Uri($"http://{stream.EdgeIp}:1985")) EdgeApi = new SrsApi(_serviceProvider.GetRequiredService<HttpClient>(),
new Uri($"http://{stream.EdgeIp}:1985"))
}; };
return new NostrStreamManager(_loggerFactory.CreateLogger<NostrStreamManager>(), ctx, _serviceProvider); return new NostrStreamManager(_loggerFactory.CreateLogger<NostrStreamManager>(), ctx, _serviceProvider);
@ -148,11 +161,13 @@ public class StreamManagerFactory
.AsNoTracking() .AsNoTracking()
.Include(a => a.User) .Include(a => a.User)
.Include(a => a.Endpoint) .Include(a => a.Endpoint)
.Include(a => a.StreamKey)
.OrderByDescending(a => a.Starts) .OrderByDescending(a => a.Starts)
.FirstOrDefaultAsync(a => .FirstOrDefaultAsync(a =>
a.User.StreamKey.Equals(info.StreamKey) && (a.StreamKey != default && a.StreamKey.Key == info.StreamKey) ||
a.Endpoint.App.Equals(info.App) && (a.User.StreamKey.Equals(info.StreamKey) &&
a.State == UserStreamState.Live); a.Endpoint.App.Equals(info.App) &&
a.State == UserStreamState.Live));
if (stream == default) if (stream == default)
{ {
@ -162,11 +177,13 @@ public class StreamManagerFactory
var ctx = new StreamManagerContext var ctx = new StreamManagerContext
{ {
Db = _db, Db = _db,
StreamKey = info.StreamKey,
UserStream = stream, UserStream = stream,
StreamInfo = info, StreamInfo = info,
EdgeApi = new SrsApi(_serviceProvider.GetRequiredService<HttpClient>(), new Uri($"http://{stream.EdgeIp}:1985")) EdgeApi = new SrsApi(_serviceProvider.GetRequiredService<HttpClient>(),
new Uri($"http://{stream.EdgeIp}:1985"))
}; };
return new NostrStreamManager(_loggerFactory.CreateLogger<NostrStreamManager>(), ctx, _serviceProvider); return new NostrStreamManager(_loggerFactory.CreateLogger<NostrStreamManager>(), ctx, _serviceProvider);
} }
} }

View File

@ -254,8 +254,8 @@ public class UserService
public async Task UpdateStreamInfo(string pubkey, PatchEvent req) public async Task UpdateStreamInfo(string pubkey, PatchEvent req)
{ {
await _db.Users await _db.Streams
.Where(a => a.PubKey == pubkey) .Where(a => a.Id == req.Id && a.PubKey == pubkey)
.ExecuteUpdateAsync(o => o.SetProperty(v => v.Title, req.Title) .ExecuteUpdateAsync(o => o.SetProperty(v => v.Title, req.Title)
.SetProperty(v => v.Summary, req.Summary) .SetProperty(v => v.Summary, req.Summary)
.SetProperty(v => v.Image, req.Image) .SetProperty(v => v.Image, req.Image)

View File

@ -4,7 +4,8 @@
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning", "Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning", "Microsoft.EntityFrameworkCore": "Warning",
"System.Net.Http.HttpClient": "Error" "System.Net.Http.HttpClient": "Error",
"NostrStreamer.NostrAuthHandler": "Error",
} }
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
@ -35,7 +36,7 @@
"PublicHost": "http://localhost:9000" "PublicHost": "http://localhost:9000"
}, },
"SnortApi": "https://api.snort.social", "SnortApi": "https://api.snort.social",
"GeoIpDatabase": "C:\\Users\\Kieran\\Downloads\\GeoLite2-City.mmdb", "GeoIpDatabase": "/home/kieran/Downloads/GeoLite2-City.mmdb",
"Edges": [ "Edges": [
{ {
"Name": "US0", "Name": "US0",

View File

@ -118,6 +118,6 @@ vhost full.in.zap.stream {
vhost __defaultVhost__ { vhost __defaultVhost__ {
forward { forward {
enabled on; enabled on;
backend http://host.docker.internal:5295/api/srs; backend http://172.17.0.1:5295/api/srs;
} }
} }

View File

@ -32,10 +32,10 @@ vhost hls.zap.stream {
http_hooks { http_hooks {
enabled on; enabled on;
on_publish http://host.docker.internal:5295/api/srs; on_publish http://172.17.0.1:5295/api/srs;
on_unpublish http://host.docker.internal:5295/api/srs; on_unpublish http://172.17.0.1:5295/api/srs;
on_hls http://host.docker.internal:5295/api/srs; on_hls http://172.17.0.1:5295/api/srs;
on_dvr http://host.docker.internal:5295/api/srs; on_dvr http://172.17.0.1:5295/api/srs;
} }
dvr { dvr {