#!/usr/bin/env dotnet-script #nullable enable #r "nuget: Microsoft.Data.Sqlite, 7.0.4" /* This script generates the static HTML pages for the series list * pages and the series detail pages using the data in the sqlite * database. */ using System.Globalization; using System.Net; using System.Text.Json; using System.Text.RegularExpressions; using Microsoft.Data.Sqlite; using SQLitePCL; private readonly string BASE_PATH = Path.GetFullPath(".."); private readonly string OUTPUT_DIR = Path.Combine(BASE_PATH, "site", "partial"); private readonly string CONNECTION_STRING = $"Data Source={Path.Combine(BASE_PATH, "db", "radiostasis.db")}"; private static readonly Regex cleanHost = new(@"^(en|www)\.", RegexOptions.Compiled); private static readonly Regex cleanFilter = new(@"[^a-z0-9 ]", RegexOptions.Compiled); private class Series { public required string Slug { get; set; } public required string Title { get; set; } public required string TitleSort { get; set; } public required DateTime DateAdded { get; set; } public required short MinYear { get; set; } public required short MaxYear { get; set; } public required IEnumerable Tags { get; set; } public string? Description { get; set; } public string? Actors { get; set; } public required int EpisodeCount { get; set; } public required IEnumerable Urls { get; set; } public string SlugEncoded { get => WebUtility.HtmlEncode(Slug); } public string YearRange { get => MinYear == MaxYear ? $"{MinYear}" : $"{MinYear}-{MaxYear}"; } public string TitleEncoded { get => WebUtility.HtmlEncode(Title); } public string HtmlDescription { get { var sb = new StringBuilder(); if (!string.IsNullOrEmpty(Description)) { sb.AppendLine("

"); sb.AppendLine(string.Join("

", Description.Split("\n") .Where(l => !string.IsNullOrWhiteSpace(l)) .Select(l => WebUtility.HtmlEncode(l)))); sb.AppendLine("

"); if (!string.IsNullOrEmpty(Actors)) { sb.AppendLine( $"

Performers: {WebUtility.HtmlEncode(Actors)}

"); } if (Urls.Count() > 0) { sb.AppendLine("

Sources: "); sb.AppendLine(string.Join(", ", Urls.Select(u => $"{cleanHost.Replace(new Uri(u).Host, string.Empty)}"))); sb.AppendLine("

"); } sb.AppendLine("
"); } return sb.ToString(); } } public string FilterText { get { var sb = new StringBuilder(); // add the show's title sb.Append(Title.ToLowerInvariant()); // add all the actors if (!string.IsNullOrEmpty(Actors)) { sb.Append(" "); sb.Append(cleanFilter.Replace(Actors.ToLowerInvariant(), string.Empty)); } // add the decades like 1930s 1940s etc var firstDecade = (int)(Math.Floor(MinYear / 10d) * 10); var lastDecade = (int)(Math.Floor(MaxYear / 10d) * 10); for (var decade = firstDecade; decade <= lastDecade; decade += 10) { sb.Append($" {decade}s"); } return WebUtility.HtmlEncode(sb.ToString()); } } } private class Episode { public required Series Series { get; set; } public required string Slug { get; set; } public required string Title { get; set; } public required string FileName { get; set; } public required long FileSize { get; set; } public required int Length { get; set; } public string? AirDate { get; set; } public string TitleEncoded { get => WebUtility.HtmlEncode(Title); } public string FileNameEncoded { get => WebUtility.HtmlEncode(FileName); } public string LengthDisplay { get => $"{Math.Round(Length / 60f)}mins"; } public string AirDateDisplay { get => !string.IsNullOrWhiteSpace(AirDate) ? $"Aired {AirDate}" : "Air date unknown"; } public string JsonEncoded { get => WebUtility.HtmlEncode(JsonSerializer.Serialize(new { id = $"{Series.Slug}/{Slug}", slug = Slug, title = Title, file = FileName, length = LengthDisplay, size = FileSize, series = new { slug = Series.Slug, title = Series.Title, cover = $"/cover/sm/{Series.SlugEncoded}.jpg", }, })); } } private IEnumerable GetTags() { using var connection = new SqliteConnection(CONNECTION_STRING); connection.Open(); var tags = new List(); using var cmd = connection.CreateCommand(); cmd.CommandText = "select distinct tag from series_tags order by tag"; using var reader = cmd.ExecuteReader(); while (reader.Read()) { tags.Add(reader.GetString(0)); } return tags; } private IEnumerable GetSeries() { using var connection = new SqliteConnection(CONNECTION_STRING); connection.Open(); // get all series tags var tags = new Dictionary>(); using (var tagcmd = connection.CreateCommand()) { tagcmd.CommandText = "select series_slug, tag from series_tags order by tag"; using var tagreader = tagcmd.ExecuteReader(); while (tagreader.Read()) { var slug = tagreader.GetString(0); var tag = tagreader.GetString(1); if (!tags.ContainsKey(slug)) tags.Add(slug, new()); tags[slug].Add(tag); } } // get all series using var cmd = connection.CreateCommand(); cmd.CommandText = @"select s.series_slug, s.title, s.date_added, s.min_year, s.max_year, s.description, s.title_sort, count(distinct e.episode_slug) episode_count, group_concat(l.link_url, '||') urls, s.actors from series s inner join episodes e on e.series_slug=s.series_slug left join series_links l on l.series_slug=s.series_slug group by s.series_slug order by s.title_sort"; var series = new List(); using var reader = cmd.ExecuteReader(); while (reader.Read()) { var slug = reader.GetString(0); series.Add(new() { Slug = slug, Title = reader.GetString(1), DateAdded = reader.GetDateTime(2), MinYear = reader.GetInt16(3), MaxYear = reader.GetInt16(4), Description = reader.IsDBNull(5) ? null : reader.GetString(5), TitleSort = reader.GetString(6), EpisodeCount = reader.GetInt32(7), Tags = tags[slug], Urls = reader.GetString(8).Split("||").Distinct().Order().ToArray(), Actors = reader.IsDBNull(9) ? null : reader.GetString(9), }); } return series; } private IEnumerable GetEpisodes(Series series) { using var connection = new SqliteConnection(CONNECTION_STRING); connection.Open(); using var cmd = connection.CreateCommand(); cmd.CommandText = @"select episode_slug, title, file_name, file_size, episode_length, air_date from episodes where series_slug=@Series order by episode_slug"; var param = cmd.CreateParameter(); param.ParameterName = "Series"; param.Value = series.Slug; cmd.Parameters.Add(param); var episodes = new List(); using var reader = cmd.ExecuteReader(); while (reader.Read()) { episodes.Add(new() { Slug = reader.GetString(0), Title = reader.GetString(1), FileName = reader.GetString(2), FileSize = reader.GetInt64(3), Length = reader.GetInt32(4), AirDate = reader.IsDBNull(5) ? null : reader.GetString(5), Series = series, }); } return episodes; } private string DisplayTag(string tag) => CultureInfo.CurrentCulture.TextInfo.ToTitleCase(tag.ToLower().Replace("-", " ")) .Replace(" ", "-"); private void GenerateSeriesListFragment( IEnumerable seriesList, string path, string title) { using var outStream = File.Open(path, FileMode.Create); using var sw = new StreamWriter(outStream); sw.Write( @$"

{WebUtility.HtmlEncode(title)}

"); foreach (var series in seriesList) { sw.Write(@$"
cover image
    {string.Join(string.Empty, series.Tags.Select(t => $"
  • {DisplayTag(t)}
  • "))}
"); } sw.Write("
"); } private void GenerateSeriesDetailsFragment(Series series) { var path = Path.Combine(OUTPUT_DIR, "series", $"{series.Slug}.html"); using var outStream = File.Open(path, FileMode.Create); using var sw = new StreamWriter(outStream); sw.Write(@$"
cover image
    {string.Join(string.Empty, series.Tags.Select(t => $"
  • {DisplayTag(t)}
  • "))}
    "); foreach (var episode in GetEpisodes(series)) { sw.Write( @$"
  1. "); } sw.Write( @$"
{series.HtmlDescription}
"); } private void InjectGenreList(IEnumerable genres) { var indexPath = Path.Combine(BASE_PATH, "site", "index.html"); var index = File.ReadAllLines(indexPath); using var outStream = File.Create(indexPath); using var sw = new StreamWriter(outStream); var inGenres = false; foreach (var line in index) { if (inGenres) { if (line.Contains("")) { sw.WriteLine(line); inGenres = false; } } else { sw.WriteLine(line); if (line.Contains("")) { foreach (var genre in genres) { sw.WriteLine( @$"
  • {DisplayTag(genre)}
  • "); } inGenres = true; } } } } private void BuildSite() { Batteries.Init(); // initialize sqlite lib // generate main series list var seriesList = GetSeries(); GenerateSeriesListFragment( seriesList, Path.Combine(OUTPUT_DIR, "series.html"), "All Series"); // inject genre list var tags = GetTags(); InjectGenreList(tags); // generate genre series lists foreach (var tag in tags) { var path = Path.Combine(OUTPUT_DIR, "genre", $"{tag}.html"); GenerateSeriesListFragment(seriesList.Where(s => s.Tags.Any(t => t == tag)) .OrderBy(s => s.TitleSort), path, $"{DisplayTag(tag)} Series"); } // generate series details foreach (var series in seriesList) { GenerateSeriesDetailsFragment(series); } } BuildSite();