Compare commits

...
25 changed files with 329 additions and 92 deletions
+16 -1
View File
@@ -5,6 +5,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
## [0.0.58-alpha] - 2021-09-15
### Added
- Add `released_notinthelast` search field for relative release date queries
- Syntax is a number and a unit (days, weeks, months, years) like `1 week` or `2 years`
- Add `released_onthisday` search field for historical queries
- Syntax is `released_onthisday:1` and will search for items released on this month number and day number in prior years
- Add tooltip explaining `Keep Multi-Part Episodes Together`
### Fixed
- Properly display watermark when no other video filters (like scaling or padding) are required
- Fix building some playouts in timezones with positive offsets (like UTC+2)
- Fix `Shuffle In Order` so all collections/shows start from the earliest episode
- You may need to rebuild playouts to see this fixed behavior more quickly
## [0.0.57-alpha] - 2021-09-10
### Added
- Add `released_inthelast` search field for relative release date queries
@@ -580,7 +594,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Initial release to facilitate testing outside of Docker.
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.57-alpha...HEAD
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.58-alpha...HEAD
[0.0.58-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.57-alpha...v0.0.57-alpha
[0.0.57-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.56-alpha...v0.0.57-alpha
[0.0.56-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.55-alpha...v0.0.56-alpha
[0.0.55-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.54-alpha...v0.0.55-alpha
@@ -67,7 +67,7 @@ namespace ErsatzTV.Application.Emby.Commands
private async Task<Unit> Synchronize(RequestParameters parameters)
{
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero);
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval);
if (parameters.ForceScan || nextScan < DateTimeOffset.Now)
{
@@ -67,7 +67,7 @@ namespace ErsatzTV.Application.Jellyfin.Commands
private async Task<Unit> Synchronize(RequestParameters parameters)
{
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero);
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval);
if (parameters.ForceScan || nextScan < DateTimeOffset.Now)
{
+2 -1
View File
@@ -1,5 +1,6 @@
using System;
using System.Linq;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Jellyfin;
@@ -43,7 +44,7 @@ namespace ErsatzTV.Application.MediaCards
bool isSearchResult) =>
new(
episodeMetadata.EpisodeId,
episodeMetadata.ReleaseDate ?? DateTime.MinValue,
episodeMetadata.ReleaseDate ?? SystemTime.MinValueUtc,
episodeMetadata.Episode.Season.Show.ShowMetadata.HeadOrNone().Match(
m => m.Title ?? string.Empty,
() => string.Empty),
@@ -78,7 +78,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
decimal progressMin = (decimal) i / localLibrary.Paths.Count;
decimal progressMax = (decimal) (i + 1) / localLibrary.Paths.Count;
var lastScan = new DateTimeOffset(libraryPath.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
var lastScan = new DateTimeOffset(libraryPath.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero);
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
if (forceScan || nextScan < DateTimeOffset.Now)
{
@@ -65,7 +65,7 @@ namespace ErsatzTV.Application.Plex.Commands
private async Task<Unit> Synchronize(RequestParameters parameters)
{
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero);
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval);
if (parameters.ForceScan || nextScan < DateTimeOffset.Now)
{
@@ -4,6 +4,7 @@ using ErsatzTV.Core.FFmpeg;
using FluentAssertions;
using LanguageExt;
using NUnit.Framework;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Tests.FFmpeg
{
@@ -113,6 +114,161 @@ namespace ErsatzTV.Core.Tests.FFmpeg
});
}
[Test]
[TestCase(
false,
false,
false,
ChannelWatermarkLocation.BottomLeft,
false,
100,
"[0:0][1:v]overlay=x=134:y=H-h-54[v]",
"0:1",
"[v]")]
[TestCase(
false,
false,
false,
ChannelWatermarkLocation.BottomRight,
false,
100,
"[0:0][1:v]overlay=x=W-w-134:y=H-h-54[v]",
"0:1",
"[v]")]
[TestCase(
false,
false,
false,
ChannelWatermarkLocation.TopLeft,
false,
100,
"[0:0][1:v]overlay=x=134:y=54[v]",
"0:1",
"[v]")]
[TestCase(
false,
false,
false,
ChannelWatermarkLocation.TopRight,
false,
100,
"[0:0][1:v]overlay=x=W-w-134:y=54[v]",
"0:1",
"[v]")]
[TestCase(
false,
false,
true,
ChannelWatermarkLocation.TopLeft,
false,
100,
"[0:0][1:v]overlay=x=134:y=54:enable='lt(mod(mod(time(0),60*60),10*60),15)'[v]",
"0:1",
"[v]")]
[TestCase(
false,
false,
false,
ChannelWatermarkLocation.TopLeft,
true,
100,
"[1:v]scale=384:-1[wmp];[0:0][wmp]overlay=x=134:y=54[v]",
"0:1",
"[v]")]
[TestCase(
false,
false,
false,
ChannelWatermarkLocation.TopLeft,
false,
90,
"[1:v]format=yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8,colorchannelmixer=aa=0.90[wmp];[0:0][wmp]overlay=x=134:y=54[v]",
"0:1",
"[v]")]
[TestCase(
false,
true,
false,
ChannelWatermarkLocation.TopLeft,
false,
100,
"[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]",
"0:1",
"[v]")]
[TestCase(
false,
true,
false,
ChannelWatermarkLocation.TopLeft,
true,
100,
"[0:0]yadif=1[vt];[1:v]scale=384:-1[wmp];[vt][wmp]overlay=x=134:y=54[v]",
"0:1",
"[v]")]
[TestCase(
true,
true,
false,
ChannelWatermarkLocation.TopLeft,
false,
100,
"[0:1]apad=whole_dur=3300000ms[a];[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]",
"[a]",
"[v]")]
[TestCase(
true,
false,
false,
ChannelWatermarkLocation.TopLeft,
false,
100,
"[0:1]apad=whole_dur=3300000ms[a];[0:0][1:v]overlay=x=134:y=54[v]",
"[a]",
"[v]")]
public void Should_Return_Watermark(
bool alignAudio,
bool deinterlace,
bool intermittent,
ChannelWatermarkLocation location,
bool scaled,
int opacity,
string expectedVideoFilter,
string expectedAudioLabel,
string expectedVideoLabel)
{
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
.WithWatermark(
Some(
new ChannelWatermark
{
Mode = intermittent
? ChannelWatermarkMode.Intermittent
: ChannelWatermarkMode.Permanent,
DurationSeconds = intermittent ? 15 : 0,
FrequencyMinutes = intermittent ? 10 : 0,
Location = location,
Size = scaled ? ChannelWatermarkSize.Scaled : ChannelWatermarkSize.ActualSize,
WidthPercent = scaled ? 20 : 0,
Opacity = opacity,
HorizontalMarginPercent = 7,
VerticalMarginPercent = 5
}),
new Resolution { Width = 1920, Height = 1080 })
.WithDeinterlace(deinterlace)
.WithAlignedAudio(alignAudio ? Some(TimeSpan.FromMinutes(55)) : None);
Option<FFmpegComplexFilter> result = builder.Build(0, 1);
result.IsSome.Should().BeTrue();
result.IfSome(
filter =>
{
filter.ComplexFilter.Should().Be(expectedVideoFilter);
filter.AudioLabel.Should().Be(expectedAudioLabel);
filter.VideoLabel.Should().Be(expectedVideoLabel);
});
}
[Test]
[TestCase(true, false, false, "[0:0]deinterlace_qsv[v]", "[v]")]
[TestCase(
+1 -1
View File
@@ -4,6 +4,6 @@ namespace ErsatzTV.Core.Tests.Fakes
{
public record FakeFileEntry(string Path)
{
public DateTime LastWriteTime { get; set; } = DateTime.MinValue;
public DateTime LastWriteTime { get; set; } = SystemTime.MinValueUtc;
}
}
@@ -41,7 +41,7 @@ namespace ErsatzTV.Core.Tests.Fakes
public DateTime GetLastWriteTime(string path) =>
Optional(_files.SingleOrDefault(f => f.Path == path))
.Map(f => f.LastWriteTime)
.IfNone(DateTime.MinValue);
.IfNone(SystemTime.MinValueUtc);
public bool IsLibraryPathAccessible(LibraryPath libraryPath) =>
_files.Any(f => f.Path.StartsWith(libraryPath.Path + Path.DirectorySeparatorChar));
@@ -233,28 +233,39 @@ namespace ErsatzTV.Core.FFmpeg
complexFilter.Append(audioLabel);
}
if (videoFilterQueue.Any())
if (videoFilterQueue.Any() || !string.IsNullOrWhiteSpace(watermarkOverlay))
{
if (hasAudioFilters)
{
complexFilter.Append(';');
}
complexFilter.Append($"[{videoLabel}]");
var filters = string.Join(",", videoFilterQueue);
complexFilter.Append(filters);
if (videoFilterQueue.Any())
{
complexFilter.Append($"[{videoLabel}]");
var filters = string.Join(",", videoFilterQueue);
complexFilter.Append(filters);
}
if (!string.IsNullOrWhiteSpace(watermarkOverlay))
{
complexFilter.Append("[vt]");
if (videoFilterQueue.Any())
{
complexFilter.Append("[vt];");
}
var watermarkLabel = "[1:v]";
if (!string.IsNullOrWhiteSpace(watermarkPreprocess))
{
complexFilter.Append($";{watermarkLabel}{watermarkPreprocess}[wmp]");
complexFilter.Append($"{watermarkLabel}{watermarkPreprocess}[wmp];");
watermarkLabel = "[wmp]";
}
complexFilter.Append($";[vt]{watermarkLabel}{watermarkOverlay}");
complexFilter.Append(
videoFilterQueue.Any()
? $"[vt]{watermarkLabel}{watermarkOverlay}"
: $"[{videoLabel}]{watermarkLabel}{watermarkOverlay}");
if (usesSoftwareFilters && acceleration != HardwareAccelerationKind.None)
{
complexFilter.Append(",hwupload");
+1 -1
View File
@@ -22,7 +22,7 @@ namespace ErsatzTV.Core.Metadata
}
public DateTime GetLastWriteTime(string path) =>
Try(File.GetLastWriteTimeUtc(path)).IfFail(() => DateTime.MinValue);
Try(File.GetLastWriteTimeUtc(path)).IfFail(() => SystemTime.MinValueUtc);
public bool IsLibraryPathAccessible(LibraryPath libraryPath) =>
Directory.Exists(libraryPath.Path);
@@ -218,7 +218,7 @@ namespace ErsatzTV.Core.Metadata
existing.Tagline = metadata.Tagline;
existing.Title = metadata.Title;
if (existing.DateAdded == DateTime.MinValue)
if (existing.DateAdded == SystemTime.MinValueUtc)
{
existing.DateAdded = metadata.DateAdded;
}
@@ -318,7 +318,7 @@ namespace ErsatzTV.Core.Metadata
existing.Tagline = metadata.Tagline;
existing.Title = metadata.Title;
if (existing.DateAdded == DateTime.MinValue)
if (existing.DateAdded == SystemTime.MinValueUtc)
{
existing.DateAdded = metadata.DateAdded;
}
@@ -423,7 +423,7 @@ namespace ErsatzTV.Core.Metadata
existing.Tagline = metadata.Tagline;
existing.Title = metadata.Title;
if (existing.DateAdded == DateTime.MinValue)
if (existing.DateAdded == SystemTime.MinValueUtc)
{
existing.DateAdded = metadata.DateAdded;
}
@@ -486,7 +486,7 @@ namespace ErsatzTV.Core.Metadata
existing.Disambiguation = metadata.Disambiguation;
existing.Biography = metadata.Biography;
if (existing.DateAdded == DateTime.MinValue)
if (existing.DateAdded == SystemTime.MinValueUtc)
{
existing.DateAdded = metadata.DateAdded;
}
@@ -581,7 +581,7 @@ namespace ErsatzTV.Core.Metadata
existing.Plot = metadata.Plot;
existing.Album = metadata.Album;
if (existing.DateAdded == DateTime.MinValue)
if (existing.DateAdded == SystemTime.MinValueUtc)
{
existing.DateAdded = metadata.DateAdded;
}
@@ -322,7 +322,7 @@ namespace ErsatzTV.Core.Metadata
async () =>
{
bool shouldUpdate = Optional(episode.EpisodeMetadata).Flatten().HeadOrNone().Match(
m => m.DateUpdated == DateTime.MinValue,
m => m.DateUpdated == SystemTime.MinValueUtc,
true);
if (shouldUpdate)
+2 -2
View File
@@ -374,9 +374,9 @@ namespace ErsatzTV.Core.Scheduling
}
bool willNotFinishInTime =
currentTime <= durationFinish.IfNone(DateTime.MinValue) &&
currentTime <= durationFinish.IfNone(SystemTime.MinValueUtc) &&
currentTime + peekVersion.Duration >
durationFinish.IfNone(DateTime.MinValue);
durationFinish.IfNone(SystemTime.MinValueUtc);
if (willNotFinishInTime)
{
_logger.LogDebug(
@@ -147,13 +147,7 @@ namespace ErsatzTV.Core.Scheduling
ordered.AddRange(larger);
}
int offset = random.Next(ordered.Count);
result.Add(
new OrderedCollection
{
Index = 0,
Items = ordered.Skip(offset).Concat(ordered.Take(offset)).ToList()
});
result.Add(new OrderedCollection { Index = 0, Items = ordered });
}
return result;
+9
View File
@@ -0,0 +1,9 @@
using System;
namespace ErsatzTV.Core
{
public static class SystemTime
{
public static DateTime MinValueUtc = new(0, DateTimeKind.Utc);
}
}
@@ -703,7 +703,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
new()
{
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.MinValue,
DateUpdated = SystemTime.MinValueUtc,
MetadataKind = MetadataKind.Fallback,
Actors = new List<Actor>(),
Guids = new List<MetadataGuid>(),
@@ -16,12 +16,12 @@
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00014" />
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00014" />
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00014" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.9">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.10" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="16.10.56">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using Lucene.Net.Analysis;
using Lucene.Net.QueryParsers.Classic;
using Lucene.Net.Search;
using Lucene.Net.Util;
namespace ErsatzTV.Infrastructure.Search
{
public class CustomMultiFieldQueryParser : MultiFieldQueryParser
{
public CustomMultiFieldQueryParser(
LuceneVersion matchVersion,
string[] fields,
Analyzer analyzer,
IDictionary<string, float> boosts) : base(matchVersion, fields, analyzer, boosts)
{
}
public CustomMultiFieldQueryParser(LuceneVersion matchVersion, string[] fields, Analyzer analyzer) : base(
matchVersion,
fields,
analyzer)
{
}
protected override Query GetFieldQuery(string field, string queryText, bool quoted)
{
if (field == "released_onthisday")
{
var todayString = DateTime.Today.ToString("*MMdd");
return base.GetWildcardQuery("release_date", todayString);
}
return base.GetFieldQuery(field, queryText, quoted);
}
protected override Query GetFieldQuery(string field, string queryText, int slop)
{
if (field == "released_inthelast" && CustomQueryParser.ParseStart(queryText, out DateTime start))
{
var todayString = DateTime.Today.ToString("yyyyMMdd");
var dateString = start.ToString("yyyyMMdd");
return base.GetRangeQuery("release_date", dateString, todayString, true, true);
}
if (field == "released_notinthelast" && CustomQueryParser.ParseStart(queryText, out DateTime finish))
{
var dateString = finish.ToString("yyyyMMdd");
return base.GetRangeQuery("release_date", "00000000", dateString, false, false);
}
return base.GetFieldQuery(field, queryText, slop);
}
}
}
@@ -1,4 +1,5 @@
using System;
using ErsatzTV.Core;
using Lucene.Net.Analysis;
using Lucene.Net.QueryParsers.Classic;
using Lucene.Net.Search;
@@ -6,19 +7,30 @@ using Lucene.Net.Util;
namespace ErsatzTV.Infrastructure.Search
{
public class RelativeDateQueryParser : QueryParser
public class CustomQueryParser : QueryParser
{
public RelativeDateQueryParser(LuceneVersion matchVersion, string f, Analyzer a) : base(matchVersion, f, a)
public CustomQueryParser(LuceneVersion matchVersion, string f, Analyzer a) : base(matchVersion, f, a)
{
}
protected internal RelativeDateQueryParser(ICharStream stream) : base(stream)
protected internal CustomQueryParser(ICharStream stream) : base(stream)
{
}
protected RelativeDateQueryParser(QueryParserTokenManager tm) : base(tm)
protected CustomQueryParser(QueryParserTokenManager tm) : base(tm)
{
}
protected override Query GetFieldQuery(string field, string queryText, bool quoted)
{
if (field == "released_onthisday")
{
var todayString = DateTime.Today.ToString("*MMdd");
return base.GetWildcardQuery("release_date", todayString);
}
return base.GetFieldQuery(field, queryText, quoted);
}
protected override Query GetFieldQuery(string field, string queryText, int slop)
{
@@ -29,13 +41,20 @@ namespace ErsatzTV.Infrastructure.Search
return base.GetRangeQuery("release_date", dateString, todayString, true, true);
}
if (field == "released_notinthelast" && ParseStart(queryText, out DateTime finish))
{
var dateString = finish.ToString("yyyyMMdd");
return base.GetRangeQuery("release_date", "00000000", dateString, false, false);
}
return base.GetFieldQuery(field, queryText, slop);
}
internal static bool ParseStart(string text, out DateTime start)
{
start = DateTime.MinValue;
start = SystemTime.MinValueUtc;
try
{
@@ -1,40 +0,0 @@
using System;
using System.Collections.Generic;
using Lucene.Net.Analysis;
using Lucene.Net.QueryParsers.Classic;
using Lucene.Net.Search;
using Lucene.Net.Util;
namespace ErsatzTV.Infrastructure.Search
{
public class RelativeDateMultiFieldQueryParser : MultiFieldQueryParser
{
public RelativeDateMultiFieldQueryParser(
LuceneVersion matchVersion,
string[] fields,
Analyzer analyzer,
IDictionary<string, float> boosts) : base(matchVersion, fields, analyzer, boosts)
{
}
public RelativeDateMultiFieldQueryParser(LuceneVersion matchVersion, string[] fields, Analyzer analyzer) : base(
matchVersion,
fields,
analyzer)
{
}
protected override Query GetFieldQuery(string field, string queryText, int slop)
{
if (field == "released_inthelast" && RelativeDateQueryParser.ParseStart(queryText, out DateTime start))
{
var todayString = DateTime.Today.ToString("yyyyMMdd");
var dateString = start.ToString("yyyyMMdd");
return base.GetRangeQuery("release_date", dateString, todayString, true, true);
}
return base.GetFieldQuery(field, queryText, slop);
}
}
}
@@ -148,8 +148,8 @@ namespace ErsatzTV.Infrastructure.Search
};
using var analyzerWrapper = new PerFieldAnalyzerWrapper(analyzer, customAnalyzers);
QueryParser parser = !string.IsNullOrWhiteSpace(searchField)
? new RelativeDateQueryParser(AppLuceneVersion, searchField, analyzerWrapper)
: new RelativeDateMultiFieldQueryParser(AppLuceneVersion, new[] { TitleField }, analyzerWrapper);
? new CustomQueryParser(AppLuceneVersion, searchField, analyzerWrapper)
: new CustomMultiFieldQueryParser(AppLuceneVersion, new[] { TitleField }, analyzerWrapper);
parser.AllowLeadingWildcard = true;
Query query = ParseQuery(searchQuery, parser);
var filter = new DuplicateFilter(TitleAndYearField);
+3 -3
View File
@@ -25,8 +25,8 @@
<PackageReference Include="Markdig" Version="0.26.0" />
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="3.0.1" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.9">
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -34,7 +34,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MudBlazor" Version="5.1.3" />
<PackageReference Include="MudBlazor" Version="5.1.4" />
<PackageReference Include="NaturalSort.Extension" Version="3.1.0" />
<PackageReference Include="PPioli.FluentValidation.Blazor" Version="5.0.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="6.0.94" />
+5 -3
View File
@@ -17,9 +17,11 @@
<MudCardContent>
<MudTextField Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/>
<MudElement HtmlTag="div" Class="mt-3">
<MudCheckBox Label="Keep Multi-Part Episodes Together"
@bind-Checked="@_model.KeepMultiPartEpisodesTogether"
For="@(() => _model.KeepMultiPartEpisodesTogether)"/>
<MudTooltip Text="Always schedule multi-part episodes chronologically when shuffling">
<MudCheckBox Label="Keep Multi-Part Episodes Together"
@bind-Checked="@_model.KeepMultiPartEpisodesTogether"
For="@(() => _model.KeepMultiPartEpisodesTogether)"/>
</MudTooltip>
</MudElement>
<MudElement HtmlTag="div" Class="mt-3">
<MudTooltip Text="This is useful for multi-part crossover episodes">
+16 -4
View File
@@ -24,7 +24,6 @@ The following fields are available for searching movies:
- `content_rating`: The movie content rating (case-sensitive)
- `language`: The movie audio stream language
- `release_date`: The movie release date (YYYYMMDD)
- `released_inthelast`: A range for the movie release date (days, weeks, months, years)
- `type`: Always `movie`
### Shows
@@ -41,7 +40,6 @@ The following fields are available for searching shows:
- `content_rating`: The show content rating (case-sensitive)
- `language`: The show audio stream language
- `release_date`: The show release date (YYYYMMDD)
- `released_inthelast`: A range for the show release date (days, weeks, months, years)
- `type`: Always `show`
### Episodes
@@ -55,7 +53,6 @@ The following fields are available for searching episodes:
- `library_name`: The name of the library that contains the episode
- `language`: The episode audio stream language
- `release_date`: The episode release date (YYYYMMDD)
- `released_inthelast`: A range for the episode release date (days, weeks, months, years)
- `type`: Always `episode`
### Artists
@@ -77,8 +74,15 @@ The following fields are available for searching music videos:
- `genre`: The music video genre
- `library_name`: The name of the library that contains the music video
- `language`: The music video audio stream language
- `release_date`: The music video release date (YYYYMMDD)
- `type`: Always `music_video`
## Special Search Fields
- `released_inthelast`: For any media type that supports `release_date`, `released_inthelast` takes a number and a unit (days, weeks, months, years) and returns items released between the specified time ago and now
- `released_notinthelast`: For any media type that supports `release_date`, `released_notinthelast` takes a number and a unit (days, weeks, months, years) and returns items released before the specified time ago
- `released_onthisday`: For any media type that supports `release_date`, `released_onthisday` takes any value (ignored) and will return items released on this month number and day number in previous years
## Sample Searches
### Christmas
@@ -103,4 +107,12 @@ The following fields are available for searching music videos:
### Episodes from the past week
`type:episode AND released_inthelast:"1 week"`
`type:episode AND released_inthelast:"1 week"`
### Episodes older than the past week
`type:episode AND released_notinthelast:"1 week"`
### Episodes released on this day
`type:episode AND released_onthisday:1`