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 DiscoKapot.
|
||||
- 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
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ namespace MuzikaGromche
|
|||
{
|
||||
["Name"] = audioTrack.Name, // may be different from selectableTrack.Name, if selectable track is a group
|
||||
["IsExplicit"] = selectableTrack.IsExplicit,
|
||||
["Season"] = selectableTrack.Season?.Name,
|
||||
["Language"] = selectableTrack.Language.Full,
|
||||
["WindUpTimer"] = audioTrack.WindUpTimer,
|
||||
["Bpm"] = audioTrack.Bpm,
|
||||
|
|
|
|||
|
|
@ -884,6 +884,7 @@ namespace MuzikaGromche
|
|||
Name = "IkWilJe",
|
||||
AudioType = AudioType.OGGVORBIS,
|
||||
Language = Language.ENGLISH,
|
||||
Season = Season.NewYear,
|
||||
WindUpTimer = 43.03f,
|
||||
Beats = 13 * 4 + 2, // = 54
|
||||
BeatsOffset = 0f,
|
||||
|
|
@ -909,6 +910,7 @@ namespace MuzikaGromche
|
|||
Name = "Paarden",
|
||||
AudioType = AudioType.OGGVORBIS,
|
||||
Language = Language.RUSSIAN,
|
||||
Season = Season.NewYear,
|
||||
WindUpTimer = 36.12f,
|
||||
Bars = 8,
|
||||
BeatsOffset = 0f,
|
||||
|
|
@ -934,6 +936,7 @@ namespace MuzikaGromche
|
|||
Name = "DiscoKapot",
|
||||
AudioType = AudioType.OGGVORBIS,
|
||||
Language = Language.RUSSIAN,
|
||||
Season = Season.NewYear,
|
||||
WindUpTimer = 30.3f,
|
||||
Bars = 8,
|
||||
BeatsOffset = 0f,
|
||||
|
|
@ -970,12 +973,19 @@ namespace MuzikaGromche
|
|||
public static ISelectableTrack ChooseTrack()
|
||||
{
|
||||
var seed = GetCurrentSeed();
|
||||
var tracks = Config.SkipExplicitTracks.Value ? [.. Tracks.Where(track => !track.IsExplicit)] : Tracks;
|
||||
int[] weights = [.. tracks.Select(track => track.Weight.Value)];
|
||||
var today = DateTime.Today;
|
||||
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 trackId = rwi.GetRandomWeightedIndex(seed);
|
||||
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];
|
||||
}
|
||||
|
||||
|
|
@ -1212,7 +1222,7 @@ namespace MuzikaGromche
|
|||
|
||||
// An instance of a track which appears as a configuration entry and
|
||||
// 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.
|
||||
public string Name { get; init; }
|
||||
|
|
@ -1423,6 +1433,7 @@ namespace MuzikaGromche
|
|||
{
|
||||
public /* required */ Language Language { get; init; }
|
||||
public bool IsExplicit { get; init; } = false;
|
||||
public Season? Season { get; init; } = null;
|
||||
ConfigEntry<int> ISelectableTrack.Weight { get; set; } = null!;
|
||||
|
||||
IAudioTrack[] ISelectableTrack.GetTracks() => [this];
|
||||
|
|
@ -1440,6 +1451,7 @@ namespace MuzikaGromche
|
|||
public /* required */ string Name { get; init; } = "";
|
||||
public /* required */ Language Language { get; init; }
|
||||
public bool IsExplicit { get; init; } = false;
|
||||
public Season? Season { get; init; } = null;
|
||||
ConfigEntry<int> ISelectableTrack.Weight { get; set; } = null!;
|
||||
|
||||
public /* required */ IAudioTrack[] Tracks = [];
|
||||
|
|
@ -2480,8 +2492,9 @@ namespace MuzikaGromche
|
|||
}
|
||||
|
||||
// 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 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(
|
||||
new ConfigDefinition(section, track.Name),
|
||||
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