1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
|
using System;
using System.Collections.Generic;
using System.Linq;
using Emby.Server.Implementations.Data;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Server.Implementations.Item;
using MediaBrowser.Controller.Entities;
using Xunit;
using BaseItemKind = Jellyfin.Data.Enums.BaseItemKind;
namespace Jellyfin.Server.Implementations.Tests.Item;
/// <summary>
/// Covers the isPlayed filter over items with alternate versions: playback is recorded against the
/// version that was actually played, so the played state belongs to the version group rather than to
/// the row that happens to carry it.
/// </summary>
public sealed class BaseItemRepositoryPlayedVersionTests : SqliteDbTestFixture
{
private const string MovieType = "MediaBrowser.Controller.Entities.Movies.Movie";
private const string SeriesType = "MediaBrowser.Controller.Entities.TV.Series";
private const string EpisodeType = "MediaBrowser.Controller.Entities.TV.Episode";
private readonly BaseItemRepository _repository;
private readonly User _user = new("test", "auth-provider", "reset-provider");
private readonly Guid _playedViaAlternate = Guid.NewGuid();
private readonly Guid _playedOnPrimary = Guid.NewGuid();
private readonly Guid _unplayedWithAlternate = Guid.NewGuid();
private readonly Guid _unplayedWithoutAlternate = Guid.NewGuid();
private readonly Guid _seriesPlayedViaAlternate = Guid.NewGuid();
private readonly Guid _unplayedSeries = Guid.NewGuid();
public BaseItemRepositoryPlayedVersionTests()
{
using (var context = CreateDbContext())
{
Seed(context);
}
_repository = CreateBaseItemRepository(new ItemTypeLookup());
}
[Fact]
public void IsPlayed_CountsAMoviePlayedThroughItsAlternateVersion()
{
Assert.Equal(
new HashSet<Guid> { _playedOnPrimary, _playedViaAlternate },
Ids(BaseItemKind.Movie, isPlayed: true));
}
[Fact]
public void IsUnplayed_DropsAMoviePlayedThroughItsAlternateVersion()
{
Assert.Equal(
new HashSet<Guid> { _unplayedWithAlternate, _unplayedWithoutAlternate },
Ids(BaseItemKind.Movie, isPlayed: false));
}
[Fact]
public void IsPlayed_KeepsAPlayedPrimaryWhoseAlternateHasNoRowOfItsOwn()
{
Assert.Contains(_playedOnPrimary, Ids(BaseItemKind.Movie, isPlayed: true));
}
[Fact]
public void IsPlayed_CountsASeriesWatchedThroughAnEpisodeAlternateVersion()
{
Assert.Equal(new HashSet<Guid> { _seriesPlayedViaAlternate }, Ids(BaseItemKind.Series, isPlayed: true));
Assert.Equal(new HashSet<Guid> { _unplayedSeries }, Ids(BaseItemKind.Series, isPlayed: false));
}
private HashSet<Guid> Ids(BaseItemKind kind, bool isPlayed)
=> _repository
.GetItemList(new InternalItemsQuery(_user)
{
IncludeItemTypes = [kind],
IsPlayed = isPlayed
})
.Select(i => i.Id)
.ToHashSet();
private void Seed(JellyfinDbContext context)
{
context.Users.Add(_user);
// Only the alternate carries the played row, which is what playing that version records.
AddMovieWithAlternate(context, _playedViaAlternate, "A", playedPrimary: false, playedAlternate: true);
AddMovieWithAlternate(context, _playedOnPrimary, "B", playedPrimary: true, playedAlternate: false);
AddMovieWithAlternate(context, _unplayedWithAlternate, "C", playedPrimary: false, playedAlternate: false);
AddItem(context, _unplayedWithoutAlternate, MovieType, "D");
AddSeriesWithAlternateEpisode(context, _seriesPlayedViaAlternate, "E", playedAlternate: true);
AddSeriesWithAlternateEpisode(context, _unplayedSeries, "F", playedAlternate: false);
context.SaveChanges();
}
private void AddMovieWithAlternate(JellyfinDbContext context, Guid primaryId, string name, bool playedPrimary, bool playedAlternate)
{
AddItem(context, primaryId, MovieType, name);
AddAlternateVersion(context, primaryId, MovieType, $"{name} 4K", playedAlternate);
if (playedPrimary)
{
AddPlayedUserData(context, primaryId);
}
}
private void AddSeriesWithAlternateEpisode(JellyfinDbContext context, Guid seriesId, string name, bool playedAlternate)
{
var episodeId = Guid.NewGuid();
context.BaseItems.Add(new BaseItemEntity
{
Id = seriesId,
Type = SeriesType,
Name = name,
SortName = name,
PresentationUniqueKey = seriesId.ToString("N"),
IsFolder = true
});
AddItem(context, episodeId, EpisodeType, $"{name} 1");
context.AncestorIds.Add(new AncestorId { ItemId = episodeId, ParentItemId = seriesId, Item = null!, ParentItem = null! });
AddAlternateVersion(context, episodeId, EpisodeType, $"{name} 1 4K", playedAlternate);
}
private void AddItem(JellyfinDbContext context, Guid id, string type, string name)
=> context.BaseItems.Add(new BaseItemEntity
{
Id = id,
Type = type,
Name = name,
SortName = name,
PresentationUniqueKey = id.ToString("N")
});
private void AddAlternateVersion(JellyfinDbContext context, Guid primaryId, string type, string name, bool played)
{
var alternateId = Guid.NewGuid();
// An alternate presents under its primary's key, which is what collapses the group in listings.
context.BaseItems.Add(new BaseItemEntity
{
Id = alternateId,
Type = type,
Name = name,
SortName = name,
PresentationUniqueKey = primaryId.ToString("N"),
PrimaryVersionId = primaryId
});
if (played)
{
AddPlayedUserData(context, alternateId);
}
}
private void AddPlayedUserData(JellyfinDbContext context, Guid itemId)
=> context.UserData.Add(new UserData
{
ItemId = itemId,
UserId = _user.Id,
CustomDataKey = itemId.ToString("N"),
Played = true,
Item = null!,
User = null!
});
}
|