Repository navigation
Expand file tree
/
Copy pathPluginConfiguration.cs
More file actions
executable file
·693 lines (602 loc) · 39 KB
/
Copy pathPluginConfiguration.cs
File metadata and controls
executable file
·693 lines (602 loc) · 39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using MediaBrowser.Model.Plugins;
namespace InfiniteDrive
{
/// <summary>
/// All persisted settings for the InfiniteDrive plugin.
/// Emby serialises this to {DataPath}/plugins/configurations/InfiniteDrive.xml.
/// Every property must carry <see cref="DataMemberAttribute"/> to be persisted.
///
/// ──────────────────────────────────────────────────────────────────────────
/// HOW AIOSTREAMS AUTHENTICATION WORKS
/// ──────────────────────────────────────────────────────────────────────────
/// AIOStreams exposes Stremio-format APIs at two URL shapes:
///
/// Unauthenticated: {base}/stremio/manifest.json
/// Authenticated: {base}/stremio/{uuid}/{token}/manifest.json
///
/// Catalog and stream endpoints follow the same pattern:
/// {stremioBase}/catalog/{type}/{catalogId}.json
/// {stremioBase}/stream/{type}/{id}.json
/// {stremioBase}/stream/series/{imdbId}:{season}:{episode}.json
///
/// Set <see cref="PrimaryManifestUrl"/> to the full manifest URL (with auth included if needed).
/// Both values appear in the manifest URL shown in your AIOStreams web UI.
///
/// ──────────────────────────────────────────────────────────────────────────
/// WHAT LIVES IN AIOSTREAMS vs WHAT LIVES HERE
/// ──────────────────────────────────────────────────────────────────────────
/// AIOStreams handles everything about *how* streams are chosen:
/// • Which debrid services to use (Real-Debrid, AllDebrid, TorBox,
/// Premiumize, Debrid-Link, StremThru, NZBDav, AltMount, Easynews)
/// • Which addon providers to query (Torrentio, Comet, MediaFusion,
/// Torrent Galaxy, EZTV, Knaben, SeaDex, Prowlarr, Newznab,
/// Torznab, Google Drive, TorBox Search, Easynews Search, Library)
/// • Resolution, quality, language, codec, audio, HDR/DV filters
/// • Sorting priority and stream expression rules
/// • Title/year/season matching
///
/// InfiniteDrive only needs to know *where* that AIOStreams instance lives.
/// Configure all quality/filter preferences inside AIOStreams itself.
/// </summary>
[DataContract]
public class PluginConfiguration : BasePluginConfiguration
{
[DataMember] public Models.ImportMode ImportRecoveryMode { get; set; } = Models.ImportMode.Observe;
[DataMember] public bool ImportProviderDiversityEnabled { get; set; } = true;
[DataMember] public string ImportCatchUpStartedAt { get; set; } = "";
[DataMember] public string ImportCatchUpUntil { get; set; } = "";
[DataMember] public string ImportHouseholdTimezone { get; set; } = "America/Chicago";
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ AIOSTREAMS CONNECTION (SIMPLIFIED) ║
// ╚══════════════════════════════════════════════════════════════════════╝
/// <summary>
/// Full manifest URL of the primary AIOStreams instance.
///
/// Paste the URL from your AIOStreams web UI → Configure section.
/// Examples:
/// - Unauthenticated: http://192.168.1.100:7860/stremio/manifest.json
/// - Authenticated: http://192.168.1.100:7860/stremio/abc123/xyz789/manifest.json
///
/// The plugin automatically extracts base URL, UUID, and token from this.
/// </summary>
[DataMember]
public string PrimaryManifestUrl { get; set; } = string.Empty;
// NOTE: the AIOStreams instance password is intentionally NOT stored here. It is a
// one-time, per-environment secret entered in the Providers tab and used only
// in-memory for the Preview/Apply formatter+sort action — never persisted.
/// <summary>
/// Optional second AIOStreams manifest. Every configured manifest is an
/// active peer: its catalogs are unioned with the first manifest and it
/// may provide transport fallback for an item whose origin is unavailable.
/// </summary>
[DataMember]
public string SecondaryManifestUrl { get; set; } = string.Empty;
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ CATALOG SYNC SELECTION ║
// ╚══════════════════════════════════════════════════════════════════════╝
/// <summary>
/// Enable fetching catalog items from AIOStreams catalog endpoints.
/// When enabled, InfiniteDrive reads the AIOStreams manifest on each sync
/// to discover all configured catalogs automatically.
/// </summary>
[DataMember]
public bool EnableAioStreamsCatalog { get; set; } = true;
/// <summary>
/// Optional comma-separated list of AIOStreams catalog IDs to sync.
///
/// Leave empty to sync <em>all</em> catalogs found in the AIOStreams manifest
/// (recommended — this captures every addon and provider the user has configured).
///
/// To restrict to specific catalogs, list their IDs exactly as they appear
/// in the AIOStreams manifest, e.g.:
/// <c>aiostreams,torrentio_movies,mediafusion_movies</c>
///
/// Known catalog ID patterns from AIOStreams addons:
/// <list type="bullet">
/// <item><c>aiostreams</c> — AIOStreams default catalog</item>
/// <item><c>gdrive</c> — Google Drive catalog</item>
/// <item><c>library</c> — Library addon catalog</item>
/// <item><c>torbox-search</c> — TorBox catalog</item>
/// <item>Plus any custom catalogs from Prowlarr, Torznab, Newznab, etc.</item>
/// </list>
/// </summary>
[DataMember]
public string AioStreamsCatalogIds { get; set; } = string.Empty;
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ .STRM FILE STORAGE PATHS ║
// ╚══════════════════════════════════════════════════════════════════════╝
/// <summary>
/// Absolute path where movie .strm files are written.
/// Emby should have a Movies library pointed at this folder.
/// Default: <c>/media/infinitedrive/movies</c>
/// </summary>
[DataMember]
public string SyncPathMovies { get; set; } = "/media/infinitedrive/movies";
/// <summary>
/// Absolute path where TV show .strm files are written.
/// Emby should have a TV Shows library pointed at this folder.
/// Default: <c>/media/infinitedrive/shows</c>
/// </summary>
[DataMember]
public string SyncPathShows { get; set; } = "/media/infinitedrive/shows";
/// <summary>Display name for the Movies library created by the plugin.</summary>
[DataMember]
public string LibraryNameMovies { get; set; } = "Streamed Movies";
/// <summary>Display name for the Series library created by the plugin.</summary>
[DataMember]
public string LibraryNameSeries { get; set; } = "Streamed Series";
/// <summary>Display name for the Anime library created by the plugin.</summary>
[DataMember]
public string LibraryNameAnime { get; set; } = "Streamed Anime";
/// <summary>
/// Absolute path where anime .strm files are written.
/// InfiniteDrive creates a Series library at this path when anime is enabled.
/// Default: <c>/media/infinitedrive/anime</c>
/// </summary>
[DataMember]
public string SyncPathAnime { get; set; } = "/media/infinitedrive/anime";
/// <summary>
/// Maximum number of simultaneous AIOStreams HTTP calls during background
/// link pre-resolution. Default: 3.
/// </summary>
[DataMember]
public int MaxConcurrentResolutions { get; set; } = 3;
/// <summary>
/// Resolved AIOStreams instance type (Shared or Private).
/// Auto-detected from <see cref="PrimaryManifestUrl"/> on every config save.
/// Not user-editable — stored in XML only.
/// Default: <see cref="Services.InstanceType.Shared"/> (safer fallback).
/// </summary>
[DataMember]
public Services.InstanceType ResolvedInstanceType { get; set; } = Services.InstanceType.Shared;
/// <summary>
/// Per-catalog item limit overrides, serialised as a JSON object mapping
/// source keys to integer limits.
///
/// Format: <c>{"aio:movie:30ae3b0.tmdb.top":200,"aio:series:668e3b0.nfx":50}</c>
///
/// Leave empty for no per-catalog limit.
/// The config page builds and saves this JSON automatically from the
/// per-row limit inputs on the Catalog panel.
/// </summary>
[DataMember]
public string CatalogItemLimitsJson { get; set; } = string.Empty;
/// <summary>
/// How many hours must pass since a catalog source's last <em>successful</em>
/// sync before it is eligible to be re-fetched.
///
/// The scheduled task may still run on its own Emby schedule; this setting
/// acts as an additional internal throttle so catalog endpoints are not
/// hammered on every task invocation.
///
/// Sources in an <c>error</c> state are always retried regardless of this value.
/// Default: 1 h.
/// </summary>
[DataMember]
public int CatalogSyncIntervalHours { get; set; } = 1;
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ STREAMING / PROXY ║
// ╚══════════════════════════════════════════════════════════════════════╝
/// <summary>
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ STREAM PRE-CACHE ║
// ╚══════════════════════════════════════════════════════════════════════╝
/// <summary>
/// PRE-CACHE SYSTEM (highly recommended)
///
/// Enables background pre-warming of stream metadata for all library items.
/// When enabled, navigation into movies/series becomes near-instant (cache hit)
/// instead of waiting 20-40 seconds for live AIO resolution.
///
/// The background task automatically respects AIO rate limits and backoff headers.
/// It processes newest items first and runs on a schedule.
///
/// Default: Enabled (42 items per run, every 6 hours)
/// </summary>
[DataMember]
public bool EnablePreCache { get; set; } = true;
/// <summary>Number of items to resolve per pre-cache run. Default: 42.</summary>
[DataMember]
public int PreCacheBatchSize { get; set; } = 42;
/// <summary>Days before a pre-cached entry expires and needs re-resolution. Default: 14.</summary>
[DataMember]
public int PreCacheTTLDays { get; set; } = 14;
/// <summary>In-memory source list TTL in minutes. Default: 360 (6h).</summary>
[DataMember]
public int InMemoryCacheTtlMinutes { get; set; } = 360;
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ STREAM SIGNING SECRET ║
// ╚══════════════════════════════════════════════════════════════════════╝
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ MULTI-PROVIDER PRIORITY ║
// ╚══════════════════════════════════════════════════════════════════════╝
/// <summary>
/// Comma-separated provider priority order. Within the same quality tier,
/// InfiniteDrive picks the stream whose provider appears earliest in this list.
///
/// Uses the <c>service.id</c> values returned by AIOStreams:
/// <c>realdebrid</c>, <c>torbox</c>, <c>alldebrid</c>, <c>premiumize</c>,
/// <c>debridlink</c>, <c>stremthru</c>, <c>easynews</c>, <c>nzbdav</c>,
/// <c>altmount</c>, <c>usenet</c>, <c>http</c>.
///
/// Providers not listed are ranked after all listed ones.
/// Quality tier always takes precedence over provider priority:
/// a 4K RD stream beats a 1080p TorBox stream regardless of this setting.
///
/// Default: <c>realdebrid,torbox,alldebrid,premiumize,stremthru,usenet,http</c>
/// </summary>
[DataMember]
public string ProviderPriorityOrder { get; set; }
= "realdebrid,torbox,alldebrid,debridlink,premiumize,stremthru,usenet,http";
/// <summary>
/// Maximum number of ranked stream candidates to store <b>per debrid provider</b>
/// per catalog item.
///
/// With <c>CandidatesPerProvider = 3</c> and three providers (RD, TorBox, Premiumize),
/// the plugin stores up to 9 candidates — 3 from each. PlaybackService tries them
/// in quality order: if all 3 RD CDN URLs have expired, it automatically falls over
/// to TorBox rank-0 before calling AIOStreams again. This costs no extra API calls —
/// AIOStreams already returns 80+ streams per request; we simply keep more of them.
///
/// Default: 3. Raise to 5 for extra resilience; lower to 1 to save DB space.
/// </summary>
[DataMember]
public int CandidatesPerProvider { get; set; } = 3;
/// <summary>
/// Max total curated streams returned per item (default 7, max 12).
/// </summary>
[DataMember]
public int MaxCuratedStreams { get; set; } = 10;
/// <summary>
/// JSON-serialized bucket definitions for stream selection.
/// Each bucket: { "resTier": int, "srcMax": int, "maxCount": int }
/// resTier: 0=4K 1=1440p 2=1080p 3=720p 4=480p
/// srcMax: 0=Remux-only 1=+BluRay 2=+WEB-DL 3=+WEB
/// maxCount: max streams from this bucket
/// </summary>
[DataMember]
public string StreamBucketsJson { get; set; } = "";
/// <summary>
/// When <c>true</c> (default), InfiniteDrive skips the Emby transcoding
/// pipeline and serves the resolved CDN URL directly to the client.
/// Set to <c>false</c> to force Emby to open the stream through its
/// normal <c>OpenMediaSource()</c> path (enables transcode/remux).
/// </summary>
[DataMember]
public bool DirectPlayEnabled { get; set; } = true;
/// <summary>
/// Prefix string prepended to version/slot labels in the UI.
/// Default: <c>"InfiniteDrive · "</c>
/// </summary>
[DataMember]
public string VersionLabelPrefix { get; set; } = "InfiniteDrive · ";
/// <summary>
/// Timeout in seconds for on-demand (synchronous) AIOStreams resolution
/// triggered by a cache miss during playback.
///
/// Acts as a minimum floor: if the AIOStreams manifest advertises a longer
/// <c>behaviorHints.requestTimeout</c>, that value is used instead (stored
/// in <see cref="AioStreamsDiscoveredTimeoutSeconds"/> automatically).
///
/// Keep high enough for AIOStreams to query all its configured addons
/// (typically 20–60 s depending on addon count). Default: 30 s.
/// </summary>
[DataMember]
public int SyncResolveTimeoutSeconds { get; set; } = 30;
/// <summary>
/// The <c>behaviorHints.requestTimeout</c> value read from the AIOStreams
/// manifest during the last successful catalog sync. Updated automatically —
/// do not set this manually. A value of 0 means not yet discovered.
///
/// PlaybackService uses <c>Max(SyncResolveTimeoutSeconds, AioStreamsDiscoveredTimeoutSeconds)</c>
/// so the manifest value acts as a ceiling that grows automatically when the
/// user adds slow addons to their AIOStreams instance.
/// </summary>
[DataMember]
public int AioStreamsDiscoveredTimeoutSeconds { get; set; } = 0;
/// <summary>
/// Display name of the connected AIOStreams instance, read from
/// <c>manifest.name</c> during the last successful sync.
/// </summary>
[DataMember]
public string AioStreamsDiscoveredName { get; set; } = string.Empty;
/// <summary>
/// Version string of the connected AIOStreams instance (<c>manifest.version</c>).
/// </summary>
[DataMember]
public string AioStreamsDiscoveredVersion { get; set; } = string.Empty;
/// <summary>
/// True when the connected AIOStreams instance has no catalog entries
/// (stream-only mode). The Emby library must be populated via Trakt or
/// MDBList; AIOStreams is used only for on-demand stream resolution.
/// </summary>
[DataMember]
public bool AioStreamsIsStreamOnly { get; set; } = false;
/// <summary>
/// Comma-separated ID prefixes the stream resource accepts
/// (e.g. <c>tt,imdb,mal:,kitsu:</c>). Populated during sync.
/// InfiniteDrive currently generates only <c>tt</c> (IMDB) IDs.
/// </summary>
[DataMember]
public string AioStreamsStreamIdPrefixes { get; set; } = string.Empty;
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ VERSIONED PLAYBACK ║
// ╚══════════════════════════════════════════════════════════════════════╝
/// <summary>
/// How many hours a normalized stream candidate remains valid before
/// it is considered expired and cleaned up. The <c>expires_at</c> column
/// in the <c>candidates</c> table is computed as
/// <c>datetime('now', '+' || CandidateTtlHours || ' hours')</c>.
///
/// Expired candidates are pruned by <c>DeleteExpiredCandidatesAsync()</c>.
/// Default: 6 hours.
/// </summary>
[DataMember]
public int CandidateTtlHours { get; set; } = 6;
/// <summary>
/// Slot key of the default quality version used for playback when no
/// specific slot is requested. Must match a row in the
/// <c>version_slots</c> table where <c>is_enabled = 1</c>.
///
/// Default: <c>hd_broad</c> (1080p SDR Broad).
/// </summary>
[DataMember]
public string DefaultSlotKey { get; set; } = "hd_broad";
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ MULTI-VERSION STRM PREWRITING ║
// ╚══════════════════════════════════════════════════════════════════════╝
/// <summary>
/// Desired quality buckets for multi-version .strm selection.
/// Buckets are matched in order — earlier buckets get priority.
/// Empty by default — users configure buckets via the Content Controls UI.
/// </summary>
[DataMember]
public List<Models.DesiredVersionBucket> DesiredVersions { get; set; } = new();
/// <summary>Allow very large REMUX releases. Off by default to prevent buffering.</summary>
[DataMember]
public bool AllowRemux { get; set; } = false;
/// <summary>Allow CAM/telesync captures. Off by default.</summary>
[DataMember]
public bool AllowCam { get; set; } = false;
/// <summary>
/// Queue of pending rehydration operations serialised as JSON.
///
/// Each entry is an object with a <c>type</c> field and a <c>slotKey</c> field:
/// <c>[{"type":"AddSlot","slotKey":"4k_hdr"}, ...]</c>
///
/// Consumed by the <c>RehydrationTask</c> on next execution to add, remove,
/// or rename .strm files across the catalog.
/// </summary>
[DataMember]
public List<string> PendingRehydrationOperations { get; set; } = new();
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ SYNC SCHEDULE ║
// ╚══════════════════════════════════════════════════════════════════════╝
/// <summary>
/// Hour of day (0–23 UTC) at which the daily catalog sync trigger fires.
/// Default: 3 (3:00 AM). Set to -1 to disable the daily trigger and
/// rely solely on the Emby Scheduled Tasks page to control timing.
/// </summary>
[DataMember]
public int SyncScheduleHour { get; set; } = 3;
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ LIBRARY RE-ADOPTION ║
// ╚══════════════════════════════════════════════════════════════════════╝
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ METADATA ENRICHMENT ║
// ╚══════════════════════════════════════════════════════════════════════╝
/// <summary>
/// Base URL for AIOMetadata API for enrichment.
/// Format: https://<instance>/meta/{type}/{id}.json
/// </summary>
[DataMember]
public string AioMetadataBaseUrl { get; set; } = string.Empty;
/// <summary>
/// JSON census of all provider ID types seen across catalog items,
/// written by CatalogSyncTask after each sync.
/// Format: <c>{"Kitsu":"142","MAL":"89","AniDB":"210"}</c>
/// Keys are provider names; values are item counts as strings.
/// </summary>
[DataMember]
public string MetadataIdTypeCensus { get; set; } = "{}";
/// <summary>
/// JSON array of provider ID types the user has opted in to for
/// metadata resolution via IRemoteMetadataProvider.
/// Format: <c>["Kitsu","MAL","AniDB","AniList"]</c>
///
/// When empty and census is non-empty, the metadata provider
/// treats ALL non-Emby-native types as opted-in (auto-opt-in
/// for fresh installs).
/// </summary>
[DataMember]
public string MetadataEnabledIdTypes { get; set; } = "[]";
/// <summary>
/// Newline-separated list of system-wide RSS feed URLs.
/// Content from these feeds is visible to ALL users — admin-only setting.
/// </summary>
[DataMember]
public string SystemRssFeedUrls { get; set; } = string.Empty;
/// <summary>
/// JSON array of SourceKey strings that the user has disabled on the Catalogs tab.
/// Empty string or "[]" means all catalogs are enabled.
/// </summary>
[DataMember]
public string DisabledSourceKeysJson { get; set; } = string.Empty;
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ FIRST-RUN WIZARD ║
// ╚══════════════════════════════════════════════════════════════════════╝
/// <summary>
/// Set to <c>true</c> by the first-run wizard after initial configuration
/// is complete. While <c>false</c>, the dashboard shows the setup wizard.
/// </summary>
[DataMember]
public bool IsFirstRunComplete { get; set; } = false;
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ PARENTAL CONTROLS (Sprint 209) ║
// ╚══════════════════════════════════════════════════════════════════════╝
/// <summary>
/// TMDB API key for fetching content certifications (MPAA/TV ratings).
/// Free key from themoviedb.org → Settings → API.
/// Required for parental filtering. When empty, no ratings are fetched.
/// </summary>
[DataMember]
public string TmdbApiKey { get; set; } = string.Empty;
/// <summary>
/// When enabled, users with parental restrictions (max rating < 999) will NOT
/// see content without known MPAA/TV ratings. Unrestricted users are never affected.
/// Default: enabled for safety.
/// </summary>
[DataMember]
public bool BlockUnratedForRestricted { get; set; } = true;
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ EXTERNAL LIST PROVIDERS ║
// ╚══════════════════════════════════════════════════════════════════════╝
/// <summary>
/// Shared Trakt API Client ID for fetching public Trakt lists.
/// Enter once in admin settings; all users share it.
/// Free to create at trakt.tv/oauth.
/// When empty, Trakt is not available as a list source.
/// </summary>
[DataMember]
public string TraktClientId { get; set; } = string.Empty;
/// <summary>
/// Maximum number of external lists a non-admin user may have.
/// Admin lists are unlimited. Default: 5.
/// </summary>
[DataMember]
public int UserCatalogLimit { get; set; } = 5;
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ CONTENT CONTROLS TAB (Sprint 502) ║
// ╚══════════════════════════════════════════════════════════════════════╝
/// <summary>Max streams shown in dropdown per quality tier. 0 = tier disabled.</summary>
[DataMember] public int MaxStreams4k51 { get; set; } = 2;
[DataMember] public int MaxStreams4kAny { get; set; } = 2;
[DataMember] public int MaxStreams1080p51 { get; set; } = 2;
[DataMember] public int MaxStreams1080pAny { get; set; } = 2;
[DataMember] public int MaxStreams720p { get; set; } = 2;
[DataMember] public int MaxStreamsSd { get; set; } = 2;
/// <summary>Default quality tier selected when no user preference is set. Default: "1080p (any)".</summary>
[DataMember]
public string DefaultQualityTier { get; set; } = "1080p (any)";
/// <summary>
/// When true, catalog items without a known rating are hidden from all users (admin-level global toggle).
/// Default: false.
/// </summary>
[DataMember]
public bool HideUnratedContent { get; set; } = false;
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ CATALOGS & LISTS TAB (Sprint 502) ║
// ╚══════════════════════════════════════════════════════════════════════╝
/// <summary>Maximum number of external lists a non-admin user may create. Default: 10.</summary>
[DataMember]
public int MaxListsPerUser { get; set; } = 10;
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ SYNC & MARVIN TAB (Sprint 502) ║
// ╚══════════════════════════════════════════════════════════════════════╝
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ ADVANCED TAB (Sprint 502) ║
// ╚══════════════════════════════════════════════════════════════════════╝
/// <summary>Minimum log verbosity level for InfiniteDrive. Default: "Info".</summary>
[DataMember]
public string PluginLogLevel { get; set; } = "Info";
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ INSTANCE TYPE DETECTION ║
// ╚══════════════════════════════════════════════════════════════════════╝
/// <summary>
/// Known public/shared AIOStreams instance hostnames.
/// Add new shared-hosting domains here as they emerge.
/// </summary>
private static readonly string[] SharedInstanceHosts =
{
"elfhosted.com",
"aiostreams.elfhosted.com",
};
/// <summary>
/// Detects the AIOStreams instance type from the manifest URL.
/// <list type="bullet">
/// <item><c>Private</c> if host is localhost, 127.0.0.1, or RFC1918 range.</item>
/// <item><c>Shared</c> if host matches known public-instance allowlist.</item>
/// <item><c>Shared</c> for everything else (safer default).</item>
/// </list>
/// </summary>
public static Services.InstanceType DetectInstanceType(string manifestUrl)
{
if (string.IsNullOrWhiteSpace(manifestUrl))
return Services.InstanceType.Shared;
try
{
var uri = new Uri(manifestUrl);
var host = uri.Host.ToLowerInvariant();
// Loopback
if (host == "localhost" || host == "127.0.0.1" || host == "::1")
return Services.InstanceType.Private;
// RFC1918 private ranges
if (host.StartsWith("10.") ||
host.StartsWith("192.168.") ||
Is17216To31(host))
return Services.InstanceType.Private;
// Known shared instances (explicit allowlist)
foreach (var shared in SharedInstanceHosts)
{
if (host == shared || host.EndsWith("." + shared))
return Services.InstanceType.Shared;
}
}
catch
{
// Unparseable URL — assume shared (safer)
}
return Services.InstanceType.Shared;
}
private static bool Is17216To31(string host)
{
// 172.16.0.0 – 172.31.255.255
if (!host.StartsWith("172."))
return false;
var parts = host.Split('.');
if (parts.Length < 2) return false;
if (!int.TryParse(parts[1], out var second)) return false;
return second >= 16 && second <= 31;
}
// ╔══════════════════════════════════════════════════════════════════════╗
// ║ BOUNDS VALIDATION ║
// ╚══════════════════════════════════════════════════════════════════════╝
/// <summary>
/// Clamps all numeric configuration fields to safe ranges after deserialisation.
/// Prevents zero / negative values from silently corrupting behaviour.
/// </summary>
public void Validate()
{
static int Clamp(int v, int min, int max) => v < min ? min : v > max ? max : v;
MaxConcurrentResolutions = Clamp(MaxConcurrentResolutions, 1, 20);
CatalogSyncIntervalHours = Clamp(CatalogSyncIntervalHours, 1, 24); // 1 h – 24 h
PreCacheBatchSize = Clamp(PreCacheBatchSize, 1, 500);
PreCacheTTLDays = Clamp(PreCacheTTLDays, 1, 90);
InMemoryCacheTtlMinutes = Clamp(InMemoryCacheTtlMinutes, 10, 1_440);
SyncResolveTimeoutSeconds = Clamp(SyncResolveTimeoutSeconds, 5, 300);
// -1 is the "disabled" sentinel; any other out-of-range value clamps to 0–23
SyncScheduleHour = SyncScheduleHour == -1 ? -1 : Clamp(SyncScheduleHour, 0, 23);
CandidatesPerProvider = Clamp(CandidatesPerProvider, 1, 10);
MaxCuratedStreams = Clamp(MaxCuratedStreams, 1, 12);
CandidateTtlHours = Clamp(CandidateTtlHours, 1, 168); // 1 h – 7 days
UserCatalogLimit = Clamp(UserCatalogLimit, 0, 50);
if (MaxListsPerUser < 0) MaxListsPerUser = 10;
// Seed a default quality bucket if none are defined — prevents Marvin running with nothing to populate
if (DesiredVersions == null || DesiredVersions.Count == 0)
DesiredVersions = new System.Collections.Generic.List<Models.DesiredVersionBucket>
{
new Models.DesiredVersionBucket { Resolution = "1080p", Audio = "Any Audio", Count = 1 }
};
// Recompute instance type from manifest URL
ResolvedInstanceType = DetectInstanceType(PrimaryManifestUrl);
}
[OnDeserialized]
private void OnDeserialized(StreamingContext _)
{
Validate();
}
}
}