11using System ;
22using System . Diagnostics . CodeAnalysis ;
3+ using System . IO ;
34using System . Threading ;
45using System . Threading . Tasks ;
56using Hangfire ;
67using Hangfire . Console ;
78using Hangfire . Server ;
89using Humanizer ;
10+ using Microsoft . AspNetCore . Hosting ;
911using Microsoft . Extensions . Hosting ;
1012using Microsoft . Extensions . Options ;
1113using StardewModdingAPI . Toolkit ;
1719using StardewModdingAPI . Web . Framework . Caching ;
1820using StardewModdingAPI . Web . Framework . Caching . CompatibilityRepo ;
1921using StardewModdingAPI . Web . Framework . Caching . CurseForgeExport ;
22+ using StardewModdingAPI . Web . Framework . Caching . ModDataset ;
2023using StardewModdingAPI . Web . Framework . Caching . ModDropExport ;
2124using StardewModdingAPI . Web . Framework . Caching . Mods ;
2225using StardewModdingAPI . Web . Framework . Caching . NexusExport ;
@@ -64,6 +67,12 @@ internal class BackgroundService : IHostedService, IDisposable
6467 /// <summary>The config settings for mod update checks.</summary>
6568 private static IOptions < ModUpdateCheckConfig > ? UpdateCheckConfig ;
6669
70+ /// <summary>The mod dataset repository.</summary>
71+ private static IModDatasetRepository ? ModDatasetRepo ;
72+
73+ /// <summary>The web root path for writing static data files.</summary>
74+ private static string ? WebRootPath ;
75+
6776 /// <summary>Whether the service has been started.</summary>
6877 [ MemberNotNullWhen ( true ,
6978 nameof ( BackgroundService . JobServer ) ,
@@ -75,7 +84,9 @@ internal class BackgroundService : IHostedService, IDisposable
7584 nameof ( BackgroundService . ModDropExportCache ) ,
7685 nameof ( BackgroundService . NexusExportApiClient ) ,
7786 nameof ( BackgroundService . NexusExportCache ) ,
78- nameof ( BackgroundService . UpdateCheckConfig )
87+ nameof ( BackgroundService . ModDatasetRepo ) ,
88+ nameof ( BackgroundService . UpdateCheckConfig ) ,
89+ nameof ( BackgroundService . WebRootPath )
7990 ) ]
8091 private static bool IsStarted { get ; set ; }
8192
@@ -90,16 +101,18 @@ internal class BackgroundService : IHostedService, IDisposable
90101 ** Hosted service
91102 ****/
92103 /// <summary>Construct an instance.</summary>
93- /// <param name="compatibilityCache">The cache in which to store compatibility list data.</param>
94- /// <param name="modCache">The cache in which to store mod data.</param>
95- /// <param name="curseForgeExportCache">The cache in which to store mod data from the CurseForge export API.</param>
96- /// <param name="curseForgeExportApiClient">The HTTP client for fetching the mod export from the CurseForge export API.</param>
97- /// /// <param name="modDropExportCache">The cache in which to store mod data from the ModDrop export API.</param>
98- /// <param name="modDropExportApiClient">The HTTP client for fetching the mod export from the ModDrop export API.</param>
99- /// <param name="nexusExportCache">The cache in which to store mod data from the Nexus export API.</param>
100- /// <param name="nexusExportApiClient">The HTTP client for fetching the mod export from the Nexus Mods export API.</param>
104+ /// <param name="compatibilityCache"><inheritdoc cref="CompatibilityCache" path="/summary"/></param>
105+ /// <param name="modCache"><inheritdoc cref="ModCache" path="/summary"/></param>
106+ /// <param name="curseForgeExportCache"><inheritdoc cref="CurseForgeExportCache" path="/summary"/></param>
107+ /// <param name="curseForgeExportApiClient"><inheritdoc cref="CurseForgeExportApiClient" path="/summary"/></param>
108+ /// <param name="modDropExportCache"><inheritdoc cref="ModDropExportCache" path="/summary"/></param>
109+ /// <param name="modDropExportApiClient"><inheritdoc cref="ModDropExportApiClient" path="/summary"/></param>
110+ /// <param name="nexusExportCache"><inheritdoc cref="NexusExportCache" path="/summary"/></param>
111+ /// <param name="nexusExportApiClient"><inheritdoc cref="NexusExportApiClient" path="/summary"/></param>
112+ /// <param name="modDatasetRepo"><inheritdoc cref="ModDatasetRepo" path="/summary"/></param>
101113 /// <param name="hangfireStorage">The Hangfire storage implementation.</param>
102- /// <param name="updateCheckConfig">The config settings for mod update checks.</param>
114+ /// <param name="updateCheckConfig"><inheritdoc cref="UpdateCheckConfig" path="/summary"/></param>
115+ /// <param name="hostEnvironment">The web host environment metadata.</param>
103116 [ SuppressMessage ( "ReSharper" , "UnusedParameter.Local" , Justification = "The Hangfire reference forces it to initialize first, since it's needed by the background service." ) ]
104117 public BackgroundService (
105118 ICompatibilityCacheRepository compatibilityCache ,
@@ -110,8 +123,10 @@ public BackgroundService(
110123 IModDropExportApiClient modDropExportApiClient ,
111124 INexusExportCacheRepository nexusExportCache ,
112125 INexusExportApiClient nexusExportApiClient ,
126+ IModDatasetRepository modDatasetRepo ,
113127 JobStorage hangfireStorage ,
114- IOptions < ModUpdateCheckConfig > updateCheckConfig
128+ IOptions < ModUpdateCheckConfig > updateCheckConfig ,
129+ IWebHostEnvironment hostEnvironment
115130 )
116131 {
117132 BackgroundService . CompatibilityCache = compatibilityCache ;
@@ -122,7 +137,9 @@ IOptions<ModUpdateCheckConfig> updateCheckConfig
122137 BackgroundService . ModDropExportCache = modDropExportCache ;
123138 BackgroundService . NexusExportCache = nexusExportCache ;
124139 BackgroundService . NexusExportApiClient = nexusExportApiClient ;
140+ BackgroundService . ModDatasetRepo = modDatasetRepo ;
125141 BackgroundService . UpdateCheckConfig = updateCheckConfig ;
142+ BackgroundService . WebRootPath = hostEnvironment . WebRootPath ;
126143
127144 _ = hangfireStorage ; // parameter is only received to initialize it before the background service
128145 }
@@ -146,6 +163,7 @@ public Task StartAsync(CancellationToken cancellationToken)
146163 if ( enableNexusExport )
147164 BackgroundJob . Enqueue ( ( ) => BackgroundService . UpdateNexusExportAsync ( null ) ) ;
148165 BackgroundJob . Enqueue ( ( ) => BackgroundService . RemoveStaleModsAsync ( ) ) ;
166+ BackgroundJob . Enqueue ( ( ) => BackgroundService . UpdateModDatasetAsync ( null ) ) ;
149167
150168 // set recurring tasks
151169 RecurringJob . AddOrUpdate ( "update compatibility list" , ( ) => BackgroundService . UpdateCompatibilityListAsync ( null ) , "*/10 * * * *" ) ; // every 10 minutes
@@ -156,6 +174,7 @@ public Task StartAsync(CancellationToken cancellationToken)
156174 if ( enableNexusExport )
157175 RecurringJob . AddOrUpdate ( "update Nexus export" , ( ) => BackgroundService . UpdateNexusExportAsync ( null ) , "*/10 * * * *" ) ;
158176 RecurringJob . AddOrUpdate ( "remove stale mods" , ( ) => BackgroundService . RemoveStaleModsAsync ( ) , "2/10 * * * *" ) ; // offset by 2 minutes so it runs after updates (e.g. 00:02, 00:12, etc)
177+ RecurringJob . AddOrUpdate ( "update mod dataset" , ( ) => BackgroundService . UpdateModDatasetAsync ( null ) , "0 * * * *" ) ; // hourly
159178
160179 BackgroundService . IsStarted = true ;
161180
@@ -242,6 +261,30 @@ await UpdateExportAsync(
242261 ) ;
243262 }
244263
264+ /// <summary>Clone or pull the Stardew mod dataset repo, then copy the latest data files into the web root for use by frontend scripts.</summary>
265+ /// <param name="context">Information about the context in which the job is performed. This is injected automatically by Hangfire.</param>
266+ [ AutomaticRetry ( Attempts = 3 , DelaysInSeconds = [ 30 , 60 , 120 ] ) ]
267+ public static async Task UpdateModDatasetAsync ( PerformContext ? context )
268+ {
269+ if ( ! BackgroundService . IsStarted )
270+ throw new InvalidOperationException ( $ "Must call { nameof ( BackgroundService . StartAsync ) } before scheduling tasks.") ;
271+
272+ // update repo
273+ context . WriteLine ( "Updating mod dataset repo..." ) ;
274+ await BackgroundService . ModDatasetRepo . UpdateAsync ( context . WriteLine ) ;
275+
276+ // copy files
277+ context . WriteLine ( "Copying data files for script use..." ) ;
278+ string copyToPath = Path . Combine ( BackgroundService . WebRootPath , "Content" , "data" , "mods-by-type.jsonl" ) ;
279+ Directory . CreateDirectory ( Path . GetDirectoryName ( copyToPath ) ! ) ;
280+ File . Copy ( BackgroundService . ModDatasetRepo . GetFilePath ( "stats/mods by type.jsonl" ) , copyToPath , overwrite : true ) ;
281+ File . Copy ( BackgroundService . ModDatasetRepo . GetFilePath ( "stats/Content Patcher packs by format version.jsonl" ) , Path . Combine ( BackgroundService . WebRootPath , "Content" , "data" , "content-packs-by-format.jsonl" ) , overwrite : true ) ;
282+ File . Copy ( BackgroundService . ModDatasetRepo . GetFilePath ( "reference-data/SMAPI costs.jsonl" ) , Path . Combine ( BackgroundService . WebRootPath , "Content" , "data" , "smapi-costs.jsonl" ) , overwrite : true ) ;
283+ File . Copy ( BackgroundService . ModDatasetRepo . GetFilePath ( "reference-data/SMAPI DNS queries.json" ) , Path . Combine ( BackgroundService . WebRootPath , "Content" , "data" , "smapi-dns-queries.json" ) , overwrite : true ) ;
284+
285+ context . WriteLine ( "Done!" ) ;
286+ }
287+
245288 /// <summary>Remove mods which haven't been requested in over 48 hours.</summary>
246289 public static Task RemoveStaleModsAsync ( )
247290 {
0 commit comments