2024-10-25 14:31:50 -04:00
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
// SPDX-License-Identifier: GPL-3.0-only.
2024-10-25 14:15:12 -04:00
2019-02-21 01:57:43 -08:00
using System ;
2024-05-08 16:27:16 +02:00
using System.Collections.Concurrent ;
2019-02-21 01:57:43 -08:00
using System.Collections.Generic ;
2022-05-05 18:10:34 -05:00
using System.IO ;
2024-09-20 14:18:04 +03:00
using System.Linq ;
2022-11-06 21:20:52 -06:00
using System.Text.RegularExpressions ;
2024-10-20 00:03:59 +02:00
using System.Xml ;
using System.Xml.Serialization ;
2024-10-19 23:50:41 +02:00
using IntroSkipper.Configuration ;
using IntroSkipper.Data ;
using IntroSkipper.Helper ;
2024-10-05 18:38:42 +02:00
using MediaBrowser.Common ;
2019-02-21 01:57:43 -08:00
using MediaBrowser.Common.Configuration ;
using MediaBrowser.Common.Plugins ;
2022-06-09 14:07:40 -05:00
using MediaBrowser.Controller.Configuration ;
2022-07-29 03:34:55 -05:00
using MediaBrowser.Controller.Entities ;
2022-06-15 01:00:03 -05:00
using MediaBrowser.Controller.Library ;
2022-11-24 00:43:23 -06:00
using MediaBrowser.Controller.Persistence ;
using MediaBrowser.Model.Entities ;
2019-02-21 01:57:43 -08:00
using MediaBrowser.Model.Plugins ;
using MediaBrowser.Model.Serialization ;
2024-10-12 16:13:37 +02:00
using MediaBrowser.Model.Updates ;
2022-09-27 20:31:18 -05:00
using Microsoft.Extensions.Logging ;
2019-02-21 01:57:43 -08:00
2024-10-19 23:50:41 +02:00
namespace IntroSkipper ;
2021-12-13 16:58:05 -07:00
/// <summary>
2022-05-01 01:24:57 -05:00
/// Intro skipper plugin. Uses audio analysis to find common sequences of audio shared between episodes.
2021-12-13 16:58:05 -07:00
/// </summary>
2024-09-25 17:23:25 +02:00
public class Plugin : BasePlugin < PluginConfiguration > , IHasWebPages
2019-02-21 01:57:43 -08:00
{
2022-10-28 02:25:57 -05:00
private readonly object _serializationLock = new ( ) ;
private readonly object _introsLock = new ( ) ;
2024-09-10 18:08:42 +02:00
private readonly ILibraryManager _libraryManager ;
private readonly IItemRepository _itemRepository ;
2024-10-05 18:38:42 +02:00
private readonly IApplicationHost _applicationHost ;
2024-09-10 18:08:42 +02:00
private readonly ILogger < Plugin > _logger ;
private readonly string _introPath ;
private readonly string _creditsPath ;
2024-09-20 14:18:04 +03:00
private string _ignorelistPath ;
2022-05-05 18:10:34 -05:00
2022-05-01 01:24:57 -05:00
/// <summary>
/// Initializes a new instance of the <see cref="Plugin"/> class.
/// </summary>
2024-10-05 18:38:42 +02:00
/// <param name="applicationHost">Application host.</param>
2022-05-01 01:24:57 -05:00
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
2022-06-09 14:07:40 -05:00
/// <param name="serverConfiguration">Server configuration manager.</param>
2022-06-15 01:00:03 -05:00
/// <param name="libraryManager">Library manager.</param>
2022-11-24 00:43:23 -06:00
/// <param name="itemRepository">Item repository.</param>
2022-09-27 20:31:18 -05:00
/// <param name="logger">Logger.</param>
2022-06-09 14:07:40 -05:00
public Plugin (
2024-10-05 18:38:42 +02:00
IApplicationHost applicationHost ,
2022-06-09 14:07:40 -05:00
IApplicationPaths applicationPaths ,
IXmlSerializer xmlSerializer ,
2022-06-15 01:00:03 -05:00
IServerConfigurationManager serverConfiguration ,
2022-09-27 20:31:18 -05:00
ILibraryManager libraryManager ,
2022-11-24 00:43:23 -06:00
IItemRepository itemRepository ,
2022-09-27 20:31:18 -05:00
ILogger < Plugin > logger )
2022-05-01 01:24:57 -05:00
: base ( applicationPaths , xmlSerializer )
{
2022-09-27 21:03:27 -05:00
Instance = this ;
2024-10-05 18:38:42 +02:00
_applicationHost = applicationHost ;
2022-06-15 01:00:03 -05:00
_libraryManager = libraryManager ;
2022-11-24 00:43:23 -06:00
_itemRepository = itemRepository ;
2022-09-27 20:31:18 -05:00
_logger = logger ;
2022-05-05 18:10:34 -05:00
2022-09-27 21:03:27 -05:00
FFmpegPath = serverConfiguration . GetEncodingOptions ( ) . EncoderAppPathDisplay ;
2022-11-06 21:20:52 -06:00
2024-03-29 15:32:23 +01:00
ArgumentNullException . ThrowIfNull ( applicationPaths ) ;
2024-05-16 18:24:36 +02:00
var pluginDirName = "introskipper" ;
var pluginCachePath = "chromaprints" ;
2024-03-18 19:52:57 +01:00
2024-05-16 18:24:36 +02:00
var introsDirectory = Path . Join ( applicationPaths . DataPath , pluginDirName ) ;
FingerprintCachePath = Path . Join ( introsDirectory , pluginCachePath ) ;
_introPath = Path . Join ( applicationPaths . DataPath , pluginDirName , "intros.xml" ) ;
_creditsPath = Path . Join ( applicationPaths . DataPath , pluginDirName , "credits.xml" ) ;
2024-09-20 14:18:04 +03:00
_ignorelistPath = Path . Join ( applicationPaths . DataPath , pluginDirName , "ignorelist.xml" ) ;
2024-05-16 18:24:36 +02:00
2024-10-07 07:44:07 +02:00
// Create the base & cache directories (if needed).
if ( ! Directory . Exists ( FingerprintCachePath ) )
{
Directory . CreateDirectory ( FingerprintCachePath ) ;
}
2024-03-29 15:32:23 +01:00
// migrate from XMLSchema to DataContract
XmlSerializationHelper . MigrateXML ( _introPath ) ;
XmlSerializationHelper . MigrateXML ( _creditsPath ) ;
2024-10-20 00:03:59 +02:00
var oldConfigFile = Path . Join ( applicationPaths . PluginConfigurationsPath , "ConfusedPolarBear.Plugin.IntroSkipper.xml" ) ;
if ( File . Exists ( oldConfigFile ) )
{
try
{
XmlSerializer serializer = new XmlSerializer ( typeof ( PluginConfiguration ) ) ;
using ( FileStream fileStream = new FileStream ( oldConfigFile , FileMode . Open ) )
{
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing . Prohibit , // Disable DTD processing
XmlResolver = null // Disable the XmlResolver
} ;
using ( var reader = XmlReader . Create ( fileStream , settings ) )
{
2024-10-30 20:37:03 +01:00
if ( serializer . Deserialize ( reader ) is PluginConfiguration oldConfig )
{
Instance . UpdateConfiguration ( oldConfig ) ;
File . Delete ( oldConfigFile ) ;
}
2024-10-20 00:03:59 +02:00
}
}
}
catch ( Exception ex )
{
// Handle exceptions, such as file not found, deserialization errors, etc.
2024-10-30 20:37:03 +01:00
_logger . LogWarning ( "Something stupid happened: {Exception}" , ex ) ;
2024-10-20 00:03:59 +02:00
}
}
2024-10-27 22:38:46 +01:00
MigrateRepoUrl ( serverConfiguration ) ;
2022-09-27 21:03:27 -05:00
// TODO: remove when https://github.com/jellyfin/jellyfin-meta/discussions/30 is complete
2022-09-27 20:31:18 -05:00
try
{
RestoreTimestamps ( ) ;
}
catch ( Exception ex )
{
_logger . LogWarning ( "Unable to load introduction timestamps: {Exception}" , ex ) ;
}
2022-11-06 21:20:52 -06:00
2024-09-20 14:18:04 +03:00
try
{
LoadIgnoreList ( ) ;
}
catch ( Exception ex )
{
_logger . LogWarning ( "Unable to load ignore list: {Exception}" , ex ) ;
}
2022-11-06 21:20:52 -06:00
// Inject the skip intro button code into the web interface.
try
{
2024-08-31 17:08:42 +00:00
InjectSkipButton ( applicationPaths . WebPath ) ;
2022-11-06 21:20:52 -06:00
}
catch ( Exception ex )
{
2022-11-06 21:25:23 -06:00
WarningManager . SetFlag ( PluginWarning . UnableToAddSkipButton ) ;
2022-11-06 21:20:52 -06:00
2024-09-19 13:06:37 +02:00
_logger . LogError ( "Failed to add skip button to web interface. See https://github.com/intro-skipper/intro-skipper/wiki/Troubleshooting#skip-button-is-not-visible for the most common issues. Error: {Error}" , ex ) ;
2022-11-06 21:20:52 -06:00
}
2024-03-07 17:08:15 -05:00
FFmpegWrapper . CheckFFmpegVersion ( ) ;
2022-05-05 18:10:34 -05:00
}
2022-05-09 22:56:03 -05:00
/// <summary>
/// Gets the results of fingerprinting all episodes.
/// </summary>
2024-09-12 08:37:47 +00:00
public ConcurrentDictionary < Guid , Segment > Intros { get ; } = new ( ) ;
2022-05-09 22:56:03 -05:00
/// <summary>
2022-11-24 00:43:23 -06:00
/// Gets all discovered ending credits.
2022-05-09 22:56:03 -05:00
/// </summary>
2024-09-12 08:37:47 +00:00
public ConcurrentDictionary < Guid , Segment > Credits { get ; } = new ( ) ;
2022-11-24 00:43:23 -06:00
2022-05-09 22:56:03 -05:00
/// <summary>
2022-11-23 02:34:28 -06:00
/// Gets the most recent media item queue.
2022-05-09 22:56:03 -05:00
/// </summary>
2024-05-08 16:27:16 +02:00
public ConcurrentDictionary < Guid , List < QueuedEpisode > > QueuedMediaItems { get ; } = new ( ) ;
2022-05-09 22:56:03 -05:00
2024-06-15 13:16:47 +02:00
/// <summary>
/// Gets all episode states.
/// </summary>
public ConcurrentDictionary < Guid , EpisodeState > EpisodeStates { get ; } = new ( ) ;
2024-09-20 14:18:04 +03:00
/// <summary>
/// Gets the ignore list.
/// </summary>
public ConcurrentDictionary < Guid , IgnoreListItem > IgnoreList { get ; } = new ( ) ;
2022-05-09 22:56:03 -05:00
/// <summary>
/// Gets or sets the total number of episodes in the queue.
/// </summary>
public int TotalQueued { get ; set ; }
2023-06-08 00:51:18 -05:00
/// <summary>
/// Gets or sets the number of seasons in the queue.
/// </summary>
public int TotalSeasons { get ; set ; }
2022-05-09 22:56:03 -05:00
/// <summary>
/// Gets the directory to cache fingerprints in.
/// </summary>
public string FingerprintCachePath { get ; private set ; }
2022-06-09 14:07:40 -05:00
/// <summary>
/// Gets the full path to FFmpeg.
/// </summary>
public string FFmpegPath { get ; private set ; }
2022-05-09 22:56:03 -05:00
/// <inheritdoc />
public override string Name = > "Intro Skipper" ;
/// <inheritdoc />
public override Guid Id = > Guid . Parse ( "c83d86bb-a1e0-4c35-a113-e2101cf4ee6b" ) ;
/// <summary>
/// Gets the plugin instance.
/// </summary>
public static Plugin ? Instance { get ; private set ; }
2022-05-05 18:10:34 -05:00
/// <summary>
/// Save timestamps to disk.
/// </summary>
2024-05-08 16:27:16 +02:00
/// <param name="mode">Mode.</param>
2024-10-16 16:05:59 +02:00
public void SaveTimestamps ( AnalysisMode mode )
2022-05-05 18:10:34 -05:00
{
2024-09-12 08:37:47 +00:00
List < Segment > introList = [ ] ;
2024-10-16 16:05:59 +02:00
var filePath = mode = = AnalysisMode . Introduction
2024-05-08 16:27:16 +02:00
? _introPath
: _creditsPath ;
2022-11-24 00:43:23 -06:00
2024-05-08 16:27:16 +02:00
lock ( _introsLock )
{
2024-10-16 16:05:59 +02:00
introList . AddRange ( mode = = AnalysisMode . Introduction
2024-05-08 16:27:16 +02:00
? Instance ! . Intros . Values
: Instance ! . Credits . Values ) ;
2022-07-17 01:54:05 -05:00
}
2022-05-05 18:10:34 -05:00
2024-04-20 16:21:20 +02:00
lock ( _serializationLock )
{
2024-10-09 19:03:17 +02:00
try
2024-04-20 16:21:20 +02:00
{
2024-05-08 16:27:16 +02:00
XmlSerializationHelper . SerializeToXml ( introList , filePath ) ;
2024-04-20 16:21:20 +02:00
}
2024-05-08 16:27:16 +02:00
catch ( Exception e )
2024-04-20 16:21:20 +02:00
{
2024-05-08 16:27:16 +02:00
_logger . LogError ( "SaveTimestamps {Message}" , e . Message ) ;
2024-04-20 16:21:20 +02:00
}
}
}
2024-09-20 14:18:04 +03:00
/// <summary>
/// Save IgnoreList to disk.
/// </summary>
public void SaveIgnoreList ( )
{
var ignorelist = Instance ! . IgnoreList . Values . ToList ( ) ;
lock ( _serializationLock )
{
try
{
XmlSerializationHelper . SerializeToXml ( ignorelist , _ignorelistPath ) ;
}
catch ( Exception e )
{
_logger . LogError ( "SaveIgnoreList {Message}" , e . Message ) ;
}
}
}
/// <summary>
/// Check if an item is ignored.
/// </summary>
/// <param name="id">Item id.</param>
/// <param name="mode">Mode.</param>
/// <returns>True if ignored, false otherwise.</returns>
2024-10-16 16:05:59 +02:00
public bool IsIgnored ( Guid id , AnalysisMode mode )
2024-09-20 14:18:04 +03:00
{
return Instance ! . IgnoreList . TryGetValue ( id , out var item ) & & item . IsIgnored ( mode ) ;
}
/// <summary>
/// Load IgnoreList from disk.
/// </summary>
public void LoadIgnoreList ( )
{
if ( File . Exists ( _ignorelistPath ) )
{
var ignorelist = XmlSerializationHelper . DeserializeFromXml < IgnoreListItem > ( _ignorelistPath ) ;
foreach ( var item in ignorelist )
{
Instance ! . IgnoreList . TryAdd ( item . SeasonId , item ) ;
}
}
}
2022-05-05 18:10:34 -05:00
/// <summary>
/// Restore previous analysis results from disk.
/// </summary>
public void RestoreTimestamps ( )
{
2022-11-24 00:43:23 -06:00
if ( File . Exists ( _introPath ) )
2022-05-05 18:10:34 -05:00
{
2022-11-24 00:43:23 -06:00
// Since dictionaries can't be easily serialized, analysis results are stored on disk as a list.
2024-09-20 14:18:04 +03:00
var introList = XmlSerializationHelper . DeserializeFromXml < Segment > ( _introPath ) ;
2022-05-05 18:10:34 -05:00
2022-11-24 00:43:23 -06:00
foreach ( var intro in introList )
{
2024-05-08 16:27:16 +02:00
Instance ! . Intros . TryAdd ( intro . EpisodeId , intro ) ;
2022-11-24 00:43:23 -06:00
}
}
2022-05-05 18:10:34 -05:00
2022-11-24 00:43:23 -06:00
if ( File . Exists ( _creditsPath ) )
2022-05-05 18:10:34 -05:00
{
2024-09-20 14:18:04 +03:00
var creditList = XmlSerializationHelper . DeserializeFromXml < Segment > ( _creditsPath ) ;
2022-11-24 00:43:23 -06:00
foreach ( var credit in creditList )
{
2024-05-08 16:27:16 +02:00
Instance ! . Credits . TryAdd ( credit . EpisodeId , credit ) ;
2022-11-24 00:43:23 -06:00
}
2022-05-05 18:10:34 -05:00
}
}
2022-11-06 21:20:52 -06:00
/// <inheritdoc />
public IEnumerable < PluginPageInfo > GetPages ( )
{
2024-09-10 18:08:42 +02:00
return
[
2022-11-06 21:20:52 -06:00
new PluginPageInfo
{
2024-09-10 18:08:42 +02:00
Name = Name ,
2022-11-06 21:20:52 -06:00
EmbeddedResourcePath = GetType ( ) . Namespace + ".Configuration.configPage.html"
} ,
new PluginPageInfo
{
Name = "visualizer.js" ,
EmbeddedResourcePath = GetType ( ) . Namespace + ".Configuration.visualizer.js"
} ,
new PluginPageInfo
{
Name = "skip-intro-button.js" ,
EmbeddedResourcePath = GetType ( ) . Namespace + ".Configuration.inject.js"
}
2024-09-10 18:08:42 +02:00
] ;
2022-11-06 21:20:52 -06:00
}
2024-06-15 13:16:47 +02:00
/// <summary>
/// Gets the Intro for this item.
/// </summary>
/// <param name="id">Item id.</param>
/// <param name="mode">Mode.</param>
/// <returns>Intro.</returns>
2024-10-16 16:05:59 +02:00
internal static Segment GetIntroByMode ( Guid id , AnalysisMode mode )
2024-06-15 13:16:47 +02:00
{
2024-10-16 16:05:59 +02:00
return mode = = AnalysisMode . Introduction
2024-06-15 13:16:47 +02:00
? Instance ! . Intros [ id ]
: Instance ! . Credits [ id ] ;
}
2024-05-01 13:45:57 +02:00
internal BaseItem ? GetItem ( Guid id )
2022-07-29 03:34:55 -05:00
{
2024-10-05 19:30:30 +02:00
return id ! = Guid . Empty ? _libraryManager . GetItemById ( id ) : null ;
2022-07-29 03:34:55 -05:00
}
2024-10-05 19:30:30 +02:00
internal IReadOnlyList < Folder > GetCollectionFolders ( Guid id )
2024-09-21 20:12:00 +02:00
{
var item = GetItem ( id ) ;
return item is not null ? _libraryManager . GetCollectionFolders ( item ) : [ ] ;
}
2022-06-15 01:00:03 -05:00
/// <summary>
/// Gets the full path for an item.
/// </summary>
/// <param name="id">Item id.</param>
/// <returns>Full path to item.</returns>
internal string GetItemPath ( Guid id )
{
2024-05-01 13:45:57 +02:00
var item = GetItem ( id ) ;
if ( item = = null )
{
// Handle the case where the item is not found
_logger . LogWarning ( "Item with ID {Id} not found." , id ) ;
return string . Empty ;
}
return item . Path ;
2022-06-15 01:00:03 -05:00
}
2022-11-24 00:43:23 -06:00
/// <summary>
/// Gets all chapters for this item.
/// </summary>
/// <param name="id">Item id.</param>
/// <returns>List of chapters.</returns>
2024-10-09 18:33:41 +02:00
internal IReadOnlyList < ChapterInfo > GetChapters ( Guid id )
2022-10-28 02:25:57 -05:00
{
2024-05-01 13:45:57 +02:00
var item = GetItem ( id ) ;
if ( item = = null )
{
// Handle the case where the item is not found
_logger . LogWarning ( "Item with ID {Id} not found." , id ) ;
2024-09-10 18:08:42 +02:00
return [ ] ;
2024-05-01 13:45:57 +02:00
}
return _itemRepository . GetChapters ( item ) ;
2022-11-24 00:43:23 -06:00
}
2024-06-15 13:16:47 +02:00
/// <summary>
/// Gets the state for this item.
/// </summary>
/// <param name="id">Item ID.</param>
/// <returns>State of this item.</returns>
internal EpisodeState GetState ( Guid id ) = > EpisodeStates . GetOrAdd ( id , _ = > new EpisodeState ( ) ) ;
2024-10-16 16:05:59 +02:00
internal void UpdateTimestamps ( IReadOnlyDictionary < Guid , Segment > newTimestamps , AnalysisMode mode )
2022-10-28 02:25:57 -05:00
{
2024-05-08 16:27:16 +02:00
foreach ( var intro in newTimestamps )
2022-10-28 02:25:57 -05:00
{
2024-10-16 16:05:59 +02:00
if ( mode = = AnalysisMode . Introduction )
2022-10-28 02:25:57 -05:00
{
2024-05-08 16:27:16 +02:00
Instance ! . Intros . AddOrUpdate ( intro . Key , intro . Value , ( key , oldValue ) = > intro . Value ) ;
}
2024-10-16 16:05:59 +02:00
else if ( mode = = AnalysisMode . Credits )
2024-05-08 16:27:16 +02:00
{
2024-10-09 19:03:17 +02:00
Instance ! . Credits . AddOrUpdate ( intro . Key , intro . Value , ( key , oldValue ) = > intro . Value ) ;
2022-10-28 02:25:57 -05:00
}
}
2024-05-08 16:27:16 +02:00
SaveTimestamps ( mode ) ;
2022-10-28 02:25:57 -05:00
}
2024-06-15 10:57:20 +02:00
internal void CleanTimestamps ( HashSet < Guid > validEpisodeIds )
{
var allKeys = new HashSet < Guid > ( Instance ! . Intros . Keys ) ;
allKeys . UnionWith ( Instance ! . Credits . Keys ) ;
foreach ( var key in allKeys )
{
if ( ! validEpisodeIds . Contains ( key ) )
{
Instance ! . Intros . TryRemove ( key , out _ ) ;
Instance ! . Credits . TryRemove ( key , out _ ) ;
}
}
2024-10-16 16:05:59 +02:00
SaveTimestamps ( AnalysisMode . Introduction ) ;
SaveTimestamps ( AnalysisMode . Credits ) ;
2024-06-15 10:57:20 +02:00
}
2024-10-12 16:13:37 +02:00
private void MigrateRepoUrl ( IServerConfigurationManager serverConfiguration )
{
try
{
List < string > oldRepos =
[
"https://raw.githubusercontent.com/intro-skipper/intro-skipper/master/manifest.json" ,
2024-10-31 11:58:56 +01:00
"https://raw.githubusercontent.com/jumoog/intro-skipper/master/manifest.json" ,
"https://manifest.intro-skipper.workers.dev/manifest.json"
2024-10-12 16:13:37 +02:00
] ;
// Access the current server configuration
var config = serverConfiguration . Configuration ;
// Get the list of current plugin repositories
2024-10-27 22:38:46 +01:00
var pluginRepositories = config . PluginRepositories . ToList ( ) ;
2024-10-12 16:13:37 +02:00
// check if old plugins exits
2024-10-27 22:38:46 +01:00
if ( pluginRepositories . Exists ( repo = > repo . Url ! = null & & oldRepos . Contains ( repo . Url ) ) )
2024-10-12 16:13:37 +02:00
{
// remove all old plugins
2024-10-27 22:38:46 +01:00
pluginRepositories . RemoveAll ( repo = > repo . Url ! = null & & oldRepos . Contains ( repo . Url ) ) ;
2024-10-12 16:13:37 +02:00
2024-10-27 22:38:46 +01:00
// Add repository only if it does not exit and the OverideManifestUrl Option is activated
if ( ! pluginRepositories . Exists ( repo = > repo . Url = = "https://manifest.intro-skipper.org/manifest.json" ) & & Instance ! . Configuration . OverrideManifestUrl )
2024-10-12 16:13:37 +02:00
{
// Add the new repository to the list
pluginRepositories . Add ( new RepositoryInfo
{
Name = "intro skipper (automatically migrated by plugin)" ,
2024-10-24 22:48:12 +02:00
Url = "https://manifest.intro-skipper.org/manifest.json" ,
2024-10-12 16:13:37 +02:00
Enabled = true ,
} ) ;
}
// Update the configuration with the new repository list
config . PluginRepositories = [ . . pluginRepositories ] ;
// Save the updated configuration
serverConfiguration . SaveConfiguration ( ) ;
}
}
catch ( Exception ex )
{
_logger . LogError ( ex , "Error occurred while migrating repo URL" ) ;
}
}
2022-11-06 21:20:52 -06:00
/// <summary>
/// Inject the skip button script into the web interface.
/// </summary>
2024-08-31 17:08:42 +00:00
/// <param name="webPath">Full path to index.html.</param>
private void InjectSkipButton ( string webPath )
2022-06-14 14:36:05 -05:00
{
2024-10-05 18:38:42 +02:00
string searchPattern = "dashboard-dashboard.*.chunk.js" ;
2024-08-31 17:08:42 +00:00
string [ ] filePaths = Directory . GetFiles ( webPath , searchPattern , SearchOption . TopDirectoryOnly ) ;
2024-10-05 18:38:42 +02:00
string pattern = @"buildVersion""\)\.innerText=""(?<buildVersion>\d+\.\d+\.\d+)"",.*?webVersion""\)\.innerText=""(?<webVersion>\d+\.\d+\.\d+)" ;
2024-10-27 22:38:46 +01:00
string webVersionString = "unknown" ;
2024-10-05 18:38:42 +02:00
// Create a Regex object
Regex regex = new Regex ( pattern ) ;
// should be only one file but this safer
foreach ( var file in filePaths )
{
string dashBoardText = File . ReadAllText ( file ) ;
// Perform the match
Match match = regex . Match ( dashBoardText ) ;
// search for buildVersion and webVersion
if ( match . Success )
{
webVersionString = match . Groups [ "webVersion" ] . Value ;
_logger . LogInformation ( "Found jellyfin-web <{WebVersion}>" , webVersionString ) ;
break ;
}
}
2024-10-27 22:38:46 +01:00
if ( webVersionString ! = "unknown" )
2024-10-05 18:38:42 +02:00
{
// append Revision
webVersionString + = ".0" ;
if ( Version . TryParse ( webVersionString , out var webversion ) )
{
if ( _applicationHost . ApplicationVersion ! = webversion )
{
_logger . LogWarning ( "The jellyfin-web <{WebVersion}> NOT compatible with Jellyfin <{JellyfinVersion}>" , webVersionString , _applicationHost . ApplicationVersion ) ;
}
else
{
_logger . LogInformation ( "The jellyfin-web <{WebVersion}> compatible with Jellyfin <{JellyfinVersion}>" , webVersionString , _applicationHost . ApplicationVersion ) ;
}
}
}
2024-08-31 17:08:42 +00:00
// Inject the skip intro button code into the web interface.
string indexPath = Path . Join ( webPath , "index.html" ) ;
2022-11-06 21:20:52 -06:00
// Parts of this code are based off of JellyScrub's script injection code.
2024-03-03 21:46:52 -05:00
// https://github.com/nicknsy/jellyscrub/blob/main/Nick.Plugin.Jellyscrub/JellyscrubPlugin.cs#L38
2022-11-06 21:20:52 -06:00
_logger . LogDebug ( "Reading index.html from {Path}" , indexPath ) ;
2024-08-31 17:08:42 +00:00
string contents = File . ReadAllText ( indexPath ) ;
2022-11-06 21:20:52 -06:00
2024-10-27 05:08:22 -04:00
if ( ! Instance ! . Configuration . SkipButtonEnabled )
2024-10-12 06:35:46 -04:00
{
pattern = @"<script src=""configurationpage\?name=skip-intro-button\.js.*<\/script>" ;
2024-10-27 22:38:46 +01:00
if ( ! Regex . IsMatch ( contents , pattern , RegexOptions . IgnoreCase ) )
{
return ;
}
2024-10-12 06:35:46 -04:00
contents = Regex . Replace ( contents , pattern , string . Empty , RegexOptions . IgnoreCase ) ;
File . WriteAllText ( indexPath , contents ) ;
return ; // Button is disabled, so remove and abort
}
2024-10-27 22:38:46 +01:00
// change URL with every release to prevent the Browsers from caching
2024-08-31 17:08:42 +00:00
string scriptTag = "<script src=\"configurationpage?name=skip-intro-button.js&release=" + GetType ( ) . Assembly . GetName ( ) . Version + "\"></script>" ;
2022-11-06 21:20:52 -06:00
// Only inject the script tag once
if ( contents . Contains ( scriptTag , StringComparison . OrdinalIgnoreCase ) )
{
2024-10-05 13:17:30 +02:00
_logger . LogInformation ( "The skip button has already been injected." ) ;
2022-11-06 21:20:52 -06:00
return ;
}
2024-07-30 21:22:09 +02:00
// remove old version if necessary
2024-10-05 18:38:42 +02:00
pattern = @"<script src=""configurationpage\?name=skip-intro-button\.js.*<\/script>" ;
2024-07-30 21:22:09 +02:00
contents = Regex . Replace ( contents , pattern , string . Empty , RegexOptions . IgnoreCase ) ;
2022-11-06 21:20:52 -06:00
// Inject a link to the script at the end of the <head> section.
// A regex is used here to ensure the replacement is only done once.
2024-09-25 17:23:25 +02:00
Regex headEnd = new Regex ( @"</head>" , RegexOptions . IgnoreCase , TimeSpan . FromSeconds ( 1 ) ) ;
2022-11-06 21:20:52 -06:00
contents = headEnd . Replace ( contents , scriptTag + "</head>" , 1 ) ;
// Write the modified file contents
File . WriteAllText ( indexPath , contents ) ;
2024-10-05 13:17:30 +02:00
_logger . LogInformation ( "Skip button added successfully." ) ;
2022-06-14 14:36:05 -05:00
}
2019-03-10 08:53:30 +09:00
}