Skip to content

Commit eb4da9e

Browse files
authored
feat: Implement subtitle provider integration including an gelato subtitle provider (#112)
1 parent 464696f commit eb4da9e

15 files changed

+677
-164
lines changed

Common.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,22 @@ public static bool TryGetActionArgument<T>(
393393
}
394394

395395
public static class BaseItemExtensions {
396+
public static bool IsGelato(this BaseItem item) {
397+
return !string.IsNullOrWhiteSpace(item.GetProviderId("Stremio"));
398+
}
399+
400+
public static bool HasStreamTag(this BaseItem item) {
401+
return item.Tags is not null && item.Tags.Contains(GelatoManager.StreamTag, StringComparer.OrdinalIgnoreCase);
402+
}
403+
404+
public static bool IsPrimaryVersion(this BaseItem item) {
405+
return !item.HasStreamTag() && string.IsNullOrWhiteSpace((item as Video)?.PrimaryVersionId) && !item.IsVirtualItem;
406+
}
407+
408+
public static bool IsStream(this BaseItem item) {
409+
return !string.IsNullOrWhiteSpace(item.GetProviderId("Stremio")) && !item.IsPrimaryVersion();
410+
}
411+
396412
public static T? GelatoData<T>(this BaseItem item, string key) {
397413
if (string.IsNullOrEmpty(item.ExternalId))
398414
return default;

Decorators/CollectionManagerDecorator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> item
4141

4242
var gelatoItems = guids
4343
.Select(libraryManager.GetItemById)
44-
.Where(item => item is not null && manager.Value.IsGelato(item))
44+
.Where(item => item is not null && item.IsGelato())
4545
.ToList();
4646

4747
if (gelatoItems.Count == 0)

Decorators/MediaSourceManagerDecorator.cs

Lines changed: 72 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Globalization;
2+
using System.IO;
23
using Jellyfin.Data;
34
using Jellyfin.Data.Enums;
45
using Jellyfin.Database.Implementations.Entities;
@@ -10,14 +11,21 @@
1011
using MediaBrowser.Controller.LiveTv;
1112
using MediaBrowser.Controller.Persistence;
1213
using MediaBrowser.Controller.Providers;
14+
using MediaBrowser.Controller.Subtitles;
1315
using MediaBrowser.Model.Dlna;
1416
using MediaBrowser.Model.Dto;
1517
using MediaBrowser.Model.Entities;
1618
using MediaBrowser.Model.MediaInfo;
19+
using MediaBrowser.Model.Providers;
1720
using Microsoft.AspNetCore.Http;
1821
using Microsoft.Extensions.Logging;
1922
using MediaBrowser.Controller.MediaSegments;
2023
using MediaBrowser.Controller.Chapters;
24+
using MediaBrowser.Model.Configuration;
25+
using MediaBrowser.Common.Configuration;
26+
using MediaBrowser.Controller.Configuration;
27+
using Gelato.Services;
28+
2129

2230
namespace Gelato.Decorators;
2331

@@ -28,21 +36,30 @@ public sealed class MediaSourceManagerDecorator(
2836
IHttpContextAccessor http,
2937
GelatoItemRepository repo,
3038
IDirectoryService directoryService,
39+
IServerConfigurationManager config,
40+
//Lazy<ISubtitleManager> subtitleManager,
3141
Lazy<GelatoManager> manager,
32-
IMediaSegmentManager mediaSegmentManager)
42+
IMediaSegmentManager mediaSegmentManager,
43+
IEnumerable<ICustomMetadataProvider<Video>> videoProbeProviders)
3344
: IMediaSourceManager {
3445
private readonly IMediaSourceManager _inner = inner ?? throw new ArgumentNullException(nameof(inner));
3546
private readonly ILogger<MediaSourceManagerDecorator> _log = log ?? throw new ArgumentNullException(nameof(log));
3647
private readonly IHttpContextAccessor _http = http ?? throw new ArgumentNullException(nameof(http));
3748
private readonly KeyLock _lock = new();
3849
private readonly IMediaSegmentManager _mediaSegmentManager = mediaSegmentManager ?? throw new ArgumentNullException(nameof(mediaSegmentManager));
3950
private readonly ILibraryManager _libraryManager = libraryManager ?? throw new ArgumentNullException(nameof(libraryManager));
51+
private readonly IServerConfigurationManager _config = config ?? throw new ArgumentNullException(nameof(config));
52+
private readonly Lazy<GelatoManager> _manager = manager;
53+
// private readonly Lazy<ISubtitleManager> _subtitleManager = subtitleManager ?? throw new ArgumentNullException(nameof(subtitleManager));
54+
private readonly ICustomMetadataProvider<Video>? _probeProvider =
55+
videoProbeProviders.FirstOrDefault(p => p.Name == "Probe Provider");
56+
4057
public IReadOnlyList<MediaSourceInfo> GetStaticMediaSources(
4158
BaseItem item,
4259
bool enablePathSubstitution,
4360
User? user = null
4461
) {
45-
var manager1 = manager.Value;
62+
var manager = _manager.Value;
4663
_log.LogDebug(
4764
"GetStaticMediaSources {Id}",
4865
item.Id
@@ -57,7 +74,7 @@ public IReadOnlyList<MediaSourceInfo> GetStaticMediaSources(
5774

5875
var cfg = GelatoPlugin.Instance!.GetConfig(userId);
5976
if (
60-
(!cfg.EnableMixed && !manager1.IsGelato(item))
77+
(!cfg.EnableMixed && !item.IsGelato())
6178
|| item.GetBaseItemKind() is not (BaseItemKind.Movie or BaseItemKind.Episode)
6279
) {
6380
return _inner.GetStaticMediaSources(item, enablePathSubstitution, user);
@@ -84,7 +101,7 @@ public IReadOnlyList<MediaSourceInfo> GetStaticMediaSources(
84101
uri?.ToString()
85102
);
86103
}
87-
else if (uri is not null && !manager1.HasStreamSync(cacheKey)) {
104+
else if (uri is not null && !manager.HasStreamSync(cacheKey)) {
88105
// Bug in web UI that calls the detail page twice. So that's why there's a lock.
89106
_lock
90107
.RunSingleFlightAsync(
@@ -95,9 +112,9 @@ public IReadOnlyList<MediaSourceInfo> GetStaticMediaSources(
95112
item.Id
96113
);
97114
try {
98-
var count = await manager1.SyncStreams(item, userId, ct).ConfigureAwait(false);
115+
var count = await manager.SyncStreams(item, userId, ct).ConfigureAwait(false);
99116
if (count > 0) {
100-
manager1.SetStreamSync(cacheKey);
117+
manager.SetStreamSync(cacheKey);
101118
}
102119
}
103120
catch (Exception ex) {
@@ -150,7 +167,7 @@ public IReadOnlyList<MediaSourceInfo> GetStaticMediaSources(
150167
.GetItemList(query)
151168
.OfType<Video>()
152169
.Where(x =>
153-
manager1.IsGelato(x) &&
170+
x.IsGelato() &&
154171
(
155172
userId == Guid.Empty ||
156173
(x.GelatoData<List<Guid>>("userIds")?.Contains(userId) ?? false)
@@ -212,80 +229,9 @@ public IReadOnlyList<MediaStream> GetMediaStreams(Guid itemId) {
212229
return _inner.GetMediaStreams(itemId);
213230
}
214231

215-
public async Task<List<MediaStream>> GetSubtitleStreams(
216-
BaseItem item,
217-
MediaSourceInfo source
218-
) {
219-
var manager1 = manager.Value;
220-
221-
var subtitles = manager1.GetStremioSubtitlesCache(item.Id);
222-
if (subtitles is null) {
223-
var uri = StremioUri.FromBaseItem(item);
224-
if (uri is null) {
225-
_log.LogError($"unable to build stremio uri for {item.Name}");
226-
return new List<MediaStream>(); // Return empty list instead of void
227-
}
228-
229-
Uri u = new Uri(source.Path);
230-
string filename = System.IO.Path.GetFileName(u.LocalPath);
231-
232-
var cfg = GelatoPlugin.Instance!.GetConfig(Guid.Empty);
233-
subtitles = await cfg
234-
.Stremio.GetSubtitlesAsync(uri, filename)
235-
.ConfigureAwait(false);
236-
manager1.SetStremioSubtitlesCache(item.Id, subtitles);
237-
}
238-
239-
var streams = new List<MediaStream>();
240-
241-
if (subtitles == null || !subtitles.Any()) {
242-
_log.LogDebug($"GetSubtitleStreams: no subtitles found");
243-
return streams;
244-
}
245-
246-
var index = 0; // Start from 0 since this is a new list
247-
var limitedSubtitles = subtitles.GroupBy(s => s.Lang).SelectMany(g => g.Take(2));
248-
foreach (var s in limitedSubtitles) {
249-
streams.Add(
250-
new MediaStream {
251-
Type = MediaStreamType.Subtitle,
252-
Index = index,
253-
Language = s.Lang,
254-
Codec = GuessSubtitleCodec(s.Url),
255-
IsExternal = true,
256-
// subtitle urls usually dont end with an extension. Breaking some clients cause they fucking check the extension instead of thr codec field.
257-
SupportsExternalStream = false,
258-
Path = s.Url,
259-
DeliveryMethod = SubtitleDeliveryMethod.External,
260-
}
261-
);
262-
index++;
263-
}
232+
264233

265-
_log.LogDebug($"GetSubtitleStreams: loaded {streams.Count} subtitles");
266-
return streams;
267-
}
268-
269-
public string GuessSubtitleCodec(string? urlOrPath) {
270-
if (string.IsNullOrWhiteSpace(urlOrPath))
271-
return "subrip";
272-
273-
var s = urlOrPath.ToLowerInvariant();
274-
275-
if (s.Contains(".vtt"))
276-
return "vtt";
277-
if (s.Contains(".srt"))
278-
return "srt";
279-
if (s.Contains(".ass") || s.Contains(".ssa"))
280-
return "ass";
281-
if (s.Contains(".subf2m"))
282-
return "subrip";
283-
if (s.Contains("subs") && s.Contains(".strem.io"))
284-
return "srt"; // Stremio proxies are always normalized to .srt
285-
286-
_log.LogWarning($"unkown subtitle format for {s}, defaulting to srt");
287-
return "srt";
288-
}
234+
289235

290236
public IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery query) {
291237
return _inner.GetMediaStreams(query).ToList();
@@ -316,7 +262,7 @@ CancellationToken ct
316262
.ConfigureAwait(false);
317263
}
318264

319-
var manager1 = manager.Value;
265+
var manager = _manager.Value;
320266
var ctx = _http.HttpContext;
321267

322268
var sources = GetStaticMediaSources(item, enablePathSubstitution, user);
@@ -327,7 +273,7 @@ CancellationToken ct
327273
&& Guid.TryParse(idStr, out var fromCtx)
328274
? fromCtx
329275
: (
330-
manager1.IsPrimaryVersion(item as Video)
276+
item.IsPrimaryVersion()
331277
&& sources.Count > 0
332278
&& Guid.TryParse(sources[0].Id, out var fromSource)
333279
? fromSource
@@ -345,7 +291,7 @@ CancellationToken ct
345291
return sources;
346292

347293
var owner = ResolveOwnerFor(selected, item);
348-
if (manager1.IsPrimaryVersion(owner as Video) && owner.Id != item.Id) {
294+
if (owner.IsPrimaryVersion() && owner.Id != item.Id) {
349295
sources = GetStaticMediaSources(owner, enablePathSubstitution, user);
350296
selected = SelectByIdOrFirst(sources, mediaSourceId);
351297
if (selected is null)
@@ -355,18 +301,11 @@ CancellationToken ct
355301
if (NeedsProbe(selected)) {
356302
var libraryOptions = _libraryManager.GetLibraryOptions(owner);
357303

358-
// Run segment providers and metadata refresh in parallel
359304
var segmentTask = _mediaSegmentManager.RunSegmentPluginProviders(owner, libraryOptions, false, ct);
360-
var metadataTask = owner.RefreshMetadata(
361-
new MetadataRefreshOptions(directoryService) {
362-
EnableRemoteContentProbe = true,
363-
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
364-
},
365-
ct
366-
);
305+
var metadataTask = ProbeStreamAsync((Video)owner, selected.Path, ct);
306+
// var subtitleTask = DownloadSubtitles((Video)owner, ct);
367307

368-
// Wait for both operations to complete
369-
await Task.WhenAll(segmentTask, metadataTask).ConfigureAwait(false);
308+
await Task.WhenAll(metadataTask, segmentTask).ConfigureAwait(false);
370309

371310
await owner
372311
.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, ct)
@@ -379,22 +318,6 @@ await owner
379318
return refreshed;
380319
}
381320

382-
if (GelatoPlugin.Instance!.Configuration.EnableSubs) {
383-
var subtitleStreams = await GetSubtitleStreams(item, selected)
384-
.ConfigureAwait(false);
385-
386-
var streams = selected.MediaStreams?.ToList() ?? new List<MediaStream>();
387-
388-
var index = streams.LastOrDefault()?.Index ?? -1;
389-
foreach (var s in subtitleStreams) {
390-
index++;
391-
s.Index = index;
392-
streams.Add(s);
393-
}
394-
395-
selected.MediaStreams = streams;
396-
}
397-
398321
if (item.RunTimeTicks is null && selected.RunTimeTicks is not null) {
399322
item.RunTimeTicks = selected.RunTimeTicks;
400323
await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, ct)
@@ -581,4 +504,44 @@ private MediaSourceInfo GetVersionInfo(
581504

582505
return info;
583506
}
507+
508+
private async Task ProbeStreamAsync(Video owner, string streamUrl, CancellationToken ct)
509+
{
510+
var gelatoFilename = owner.GelatoData<string>("filename");
511+
var strmBaseName = !string.IsNullOrEmpty(gelatoFilename)
512+
? Path.GetFileNameWithoutExtension(gelatoFilename)
513+
: $"{owner.Id:N}";
514+
var tmpStrm = Path.Combine(Path.GetTempPath(), $"{strmBaseName}.strm");
515+
await File.WriteAllTextAsync(tmpStrm, streamUrl, ct).ConfigureAwait(false);
516+
517+
var origPath = owner.Path;
518+
var origShortcut = owner.IsShortcut;
519+
owner.Path = tmpStrm;
520+
owner.IsShortcut = true;
521+
owner.DateModified = new FileInfo(tmpStrm).LastWriteTimeUtc;
522+
523+
try
524+
{
525+
_log.LogInformation("Probing stream for {Id} via {Url}", owner.Id, streamUrl);
526+
await owner.RefreshMetadata(
527+
new MetadataRefreshOptions(directoryService) {
528+
EnableRemoteContentProbe = true,
529+
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
530+
},
531+
ct
532+
);
533+
}
534+
catch (Exception ex)
535+
{
536+
_log.LogError(ex, "Stream probe failed for {Id}", owner.Id);
537+
}
538+
finally
539+
{
540+
owner.Path = origPath;
541+
owner.IsShortcut = origShortcut;
542+
try { File.Delete(tmpStrm); } catch { /* best effort */ }
543+
}
544+
}
545+
546+
584547
}

Decorators/PlaylistManagerDecorator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public async Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Gu
4343
.Where(item => item is not null)
4444
.ToList();
4545

46-
var gelatoItems = addedItems.Where(manager.Value.IsGelato).ToList();
46+
var gelatoItems = addedItems.Where(item => item.IsGelato()).ToList();
4747
if (gelatoItems.Count == 0)
4848
return;
4949

0 commit comments

Comments
 (0)