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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
|
using System;
using Emby.Server.Implementations.Data;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Server.Implementations.Item;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Querying;
using Xunit;
using BaseItemKind = Jellyfin.Data.Enums.BaseItemKind;
namespace Jellyfin.Server.Implementations.Tests.Item;
/// <summary>
/// The by-name listings count what a cleaned value is attached to by joining ItemValuesMap to
/// BaseItems. One item can reach the same clean value through more than one value row, so the
/// join has to be counted per distinct item; counting rows reports a multiple of the truth.
/// </summary>
public sealed class BaseItemRepositoryByNameItemCountsTests : SqliteDbTestFixture
{
private readonly BaseItemRepository _repository;
private readonly ItemTypeLookup _itemTypeLookup;
public BaseItemRepositoryByNameItemCountsTests()
{
_itemTypeLookup = new ItemTypeLookup();
_repository = CreateBaseItemRepository(_itemTypeLookup);
}
[Fact]
public void GetAllArtists_AlbumCreditedAsArtistAndAlbumArtist_CountsTheAlbumOnce()
{
// GetAllArtists spans both credit types, so an album whose artist is also its album artist
// reaches the one clean value through two rows.
SeedArtistWithAlbum(ItemValueType.Artist, ItemValueType.AlbumArtist);
var result = _repository.GetAllArtists(CreateCountingQuery());
var (_, counts) = Assert.Single(result.Items);
Assert.NotNull(counts);
Assert.Equal(1, counts.AlbumCount);
Assert.Equal(1, counts.ItemCount);
}
[Fact]
public void GetAlbumArtists_TwoValueRowsCleaningToOneName_CountsTheAlbumOnce()
{
// The shape that actually reaches users: only (Type, Value) is unique, so two differently
// cased credits of one type both clean down to a single name and both map the album.
SeedArtistWithAlbum(ItemValueType.AlbumArtist, ItemValueType.AlbumArtist);
var result = _repository.GetAlbumArtists(CreateCountingQuery());
var (_, counts) = Assert.Single(result.Items);
Assert.NotNull(counts);
Assert.Equal(1, counts.AlbumCount);
}
[Fact]
public void GetArtists_TwoValueRowsCleaningToOneName_CountsTheAlbumOnce()
{
SeedArtistWithAlbum(ItemValueType.Artist, ItemValueType.Artist);
var result = _repository.GetArtists(CreateCountingQuery());
var (_, counts) = Assert.Single(result.Items);
Assert.NotNull(counts);
Assert.Equal(1, counts.AlbumCount);
}
[Theory]
[InlineData(BaseItemKind.Book)]
[InlineData(BaseItemKind.BoxSet)]
public void GetGenres_TaggedBookOrBoxSet_CountsIt(BaseItemKind kind)
{
// The listing used to dispatch only nine of the eleven counted types, so a genre on a book
// or a box set read as zero in a list and as one on the genre's own page.
SeedGenreWith(kind);
var result = _repository.GetGenres(CreateCountingQuery());
var (_, counts) = Assert.Single(result.Items);
Assert.NotNull(counts);
Assert.Equal(1, kind == BaseItemKind.Book ? counts.BookCount : counts.BoxSetCount);
Assert.Equal(1, counts.ItemCount);
}
/// <summary>
/// Seeds one genre carried by a single item of the given kind.
/// </summary>
/// <param name="kind">The kind of the tagged item.</param>
private void SeedGenreWith(BaseItemKind kind)
{
const string Name = "Reference";
const string CleanName = "reference";
using var ctx = CreateDbContext();
var genreId = Guid.Parse("dddddddd-0000-0000-0000-000000000001");
var taggedId = Guid.Parse("eeeeeeee-0000-0000-0000-000000000001");
ctx.BaseItems.Add(new BaseItemEntity
{
Id = genreId,
Type = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre],
Name = Name,
CleanName = CleanName,
PresentationUniqueKey = genreId.ToString("N"),
IsFolder = true,
IsVirtualItem = false
});
var tagged = new BaseItemEntity
{
Id = taggedId,
Type = _itemTypeLookup.BaseItemKindNames[kind],
Name = "Tagged",
CleanName = "tagged",
PresentationUniqueKey = taggedId.ToString("N"),
IsFolder = false,
IsVirtualItem = false
};
ctx.BaseItems.Add(tagged);
var itemValue = new ItemValue
{
ItemValueId = Guid.Parse("ffffffff-0000-0000-0000-000000000001"),
Type = ItemValueType.Genre,
Value = Name,
CleanValue = CleanName
};
ctx.ItemValues.Add(itemValue);
ctx.ItemValuesMap.Add(new ItemValueMap
{
ItemId = taggedId,
ItemValueId = itemValue.ItemValueId,
Item = tagged,
ItemValue = itemValue
});
ctx.SaveChanges();
}
private static InternalItemsQuery CreateCountingQuery()
{
return new InternalItemsQuery(new User("test", "auth", "reset"))
{
DtoOptions = new DtoOptions(true) { Fields = [ItemFields.ItemCounts] }
};
}
/// <summary>
/// Seeds one artist and a single album mapped to that artist's clean name through two value
/// rows of the given types.
/// </summary>
/// <param name="first">The type of the first value row.</param>
/// <param name="second">The type of the second value row.</param>
private void SeedArtistWithAlbum(ItemValueType first, ItemValueType second)
{
const string Name = "Tangerine Dream";
const string CleanName = "tangerine dream";
using var ctx = CreateDbContext();
var artistId = Guid.Parse("aaaaaaaa-0000-0000-0000-000000000001");
var albumId = Guid.Parse("bbbbbbbb-0000-0000-0000-000000000001");
ctx.BaseItems.Add(new BaseItemEntity
{
Id = artistId,
Type = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist],
Name = Name,
CleanName = CleanName,
PresentationUniqueKey = artistId.ToString("N"),
IsFolder = true,
IsVirtualItem = false
});
var album = new BaseItemEntity
{
Id = albumId,
Type = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum],
Name = "Phaedra",
CleanName = "phaedra",
PresentationUniqueKey = albumId.ToString("N"),
IsFolder = true,
IsVirtualItem = false
};
ctx.BaseItems.Add(album);
var types = new[] { first, second };
for (var i = 0; i < types.Length; i++)
{
var itemValue = new ItemValue
{
ItemValueId = Guid.Parse($"cccccccc-0000-0000-0000-{i:D12}"),
Type = types[i],
// Distinct values, one clean name: exactly what the unique index permits.
Value = i == 0 ? Name : Name.ToUpperInvariant(),
CleanValue = CleanName
};
ctx.ItemValues.Add(itemValue);
ctx.ItemValuesMap.Add(new ItemValueMap
{
ItemId = albumId,
ItemValueId = itemValue.ItemValueId,
Item = album,
ItemValue = itemValue
});
}
ctx.SaveChanges();
}
}
|