diff --git a/Shoko.Server/API/v3/Controllers/FileController.cs b/Shoko.Server/API/v3/Controllers/FileController.cs
index c9a0e6546..bf10877a3 100644
--- a/Shoko.Server/API/v3/Controllers/FileController.cs
+++ b/Shoko.Server/API/v3/Controllers/FileController.cs
@@ -61,14 +61,11 @@ public FileController(TraktTVHelper traktHelper, ICommandRequestFactory commandF
///
/// Limits the number of results per page. Set to 0 to disable the limit.
/// Page number.
+ /// Include items that are not included by default
+ /// Exclude items of certain types
+ /// Filter to only include items of certain types
/// Sort ordering. Attach '-' at the start to reverse the order of the criteria.
/// Include data from selected s.
- /// Filter the search to only files for a given shoko series.
- /// Filter the search to only files for a given shoko episode.
- /// Filter the search to only files for a given anidb series.
- /// Filter the search to only files for a given anidb episode.
- /// An optional search query to filter files based on their absolute paths.
- /// Indicates that fuzzy-matching should be used for the search query.
/// A sliced part of the results for the current query.
[HttpGet]
public ActionResult> GetFiles(
@@ -78,65 +75,15 @@ public ActionResult> GetFiles(
[FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] FileExcludeTypes[] exclude = default,
[FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] FileIncludeOnlyType[] include_only = default,
[FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] List sortOrder = null,
- [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet includeDataFrom = null,
- [FromQuery] int? seriesID = null,
- [FromQuery] int? episodeID = null,
- [FromQuery] int? anidbSeriesID = null,
- [FromQuery] int? anidbEpisodeID = null,
- [FromQuery] string search = null,
- [FromQuery] bool fuzzy = true)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet includeDataFrom = null)
{
include ??= Array.Empty();
exclude ??= Array.Empty();
include_only ??= Array.Empty();
- // Map shoko series id to anidb series id and check if the series
- // exists.
- if (seriesID.HasValue)
- {
- if (seriesID.Value <= 0)
- return new ListResult();
-
- var series = RepoFactory.AnimeSeries.GetByID(seriesID.Value);
- if (series == null)
- return new ListResult();
-
- anidbSeriesID = series.AniDB_ID;
- }
- // Map shoko episode id to anidb episode id and check if the episode
- // exists.
- if (episodeID.HasValue)
- {
- if (episodeID.Value <= 0)
- return new ListResult();
-
- var episode = RepoFactory.AnimeEpisode.GetByID(episodeID.Value);
- if (episode == null)
- return new ListResult();
-
- anidbEpisodeID = episode.AniDB_EpisodeID;
- }
- // Check if the anidb episode exists locally, and if it is part of the
- // same anidb series if both are provided.
- if (anidbEpisodeID.HasValue)
- {
- var anidbEpisode = RepoFactory.AniDB_Episode.GetByEpisodeID(anidbEpisodeID.Value);
- if (anidbEpisode == null)
- return new ListResult();
-
- if (anidbSeriesID.HasValue && anidbEpisode.AnimeID != anidbSeriesID.Value)
- return new ListResult();
- }
- // Check if the anidb anime exists locally.
- else if (anidbSeriesID.HasValue)
- {
- var anidbSeries = RepoFactory.AniDB_Anime.GetByAnimeID(anidbSeriesID.Value);
- if (anidbSeries == null)
- return new ListResult();
- }
// Filtering.
var user = User;
- var includeLocations = exclude.Contains(FileExcludeTypes.Duplicates) || !string.IsNullOrEmpty(search) ||
+ var includeLocations = exclude.Contains(FileExcludeTypes.Duplicates) ||
(sortOrder?.Any(criteria => criteria.Contains(FileSortCriteria.DuplicateCount.ToString())) ?? false);
var includeUserRecord = exclude.Contains(FileExcludeTypes.Watched) || (sortOrder?.Any(criteria =>
criteria.Contains(FileSortCriteria.ViewedAt.ToString()) || criteria.Contains(FileSortCriteria.WatchedAt.ToString())) ?? false);
@@ -149,7 +96,7 @@ public ActionResult> GetFiles(
))
.Where(tuple =>
{
- var (video, bestLocation, locations, userRecord) = tuple;
+ var (video, _, locations, userRecord) = tuple;
var xrefs = video.EpisodeCrossRefs;
var isAnimeAllowed = xrefs
.Select(xref => xref.AnimeID)
@@ -160,23 +107,6 @@ public ActionResult> GetFiles(
if (!isAnimeAllowed)
return false;
- if (anidbSeriesID.HasValue || anidbEpisodeID.HasValue)
- {
- var isLinkedToAnimeOrEpisode = xrefs
- .Where(
- anidbSeriesID.HasValue && anidbEpisodeID.HasValue ? (
- xref => xref.AnimeID == anidbSeriesID.Value && xref.EpisodeID == anidbEpisodeID.Value
- ) : anidbSeriesID.HasValue ? (
- xref => xref.AnimeID == anidbSeriesID.Value
- ) : (
- xref => xref.EpisodeID == anidbEpisodeID.Value
- )
- )
- .Any();
- if (!isLinkedToAnimeOrEpisode)
- return false;
- }
-
if (!include.Contains(FileNonDefaultIncludeType.Ignored) && video.IsIgnored) return false;
if (include_only.Contains(FileIncludeOnlyType.Ignored) && !video.IsIgnored) return false;
@@ -195,16 +125,10 @@ public ActionResult> GetFiles(
return true;
});
- // Search.
- if (!string.IsNullOrEmpty(search))
- enumerable = enumerable
- .Search(search, tuple => tuple.Locations.Select(place => place.FullServerPath).Where(path => path != null), fuzzy)
- .Select(result => result.Result);
-
// Sorting.
if (sortOrder != null && sortOrder.Count > 0)
enumerable = Models.Shoko.File.OrderBy(enumerable, sortOrder);
- else if (string.IsNullOrEmpty(search))
+ else
enumerable = Models.Shoko.File.OrderBy(enumerable, new()
{
// First sort by import folder from A-Z.
@@ -218,6 +142,91 @@ public ActionResult> GetFiles(
tuple => new File(tuple.UserRecord, tuple.Video, include.Contains(FileNonDefaultIncludeType.XRefs), includeDataFrom,
include.Contains(FileNonDefaultIncludeType.MediaInfo), include.Contains(FileNonDefaultIncludeType.AbsolutePaths)), page, pageSize);
}
+
+ ///
+ /// Get or search through the files accessible to the current user.
+ ///
+ /// Limits the number of results per page. Set to 0 to disable the limit.
+ /// Page number.
+ /// Include items that are not included by default
+ /// Exclude items of certain types
+ /// Filter to only include items of certain types
+ /// Sort ordering. Attach '-' at the start to reverse the order of the criteria.
+ /// Include data from selected s.
+ /// An optional search query to filter files based on their absolute paths.
+ /// Indicates that fuzzy-matching should be used for the search query.
+ /// A sliced part of the results for the current query.
+ [HttpGet("Search/{*query}")]
+ public ActionResult> Search([FromRoute] string query,
+ [FromQuery, Range(0, 1000)] int pageSize = 100,
+ [FromQuery, Range(1, int.MaxValue)] int page = 1,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] FileNonDefaultIncludeType[] include = default,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] FileExcludeTypes[] exclude = default,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] FileIncludeOnlyType[] include_only = default,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] List sortOrder = null,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet includeDataFrom = null,
+ [FromQuery] bool fuzzy = true)
+ {
+ include ??= Array.Empty();
+ exclude ??= Array.Empty();
+ include_only ??= Array.Empty();
+
+ // Filtering.
+ var user = User;
+ var includeUserRecord = exclude.Contains(FileExcludeTypes.Watched) || (sortOrder?.Any(criteria =>
+ criteria.Contains(FileSortCriteria.ViewedAt.ToString()) || criteria.Contains(FileSortCriteria.WatchedAt.ToString())) ?? false);
+ var enumerable = RepoFactory.VideoLocal.GetAll()
+ .Select(video => (
+ Video: video,
+ BestLocation: video.GetBestVideoLocalPlace(),
+ Locations: video.Places,
+ UserRecord: includeUserRecord ? video.GetUserRecord(user.JMMUserID) : null
+ ))
+ .Where(tuple =>
+ {
+ var (video, _, locations, userRecord) = tuple;
+ var xrefs = video.EpisodeCrossRefs;
+ var isAnimeAllowed = xrefs
+ .Select(xref => xref.AnimeID)
+ .Distinct()
+ .Select(anidbID => RepoFactory.AniDB_Anime.GetByAnimeID(anidbID))
+ .Where(anime => anime != null)
+ .All(user.AllowedAnime);
+ if (!isAnimeAllowed)
+ return false;
+
+ if (!include.Contains(FileNonDefaultIncludeType.Ignored) && video.IsIgnored) return false;
+ if (include_only.Contains(FileIncludeOnlyType.Ignored) && !video.IsIgnored) return false;
+
+ if (exclude.Contains(FileExcludeTypes.Duplicates) && locations.Count > 1) return false;
+ if (include_only.Contains(FileIncludeOnlyType.Duplicates) && locations.Count <= 1) return false;
+
+ if (exclude.Contains(FileExcludeTypes.Unrecognized) && xrefs.Count == 0) return false;
+ if (include_only.Contains(FileIncludeOnlyType.Unrecognized) && xrefs.Count > 0) return false;
+
+ if (exclude.Contains(FileExcludeTypes.ManualLinks) && xrefs.Count > 0 && xrefs.Any(xref => xref.CrossRefSource != (int)CrossRefSource.AniDB)) return false;
+ if (include_only.Contains(FileIncludeOnlyType.ManualLinks) && xrefs.Count == 0 || xrefs.Any(xref => xref.CrossRefSource == (int)CrossRefSource.AniDB)) return false;
+
+ if (exclude.Contains(FileExcludeTypes.Watched) && userRecord?.WatchedDate != null) return false;
+ if (include_only.Contains(FileIncludeOnlyType.Watched) && userRecord?.WatchedDate == null) return false;
+
+ return true;
+ });
+
+ // Search.
+ enumerable = enumerable
+ .Search(query, tuple => tuple.Locations.Select(place => place.FullServerPath).Where(path => path != null), fuzzy)
+ .Select(result => result.Result);
+
+ // Sorting.
+ if (sortOrder != null && sortOrder.Count > 0)
+ enumerable = Models.Shoko.File.OrderBy(enumerable, sortOrder);
+
+ // Skip and limit.
+ return enumerable.ToListResult(
+ tuple => new File(tuple.UserRecord, tuple.Video, include.Contains(FileNonDefaultIncludeType.XRefs), includeDataFrom,
+ include.Contains(FileNonDefaultIncludeType.MediaInfo), include.Contains(FileNonDefaultIncludeType.AbsolutePaths)), page, pageSize);
+ }
///
/// Batch delete files using file ids.
diff --git a/Shoko.Server/API/v3/Controllers/TreeController.cs b/Shoko.Server/API/v3/Controllers/TreeController.cs
index 00a93aa5b..cf25ab725 100644
--- a/Shoko.Server/API/v3/Controllers/TreeController.cs
+++ b/Shoko.Server/API/v3/Controllers/TreeController.cs
@@ -6,8 +6,6 @@
using Microsoft.AspNetCore.Mvc;
using Shoko.Commons.Extensions;
using Shoko.Models.Enums;
-using Shoko.Plugin.Abstractions.DataModels;
-using Shoko.Plugin.Abstractions.Extensions;
using Shoko.Server.API.Annotations;
using Shoko.Server.API.ModelBinders;
using Shoko.Server.API.v3.Helpers;
@@ -17,11 +15,8 @@
using Shoko.Server.Models;
using Shoko.Server.Repositories;
using Shoko.Server.Settings;
-using Shoko.Server.Utilities;
-
-using EpisodeType = Shoko.Server.API.v3.Models.Shoko.EpisodeType;
-using AniDBEpisodeType = Shoko.Models.Enums.EpisodeType;
using DataSource = Shoko.Server.API.v3.Models.Common.DataSource;
+using FileSortCriteria = Shoko.Server.API.v3.Models.Shoko.File.FileSortCriteria;
namespace Shoko.Server.API.v3.Controllers;
@@ -542,18 +537,23 @@ public ActionResult GetMainSeriesInGroup([FromRoute] int groupID, [FromQ
/// Get the s for the with the given .
///
/// Episode ID
- /// Set to true to include series and episode cross-references.
+ /// Limits the number of results per page. Set to 0 to disable the limit.
+ /// Page number.
+ /// Include items that are not included by default
+ /// Exclude items of certain types
+ /// Filter to only include items of certain types
+ /// Sort ordering. Attach '-' at the start to reverse the order of the criteria.
/// Include data from selected s.
- /// Omit to select all files. Set to true to only select manually
- /// linked files, or set to false to only select automatically linked files.
- /// Include media info data.
- /// Include absolute paths for the file locations.
///
[HttpGet("Episode/{episodeID}/File")]
- public ActionResult> GetFilesForEpisode([FromRoute] int episodeID, [FromQuery] bool includeXRefs = false,
- [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet includeDataFrom = null,
- [FromQuery] bool? isManuallyLinked = null, [FromQuery] bool includeMediaInfo = false,
- [FromQuery] bool includeAbsolutePaths = false)
+ public ActionResult> GetFilesForEpisode([FromRoute] int episodeID,
+ [FromQuery, Range(0, 1000)] int pageSize = 100,
+ [FromQuery, Range(1, int.MaxValue)] int page = 1,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] FileNonDefaultIncludeType[] include = default,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] FileExcludeTypes[] exclude = default,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] FileIncludeOnlyType[] include_only = default,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] List sortOrder = null,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet includeDataFrom = null)
{
var episode = RepoFactory.AnimeEpisode.GetByID(episodeID);
if (episode == null)
@@ -561,6 +561,10 @@ public ActionResult> GetFilesForEpisode([FromRoute] int episodeID, [F
return NotFound(EpisodeController.EpisodeNotFoundWithEpisodeID);
}
+ include ??= Array.Empty();
+ exclude ??= Array.Empty();
+ include_only ??= Array.Empty();
+
var series = episode.GetAnimeSeries();
if (series == null)
{
@@ -572,9 +576,65 @@ public ActionResult> GetFilesForEpisode([FromRoute] int episodeID, [F
return Forbid(EpisodeController.EpisodeForbiddenForUser);
}
- return episode.GetVideoLocals(isManuallyLinked.HasValue ? isManuallyLinked.Value ? CrossRefSource.User : CrossRefSource.AniDB : null)
- .Select(file => new File(HttpContext, file, includeXRefs, includeDataFrom, includeMediaInfo, includeAbsolutePaths))
- .ToList();
+ var user = User;
+ var includeLocations = exclude.Contains(FileExcludeTypes.Duplicates) ||
+ (sortOrder?.Any(criteria => criteria.Contains(FileSortCriteria.DuplicateCount.ToString())) ?? false);
+ var includeUserRecord = exclude.Contains(FileExcludeTypes.Watched) || (sortOrder?.Any(criteria =>
+ criteria.Contains(FileSortCriteria.ViewedAt.ToString()) || criteria.Contains(FileSortCriteria.WatchedAt.ToString())) ?? false);
+ var enumerable = episode.GetVideoLocals()
+ .Select(video => (
+ Video: video,
+ BestLocation: video.GetBestVideoLocalPlace(),
+ Locations: includeLocations ? video.Places : null,
+ UserRecord: includeUserRecord ? video.GetUserRecord(user.JMMUserID) : null
+ ))
+ .Where(tuple =>
+ {
+ var (video, bestLocation, locations, userRecord) = tuple;
+ var xrefs = video.EpisodeCrossRefs;
+ var isAnimeAllowed = xrefs
+ .Select(xref => xref.AnimeID)
+ .Distinct()
+ .Select(anidbID => RepoFactory.AniDB_Anime.GetByAnimeID(anidbID))
+ .Where(anime => anime != null)
+ .All(user.AllowedAnime);
+ if (!isAnimeAllowed)
+ return false;
+
+ if (!include.Contains(FileNonDefaultIncludeType.Ignored) && video.IsIgnored) return false;
+ if (include_only.Contains(FileIncludeOnlyType.Ignored) && !video.IsIgnored) return false;
+
+ if (exclude.Contains(FileExcludeTypes.Duplicates) && locations.Count > 1) return false;
+ if (include_only.Contains(FileIncludeOnlyType.Duplicates) && locations.Count <= 1) return false;
+
+ if (exclude.Contains(FileExcludeTypes.Unrecognized) && xrefs.Count == 0) return false;
+ if (include_only.Contains(FileIncludeOnlyType.Unrecognized) && xrefs.Count > 0) return false;
+
+ if (exclude.Contains(FileExcludeTypes.ManualLinks) && xrefs.Count > 0 && xrefs.Any(xref => xref.CrossRefSource != (int)CrossRefSource.AniDB)) return false;
+ if (include_only.Contains(FileIncludeOnlyType.ManualLinks) && xrefs.Count == 0 || xrefs.Any(xref => xref.CrossRefSource == (int)CrossRefSource.AniDB)) return false;
+
+ if (exclude.Contains(FileExcludeTypes.Watched) && userRecord?.WatchedDate != null) return false;
+ if (include_only.Contains(FileIncludeOnlyType.Watched) && userRecord?.WatchedDate == null) return false;
+
+ return true;
+ });
+
+ // Sorting.
+ if (sortOrder != null && sortOrder.Count > 0)
+ enumerable = Models.Shoko.File.OrderBy(enumerable, sortOrder);
+ else
+ enumerable = Models.Shoko.File.OrderBy(enumerable, new()
+ {
+ // First sort by import folder from A-Z.
+ FileSortCriteria.ImportFolderName.ToString(),
+ // Then by the relative path inside the import folder, from A-Z.
+ FileSortCriteria.RelativePath.ToString(),
+ });
+
+ // Skip and limit.
+ return enumerable.ToListResult(
+ tuple => new File(tuple.UserRecord, tuple.Video, include.Contains(FileNonDefaultIncludeType.XRefs), includeDataFrom,
+ include.Contains(FileNonDefaultIncludeType.MediaInfo), include.Contains(FileNonDefaultIncludeType.AbsolutePaths)), page, pageSize);
}
#endregion