Skip to content

Commit 0977a0a

Browse files
authored
support audio playback in harmony (#1819)
* implement simple file downloading * convert lexbox media files into remote crdt resources * fix race condition when opening project page directly * store local resource cache files in folder by project name, cleanup when deleting a project
1 parent 347f8eb commit 0977a0a

File tree

44 files changed

+1424
-67
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1424
-67
lines changed

backend/FwHeadless/Controllers/MediaFileController.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Security.Cryptography;
88
using Microsoft.Net.Http.Headers;
99
using System.Globalization;
10+
using FwHeadless.Media;
1011
using MimeMapping;
1112

1213
namespace FwHeadless.Controllers;
@@ -42,16 +43,13 @@ public static async Task<Results<Ok<FileListing>, NotFound, BadRequest>> ListFil
4243
public static async Task<Results<PhysicalFileHttpResult, NotFound>> GetFile(
4344
Guid fileId,
4445
IOptions<FwHeadlessConfig> config,
45-
LexBoxDbContext lexBoxDb)
46+
LexBoxDbContext lexBoxDb,
47+
MediaFileService mediaFileService)
4648
{
4749
var madeChanges = false;
4850
var mediaFile = await lexBoxDb.Files.FindAsync(fileId);
4951
if (mediaFile is null) return TypedResults.NotFound();
50-
var projectId = mediaFile.ProjectId;
51-
var project = await lexBoxDb.Projects.FindAsync(projectId);
52-
if (project is null) return TypedResults.NotFound();
53-
var projectFolder = config.Value.GetFwDataProject(project.Code, projectId).ProjectFolder;
54-
var filePath = Path.Join(projectFolder, mediaFile.Filename);
52+
var filePath = mediaFileService.FilePath(mediaFile);
5553
if (!File.Exists(filePath)) return TypedResults.NotFound();
5654
mediaFile.InitializeMetadataIfNeeded(filePath);
5755
var contentType = mediaFile.Metadata.MimeType;

backend/FwHeadless/FwHeadlessConfig.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ public FwDataProject GetFwDataProject(string projectCode, Guid projectId)
6464
return new FwDataProject(FwDataSubFolder, GetProjectFolder(projectCode, projectId));
6565
}
6666

67+
public string GetFwDataFolder(Guid projectId)
68+
{
69+
return GetFwDataFolder(GetProjectFolder(projectId));
70+
}
6771
public string GetFwDataFolder(string projectRootFolder)
6872
{
6973
return Path.Join(projectRootFolder, FwDataSubFolder);

backend/FwHeadless/FwHeadlessKernel.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using LcmCrdt;
77
using Microsoft.Extensions.DependencyInjection.Extensions;
88
using Microsoft.Extensions.Options;
9+
using MiniLcm.Project;
910

1011
namespace FwHeadless;
1112

@@ -32,6 +33,7 @@ public static void AddFwHeadless(this IServiceCollection services)
3233
services.RemoveAll(typeof(IMediaAdapter));
3334
services.AddScoped<IMediaAdapter, LexboxFwDataMediaAdapter>();
3435
services.AddScoped<MediaFileService>();
36+
services.AddScoped<IServerHttpClientProvider, LexboxServerHttpClientProvider>();
3537

3638
services.AddSingleton<SyncHostedService>();
3739
services.AddHostedService(s => s.GetRequiredService<SyncHostedService>());

backend/FwHeadless/LexboxFwDataMediaAdapter.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using FwDataMiniLcmBridge.Media;
22
using FwHeadless.Media;
33
using LexCore.Entities;
4+
using LexCore.Exceptions;
45
using Microsoft.Extensions.Options;
56
using MiniLcm;
67
using SIL.LCModel;
@@ -19,7 +20,8 @@ public MediaUri MediaUriFromPath(string path, LcmCache cache)
1920

2021
public string PathFromMediaUri(MediaUri mediaUri, LcmCache cache)
2122
{
22-
var mediaFile = mediaFileService.FindMediaFile(mediaUri.FileId);
23+
var mediaFile = mediaFileService.FindMediaFile(mediaUri.FileId) ??
24+
throw new NotFoundException($"Unable to find file {mediaUri.FileId}.", nameof(MediaFile));
2325
var fullFilePath = Path.Join(cache.ProjectId.ProjectFolder, mediaFile.Filename);
2426
return Path.GetRelativePath(cache.LangProject.LinkedFilesRootDir, fullFilePath);
2527
}

backend/FwHeadless/Media/MediaFileService.cs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using LcmCrdt;
2+
using LcmCrdt.MediaServer;
13
using LexCore.Entities;
24
using LexCore.Exceptions;
35
using LexData;
@@ -86,9 +88,37 @@ public MediaFile FindMediaFile(Guid projectId, string path)
8688
nameof(MediaFile));
8789
}
8890

89-
public MediaFile FindMediaFile(Guid fileId)
91+
public MediaFile? FindMediaFile(Guid fileId)
9092
{
91-
return dbContext.Files.Find(fileId) ??
92-
throw new NotFoundException($"Unable to find file {fileId}.", nameof(MediaFile));
93+
return dbContext.Files.Find(fileId);
94+
}
95+
96+
public async ValueTask<MediaFile?> FindMediaFileAsync(Guid fileId)
97+
{
98+
return await dbContext.Files.FindAsync(fileId);
99+
}
100+
101+
public string FilePath(MediaFile mediaFile)
102+
{
103+
return Path.Join(config.Value.GetFwDataFolder(mediaFile.ProjectId), mediaFile.Filename);
104+
}
105+
106+
public async Task SyncMediaFiles(Guid projectId, LcmMediaService lcmMediaService)
107+
{
108+
var lcmResources = (await lcmMediaService.AllResources()).ToDictionary(r => r.Id);
109+
var existingDbFiles = dbContext.Files.Where(p => p.ProjectId == projectId).AsAsyncEnumerable();
110+
await foreach (var existingDbFile in existingDbFiles)
111+
{
112+
if (lcmResources.Remove(existingDbFile.Id))
113+
{
114+
//nothing to do, the file was already tracked in harmony
115+
continue;
116+
}
117+
await lcmMediaService.AddExistingRemoteResource(existingDbFile.Id, FilePath(existingDbFile));
118+
}
119+
foreach (var lcmResource in lcmResources.Values)
120+
{
121+
await lcmMediaService.DeleteResource(lcmResource.Id);
122+
}
93123
}
94124
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using MiniLcm.Project;
2+
3+
namespace FwHeadless.Services;
4+
5+
public class LexboxServerHttpClientProvider(IHttpClientFactory httpClientFactory) : IServerHttpClientProvider
6+
{
7+
public ValueTask<HttpClient> GetHttpClient()
8+
{
9+
return ValueTask.FromResult(httpClientFactory.CreateClient(FwHeadlessKernel.LexboxHttpClientName));
10+
}
11+
12+
public ValueTask<ConnectionStatus> ConnectionStatus(bool forceRefresh = false)
13+
{
14+
return new ValueTask<ConnectionStatus>(MiniLcm.Project.ConnectionStatus.Online);
15+
}
16+
}

backend/FwHeadless/Services/SyncHostedService.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using FwHeadless.Media;
77
using FwLiteProjectSync;
88
using LcmCrdt;
9+
using LcmCrdt.MediaServer;
910
using LcmCrdt.RemoteSync;
1011
using LexCore.Sync;
1112
using LexCore.Utils;
@@ -144,6 +145,9 @@ public async Task<SyncJobResult> ExecuteSync(CancellationToken stoppingToken)
144145
logger.LogDebug("fwDataFile: {fwDataFile}", fwDataProject.FilePath);
145146

146147
var fwdataApi = await SetupFwData(fwDataProject, projectCode);
148+
//always do this as existing projects need to run this even if they didn't S&R due to no pending changes
149+
await mediaFileService.SyncMediaFiles(fwdataApi.Cache);
150+
147151
using var deferCloseFwData = fwDataFactory.DeferClose(fwDataProject);
148152
var crdtProject = await SetupCrdtProject(crdtFile,
149153
projectLookupService,
@@ -161,6 +165,7 @@ public async Task<SyncJobResult> ExecuteSync(CancellationToken stoppingToken)
161165
{
162166
await crdtSyncService.SyncHarmonyProject();
163167
}
168+
await mediaFileService.SyncMediaFiles(projectId, services.GetRequiredService<LcmMediaService>());
164169

165170
var result = await syncService.Sync(miniLcmApi, fwdataApi);
166171
logger.LogInformation("Sync result, CrdtChanges: {CrdtChanges}, FwdataChanges: {FwdataChanges}",
@@ -203,8 +208,6 @@ private async Task<FwDataMiniLcmApi> SetupFwData(FwDataProject fwDataProject, st
203208
}
204209

205210
var fwdataApi = fwDataFactory.GetFwDataMiniLcmApi(fwDataProject, true);
206-
//always do this as existing projects need to run this even if they didn't S&R due to no pending changes
207-
await mediaFileService.SyncMediaFiles(fwdataApi.Cache);
208211
return fwdataApi;
209212
}
210213

backend/FwLite/FwDataMiniLcmBridge.Tests/MediaFileTests.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,10 @@ public async Task CanOpenAFile()
134134
var entry = await _api.GetEntry(entryId);
135135

136136
entry.Should().NotBeNull();
137-
await using var file = await _api.GetFileStream(new MediaUri(entry.CitationForm[_audioWs]));
138-
file.Should().NotBeNull();
139-
using var streamReader = new StreamReader(file);
137+
var file = await _api.GetFileStream(new MediaUri(entry.CitationForm[_audioWs]));
138+
await using var stream = file.Stream;
139+
stream.Should().NotBeNull();
140+
using var streamReader = new StreamReader(stream);
140141
var contents = await streamReader.ReadToEndAsync();
141142
contents.Should().Be("test");
142143
}
@@ -179,6 +180,7 @@ await _api.UpdateEntry(entryId,
179180
public async Task GetStreamForNotFoundIsNull()
180181
{
181182
var fileStream = await _api.GetFileStream(MediaUri.NotFound);
182-
fileStream.Should().BeNull();
183+
fileStream.Stream.Should().BeNull();
184+
fileStream.Result.Should().Be(ReadFileResult.NotFound);
183185
}
184186
}

backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1533,10 +1533,11 @@ private static void ValidateOwnership(ILexExampleSentence lexExampleSentence, Gu
15331533
}
15341534
}
15351535

1536-
public Task<Stream?> GetFileStream(MediaUri mediaUri)
1536+
public Task<ReadFileResponse> GetFileStream(MediaUri mediaUri)
15371537
{
1538-
if (mediaUri == MediaUri.NotFound) return Task.FromResult<Stream?>(null);
1538+
if (mediaUri == MediaUri.NotFound) return Task.FromResult(new ReadFileResponse(ReadFileResult.NotFound));
15391539
string fullPath = Path.Combine(Cache.LangProject.LinkedFilesRootDir, mediaAdapter.PathFromMediaUri(mediaUri, Cache));
1540-
return Task.FromResult<Stream?>(File.OpenRead(fullPath));
1540+
if (!File.Exists(fullPath)) return Task.FromResult(new ReadFileResponse(ReadFileResult.NotFound));
1541+
return Task.FromResult(new ReadFileResponse(File.OpenRead(fullPath), Path.GetFileName(fullPath)));
15411542
}
15421543
}

backend/FwLite/FwLiteProjectSync.Tests/Fixtures/TestingKernel.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using FwDataMiniLcmBridge;
22
using FwDataMiniLcmBridge.Tests.Fixtures;
33
using LcmCrdt;
4+
using LcmCrdt.Tests;
45
using Microsoft.Extensions.DependencyInjection;
56
using Microsoft.Extensions.Logging;
67

@@ -10,7 +11,7 @@ public static class TestingKernel
1011
{
1112
public static IServiceCollection AddSyncServices(this IServiceCollection services, string projectName, bool mockFwProjectLoader = true)
1213
{
13-
return services.AddLcmCrdtClient()
14+
return services.AddTestLcmCrdtClient()
1415
.AddTestFwDataBridge(mockFwProjectLoader)
1516
.AddFwLiteProjectSync()
1617
.Configure<FwDataBridgeConfig>(c => c.ProjectsFolder = Path.Combine(".", projectName, "FwData"))

0 commit comments

Comments
 (0)