Add ProgressState field for precise EPUB location tracking

Adds a generic string-based ProgressState field to support complex
media position tracking that cannot be represented as simple ticks.
Primary use case is EPUB CFI (Canonical Fragment Identifier) strings
for precise ebook location resumption.

Changes:
- Add ProgressState to UserData database entity
- Add EF Core migration for ProgressState column
- Add ProgressState to UserItemData, UserItemDataDto, UpdateUserItemDataDto
- Add ProgressState to PlaybackProgressInfo
- Update UserDataManager to map and save ProgressState
- Update SessionManager.OnPlaybackProgress to persist ProgressState
This commit is contained in:
bluesam1 2025-11-25 16:58:09 -06:00
parent 21042ad0c2
commit 72aeb28348
9 changed files with 1824 additions and 1 deletions

View file

@ -136,6 +136,11 @@ namespace Emby.Server.Implementations.Library
userData.Rating = userDataDto.Rating.Value;
}
if (userDataDto.ProgressState is not null)
{
userData.ProgressState = userDataDto.ProgressState;
}
SaveUserData(user, item, userData, reason, CancellationToken.None);
}
@ -157,6 +162,7 @@ namespace Emby.Server.Implementations.Library
Rating = dto.Rating,
UserId = userId,
SubtitleStreamIndex = dto.SubtitleStreamIndex,
ProgressState = dto.ProgressState,
};
}
@ -174,6 +180,7 @@ namespace Emby.Server.Implementations.Library
Played = dto.Played,
Rating = dto.Rating,
SubtitleStreamIndex = dto.SubtitleStreamIndex,
ProgressState = dto.ProgressState,
};
}
@ -284,7 +291,8 @@ namespace Emby.Server.Implementations.Library
Played = data.Played,
LastPlayedDate = data.LastPlayedDate,
ItemId = itemId,
Key = data.Key
Key = data.Key,
ProgressState = data.ProgressState
};
}

View file

@ -949,6 +949,13 @@ namespace Emby.Server.Implementations.Session
changed = true;
}
// Handle ProgressState for complex media types (e.g., EPUB CFI)
if (!string.IsNullOrEmpty(info.ProgressState))
{
data.ProgressState = info.ProgressState;
changed = true;
}
var tracksChanged = UpdatePlaybackSettings(user, info, data);
if (!tracksChanged)
{

View file

@ -87,6 +87,17 @@ namespace MediaBrowser.Controller.Entities
/// <value>The index of the subtitle stream.</value>
public int? SubtitleStreamIndex { get; set; }
/// <summary>
/// Gets or sets the generic progress state for complex media types.
/// </summary>
/// <remarks>
/// This field stores position information that cannot be represented as simple ticks,
/// such as EPUB CFI (Canonical Fragment Identifier) strings for precise ebook locations.
/// This field is null for standard video/audio content where <see cref="PlaybackPositionTicks"/> is sufficient.
/// </remarks>
/// <value>The progress state string, or null if not applicable.</value>
public string? ProgressState { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the item is liked or not.
/// This should never be serialized.

View file

@ -72,5 +72,15 @@ namespace MediaBrowser.Model.Dto
/// </summary>
/// <value>The item identifier.</value>
public string? ItemId { get; set; }
/// <summary>
/// Gets or sets the generic progress state for complex media types.
/// </summary>
/// <remarks>
/// This field stores position information that cannot be represented as simple ticks,
/// such as EPUB CFI (Canonical Fragment Identifier) strings for precise ebook locations.
/// </remarks>
/// <value>The progress state string, or null if not applicable.</value>
public string? ProgressState { get; set; }
}
}

View file

@ -72,5 +72,15 @@ namespace MediaBrowser.Model.Dto
/// </summary>
/// <value>The item identifier.</value>
public Guid ItemId { get; set; }
/// <summary>
/// Gets or sets the generic progress state for complex media types.
/// </summary>
/// <remarks>
/// This field stores position information that cannot be represented as simple ticks,
/// such as EPUB CFI (Canonical Fragment Identifier) strings for precise ebook locations.
/// </remarks>
/// <value>The progress state string, or null if not applicable.</value>
public string? ProgressState { get; set; }
}
}

View file

@ -71,6 +71,17 @@ namespace MediaBrowser.Model.Session
/// <value>The position ticks.</value>
public long? PositionTicks { get; set; }
/// <summary>
/// Gets or sets the generic progress state for complex media types.
/// </summary>
/// <remarks>
/// This field stores position information that cannot be represented as simple ticks,
/// such as EPUB CFI (Canonical Fragment Identifier) strings for precise ebook locations.
/// When set, this takes precedence over <see cref="PositionTicks"/> for media types that support it.
/// </remarks>
/// <value>The progress state string, or null if not applicable.</value>
public string ProgressState { get; set; }
public long? PlaybackStartTimeTicks { get; set; }
/// <summary>

View file

@ -93,4 +93,18 @@ public class UserData
/// Gets or Sets the User.
/// </summary>
public required User? User { get; set; }
/// <summary>
/// Gets or sets the generic progress state for complex media types.
/// </summary>
/// <remarks>
/// This field stores position information that cannot be represented as simple ticks.
/// Examples include:
/// <list type="bullet">
/// <item><description>EPUB CFI (Canonical Fragment Identifier) strings for precise ebook locations</description></item>
/// </list>
/// This field is null for standard video/audio content where <see cref="PlaybackPositionTicks"/> is sufficient.
/// </remarks>
/// <value>The progress state string, or null if not applicable.</value>
public string? ProgressState { get; set; }
}

View file

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Database.Providers.Sqlite.Migrations
{
/// <inheritdoc />
public partial class AddProgressStateToUserData : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ProgressState",
table: "UserData",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ProgressState",
table: "UserData");
}
}
}