├── .gitignore
├── Images
├── preview-button-ink.svg
├── preview-button.svg
└── preview.gif
├── InPlayerEpisodePreview.sln
├── LICENSE.md
├── Namo.Plugin.InPlayerEpisodePreview
├── Api
│ ├── DTOs
│ │ └── EpisodeDescriptionDto.cs
│ └── InPlayerPreviewController.cs
├── BuildFiles
│ └── 7za.exe
├── Configuration
│ ├── PluginConfiguration.cs
│ └── config.html
├── InPlayerEpisodePreviewPlugin.cs
├── Namo.Plugin.InPlayerEpisodePreview.csproj
├── Web
│ ├── Components
│ │ ├── BaseTemplate.ts
│ │ ├── DialogBackdropContainerTemplate.ts
│ │ ├── DialogContainerTemplate.ts
│ │ ├── EpisodeDetails.ts
│ │ ├── ListElementTemplate.ts
│ │ ├── PopupContentContainerTemplate.ts
│ │ ├── PopupFocusContainer.ts
│ │ ├── PopupTitleTemplate.ts
│ │ ├── PreviewButtonTemplate.ts
│ │ ├── QuickActions
│ │ │ ├── FavoriteIconTemplate.ts
│ │ │ └── PlayStateIconTemplate.ts
│ │ └── SeasonListElementTemplate.ts
│ ├── Endpoints.ts
│ ├── InPlayerPreview.ts
│ ├── ListElementFactory.ts
│ ├── Models
│ │ ├── Episode.ts
│ │ ├── ItemType.ts
│ │ ├── Playbackinfo.ts
│ │ ├── ProgramData.ts
│ │ └── Season.ts
│ └── Services
│ │ ├── AuthService.ts
│ │ ├── DataFetcher.ts
│ │ ├── DataLoader.ts
│ │ ├── Logger.ts
│ │ ├── PlaybackHandler.ts
│ │ └── ProgramDataStore.ts
├── build.bat
├── package-lock.json
├── package.json
├── tsconfig.json
└── webpack.config.js
├── README.md
├── jellyfin.ruleset
└── manifest.json
/.gitignore:
--------------------------------------------------------------------------------
1 | .directory
2 |
3 | #################
4 | ## Eclipse
5 | #################
6 |
7 | *.pydevproject
8 | .project
9 | .metadata
10 | bin/
11 | tmp/
12 | *.tmp
13 | *.bak
14 | *.swp
15 | *~.nib
16 | project.fragment.lock.json
17 | project.lock.json
18 | local.properties
19 | .classpath
20 | .settings/
21 | .loadpath
22 |
23 | # External tool builders
24 | .externalToolBuilders/
25 |
26 | # Locally stored "Eclipse launch configurations"
27 | *.launch
28 |
29 | # CDT-specific
30 | .cproject
31 |
32 | # PDT-specific
33 | .buildpath
34 |
35 | #################
36 | ## Media Browser
37 | #################
38 | ProgramData*/
39 | CorePlugins*/
40 | ProgramData-Server*/
41 | ProgramData-UI*/
42 |
43 | #################
44 | ## Visual Studio
45 | #################
46 |
47 | ## Ignore Visual Studio temporary files, build results, and
48 | ## files generated by popular Visual Studio add-ons.
49 |
50 | .vs/
51 |
52 | # User-specific files
53 | *.suo
54 | *.user
55 | *.sln.docstates
56 |
57 | # Build results
58 |
59 | [Dd]ebug/
60 | [Rr]elease/
61 | build/
62 | [Bb]in/
63 | [Oo]bj/
64 |
65 | # MSTest test Results
66 | [Tt]est[Rr]esult*/
67 | [Bb]uild[Ll]og.*
68 |
69 | *_i.c
70 | *_p.c
71 | *.ilk
72 | *.meta
73 | *.obj
74 | *.pch
75 | *.pdb
76 | *.pgc
77 | *.pgd
78 | *.rsp
79 | *.sbr
80 | *.tlb
81 | *.tli
82 | *.tlh
83 | *.tmp
84 | *.tmp_proj
85 | *.log
86 | *.vspscc
87 | *.vssscc
88 | .builds
89 | *.pidb
90 | *.log
91 | *.scc
92 | *.scc
93 | *.psess
94 | *.vsp
95 | *.vspx
96 | *.orig
97 | *.rej
98 | *.sdf
99 | *.opensdf
100 | *.ipch
101 |
102 | # Visual C++ cache files
103 | ipch/
104 | *.aps
105 | *.ncb
106 | *.opensdf
107 | *.sdf
108 | *.cachefile
109 |
110 | # Visual Studio profiler
111 | *.psess
112 | *.vsp
113 | *.vspx
114 |
115 | # Guidance Automation Toolkit
116 | *.gpState
117 |
118 | # ReSharper is a .NET coding add-in
119 | _ReSharper*/
120 | *.[Rr]e[Ss]harper
121 |
122 | # TeamCity is a build add-in
123 | _TeamCity*
124 |
125 | # DotCover is a Code Coverage Tool
126 | *.dotCover
127 |
128 | # NCrunch
129 | *.ncrunch*
130 | .*crunch*.local.xml
131 |
132 | # Installshield output folder
133 | [Ee]xpress/
134 |
135 | # DocProject is a documentation generator add-in
136 | DocProject/buildhelp/
137 | DocProject/Help/*.HxT
138 | DocProject/Help/*.HxC
139 | DocProject/Help/*.hhc
140 | DocProject/Help/*.hhk
141 | DocProject/Help/*.hhp
142 | DocProject/Help/Html2
143 | DocProject/Help/html
144 |
145 | # Click-Once directory
146 | publish/
147 |
148 | # Publish Web Output
149 | *.Publish.xml
150 | *.pubxml
151 |
152 | # NuGet Packages Directory
153 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line
154 | # packages/
155 | dlls/
156 | dllssigned/
157 |
158 | # Windows Azure Build Output
159 | csx
160 | *.build.csdef
161 |
162 | # Windows Store app package directory
163 | AppPackages/
164 |
165 | # Others
166 | sql/
167 | *.Cache
168 | ClientBin/
169 | [Ss]tyle[Cc]op.*
170 | ~$*
171 | *~
172 | *.dbmdl
173 | *.[Pp]ublish.xml
174 | *.publishsettings
175 |
176 | # RIA/Silverlight projects
177 | Generated_Code/
178 |
179 | # Backup & report files from converting an old project file to a newer
180 | # Visual Studio version. Backup files are not needed, because we have git ;-)
181 | _UpgradeReport_Files/
182 | Backup*/
183 | UpgradeLog*.XML
184 | UpgradeLog*.htm
185 |
186 | # SQL Server files
187 | App_Data/*.mdf
188 | App_Data/*.ldf
189 |
190 | #############
191 | ## Windows detritus
192 | #############
193 |
194 | # Windows image file caches
195 | Thumbs.db
196 | ehthumbs.db
197 |
198 | # Folder config file
199 | Desktop.ini
200 |
201 | # Recycle Bin used on file shares
202 | $RECYCLE.BIN/
203 |
204 | # Mac crap
205 | .DS_Store
206 |
207 | #############
208 | ## Python
209 | #############
210 |
211 | *.py[co]
212 |
213 | # Packages
214 | *.egg
215 | *.egg-info
216 | build/
217 | eggs/
218 | parts/
219 | var/
220 | sdist/
221 | develop-eggs/
222 | .installed.cfg
223 |
224 | # Installer logs
225 | pip-log.txt
226 |
227 | # Unit test / coverage reports
228 | .coverage
229 | .tox
230 |
231 | #Translations
232 | *.mo
233 |
234 | #Mr Developer
235 | .mr.developer.cfg
236 |
237 | ##########
238 | # Rider
239 | ##########
240 | .idea/
241 |
242 | #########################
243 | # Build artifacts
244 | #########################
245 |
246 | # Artifacts for debian-x64
247 | debian/.debhelper/
248 | debian/*.debhelper
249 | debian/debhelper-build-stamp
250 | debian/files
251 | debian/jellyfin.substvars
252 | debian/jellyfin/
253 | # Don't ignore the debian/bin folder
254 | !debian/bin/
255 |
256 | deployment/**/dist/
257 | deployment/**/pkg-dist/
258 | deployment/**/pkg-dist-tmp/
259 | deployment/collect-dist/
260 |
261 | jellyfin_version.ini
262 |
263 | Namo.Plugin.InPlayerEpisodePreview/.build/
264 |
265 | ci/
266 |
267 | # Doxygen
268 | doc/
269 |
270 | # Deployment artifacts
271 | dist
272 | *.exe
273 | *.dll
274 |
275 | # BenchmarkDotNet artifacts
276 | BenchmarkDotNet.Artifacts
277 |
278 | # Ignore web artifacts from native builds
279 | node_modules/
280 |
281 | # Ignore JetBrains Rider run configs
282 | .run/
283 |
284 | # Omnisharp crash logs
285 | mono_crash.*.json
--------------------------------------------------------------------------------
/Images/preview-button-ink.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
131 |
--------------------------------------------------------------------------------
/Images/preview-button.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
90 |
--------------------------------------------------------------------------------
/Images/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Namo2/InPlayerEpisodePreview/e358db0200978c047cc2d63e8167424a63fc8e85/Images/preview.gif
--------------------------------------------------------------------------------
/InPlayerEpisodePreview.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.2.32630.192
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Namo.Plugin.InPlayerEpisodePreview", "Namo.Plugin.InPlayerEpisodePreview\Namo.Plugin.InPlayerEpisodePreview.csproj", "{DF526B2C-5F30-40A0-ACDA-17D8933962B1}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Debug|x64 = Debug|x64
12 | Release|Any CPU = Release|Any CPU
13 | Release|x64 = Release|x64
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {DF526B2C-5F30-40A0-ACDA-17D8933962B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {DF526B2C-5F30-40A0-ACDA-17D8933962B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {DF526B2C-5F30-40A0-ACDA-17D8933962B1}.Debug|x64.ActiveCfg = Debug|Any CPU
19 | {DF526B2C-5F30-40A0-ACDA-17D8933962B1}.Debug|x64.Build.0 = Debug|Any CPU
20 | {DF526B2C-5F30-40A0-ACDA-17D8933962B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
21 | {DF526B2C-5F30-40A0-ACDA-17D8933962B1}.Release|Any CPU.Build.0 = Release|Any CPU
22 | {DF526B2C-5F30-40A0-ACDA-17D8933962B1}.Release|x64.ActiveCfg = Release|Any CPU
23 | {DF526B2C-5F30-40A0-ACDA-17D8933962B1}.Release|x64.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ExtensibilityGlobals) = postSolution
29 | SolutionGuid = {9BC84320-622C-488E-BD8F-A2A6E26799AA}
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) Media Browser http://mediabrowser.tv
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/Namo.Plugin.InPlayerEpisodePreview/Api/DTOs/EpisodeDescriptionDto.cs:
--------------------------------------------------------------------------------
1 | namespace Namo.Plugin.InPlayerEpisodePreview.Api.DTOs;
2 |
3 | public record EpisodeDescriptionDto(string Description);
--------------------------------------------------------------------------------
/Namo.Plugin.InPlayerEpisodePreview/Api/InPlayerPreviewController.cs:
--------------------------------------------------------------------------------
1 | using MediaBrowser.Common.Configuration;
2 | using MediaBrowser.Controller.Configuration;
3 | using MediaBrowser.Controller.Library;
4 | using MediaBrowser.Controller.MediaEncoding;
5 | using MediaBrowser.Model.IO;
6 | using Microsoft.AspNetCore.Http;
7 | using Microsoft.AspNetCore.Mvc;
8 | using Microsoft.Extensions.Logging;
9 | using System.Reflection;
10 | using MediaBrowser.Controller.Entities;
11 | using MediaBrowser.Controller.Persistence;
12 | using MediaBrowser.Controller.Session;
13 | using MediaBrowser.Model.Session;
14 | using Namo.Plugin.InPlayerEpisodePreview.Api.DTOs;
15 | using Namo.Plugin.InPlayerEpisodePreview.Configuration;
16 |
17 | namespace Namo.Plugin.InPlayerEpisodePreview.Api;
18 |
19 | ///
20 | /// Controller for accessing show data.
21 | ///
22 | [ApiController]
23 | [Route("InPlayerPreview")]
24 | public class InPlayerPreviewController : ControllerBase
25 | {
26 | private readonly Assembly _assembly;
27 | private readonly string _playerPreviewScriptPath;
28 |
29 | private readonly ILogger _logger;
30 | private readonly ILibraryManager _libraryManager;
31 | private readonly IItemRepository _itemRepository;
32 | private readonly IFileSystem _fileSystem;
33 | private readonly ILoggerFactory _loggerFactory;
34 | private readonly IApplicationPaths _appPaths;
35 | private readonly ILibraryMonitor _libraryMonitor;
36 | private readonly IMediaEncoder _mediaEncoder;
37 | private readonly IServerConfigurationManager _configurationManager;
38 | private readonly ISessionManager _sessionManager;
39 | private readonly EncodingHelper _encodingHelper;
40 |
41 | private readonly PluginConfiguration _config;
42 |
43 | ///
44 | /// Initializes a new instance of the class.
45 | ///
46 | public InPlayerPreviewController(
47 | ILibraryManager libraryManager,
48 | IItemRepository itemRepository,
49 | IFileSystem fileSystem,
50 | ILogger logger,
51 | ILoggerFactory loggerFactory,
52 | IApplicationPaths appPaths,
53 | ILibraryMonitor libraryMonitor,
54 | IMediaEncoder mediaEncoder,
55 | IServerConfigurationManager configurationManager,
56 | ISessionManager sessionManager,
57 | EncodingHelper encodingHelper)
58 | {
59 | _assembly = Assembly.GetExecutingAssembly();
60 | _playerPreviewScriptPath = $"{InPlayerEpisodePreviewPlugin.Instance?.GetType().Namespace}.Web.InPlayerPreview.js";
61 |
62 | _libraryManager = libraryManager;
63 | _itemRepository = itemRepository;
64 | _logger = logger;
65 | _fileSystem = fileSystem;
66 | _loggerFactory = loggerFactory;
67 | _appPaths = appPaths;
68 | _libraryMonitor = libraryMonitor;
69 | _mediaEncoder = mediaEncoder;
70 | _configurationManager = configurationManager;
71 | _sessionManager = sessionManager;
72 | _encodingHelper = encodingHelper;
73 |
74 | _config = InPlayerEpisodePreviewPlugin.Instance!.Configuration;
75 | }
76 |
77 | ///
78 | /// Get embedded javascript file for client-side code.
79 | ///
80 | /// Javascript file successfully returned.
81 | /// File not found.
82 | /// The "inPlayerPreview.js" embedded file.
83 | [HttpGet("ClientScript")]
84 | [ProducesResponseType(StatusCodes.Status200OK)]
85 | [ProducesResponseType(StatusCodes.Status404NotFound)]
86 | [Produces("application/javascript")]
87 | public ActionResult GetClientScript()
88 | {
89 | var scriptStream = _assembly.GetManifestResourceStream(_playerPreviewScriptPath);
90 | _logger.LogError("InPlayerEpisodePreviewPlugin: {0}", _playerPreviewScriptPath);
91 | if (scriptStream == null)
92 | return NotFound();
93 |
94 | return File(scriptStream, "application/javascript");
95 | }
96 |
97 | ///
98 | /// This controller starts a new episode.
99 | /// Could be replaced by /Sessions/{sessionId}/Playing, if frontend loads session itself
100 | ///
101 | ///
102 | ///
103 | ///
104 | ///
105 | [HttpGet("Users/{userId}/Items/{id}/Play/{ticks}")]
106 | [ProducesResponseType(StatusCodes.Status204NoContent)]
107 | [ProducesResponseType(StatusCodes.Status404NotFound)]
108 | public ActionResult StartMedia([FromRoute] Guid userId, [FromRoute] Guid id, [FromRoute] long ticks = 0)
109 | {
110 | SessionInfo? session = _sessionManager.Sessions.FirstOrDefault(session => session.UserId.Equals(userId));
111 | if (session is null)
112 | {
113 | _logger.LogInformation("Couldn't find a valid session for this user");
114 | return NotFound("Couldn't find a valid session for this user");
115 | }
116 |
117 | BaseItem? item = _libraryManager.GetItemById(id);
118 | if (item is null)
119 | {
120 | const string message = "Couldn't find item to play";
121 | _logger.LogInformation(message);
122 | return NotFound(message);
123 | }
124 |
125 | _sessionManager.SendPlayCommand(session.Id, session.Id,
126 | new PlayRequest
127 | {
128 | ItemIds = [item.Id],
129 | StartPositionTicks = ticks,
130 | PlayCommand = PlayCommand.PlayNow,
131 | }, CancellationToken.None);
132 |
133 | return NoContent();
134 | }
135 |
136 | ///
137 | /// This controller returns the description of the given episode.
138 | /// Could be replaced by /Users/{userId}/Items/{episodeId}, if frontend loads whole data
139 | ///
140 | ///
141 | ///
142 | [HttpGet("Items/{id}")]
143 | [ProducesResponseType(StatusCodes.Status200OK)]
144 | [ProducesResponseType(StatusCodes.Status404NotFound)]
145 | public ActionResult GetMediaDescription([FromRoute] Guid id)
146 | {
147 | BaseItem? item = _libraryManager.GetItemById(id);
148 | if (item is not null)
149 | return new OkObjectResult(new EpisodeDescriptionDto(item.Overview));
150 |
151 | // Error case
152 | const string message = "Couldn't find item to play";
153 | _logger.LogInformation(message);
154 | return NotFound(message);
155 | }
156 | }
--------------------------------------------------------------------------------
/Namo.Plugin.InPlayerEpisodePreview/BuildFiles/7za.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Namo2/InPlayerEpisodePreview/e358db0200978c047cc2d63e8167424a63fc8e85/Namo.Plugin.InPlayerEpisodePreview/BuildFiles/7za.exe
--------------------------------------------------------------------------------
/Namo.Plugin.InPlayerEpisodePreview/Configuration/PluginConfiguration.cs:
--------------------------------------------------------------------------------
1 | using MediaBrowser.Model.Plugins;
2 |
3 | namespace Namo.Plugin.InPlayerEpisodePreview.Configuration;
4 |
5 | ///
6 | /// Class PluginConfiguration
7 | ///
8 | public class PluginConfiguration : BasePluginConfiguration
9 | {
10 | ///
11 | /// Initializes a new instance of the class.
12 | ///
13 | public PluginConfiguration() {}
14 |
15 | ///
16 | /// Whether or not the plugin should inject the client-side script tag into jellyfin-web.
17 | /// default = true
18 | ///
19 | public bool InjectClientScript { get; set; } = true;
20 | }
21 |
--------------------------------------------------------------------------------
/Namo.Plugin.InPlayerEpisodePreview/Configuration/config.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | InPlayerEpisodePreview
6 |
7 |
8 |
32 |
33 |
--------------------------------------------------------------------------------
/Namo.Plugin.InPlayerEpisodePreview/InPlayerEpisodePreviewPlugin.cs:
--------------------------------------------------------------------------------
1 | using System.Text.RegularExpressions;
2 | using MediaBrowser.Common.Configuration;
3 | using MediaBrowser.Common.Plugins;
4 | using MediaBrowser.Controller.Configuration;
5 | using MediaBrowser.Controller.Session;
6 | using MediaBrowser.Model.Plugins;
7 | using MediaBrowser.Model.Serialization;
8 | using Microsoft.Extensions.Logging;
9 | using Namo.Plugin.InPlayerEpisodePreview.Configuration;
10 |
11 | namespace Namo.Plugin.InPlayerEpisodePreview;
12 |
13 | ///
14 | /// InPlayerEpisodePreview plugin.
15 | ///
16 | public class InPlayerEpisodePreviewPlugin : BasePlugin, IHasWebPages
17 | {
18 | ///
19 | public override string Name => "InPlayerEpisodePreview";
20 |
21 | ///
22 | public override Guid Id => Guid.Parse("73833d5f-0bcb-45dc-ab8b-7ce668f4345d");
23 |
24 | ///
25 | public override string Description => "Adds episode preview functionality to Jellyfin.";
26 |
27 | ///
28 | /// Initializes a new instance of the class.
29 | ///
30 | public InPlayerEpisodePreviewPlugin(
31 | IApplicationPaths applicationPaths,
32 | IXmlSerializer xmlSerializer,
33 | ILogger logger,
34 | IServerConfigurationManager configurationManager)
35 | : base(applicationPaths, xmlSerializer)
36 | {
37 | Instance = this;
38 |
39 | if (!Configuration.InjectClientScript)
40 | return;
41 |
42 | if (string.IsNullOrWhiteSpace(applicationPaths.WebPath))
43 | return;
44 |
45 | var indexFile = Path.Combine(applicationPaths.WebPath, "index.html");
46 | if (!File.Exists(indexFile))
47 | return;
48 |
49 | string indexContents = File.ReadAllText(indexFile);
50 | string basePath = "";
51 |
52 | // Get base path from network config
53 | try
54 | {
55 | var networkConfig = configurationManager.GetConfiguration("network");
56 | var configType = networkConfig.GetType();
57 | var basePathField = configType.GetProperty("BaseUrl");
58 | var confBasePath = basePathField?.GetValue(networkConfig)?.ToString()?.Trim('/');
59 |
60 | if (!string.IsNullOrEmpty(confBasePath))
61 | basePath = $"/{confBasePath}";
62 | }
63 | catch (Exception e)
64 | {
65 | logger.LogError("Unable to get base path from config, using '/': {0}", e);
66 | }
67 |
68 | // Don't run if script already exists
69 | string scriptReplace = "";
70 | string scriptElement =
71 | string.Format(
72 | "",
73 | basePath);
74 |
75 | if (!indexContents.Contains(scriptElement))
76 | {
77 | logger.LogInformation("Attempting to inject preview script code in {0}", indexFile);
78 |
79 | // Replace old Jellyscrub scrips
80 | indexContents = Regex.Replace(indexContents, scriptReplace, "");
81 |
82 | // Insert script last in body
83 | int bodyClosing = indexContents.LastIndexOf("