Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ffc6dd4
initial channels impl, TODO decouple submitters and channels, transport
yellowsink Jan 2, 2023
7426339
[server] more flexible ingest system
yellowsink Jan 4, 2023
bb96d7c
update server transport things for channels
yellowsink Jan 4, 2023
51d2bc8
the docs be docsing
yellowsink Jan 4, 2023
e87f05a
re add /ping: apparently kasi uses this
yellowsink Jan 4, 2023
f4909d9
bare minimum to make web client "work"
yellowsink Jan 4, 2023
890c41e
[server] invert global songs condition so it's actually correct
redstonekasi Jan 5, 2023
473c384
hi, can i take your order? more docs? coming right up!
yellowsink Jan 27, 2023
8539773
Merge branch 'master' into channels
redstonekasi Jan 27, 2023
bbb27e8
[clients/web] somewhat working, bad, channel implementation
redstonekasi Jan 29, 2023
3d98498
Merge branch 'master' into channels
yellowsink Apr 7, 2023
7e8c8e0
[server] fixups after master merge
yellowsink Apr 7, 2023
dfba037
[server] collect types together
yellowsink Apr 8, 2023
3895d1a
[server] lol tiny change
yellowsink Apr 8, 2023
61231fe
[ingest] move to new-new channels format + add a channel
yellowsink Apr 8, 2023
3c85482
[server] implement new channels format
yellowsink Apr 8, 2023
b424f11
[server] fix the global channel
yellowsink Apr 8, 2023
16d54c9
[server] a load of cleanups and stuff
yellowsink Apr 9, 2023
9eeb153
[server] setup basic serilog
yellowsink Apr 9, 2023
38d814a
[server] im now happy with logging
yellowsink Apr 9, 2023
0ca70ce
[clients/web] cleanup audio, enable img preloads
yellowsink Apr 11, 2023
33dc008
[clients/web] fix duplicate channels on reconnect
yellowsink Apr 11, 2023
b454a7a
[clients/web] attempt (?) to fix audio overlaps issue on some browsers
yellowsink Apr 11, 2023
ee644c3
[clients/web] ACTUALLY fix audio bug
yellowsink Apr 11, 2023
6d5d4f4
[server] fix picker service for small channels
redstonekasi Oct 3, 2023
f9ba105
[server] make channels use their filename as key
redstonekasi Oct 3, 2023
0d96c88
[docs] update storage and transport docs
redstonekasi Oct 3, 2023
68e3b05
[server] add submitter channels to an implicit category
redstonekasi Oct 3, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,12 @@ resharper_align_multline_type_parameter_list = true
resharper_align_tuple_components = true
resharper_allow_comment_after_lbrace = true
resharper_csharp_alignment_tab_fill_style = optimal_fill
resharper_csharp_align_first_arg_by_paren = true
resharper_csharp_align_first_arg_by_paren = false
resharper_csharp_outdent_commas = true
resharper_csharp_outdent_dots = true
resharper_csharp_wrap_arguments_style = chop_if_long
resharper_csharp_wrap_before_binary_opsign = true
resharper_csharp_wrap_before_invocation_rpar = true
resharper_enforce_line_ending_style = true
resharper_fsharp_keep_if_then_in_same_line = true
resharper_fsharp_space_around_delimiter = false
Expand Down Expand Up @@ -109,5 +110,5 @@ resharper_web_config_wrong_module_highlighting = warning
tab_width = 4

[*.{js,ts,jsx,tsx,json,vue}]
indent_style = spaces
indent_size = 2
indent_style = space
indent_size = 2
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
dotnet-version: 7.0.x

- name: Publish
run: |
Expand Down
11 changes: 9 additions & 2 deletions UwuRadio.Server/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ namespace UwuRadio.Server;

[SuppressMessage("ReSharper", "UnassignedField.Global")]
[SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Global")]
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
// ReSharper disable once ClassNeverInstantiated.Global
public class Constants
{
Expand All @@ -23,7 +26,7 @@ public static Constants
public int BufferTime { get; set; }

/// <summary>
/// Time remaining when the next song should be broadcasted for preloading
/// Time remaining when the next song should be broadcast for preloading
/// </summary>
public int PreloadTime { get; set; }

Expand All @@ -46,4 +49,8 @@ public static Constants
/// Where the raw data is stored
/// </summary>
public string IngestFolder { get; set; } = null!;
}

// these two are not in the JSON but are here for convenience
public string IngestSubmittersFolder => Path.Combine(IngestFolder, "submitters");
public string IngestChannelsFolder => Path.Combine(IngestFolder, "channels");
}
36 changes: 23 additions & 13 deletions UwuRadio.Server/Controllers/ApiController.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Immutable;
using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
Expand All @@ -21,33 +22,42 @@ public ApiController(DataService dataService, DownloadService downloadService)
public IActionResult Ping() => Ok("Pong!");

// /api/time
public IActionResult Time() => Json(Helpers.Now().ToUnixTimeSeconds());
public IActionResult Time() => Ok(Helpers.Now().ToUnixTimeSeconds());

// /api/data
public IActionResult Data() => Json(new
{
//Songs = _dataService.AllSongs,
Submitters = _dataService.Submitters.Values.ToArray()
});
{
Submitters = _dataService.Submitters.Values.ToArray(),
Channels = _dataService.Channels
.Select(c => new KeyValuePair<string, Channel>(
c.Key,
c.Value with { Songs = Array.Empty<Song>() }
)
)
.ToImmutableSortedDictionary()
}
);

// /api/file/id
public IActionResult File(string id)
{
if (!_downloadService.IsDownloaded(id))
return StatusCode((int) HttpStatusCode.ServiceUnavailable, "The server does not have this file cached");
return StatusCode((int) HttpStatusCode.ServiceUnavailable,
"The server does not have this file cached"
);

var fileInfo = _downloadService.GetFileInfo(id);

// check ETag to facilitate caching
if (Request.Headers.IfNoneMatch.FirstOrDefault() == ('"' + fileInfo.Md5 + '"'))
if (Request.Headers.IfNoneMatch.FirstOrDefault() == '"' + fileInfo.Md5 + '"')
return StatusCode(StatusCodes.Status304NotModified);

// if range processing is enabled it makes the client get very angy
return File(
fileInfo.File.OpenRead(),
"audio/mpeg",
null,
new EntityTagHeaderValue(new StringSegment('"' + fileInfo.Md5 + '"')),
false);
return File(fileInfo.File.OpenRead(),
"audio/mpeg",
null,
new EntityTagHeaderValue(new StringSegment('"' + fileInfo.Md5 + '"')),
false
);
}
}
38 changes: 38 additions & 0 deletions UwuRadio.Server/Domain.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace UwuRadio.Server;

public record Channel(string Name, Song[] Songs, string? Category = null);

public record Song(string Name, string Artist, string StreamUrl, string? ArtUrl, string? Album,
string Submitter, string[]? Channels = null, bool? IncludeInGlobal = null)
{
// lazy & cached
private string? _id;
public string Id => _id ??= Helpers.ComputeSongId(this);
}

public record TransitSong(string Name, string Artist, string? DlUrl, string? SourceUrl,
string? ArtUrl, string? Album, string Submitter)
{
public TransitSong(Song song) : this(song.Name,
song.Artist,
Constants.C.ServerDlUrl + song.Id,
song.StreamUrl,
song.ArtUrl,
song.Album,
song.Submitter
)
{
}
}

public record Submitter(string Name, string PfpUrl, string[] Quotes);

public record IngestChannel(string Name, string? Category = null)
{
public Channel ToChannel(Song[] songs) => new(Name, songs, Category);
}

public record IngestSubmitter(string Name, string PfpUrl, string[] Quotes, Song[] Songs)
{
public Submitter ToSubmitter() => new(Name, PfpUrl, Quotes);
}
47 changes: 14 additions & 33 deletions UwuRadio.Server/Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ public static class Helpers
{
public static Instant Now() => SystemClock.Instance.GetCurrentInstant();

/*
/// <summary>
/// Only the date component of an Instant
/// </summary>
public static Instant StripTime(Instant inst) => inst.InUtc().Date.AtStartOfDayInZone(DateTimeZone.Utc).ToInstant();
*/

// ReSharper disable once InconsistentNaming
public static string MD5(byte[] data)
Expand All @@ -38,46 +40,18 @@ public static Duration ParseDuration(string raw)
return parsed;
}

public static void Log(string? subSeg, params object[] payload)
{
var segments = subSeg == null
? new[] { ("uwu radio", ConsoleColor.Green) }
: new[]
{
("uwu radio", ConsoleColor.Green),
(subSeg, ConsoleColor.Blue)
};

foreach (var (seg, col) in segments)
{
Console.Write("⸨");

var origCol = Console.ForegroundColor;
Console.ForegroundColor = col;
Console.Write(seg);
Console.ForegroundColor = origCol;

Console.Write("⸩ ");
}

foreach (var p in payload)
Console.Write(p.ToString());

Console.WriteLine();
}

private static char[] base60Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwx".ToCharArray();
private static readonly char[] Base60Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwx".ToCharArray();
private static string ToBase60(this ulong num) {
var i = 32;
char[] buffer = new char[i];
var buffer = new char[i];
do
{
buffer[--i] = base60Chars[num % 60];
num = num / 60;
buffer[--i] = Base60Chars[num % 60];
num /= 60;
}
while (num > 0);

char[] result = new char[32 - i];
var result = new char[32 - i];
Array.Copy(buffer, i, result, 0, 32 - i);
return new string(result);
}
Expand All @@ -87,4 +61,11 @@ public static string ComputeSongId(Song song) {
var hash = XXHash.Hash64(buffer);
return hash.ToBase60();
}

public static TV? GetOrDefault<TK, TV>(this IDictionary<TK, TV> dict, TK k)
=> dict.TryGetValue(k, out var val) ? val : default;

public static PrettyLogger<T> PrettyNamed<T>(this ILogger<T> logger, T self)
where T : IPrettyNamed
=> new(logger, self);
}
66 changes: 66 additions & 0 deletions UwuRadio.Server/Logging.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Serilog.Context;
using Serilog.Core;
using Serilog.Events;

namespace UwuRadio.Server;

/// <summary>
/// Enriches Serilog logs with the source class name, like built-in SourceContext but better.
/// </summary>
public class SourceContextEnricher : ILogEventEnricher
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
if (logEvent.Properties.TryGetValue("PrettyName", out var prettyName))
{
var pnScalar = prettyName as ScalarValue;
var ptStr = pnScalar?.Value as string;

if (!string.IsNullOrWhiteSpace(ptStr))
{
logEvent.AddOrUpdateProperty(
new LogEventProperty("SourceContext", new ScalarValue(ptStr))
);
return;
}
}

if (!logEvent.Properties.TryGetValue("SourceContext", out var fullCtxtRaw)) return;


// convert into Serilog's ScalarValue wrapper first
// https://stackoverflow.com/a/75332511/8388655
var scalar = fullCtxtRaw as ScalarValue;
var ctxtStr = scalar?.Value as string;

// when comparing nullables you must either ==true, ==false, ??true, or ??false
if (ctxtStr?.StartsWith("UwuRadio.Server") != true) return;

var className = ctxtStr.Split(".").LastOrDefault();

if (!string.IsNullOrWhiteSpace(className))
logEvent.AddOrUpdateProperty(new LogEventProperty("SourceContext", new ScalarValue(
className)));
}
}

public interface IPrettyNamed
{
public string PrettyName { get; }
}

public record PrettyLogger<T>(ILogger<T> Underlying, T Owner) : ILogger<T>
where T : IPrettyNamed
{
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
=> Underlying.BeginScope(state);

public bool IsEnabled(LogLevel logLevel) => Underlying.IsEnabled(logLevel);

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception?, string> formatter)
{
using (LogContext.PushProperty("PrettyName", Owner.PrettyName))
Underlying.Log(logLevel, eventId, state, exception, formatter);
}
}
52 changes: 38 additions & 14 deletions UwuRadio.Server/Program.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
using Serilog;
using Serilog.Events;
using UwuRadio.Server;
using UwuRadio.Server.Services;

Helpers.Log(null, "Hello, world!");
Log.Logger = new LoggerConfiguration().MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateBootstrapLogger();

Log.Information("Hello, world!");

var builder = WebApplication.CreateBuilder(args);

Expand All @@ -12,26 +19,43 @@

// our own custom services
builder.Services.AddSingleton<DataService>();
builder.Services.AddSingleton<PickerService>();
builder.Services.AddTransient<PickerService>();
builder.Services.AddSingleton<DownloadService>();
builder.Services.AddSingleton<CoordinatorService>();
builder.Services.AddTransient<CoordinatorService>();
builder.Services.AddSingleton<CoordServOwnerService>();

builder.Host.UseSerilog(
(ctxt, services, cfg) => cfg.ReadFrom.Configuration(ctxt.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext()
.Enrich.With<SourceContextEnricher>()
#if DEBUG
.MinimumLevel.Debug()
#else
.MinimumLevel.Information()
#endif
.WriteTo.Console(
outputTemplate:
"[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}"
)
);

var app = builder.Build();

// https://stackoverflow.com/a/66240442/8388655
app.UseCors(cors => cors.AllowAnyHeader().AllowAnyMethod().SetIsOriginAllowed(_ => true).AllowCredentials());

app.UseRouting();
app.UseCors(
cors => cors.AllowAnyHeader().AllowAnyMethod().SetIsOriginAllowed(_ => true).AllowCredentials()
);

app.UseEndpoints(endpoints =>
{
endpoints.MapHub<SyncHub>("/sync");
endpoints.MapDefaultControllerRoute();
});

// start the services way before they're technically needed so that it starts downloading instantly
app.Services.GetService<CoordinatorService>();
app.MapHub<SyncHub>("/sync");
app.MapDefaultControllerRoute();

Helpers.Log(null, "Kickstarted services successfully, starting web server now");
// start the services before a web req so that it starts downloading songs instantly
// we need to pass this service our service provider so it can instantiate services that are
// managed and disposed correctly
app.Services.GetService<CoordServOwnerService>()!.StartCoordinators(app.Services);

app.Run();

Log.CloseAndFlush();
Loading