diff --git a/SkipMe.Db.Plugin/Services/ShareSubmissionService.cs b/SkipMe.Db.Plugin/Services/ShareSubmissionService.cs index 0a4e48b..2ef410a 100644 --- a/SkipMe.Db.Plugin/Services/ShareSubmissionService.cs +++ b/SkipMe.Db.Plugin/Services/ShareSubmissionService.cs @@ -38,6 +38,7 @@ public sealed class ShareSubmissionService private readonly TvMazeClient _tvMazeClient; private readonly ILogger _logger; private readonly string _introSkipperDbPath; + private readonly string _legacyIntroSkipperDbPath; /// Initializes a new instance of the class. /// Jellyfin library manager. @@ -59,7 +60,9 @@ public ShareSubmissionService( _httpClientFactory = httpClientFactory; _tvMazeClient = tvMazeClient; _logger = logger; - _introSkipperDbPath = Path.Join(applicationPaths.DataPath, "introskipper", "introskipper.db"); + // Intro Skipper keeps the legacy database after migrating, so the v2 file wins when both exist. + _introSkipperDbPath = Path.Join(applicationPaths.DataPath, "introskipper", "introskipper-v2.db"); + _legacyIntroSkipperDbPath = Path.Join(applicationPaths.DataPath, "introskipper", "introskipper.db"); } /// Shares selected movies, episodes, and seasons. @@ -211,7 +214,7 @@ public async Task ShareAsync(ShareSubmitRequest request, Ca /// Counts of shareable timestamps. public IReadOnlyDictionary GetShareableSegmentCountsByItemId() { - if (!File.Exists(_introSkipperDbPath)) + if (ResolveIntroSkipperDbPath() is null) { return new Dictionary(); } @@ -635,45 +638,91 @@ private static SeasonSubmitRequest CopySeasonRequest( }; } + private static SqliteConnection OpenReadOnly(string dbPath) + { + var connection = new SqliteConnection(new SqliteConnectionStringBuilder + { + DataSource = dbPath, + Mode = SqliteOpenMode.ReadOnly, + }.ToString()); + connection.Open(); + return connection; + } + + private static bool HasV2SegmentsSchema(string dbPath) + { + using var connection = OpenReadOnly(dbPath); + using var command = connection.CreateCommand(); + command.CommandText = """ + SELECT COUNT(*) FROM pragma_table_info('Segments') + WHERE name IN ('ItemId', 'Type', 'StartTicks', 'EndTicks', 'Source', 'State') + """; + return (long)(command.ExecuteScalar() ?? 0L) == 6; + } + + private string? ResolveIntroSkipperDbPath() + { + // Fall back to the legacy database while the v2 file is missing, still being + // migrated, or from a schema revision this plugin does not understand. + if (File.Exists(_introSkipperDbPath) && HasV2SegmentsSchema(_introSkipperDbPath)) + { + return _introSkipperDbPath; + } + + return File.Exists(_legacyIntroSkipperDbPath) ? _legacyIntroSkipperDbPath : null; + } + [SuppressMessage("Security", "CA2100:Review SQL queries for security vulnerabilities", Justification = "IN-clause placeholders are generated internally and each value is parameterized.")] private Dictionary> LoadIntroSkipperSegments(List itemIds) { - if (itemIds.Count == 0 || !File.Exists(_introSkipperDbPath)) + var dbPath = ResolveIntroSkipperDbPath(); + if (itemIds.Count == 0 || dbPath is null) { return []; } - var connectionString = new SqliteConnectionStringBuilder - { - DataSource = _introSkipperDbPath, - Mode = SqliteOpenMode.ReadOnly, - }.ToString(); + using var connection = OpenReadOnly(dbPath); - using var connection = new SqliteConnection(connectionString); - connection.Open(); + bool hasIsUserProvided; + bool hasSource; + string selectFields; + string fromClause; + if (dbPath == _introSkipperDbPath) + { + // Intro Skipper v2 stores ticks, records segment-editor entries as + // SegmentSource.User (5), and keeps suppressed segments (State 1). + hasIsUserProvided = true; + hasSource = true; + selectFields = "ItemId, Type, StartTicks / 10000000.0, EndTicks / 10000000.0, Source = 5, Source"; + fromClause = "Segments WHERE State = 0 AND"; + } + else + { + // Support databases from before provenance tracking was added. + using var checkCol = connection.CreateCommand(); + checkCol.CommandText = "SELECT COUNT(*) FROM pragma_table_info('DbSegment') WHERE name = 'IsUserProvided'"; + hasIsUserProvided = (long)(checkCol.ExecuteScalar() ?? 0L) > 0; - // Support databases from before provenance tracking was added. - using var checkCol = connection.CreateCommand(); - checkCol.CommandText = "SELECT COUNT(*) FROM pragma_table_info('DbSegment') WHERE name = 'IsUserProvided'"; - var hasIsUserProvided = (long)(checkCol.ExecuteScalar() ?? 0L) > 0; + using var sourceCheck = connection.CreateCommand(); + sourceCheck.CommandText = "SELECT COUNT(*) FROM pragma_table_info('DbSegment') WHERE name = 'Source'"; + hasSource = (long)(sourceCheck.ExecuteScalar() ?? 0L) > 0; - using var sourceCheck = connection.CreateCommand(); - sourceCheck.CommandText = "SELECT COUNT(*) FROM pragma_table_info('DbSegment') WHERE name = 'Source'"; - var hasSource = (long)(sourceCheck.ExecuteScalar() ?? 0L) > 0; + selectFields = "ItemId, Type, Start, End"; + if (hasIsUserProvided) + { + selectFields += ", IsUserProvided"; + } - // Track each range and its source. - var tracking = new Dictionary>(); + if (hasSource) + { + selectFields += ", Source"; + } - var selectFields = "ItemId, Type, Start, End"; - if (hasIsUserProvided) - { - selectFields += ", IsUserProvided"; + fromClause = "DbSegment WHERE"; } - if (hasSource) - { - selectFields += ", Source"; - } + // Track each range and its source. + var tracking = new Dictionary>(); const int chunkSize = 400; for (var offset = 0; offset < itemIds.Count; offset += chunkSize) @@ -695,8 +744,7 @@ private Dictionary> LoadIntroSki command.CommandText = $""" SELECT {selectFields} - FROM DbSegment - WHERE Type IN (0, 1, 2, 3) + FROM {fromClause} Type IN (0, 1, 2, 3) AND ItemId COLLATE NOCASE IN ({string.Join(",", placeholders)}) """;