diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index fbf438c5..d3506d4d 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -62,24 +62,28 @@ namespace Emby.Server.Implementations.Library var keys = item.GetUserDataKeys(); using var dbContext = _repository.CreateDbContext(); - using var transaction = dbContext.Database.BeginTransaction(); - - foreach (var key in keys) + var executionStrategy = dbContext.Database.CreateExecutionStrategy(); + executionStrategy.Execute(() => { - userData.Key = key; - var userDataEntry = Map(userData, user.Id, item.Id); - if (dbContext.UserData.Any(f => f.ItemId == userDataEntry.ItemId && f.UserId == userDataEntry.UserId && f.CustomDataKey == userDataEntry.CustomDataKey)) - { - dbContext.UserData.Attach(userDataEntry).State = EntityState.Modified; - } - else - { - dbContext.UserData.Add(userDataEntry); - } - } + using var transaction = dbContext.Database.BeginTransaction(); - dbContext.SaveChanges(); - transaction.Commit(); + foreach (var key in keys) + { + userData.Key = key; + var userDataEntry = Map(userData, user.Id, item.Id); + if (dbContext.UserData.Any(f => f.ItemId == userDataEntry.ItemId && f.UserId == userDataEntry.UserId && f.CustomDataKey == userDataEntry.CustomDataKey)) + { + dbContext.UserData.Attach(userDataEntry).State = EntityState.Modified; + } + else + { + dbContext.UserData.Add(userDataEntry); + } + } + + dbContext.SaveChanges(); + transaction.Commit(); + }); var userId = user.InternalId; var cacheKey = GetCacheKey(userId, item.Id); diff --git a/docs/DATABASE_VIEWS.md b/docs/DATABASE_VIEWS.md new file mode 100644 index 00000000..f6bcf459 --- /dev/null +++ b/docs/DATABASE_VIEWS.md @@ -0,0 +1,233 @@ +# PostgreSQL Database Views + +All views live in the `library` schema unless otherwise noted. They are read-only and safe to query from pgAdmin, BI tools, or any reporting/debugging context without risk of affecting the application. + +--- + +## `library."MediaStreamInfos_v"` + +Reassembles the original flat `MediaStreamInfos` table shape that existed before the `SplitMediaStreamInfos` migration split audio- and video-specific columns into dedicated tables. + +**Use when:** you want a single flat result of all stream data without writing the three-way JOIN yourself. + +**Columns of note:** + +| Column | Source | +|---|---| +| `ItemId`, `StreamIndex`, `StreamType`, `Codec`, … | `MediaStreamInfos` (always present) | +| `ChannelLayout`, `Channels`, `SampleRate`, `IsHearingImpaired` | `AudioStreamDetails` — `NULL` for non-audio streams | +| `Height`, `Width`, `AverageFrameRate`, `BitDepth`, `ColorTransfer`, `DvProfile`, … | `VideoStreamDetails` — `NULL` for non-video streams | + +**Example:** + +```sql +-- All audio streams for a specific item +SELECT "StreamIndex", "Codec", "Language", "Channels", "SampleRate", "ChannelLayout" +FROM library."MediaStreamInfos_v" +WHERE "ItemId" = '' + AND "StreamType" = 0 -- 0 = Audio +ORDER BY "StreamIndex"; +``` + +--- + +## `library."VideoItems_v"` + +Every non-folder, non-virtual item that has at least one video stream (movies, episodes, home videos, etc.), joined with the primary video stream's technical details. + +**Use when:** auditing codec/resolution/HDR distribution across the library, or finding items that need re-encoding. + +**Columns of note:** + +| Column | Description | +|---|---| +| `Type` | Fully-qualified Jellyfin type name (e.g. `MediaBrowser.Controller.Entities.Movies.Movie`) | +| `VideoCodec` | Codec of the primary video stream (e.g. `hevc`, `h264`) | +| `Width` / `Height` | Resolution of the primary video stream | +| `BitDepth` | Colour bit depth (8, 10, 12) | +| `HDRFormat` | Derived string: `Dolby Vision`, `HDR10+`, `HDR10`, `HLG`, or `SDR` | +| `IsDolbyVision` | Boolean shortcut for `DvProfile IS NOT NULL` | +| `AudioTrackCount` | Number of audio streams on the item | +| `SubtitleTrackCount` | Number of subtitle streams on the item | + +**Example queries:** + +```sql +-- All 4K HDR movies +SELECT "Name", "ProductionYear", "VideoCodec", "Width", "Height", "HDRFormat", "TotalBitrate" +FROM library."VideoItems_v" +WHERE "Type" LIKE '%Movie%' + AND "Height" >= 2160 + AND "HDRFormat" <> 'SDR' +ORDER BY "Name"; + +-- Episodes still encoded in H.264 (candidates for HEVC re-encode) +SELECT "SeriesName", "SeasonName", "EpisodeNumber", "Name", "Width", "Height" +FROM library."VideoItems_v" +WHERE "Type" LIKE '%Episode%' + AND "VideoCodec" = 'h264' +ORDER BY "SeriesName", "SeasonNumber", "EpisodeNumber"; +``` + +--- + +## `library."UserPlaybackHistory_v"` + +Per-user watch history joined with item metadata and a calculated playback progress percentage. + +**Use when:** reporting on viewing activity, finding in-progress items, or checking which users have watched what. + +**Columns of note:** + +| Column | Description | +|---|---| +| `Username` | Jellyfin username | +| `ItemName` | Name of the item | +| `ItemType` | Jellyfin type name | +| `Played` | `true` if the item has been marked as played | +| `PlayCount` | Number of times fully played | +| `LastPlayedDate` | Timestamp of the most recent playback | +| `PlaybackPositionTicks` | Raw position (divide by `10,000,000` for seconds) | +| `ProgressPct` | Calculated percentage through the item (0–100), `NULL` if no runtime | +| `AudioStreamIndex` | Last-used audio track index | +| `SubtitleStreamIndex` | Last-used subtitle track index | + +**Example queries:** + +```sql +-- All in-progress items for a user (started but not finished) +SELECT "ItemName", "ItemType", "SeriesName", "ProgressPct", "LastPlayedDate" +FROM library."UserPlaybackHistory_v" +WHERE "Username" = 'alice' + AND "Played" = false + AND "ProgressPct" > 0 +ORDER BY "LastPlayedDate" DESC; + +-- Top 10 most-played items across all users +SELECT "ItemName", "ItemType", SUM("PlayCount") AS "TotalPlays" +FROM library."UserPlaybackHistory_v" +GROUP BY "ItemName", "ItemType" +ORDER BY "TotalPlays" DESC +LIMIT 10; +``` + +--- + +## `library."ItemProviders_v"` + +Items with their external provider IDs (IMDb, TMDB, TVDB, etc.) pivoted into named columns. Providers not explicitly named appear in a JSONB `OtherProviders` column. + +**Use when:** cross-referencing Jellyfin items against external databases, finding items with missing metadata IDs, or bulk-exporting IDs for external tools. + +**Columns of note:** + +| Column | Description | +|---|---| +| `ImdbId` | IMDb identifier (e.g. `tt0111161`) | +| `TmdbId` | The Movie Database ID | +| `TvdbId` | TheTVDB series/episode ID | +| `TvRageId` | TVRage ID (legacy) | +| `MusicBrainzAlbumId` | MusicBrainz album MBID | +| `MusicBrainzArtistId` | MusicBrainz artist MBID | +| `OtherProviders` | JSONB object of any remaining provider key/value pairs | + +**Example queries:** + +```sql +-- Movies missing a TMDB ID +SELECT "Name", "ProductionYear", "ImdbId" +FROM library."ItemProviders_v" +WHERE "Type" LIKE '%Movie%' + AND "TmdbId" IS NULL +ORDER BY "Name"; + +-- Look up an item by IMDb ID +SELECT "ItemId", "Type", "Name", "ProductionYear" +FROM library."ItemProviders_v" +WHERE "ImdbId" = 'tt0111161'; +``` + +--- + +## `library."ItemPeople_v"` + +All cast and crew entries linked to the items they appear in. + +**Use when:** browsing which items feature a specific actor or director, building cast-focused reports, or auditing people metadata. + +**Columns of note:** + +| Column | Description | +|---|---| +| `PersonName` | Name of the person | +| `PersonType` | Role type: `Actor`, `Director`, `Writer`, `Producer`, etc. | +| `Role` | Specific character name or role description (may be empty) | +| `SortOrder` / `ListOrder` | Display ordering within the item's people list | + +**Example queries:** + +```sql +-- All items featuring a specific actor +SELECT "ItemType", "ItemName", "SeriesName", "ProductionYear", "Role" +FROM library."ItemPeople_v" +WHERE "PersonName" = 'Bryan Cranston' + AND "PersonType" = 'Actor' +ORDER BY "ProductionYear"; + +-- Directors with the most items in the library +SELECT "PersonName", COUNT(*) AS "DirectedItems" +FROM library."ItemPeople_v" +WHERE "PersonType" = 'Director' +GROUP BY "PersonName" +ORDER BY "DirectedItems" DESC +LIMIT 20; +``` + +--- + +## `library."LibrarySummary_v"` + +Aggregate statistics broken down by item type — one row per type. Provides a quick dashboard overview of the entire library. + +**Use when:** getting a high-level snapshot of library size, total runtime, storage usage, and video quality distribution. + +**Columns of note:** + +| Column | Description | +|---|---| +| `Type` | Jellyfin item type | +| `ItemCount` | Total number of items of this type | +| `TotalRuntimeHours` | Sum of all runtimes in hours | +| `TotalSizeGB` | Sum of all file sizes in gigabytes | +| `AvgCommunityRating` | Average community rating across items with a rating | +| `Count4K` | Items with primary video height ≥ 2160 | +| `Count1080p` | Items with height 1080–2159 | +| `Count720p` | Items with height 720–1079 | +| `CountSD` | Items with height < 720 | +| `CountDolbyVision` | Items with a Dolby Vision video stream | +| `CountHDR10Plus` | Items with HDR10+ flag set | +| `CountHDR10` | Items with HDR10 (`smpte2084` transfer, no DV or HDR10+) | + +**Example:** + +```sql +-- Full summary +SELECT "Type", "ItemCount", "TotalRuntimeHours", "TotalSizeGB", + "Count4K", "Count1080p", "Count720p", "CountSD", + "CountDolbyVision", "CountHDR10Plus", "CountHDR10" +FROM library."LibrarySummary_v"; +``` + +--- + +## StreamType Reference + +The `StreamType` integer used in `MediaStreamInfos` and `MediaStreamInfos_v` maps to: + +| Value | Type | +|---|---| +| 0 | Audio | +| 1 | Video | +| 2 | Subtitle | +| 3 | EmbeddedImage | +| 4 | Data / Attachment | diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Migrations/SplitMediaStreamInfos.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Migrations/20260502000000_SplitMediaStreamInfos.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Migrations/SplitMediaStreamInfos.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Migrations/20260502000000_SplitMediaStreamInfos.cs