fix mixed platform directory mapping (#106)
* sync plex platform and platform version * fix mixed-platform path replacements
This commit is contained in:
@@ -64,6 +64,8 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
await maybeExisting.Match(
|
||||
existing =>
|
||||
{
|
||||
existing.Platform = server.Platform;
|
||||
existing.PlatformVersion = server.PlatformVersion;
|
||||
existing.ProductVersion = server.ProductVersion;
|
||||
existing.ServerName = server.ServerName;
|
||||
var toAdd = server.Connections
|
||||
|
||||
+10
-33
@@ -1,17 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Plex;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.Streaming.Queries
|
||||
@@ -22,26 +19,23 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly FFmpegProcessService _ffmpegProcessService;
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly ILogger<GetPlayoutItemProcessByChannelNumberHandler> _logger;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IPlayoutRepository _playoutRepository;
|
||||
private readonly IPlexPathReplacementService _plexPathReplacementService;
|
||||
|
||||
public GetPlayoutItemProcessByChannelNumberHandler(
|
||||
IChannelRepository channelRepository,
|
||||
IConfigElementRepository configElementRepository,
|
||||
IPlayoutRepository playoutRepository,
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
FFmpegProcessService ffmpegProcessService,
|
||||
ILocalFileSystem localFileSystem,
|
||||
ILogger<GetPlayoutItemProcessByChannelNumberHandler> logger)
|
||||
IPlexPathReplacementService plexPathReplacementService)
|
||||
: base(channelRepository, configElementRepository)
|
||||
{
|
||||
_configElementRepository = configElementRepository;
|
||||
_playoutRepository = playoutRepository;
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_ffmpegProcessService = ffmpegProcessService;
|
||||
_localFileSystem = localFileSystem;
|
||||
_logger = logger;
|
||||
_plexPathReplacementService = plexPathReplacementService;
|
||||
}
|
||||
|
||||
protected override async Task<Either<BaseError, Process>> GetProcess(
|
||||
@@ -166,33 +160,16 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
string path = file.Path;
|
||||
return playoutItem.MediaItem switch
|
||||
{
|
||||
PlexMovie plexMovie => await GetReplacementPlexPath(plexMovie.LibraryPathId, path),
|
||||
PlexEpisode plexEpisode => await GetReplacementPlexPath(plexEpisode.LibraryPathId, path),
|
||||
PlexMovie plexMovie => await _plexPathReplacementService.GetReplacementPlexPath(
|
||||
plexMovie.LibraryPathId,
|
||||
path),
|
||||
PlexEpisode plexEpisode => await _plexPathReplacementService.GetReplacementPlexPath(
|
||||
plexEpisode.LibraryPathId,
|
||||
path),
|
||||
_ => path
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<string> GetReplacementPlexPath(int libraryPathId, string path)
|
||||
{
|
||||
List<PlexPathReplacement> replacements =
|
||||
await _mediaSourceRepository.GetPlexPathReplacementsByLibraryId(libraryPathId);
|
||||
// TODO: this might barf mixing platforms (i.e. plex on linux, etv on windows)
|
||||
Option<PlexPathReplacement> maybeReplacement = replacements
|
||||
.SingleOrDefault(r => path.StartsWith(r.PlexPath + Path.DirectorySeparatorChar));
|
||||
return maybeReplacement.Match(
|
||||
replacement =>
|
||||
{
|
||||
string finalPath = path.Replace(replacement.PlexPath, replacement.LocalPath);
|
||||
_logger.LogInformation(
|
||||
"Replacing plex path {PlexPath} with {LocalPath} resulting in {FinalPath}",
|
||||
replacement.PlexPath,
|
||||
replacement.LocalPath,
|
||||
finalPath);
|
||||
return finalPath;
|
||||
},
|
||||
() => path);
|
||||
}
|
||||
|
||||
private record PlayoutItemWithPath(PlayoutItem PlayoutItem, string Path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Runtime;
|
||||
using ErsatzTV.Core.Plex;
|
||||
using FluentAssertions;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Plex
|
||||
{
|
||||
[TestFixture]
|
||||
public class PlexPathReplacementServiceTests
|
||||
{
|
||||
[Test]
|
||||
public async Task PlexWindows_To_EtvWindows()
|
||||
{
|
||||
var replacements = new List<PlexPathReplacement>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
PlexPath = @"C:\Something\Some Shared Folder",
|
||||
LocalPath = @"C:\Something Else\Some Shared Folder",
|
||||
PlexMediaSource = new PlexMediaSource { Platform = "Windows" }
|
||||
}
|
||||
};
|
||||
|
||||
var repo = new Mock<IMediaSourceRepository>();
|
||||
repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||
|
||||
var runtime = new Mock<IRuntimeInfo>();
|
||||
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(true);
|
||||
|
||||
var service = new PlexPathReplacementService(
|
||||
repo.Object,
|
||||
runtime.Object,
|
||||
new Mock<ILogger<PlexPathReplacementService>>().Object);
|
||||
|
||||
string result = await service.GetReplacementPlexPath(
|
||||
0,
|
||||
@"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PlexWindows_To_EtvLinux()
|
||||
{
|
||||
var replacements = new List<PlexPathReplacement>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
PlexPath = @"C:\Something\Some Shared Folder",
|
||||
LocalPath = @"/mnt/something else/Some Shared Folder",
|
||||
PlexMediaSource = new PlexMediaSource { Platform = "Windows" }
|
||||
}
|
||||
};
|
||||
|
||||
var repo = new Mock<IMediaSourceRepository>();
|
||||
repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||
|
||||
var runtime = new Mock<IRuntimeInfo>();
|
||||
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false);
|
||||
|
||||
var service = new PlexPathReplacementService(
|
||||
repo.Object,
|
||||
runtime.Object,
|
||||
new Mock<ILogger<PlexPathReplacementService>>().Object);
|
||||
|
||||
string result = await service.GetReplacementPlexPath(
|
||||
0,
|
||||
@"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PlexLinux_To_EtvWindows()
|
||||
{
|
||||
var replacements = new List<PlexPathReplacement>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
PlexPath = @"/mnt/something/Some Shared Folder",
|
||||
LocalPath = @"C:\Something Else\Some Shared Folder",
|
||||
PlexMediaSource = new PlexMediaSource { Platform = "Linux" }
|
||||
}
|
||||
};
|
||||
|
||||
var repo = new Mock<IMediaSourceRepository>();
|
||||
repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||
|
||||
var runtime = new Mock<IRuntimeInfo>();
|
||||
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(true);
|
||||
|
||||
var service = new PlexPathReplacementService(
|
||||
repo.Object,
|
||||
runtime.Object,
|
||||
new Mock<ILogger<PlexPathReplacementService>>().Object);
|
||||
|
||||
string result = await service.GetReplacementPlexPath(
|
||||
0,
|
||||
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PlexLinux_To_EtvLinux()
|
||||
{
|
||||
var replacements = new List<PlexPathReplacement>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
PlexPath = @"/mnt/something/Some Shared Folder",
|
||||
LocalPath = @"/mnt/something else/Some Shared Folder",
|
||||
PlexMediaSource = new PlexMediaSource { Platform = "Linux" }
|
||||
}
|
||||
};
|
||||
|
||||
var repo = new Mock<IMediaSourceRepository>();
|
||||
repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||
|
||||
var runtime = new Mock<IRuntimeInfo>();
|
||||
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false);
|
||||
|
||||
var service = new PlexPathReplacementService(
|
||||
repo.Object,
|
||||
runtime.Object,
|
||||
new Mock<ILogger<PlexPathReplacementService>>().Object);
|
||||
|
||||
string result = await service.GetReplacementPlexPath(
|
||||
0,
|
||||
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public string ServerName { get; set; }
|
||||
public string ProductVersion { get; set; }
|
||||
public string Platform { get; set; }
|
||||
public string PlatformVersion { get; set; }
|
||||
public string ClientIdentifier { get; set; }
|
||||
|
||||
// public bool IsOwned { get; set; }
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Plex
|
||||
{
|
||||
public interface IPlexPathReplacementService
|
||||
{
|
||||
Task<string> GetReplacementPlexPath(int libraryPathId, string path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Runtime
|
||||
{
|
||||
public interface IRuntimeInfo
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
bool IsOSPlatform(OSPlatform osPlatform);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Plex;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Runtime;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Core.Plex
|
||||
{
|
||||
public class PlexPathReplacementService : IPlexPathReplacementService
|
||||
{
|
||||
private readonly ILogger<PlexPathReplacementService> _logger;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IRuntimeInfo _runtimeInfo;
|
||||
|
||||
public PlexPathReplacementService(
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
IRuntimeInfo runtimeInfo,
|
||||
ILogger<PlexPathReplacementService> logger)
|
||||
{
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_runtimeInfo = runtimeInfo;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<string> GetReplacementPlexPath(int libraryPathId, string path)
|
||||
{
|
||||
List<PlexPathReplacement> replacements =
|
||||
await _mediaSourceRepository.GetPlexPathReplacementsByLibraryId(libraryPathId);
|
||||
Option<PlexPathReplacement> maybeReplacement = replacements
|
||||
.SingleOrDefault(
|
||||
r =>
|
||||
{
|
||||
string separatorChar = IsWindows(r.PlexMediaSource) ? @"\" : @"/";
|
||||
return path.StartsWith(r.PlexPath + separatorChar);
|
||||
});
|
||||
return maybeReplacement.Match(
|
||||
replacement =>
|
||||
{
|
||||
string finalPath = path.Replace(replacement.PlexPath, replacement.LocalPath);
|
||||
if (IsWindows(replacement.PlexMediaSource) && !_runtimeInfo.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
finalPath = finalPath.Replace(@"\", @"/");
|
||||
}
|
||||
else if (!IsWindows(replacement.PlexMediaSource) && _runtimeInfo.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
finalPath = finalPath.Replace(@"/", @"\");
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Replacing plex path {PlexPath} with {LocalPath} resulting in {FinalPath}",
|
||||
replacement.PlexPath,
|
||||
replacement.LocalPath,
|
||||
finalPath);
|
||||
return finalPath;
|
||||
},
|
||||
() => path);
|
||||
}
|
||||
|
||||
private static bool IsWindows(PlexMediaSource plexMediaSource) =>
|
||||
plexMediaSource.Platform.ToLowerInvariant() == "windows";
|
||||
}
|
||||
}
|
||||
@@ -135,6 +135,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
inner join PlexPathReplacement ppr on ppr.PlexMediaSourceId = l.MediaSourceId
|
||||
where lp.Id = {0}",
|
||||
plexLibraryPathId)
|
||||
.Include(ppr => ppr.PlexMediaSource)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
@@ -158,8 +159,20 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
List<PlexConnection> toDelete)
|
||||
{
|
||||
await _dbConnection.ExecuteAsync(
|
||||
@"UPDATE PlexMediaSource SET ProductVersion = @ProductVersion, ServerName = @ServerName WHERE Id = @Id",
|
||||
new { plexMediaSource.ProductVersion, plexMediaSource.ServerName, plexMediaSource.Id });
|
||||
@"UPDATE PlexMediaSource SET
|
||||
ProductVersion = @ProductVersion,
|
||||
Platform = @Platform,
|
||||
PlatformVersion = @PlatformVersion,
|
||||
ServerName = @ServerName
|
||||
WHERE Id = @Id",
|
||||
new
|
||||
{
|
||||
plexMediaSource.ProductVersion,
|
||||
plexMediaSource.Platform,
|
||||
plexMediaSource.PlatformVersion,
|
||||
plexMediaSource.ServerName,
|
||||
plexMediaSource.Id
|
||||
});
|
||||
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
|
||||
|
||||
+1755
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Add_PlexMediaSourcePlatform : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
"Platform",
|
||||
"PlexMediaSource",
|
||||
"TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
"PlatformVersion",
|
||||
"PlexMediaSource",
|
||||
"TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
"Platform",
|
||||
"PlexMediaSource");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
"PlatformVersion",
|
||||
"PlexMediaSource");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1026,6 +1026,12 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<string>("ClientIdentifier")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Platform")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlatformVersion")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProductVersion")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace ErsatzTV.Infrastructure.Plex.Models
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string ProductVersion { get; set; }
|
||||
public string Platform { get; set; }
|
||||
public string PlatformVersion { get; set; }
|
||||
public string ClientIdentifier { get; set; }
|
||||
|
||||
public string AccessToken { get; set; }
|
||||
|
||||
@@ -70,6 +70,8 @@ namespace ErsatzTV.Infrastructure.Plex
|
||||
{
|
||||
ServerName = resource.Name,
|
||||
ProductVersion = resource.ProductVersion,
|
||||
Platform = resource.Platform,
|
||||
PlatformVersion = resource.PlatformVersion,
|
||||
ClientIdentifier = resource.ClientIdentifier,
|
||||
Connections = sortedConnections
|
||||
.Map(c => new PlexConnection { Uri = c.Uri }).ToList()
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using ErsatzTV.Core.Interfaces.Runtime;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Runtime
|
||||
{
|
||||
public class RuntimeInfo : IRuntimeInfo
|
||||
{
|
||||
public bool IsOSPlatform(OSPlatform osPlatform) => RuntimeInformation.IsOSPlatform(osPlatform);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ using ErsatzTV.Core.Interfaces.Locking;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Plex;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Runtime;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
@@ -25,6 +26,7 @@ using ErsatzTV.Infrastructure.Data.Repositories;
|
||||
using ErsatzTV.Infrastructure.Images;
|
||||
using ErsatzTV.Infrastructure.Locking;
|
||||
using ErsatzTV.Infrastructure.Plex;
|
||||
using ErsatzTV.Infrastructure.Runtime;
|
||||
using ErsatzTV.Infrastructure.Search;
|
||||
using ErsatzTV.Serialization;
|
||||
using ErsatzTV.Services;
|
||||
@@ -212,6 +214,8 @@ namespace ErsatzTV
|
||||
services.AddScoped<IPlexTelevisionLibraryScanner, PlexTelevisionLibraryScanner>();
|
||||
services.AddScoped<IPlexServerApiClient, PlexServerApiClient>();
|
||||
services.AddScoped<ISearchIndex, SearchIndex>();
|
||||
services.AddScoped<IRuntimeInfo, RuntimeInfo>();
|
||||
services.AddScoped<IPlexPathReplacementService, PlexPathReplacementService>();
|
||||
|
||||
services.AddHostedService<PlexService>();
|
||||
services.AddHostedService<FFmpegLocatorService>();
|
||||
|
||||
Reference in New Issue
Block a user