Jellyfin merges several videos in one non-season folder into a single episode with versions, which collapsed a night of helmet cams into one entry. Recording still lands in <Title>/.live/. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
34 lines
1.5 KiB
C#
34 lines
1.5 KiB
C#
using MediaBrowser.Controller.Library;
|
|
|
|
namespace Jellyfin.Plugin.Vanguard.Services;
|
|
|
|
/// <summary>Splits a media path into (series folder, dock title or null, file stem) relative to its library root.</summary>
|
|
public sealed class LibraryPaths
|
|
{
|
|
private readonly ILibraryManager _library;
|
|
public LibraryPaths(ILibraryManager library) { _library = library; }
|
|
|
|
public sealed record Parts(string SeriesFolder, string? Title, string Stem);
|
|
|
|
public Parts? Split(string path)
|
|
{
|
|
var full = Path.GetFullPath(path);
|
|
string? root = null;
|
|
foreach (var vf in _library.GetVirtualFolders())
|
|
{
|
|
foreach (var loc in vf.Locations ?? [])
|
|
{
|
|
var l = Path.GetFullPath(loc).TrimEnd(Path.DirectorySeparatorChar);
|
|
if (full.StartsWith(l + Path.DirectorySeparatorChar, StringComparison.Ordinal) && (root is null || l.Length > root.Length)) root = l;
|
|
}
|
|
}
|
|
if (root is null) return null;
|
|
var rel = full[(root.Length + 1)..].Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries);
|
|
if (rel.Length < 2) return null;
|
|
var stem = Path.GetFileNameWithoutExtension(rel[^1]);
|
|
if (rel.Length >= 3) return new Parts(rel[0], rel[^2], stem); // legacy: <user>/<Title>/<ts>.mp4
|
|
var sep = stem.IndexOf("__", StringComparison.Ordinal); // flat: <user>/<Title>__<ts>.mp4
|
|
return sep > 0 ? new Parts(rel[0], stem[..sep], stem[(sep + 2)..]) : new Parts(rel[0], null, stem);
|
|
}
|
|
}
|