forked from nikita/muzika-gromche
Implement seasonal content framework
to ensure that New Year's songs won't play in summer.
This commit is contained in:
parent
ebd7811b12
commit
afb3e34e71
|
|
@ -7,6 +7,7 @@
|
||||||
- Added a new track Paarden.
|
- Added a new track Paarden.
|
||||||
- Added a new track DiscoKapot.
|
- Added a new track DiscoKapot.
|
||||||
- Added an accessibility option to reduce the intensity of overly distracting visual effects.
|
- Added an accessibility option to reduce the intensity of overly distracting visual effects.
|
||||||
|
- Seasonal content like New Year's songs (IkWilJe, Paarden, DiscoKapot) will only be available for selection during their respective seasons.
|
||||||
|
|
||||||
## MuzikaGromche 1337.9001.3 - v73 Happy New Year Edition
|
## MuzikaGromche 1337.9001.3 - v73 Happy New Year Edition
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ namespace MuzikaGromche
|
||||||
{
|
{
|
||||||
["Name"] = audioTrack.Name, // may be different from selectableTrack.Name, if selectable track is a group
|
["Name"] = audioTrack.Name, // may be different from selectableTrack.Name, if selectable track is a group
|
||||||
["IsExplicit"] = selectableTrack.IsExplicit,
|
["IsExplicit"] = selectableTrack.IsExplicit,
|
||||||
|
["Season"] = selectableTrack.Season?.Name,
|
||||||
["Language"] = selectableTrack.Language.Full,
|
["Language"] = selectableTrack.Language.Full,
|
||||||
["WindUpTimer"] = audioTrack.WindUpTimer,
|
["WindUpTimer"] = audioTrack.WindUpTimer,
|
||||||
["Bpm"] = audioTrack.Bpm,
|
["Bpm"] = audioTrack.Bpm,
|
||||||
|
|
|
||||||
|
|
@ -884,6 +884,7 @@ namespace MuzikaGromche
|
||||||
Name = "IkWilJe",
|
Name = "IkWilJe",
|
||||||
AudioType = AudioType.OGGVORBIS,
|
AudioType = AudioType.OGGVORBIS,
|
||||||
Language = Language.ENGLISH,
|
Language = Language.ENGLISH,
|
||||||
|
Season = Season.NewYear,
|
||||||
WindUpTimer = 43.03f,
|
WindUpTimer = 43.03f,
|
||||||
Beats = 13 * 4 + 2, // = 54
|
Beats = 13 * 4 + 2, // = 54
|
||||||
BeatsOffset = 0f,
|
BeatsOffset = 0f,
|
||||||
|
|
@ -909,6 +910,7 @@ namespace MuzikaGromche
|
||||||
Name = "Paarden",
|
Name = "Paarden",
|
||||||
AudioType = AudioType.OGGVORBIS,
|
AudioType = AudioType.OGGVORBIS,
|
||||||
Language = Language.RUSSIAN,
|
Language = Language.RUSSIAN,
|
||||||
|
Season = Season.NewYear,
|
||||||
WindUpTimer = 36.12f,
|
WindUpTimer = 36.12f,
|
||||||
Bars = 8,
|
Bars = 8,
|
||||||
BeatsOffset = 0f,
|
BeatsOffset = 0f,
|
||||||
|
|
@ -934,6 +936,7 @@ namespace MuzikaGromche
|
||||||
Name = "DiscoKapot",
|
Name = "DiscoKapot",
|
||||||
AudioType = AudioType.OGGVORBIS,
|
AudioType = AudioType.OGGVORBIS,
|
||||||
Language = Language.RUSSIAN,
|
Language = Language.RUSSIAN,
|
||||||
|
Season = Season.NewYear,
|
||||||
WindUpTimer = 30.3f,
|
WindUpTimer = 30.3f,
|
||||||
Bars = 8,
|
Bars = 8,
|
||||||
BeatsOffset = 0f,
|
BeatsOffset = 0f,
|
||||||
|
|
@ -970,12 +973,19 @@ namespace MuzikaGromche
|
||||||
public static ISelectableTrack ChooseTrack()
|
public static ISelectableTrack ChooseTrack()
|
||||||
{
|
{
|
||||||
var seed = GetCurrentSeed();
|
var seed = GetCurrentSeed();
|
||||||
var tracks = Config.SkipExplicitTracks.Value ? [.. Tracks.Where(track => !track.IsExplicit)] : Tracks;
|
var today = DateTime.Today;
|
||||||
int[] weights = [.. tracks.Select(track => track.Weight.Value)];
|
var season = SeasonalContentManager.CurrentSeason(today);
|
||||||
|
var tracksEnumerable = SeasonalContentManager.Filter(Tracks, season);
|
||||||
|
if (Config.SkipExplicitTracks.Value)
|
||||||
|
{
|
||||||
|
tracksEnumerable = tracksEnumerable.Where(track => !track.IsExplicit);
|
||||||
|
}
|
||||||
|
var tracks = tracksEnumerable.ToArray();
|
||||||
|
int[] weights = tracks.Select(track => track.Weight.Value).ToArray();
|
||||||
var rwi = new RandomWeightedIndex(weights);
|
var rwi = new RandomWeightedIndex(weights);
|
||||||
var trackId = rwi.GetRandomWeightedIndex(seed);
|
var trackId = rwi.GetRandomWeightedIndex(seed);
|
||||||
var track = tracks[trackId];
|
var track = tracks[trackId];
|
||||||
Log.LogInfo($"Seed is {seed}, chosen track is \"{track.Name}\", #{trackId} of {rwi}");
|
Log.LogInfo($"Seed is {seed}, season is {season?.Name ?? "<none>"}, chosen track is \"{track.Name}\", #{trackId} of {rwi}");
|
||||||
return tracks[trackId];
|
return tracks[trackId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1212,7 +1222,7 @@ namespace MuzikaGromche
|
||||||
|
|
||||||
// An instance of a track which appears as a configuration entry and
|
// An instance of a track which appears as a configuration entry and
|
||||||
// can be selected using weighted random from a list of selectable tracks.
|
// can be selected using weighted random from a list of selectable tracks.
|
||||||
public interface ISelectableTrack
|
public interface ISelectableTrack : ISeasonalContent
|
||||||
{
|
{
|
||||||
// Name of the track, as shown in config entry UI; also used for default file names.
|
// Name of the track, as shown in config entry UI; also used for default file names.
|
||||||
public string Name { get; init; }
|
public string Name { get; init; }
|
||||||
|
|
@ -1423,6 +1433,7 @@ namespace MuzikaGromche
|
||||||
{
|
{
|
||||||
public /* required */ Language Language { get; init; }
|
public /* required */ Language Language { get; init; }
|
||||||
public bool IsExplicit { get; init; } = false;
|
public bool IsExplicit { get; init; } = false;
|
||||||
|
public Season? Season { get; init; } = null;
|
||||||
ConfigEntry<int> ISelectableTrack.Weight { get; set; } = null!;
|
ConfigEntry<int> ISelectableTrack.Weight { get; set; } = null!;
|
||||||
|
|
||||||
IAudioTrack[] ISelectableTrack.GetTracks() => [this];
|
IAudioTrack[] ISelectableTrack.GetTracks() => [this];
|
||||||
|
|
@ -1440,6 +1451,7 @@ namespace MuzikaGromche
|
||||||
public /* required */ string Name { get; init; } = "";
|
public /* required */ string Name { get; init; } = "";
|
||||||
public /* required */ Language Language { get; init; }
|
public /* required */ Language Language { get; init; }
|
||||||
public bool IsExplicit { get; init; } = false;
|
public bool IsExplicit { get; init; } = false;
|
||||||
|
public Season? Season { get; init; } = null;
|
||||||
ConfigEntry<int> ISelectableTrack.Weight { get; set; } = null!;
|
ConfigEntry<int> ISelectableTrack.Weight { get; set; } = null!;
|
||||||
|
|
||||||
public /* required */ IAudioTrack[] Tracks = [];
|
public /* required */ IAudioTrack[] Tracks = [];
|
||||||
|
|
@ -2480,8 +2492,9 @@ namespace MuzikaGromche
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create slider entry for track
|
// Create slider entry for track
|
||||||
|
var seasonal = track.Season is Season season ? $"This is seasonal content for {season.Name}.\n\n" : "";
|
||||||
string warning = track.IsExplicit ? "Explicit Content/Lyrics!\n\n" : "";
|
string warning = track.IsExplicit ? "Explicit Content/Lyrics!\n\n" : "";
|
||||||
string description = $"Language: {language.Full}\n\n{warning}Random (relative) chance of selecting this track.\n\nSet to zero to effectively disable the track.";
|
string description = $"Language: {language.Full}\n\n{seasonal}{warning}Random (relative) chance of selecting this track.\n\nSet to zero to effectively disable the track.";
|
||||||
track.Weight = configFile.Bind(
|
track.Weight = configFile.Bind(
|
||||||
new ConfigDefinition(section, track.Name),
|
new ConfigDefinition(section, track.Name),
|
||||||
50,
|
50,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace MuzikaGromche;
|
||||||
|
|
||||||
|
public delegate bool SeasonalContentPredicate(DateTime dateTime);
|
||||||
|
|
||||||
|
// I'm not really sure what to do with seasonal content yet.
|
||||||
|
//
|
||||||
|
// There could be two approaches:
|
||||||
|
// - Force seasonal content tracks to be the only available tracks during their season.
|
||||||
|
// Then seasons must be short, so they don't cut off too much content for too long.
|
||||||
|
// - Exclude seasonal content tracks from the pool when their season is not active.
|
||||||
|
// Considering how many tracks are there in the playlist permanently already,
|
||||||
|
// this might not give the seasonal content enough visibility.
|
||||||
|
//
|
||||||
|
// Either way, seasonal content tracks would be listed in the config UI at all times,
|
||||||
|
// which makes it confusing if you try to select only seasonal tracks outside of their season.
|
||||||
|
|
||||||
|
// Seasons may NOT overlap. There is at most ONE active season at any given date.
|
||||||
|
public readonly record struct Season(string Name, string Description, SeasonalContentPredicate IsActive)
|
||||||
|
{
|
||||||
|
public override string ToString() => Name;
|
||||||
|
|
||||||
|
public static readonly Season NewYear = new("New Year", "New Year and Christmas holiday season", dateTime =>
|
||||||
|
{
|
||||||
|
// December 10 - February 29
|
||||||
|
var month = dateTime.Month;
|
||||||
|
var day = dateTime.Day;
|
||||||
|
return (month == 12 && day >= 10) || (month == 1) || (month == 2 && day <= 29);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note: it is important that this property goes last
|
||||||
|
public static readonly Season[] All = [NewYear];
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ISeasonalContent
|
||||||
|
{
|
||||||
|
public Season? Season { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SeasonalContentManager
|
||||||
|
{
|
||||||
|
public static Season? CurrentSeason(DateTime dateTime)
|
||||||
|
{
|
||||||
|
foreach (var season in Season.All)
|
||||||
|
{
|
||||||
|
if (season.IsActive(dateTime))
|
||||||
|
{
|
||||||
|
return season;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Season? CurrentSeason() => CurrentSeason(DateTime.Today);
|
||||||
|
|
||||||
|
// Take second approach: filter out seasonal content that is not in the current season.
|
||||||
|
public static IEnumerable<T> Filter<T>(this IEnumerable<T> items, Season? season) where T : ISeasonalContent
|
||||||
|
{
|
||||||
|
return items.Where(item =>
|
||||||
|
{
|
||||||
|
if (item.Season == null)
|
||||||
|
{
|
||||||
|
return true; // always available
|
||||||
|
}
|
||||||
|
return item.Season == season;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<T> Filter<T>(this IEnumerable<T> items, DateTime dateTime) where T : ISeasonalContent
|
||||||
|
{
|
||||||
|
var season = CurrentSeason(dateTime);
|
||||||
|
return Filter(items, season);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<T> Filter<T>(this IEnumerable<T> items) where T : ISeasonalContent
|
||||||
|
{
|
||||||
|
return Filter(items, DateTime.Today);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue