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
{
[JsonProperty("event")]
public PatchEvent? Event { get; init; }
[JsonProperty("endpoints")]
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
{
[JsonProperty("id")]
public Guid Id { get; init; }
[JsonProperty("title")]
public string Title { get; init; } = null!;

View File

@ -27,10 +27,12 @@ public class NostrController : Controller
private readonly IClipService _clipService;
private readonly ILogger<NostrController> _logger;
private readonly PushSender _pushSender;
private readonly StreamEventBuilder _eventBuilder;
public NostrController(StreamerContext db, Config config, StreamManagerFactory streamManager,
UserService userService,
IClipService clipService, ILogger<NostrController> logger, PushSender pushSender)
IClipService clipService, ILogger<NostrController> logger, PushSender pushSender,
StreamEventBuilder eventBuilder)
{
_db = db;
_config = config;
@ -39,6 +41,7 @@ public class NostrController : Controller
_clipService = clipService;
_logger = logger;
_pushSender = pushSender;
_eventBuilder = eventBuilder;
}
[HttpGet("account")]
@ -57,15 +60,6 @@ public class NostrController : Controller
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
{
Name = a.Name,
@ -100,9 +94,9 @@ public class NostrController : Controller
var pubkey = GetPubKey();
if (string.IsNullOrEmpty(pubkey)) return Unauthorized();
await _userService.UpdateStreamInfo(pubkey, req);
try
{
await _userService.UpdateStreamInfo(pubkey, req);
var streamManager = await _streamManagerFactory.ForCurrentStream(pubkey);
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()
{
var pk = GetPubKey();

View File

@ -41,7 +41,7 @@ public class PlaylistController : Controller
var streamManager = await _streamManagerFactory.ForStream(id);
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)
{
Path = path,
@ -130,7 +130,7 @@ public class PlaylistController : Controller
foreach (var variant in userStream.Endpoint.GetVariants().OrderBy(a => a.Bandwidth))
{
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}" :
variant.ToResolutionArg();
@ -171,7 +171,7 @@ public class PlaylistController : Controller
var streamManager = await _streamManagerFactory.ForStream(id);
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);
}
catch
@ -253,7 +253,7 @@ public class PlaylistController : Controller
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 req = CreateProxyRequest(ub);
using var rsp = await _client.SendAsync(req);

View File

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

View File

@ -8,8 +8,6 @@ public class UserStreamConfiguration : IEntityTypeConfiguration<UserStream>
public void Configure(EntityTypeBuilder<UserStream> builder)
{
builder.HasKey(a => a.Id);
builder.Property(a => a.StreamId)
.IsRequired();
builder.Property(a => a.Starts)
.IsRequired();
@ -35,6 +33,13 @@ public class UserStreamConfiguration : IEntityTypeConfiguration<UserStream>
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)
.WithMany()
.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<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<UserStream> Streams { 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 User User { get; init; } = null!;
public string StreamId { get; init; } = null!;
public DateTime Starts { get; init; } = DateTime.UtcNow;
public DateTime? Ends { 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>
/// Nostr Event for this stream
/// </summary>
@ -25,7 +53,7 @@ public class UserStream
/// </summary>
public string? Thumbnail { get; set; }
public Guid EndpointId { get; init; }
public Guid EndpointId { get; set; }
public IngestEndpoint Endpoint { get; init; } = null!;
/// <summary>
@ -58,10 +86,15 @@ public class UserStream
public List<UserStreamGuest> Guests { 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
{
Unknown = 0,
Planned = 1,
Live = 2,
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 Igdb;
using MaxMind.GeoIP2;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using Nostr.Client.Identifiers;
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) ?
user.Tags.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) : Array.Empty<string>();
return !string.IsNullOrEmpty(stream.Tags)
? stream.Tags.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
: Array.Empty<string>();
}
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 d2 = otherLatitude * (Math.PI / 180.0);
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)));
}
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))
{
return hostTag;
@ -132,6 +136,33 @@ public static class Extensions
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
@ -161,7 +192,8 @@ public class Variant
}
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();
}

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")
.HasColumnType("numeric");
b.Property<string>("ContentWarning")
.HasColumnType("text");
b.Property<string>("EdgeIp")
.IsRequired()
.HasColumnType("text");
@ -256,6 +259,12 @@ namespace NostrStreamer.Migrations
.IsRequired()
.HasColumnType("text");
b.Property<string>("Goal")
.HasColumnType("text");
b.Property<string>("Image")
.HasColumnType("text");
b.Property<DateTime>("LastSegment")
.HasColumnType("timestamp with time zone");
@ -275,13 +284,18 @@ namespace NostrStreamer.Migrations
b.Property<int>("State")
.HasColumnType("integer");
b.Property<string>("StreamId")
.IsRequired()
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");
@ -376,6 +390,39 @@ namespace NostrStreamer.Migrations
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")
@ -476,6 +523,25 @@ namespace NostrStreamer.Migrations
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")
@ -493,6 +559,8 @@ namespace NostrStreamer.Migrations
b.Navigation("Payments");
b.Navigation("StreamKeys");
b.Navigation("Streams");
});
@ -501,6 +569,8 @@ namespace NostrStreamer.Migrations
b.Navigation("Guests");
b.Navigation("Recordings");
b.Navigation("StreamKey");
});
#pragma warning restore 612, 618
}

View File

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

View File

@ -22,7 +22,7 @@ public class StreamEventBuilder
_nostrClient = nostrClient;
}
public NostrEvent CreateStreamEvent(User user, UserStream stream)
public NostrEvent CreateStreamEvent(UserStream stream)
{
var status = stream.State switch
{
@ -35,11 +35,11 @@ public class StreamEventBuilder
var tags = new List<NostrEventTag>
{
new("d", stream.Id.ToString()),
new("title", user.Title ?? ""),
new("summary", user.Summary ?? ""),
new("image", stream.Thumbnail ?? user.Image ?? ""),
new("title", stream.Title ?? ""),
new("summary", stream.Summary ?? ""),
new("image", stream.Thumbnail ?? stream.Image ?? ""),
new("status", status),
new("p", user.PubKey, "", "host"),
new("p", stream.PubKey, "wss://relay.zap.stream", "host"),
new("relays", _config.Relays),
new("starts", new DateTimeOffset(stream.Starts).ToUnixTimeSeconds().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("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")
@ -70,14 +70,14 @@ public class StreamEventBuilder
}
}
foreach (var tag in user.SplitTags())
foreach (var tag in stream.SplitTags())
{
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

View File

@ -57,7 +57,7 @@ public class NostrStreamManager : IStreamManager
TestCanStream();
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");
@ -103,7 +103,6 @@ public class NostrStreamManager : IStreamManager
}
}
});
}
public async Task StreamStopped()
@ -194,20 +193,27 @@ public class NostrStreamManager : IStreamManager
{
//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);
_context.Db.Recordings.Add(new()
if (_context.UserStream.Endpoint.Capabilities.Contains("dvr:source"))
{
Id = result.Id,
UserStreamId = _context.UserStream.Id,
Url = result.Result.ToString(),
Duration = result.Duration,
Timestamp = DateTime
.UtcNow //DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(matches.Groups[1].Value)).UtcDateTime
});
var result = await _dvrStore.UploadRecording(_context.UserStream, segment);
_context.Db.Recordings.Add(new()
{
Id = result.Id,
UserStreamId = _context.UserStream.Id,
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
@ -242,7 +248,7 @@ public class NostrStreamManager : IStreamManager
var existingEvent = _context.UserStream.GetEvent();
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");
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;
_context.UserStream.State = state;
_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)
.ExecuteUpdateAsync(o => o.SetProperty(v => v.State, state)

View File

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

View File

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

View File

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

View File

@ -118,6 +118,6 @@ vhost full.in.zap.stream {
vhost __defaultVhost__ {
forward {
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 {
enabled on;
on_publish http://host.docker.internal:5295/api/srs;
on_unpublish http://host.docker.internal:5295/api/srs;
on_hls http://host.docker.internal:5295/api/srs;
on_dvr http://host.docker.internal:5295/api/srs;
on_publish http://172.17.0.1:5295/api/srs;
on_unpublish http://172.17.0.1:5295/api/srs;
on_hls http://172.17.0.1:5295/api/srs;
on_dvr http://172.17.0.1:5295/api/srs;
}
dvr {