mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-01-16 08:08:16 +00:00
Compare commits
404 Commits
v10.8.0-al
...
v10.8.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75a46a1072 | ||
|
|
6aa37ebb8f | ||
|
|
072526d798 | ||
|
|
4da240d731 | ||
|
|
b5b4e2ea89 | ||
|
|
7d63377194 | ||
|
|
71ebe220f0 | ||
|
|
3eb4bbbb86 | ||
|
|
c6a1dcf420 | ||
|
|
fddcaf17e1 | ||
|
|
b9cd9487bd | ||
|
|
fcb8ff7c0f | ||
|
|
e51777fe4b | ||
|
|
6d299eed49 | ||
|
|
8952819494 | ||
|
|
f802763e8f | ||
|
|
37061e5b6b | ||
|
|
1c5c0ebb0f | ||
|
|
0c4b5a5ce8 | ||
|
|
65bf393efb | ||
|
|
38d4300309 | ||
|
|
11d0c6827f | ||
|
|
0765fd568f | ||
|
|
28c2ac9cc0 | ||
|
|
58b9e5af79 | ||
|
|
1725b2ee69 | ||
|
|
25abe479eb | ||
|
|
1b9cb8cca6 | ||
|
|
c3071f8996 | ||
|
|
dc9e1ece29 | ||
|
|
226a43619f | ||
|
|
53447976f0 | ||
|
|
b2a2bdb088 | ||
|
|
bb713d1931 | ||
|
|
73a5fc6e26 | ||
|
|
019b631d7b | ||
|
|
6f4a0683ed | ||
|
|
5a1b347b83 | ||
|
|
828bf1f0ac | ||
|
|
4ec7488bab | ||
|
|
7ad1527fd0 | ||
|
|
80fce87f7a | ||
|
|
2ab3bf97ef | ||
|
|
57db188c2e | ||
|
|
2749509f00 | ||
|
|
7bfc6b5679 | ||
|
|
251b9a5235 | ||
|
|
ea8f40e84a | ||
|
|
4441513ca4 | ||
|
|
ebbde383e8 | ||
|
|
78bb581f0c | ||
|
|
a615f87680 | ||
|
|
d594f81f65 | ||
|
|
6804624c15 | ||
|
|
cf29aae690 | ||
|
|
c0ab54f0bd | ||
|
|
60e75f7b70 | ||
|
|
17e6bd5f88 | ||
|
|
42c661e30f | ||
|
|
9f5873b8c4 | ||
|
|
cbfa355e31 | ||
|
|
728a5988b3 | ||
|
|
2e7d173188 | ||
|
|
ec2645c0c0 | ||
|
|
7db753d247 | ||
|
|
d0832c60d4 | ||
|
|
b2d85a02c2 | ||
|
|
4b9c84c52e | ||
|
|
daa76d3d34 | ||
|
|
599fc9e671 | ||
|
|
17f43c8e01 | ||
|
|
634ce40c2f | ||
|
|
b5459f49d3 | ||
|
|
932c2c6665 | ||
|
|
a04ab6b876 | ||
|
|
a8a8ce4e7b | ||
|
|
8c7dd0a691 | ||
|
|
55b429edb7 | ||
|
|
076a13abeb | ||
|
|
259bc231d6 | ||
|
|
7774cb2067 | ||
|
|
00211ca056 | ||
|
|
ca4769ab68 | ||
|
|
84ac692312 | ||
|
|
a7a7173cd5 | ||
|
|
c86b064f80 | ||
|
|
05c8834a3a | ||
|
|
9158511017 | ||
|
|
afaff1310f | ||
|
|
b880dc8a4a | ||
|
|
91292b8ea5 | ||
|
|
83a94aa612 | ||
|
|
cb41dda5b3 | ||
|
|
a633a3052f | ||
|
|
e8ae501430 | ||
|
|
db46eaa744 | ||
|
|
71f6326e1a | ||
|
|
91f3ce3109 | ||
|
|
3f3295a5d6 | ||
|
|
76c2775d8c | ||
|
|
cd760943a9 | ||
|
|
ea9fc9f9cc | ||
|
|
d9bc65469f | ||
|
|
077f13ae4c | ||
|
|
a4565da4a9 | ||
|
|
c35b704a83 | ||
|
|
55146334e8 | ||
|
|
232a148d3f | ||
|
|
968c534864 | ||
|
|
f8fcbc88fc | ||
|
|
92448ffabd | ||
|
|
923720c988 | ||
|
|
16bf4a013b | ||
|
|
b3c48e8e78 | ||
|
|
42a7318fd0 | ||
|
|
8ddba55214 | ||
|
|
0dd304f86c | ||
|
|
249a1ca955 | ||
|
|
dea5a3f3bc | ||
|
|
b5d4fdb56e | ||
|
|
e34fd15196 | ||
|
|
7405450da2 | ||
|
|
9a0618552b | ||
|
|
4a58582ad5 | ||
|
|
4c9bd905c6 | ||
|
|
5e8aaa68cf | ||
|
|
543b0127b3 | ||
|
|
87439665d7 | ||
|
|
c3c4dc6839 | ||
|
|
e575d815cc | ||
|
|
ec9cff29df | ||
|
|
0f4da9f635 | ||
|
|
0edf77994a | ||
|
|
38f0e611c8 | ||
|
|
4e03801931 | ||
|
|
250332104b | ||
|
|
0872ede57b | ||
|
|
0120d80b78 | ||
|
|
9e665ff520 | ||
|
|
5c3d0b5510 | ||
|
|
3a4e7fb075 | ||
|
|
c5569c701c | ||
|
|
32629cd7da | ||
|
|
2c6d6dbbf8 | ||
|
|
bdf9bdb9e6 | ||
|
|
0e8c97ed60 | ||
|
|
843ba4506e | ||
|
|
3699fa43f0 | ||
|
|
296a61cbc4 | ||
|
|
a04f8f5efb | ||
|
|
58f788e8ab | ||
|
|
7ee96a59d3 | ||
|
|
510f92f4c5 | ||
|
|
a90614d194 | ||
|
|
5e8f27d473 | ||
|
|
1f02ab83cb | ||
|
|
2a82f8dc40 | ||
|
|
584cf47ef5 | ||
|
|
3223ef0e49 | ||
|
|
3797879d05 | ||
|
|
8b4a36d6f7 | ||
|
|
4d1b824651 | ||
|
|
fbc79cb4ce | ||
|
|
6d6fb1bba3 | ||
|
|
681bfbd535 | ||
|
|
220443eca1 | ||
|
|
4d446eb614 | ||
|
|
d707a201c9 | ||
|
|
7d86ef6f22 | ||
|
|
eeb8192602 | ||
|
|
de2d292197 | ||
|
|
593b2fd359 | ||
|
|
13ac3e3665 | ||
|
|
5c407dbab6 | ||
|
|
f50b2f7959 | ||
|
|
1acb9e1d45 | ||
|
|
6640f3f782 | ||
|
|
8aef2cb282 | ||
|
|
8bd331f5fa | ||
|
|
ee80602a0d | ||
|
|
03b3f08354 | ||
|
|
65833076db | ||
|
|
e18d966874 | ||
|
|
4cdb590291 | ||
|
|
d47811bdaf | ||
|
|
01a0a4a87c | ||
|
|
87a6fdf847 | ||
|
|
3513f5a84b | ||
|
|
a327b43ab7 | ||
|
|
3f69eeab27 | ||
|
|
dd8b9e9d23 | ||
|
|
6030946d78 | ||
|
|
fde84a1e00 | ||
|
|
3176a4ddd9 | ||
|
|
9cafa2cab4 | ||
|
|
c4c7d7431f | ||
|
|
f2648f3eee | ||
|
|
29095ab390 | ||
|
|
b9b3959223 | ||
|
|
9b71bf8cfe | ||
|
|
5eff80fb8c | ||
|
|
2d77c729f2 | ||
|
|
9d387c542a | ||
|
|
60f4d70cc5 | ||
|
|
bd48b74d5b | ||
|
|
838adaea48 | ||
|
|
46543ead27 | ||
|
|
707e5bab97 | ||
|
|
cf75f99f0e | ||
|
|
fa6f6515a6 | ||
|
|
0e491025ae | ||
|
|
40e05a7993 | ||
|
|
6193fdea69 | ||
|
|
ca2d94ee97 | ||
|
|
99a48554a6 | ||
|
|
120828d8d0 | ||
|
|
9cea773d29 | ||
|
|
a474e4cf3b | ||
|
|
b1025121b8 | ||
|
|
2029076ce8 | ||
|
|
5535b9c01f | ||
|
|
e446e9fde9 | ||
|
|
180e2dc329 | ||
|
|
40be86eec0 | ||
|
|
beafd6eaab | ||
|
|
f6d8c19a7a | ||
|
|
2da7777e6d | ||
|
|
a0df79d8a5 | ||
|
|
01b95cf8e6 | ||
|
|
6bbfcf1906 | ||
|
|
7b50048020 | ||
|
|
a9a53dc657 | ||
|
|
0d8170cedb | ||
|
|
1a35690834 | ||
|
|
c61b9ef05a | ||
|
|
b5b994b22f | ||
|
|
0894a6193f | ||
|
|
9d34d6339a | ||
|
|
bbf1399826 | ||
|
|
d016d483ae | ||
|
|
61b191d345 | ||
|
|
9433072f90 | ||
|
|
a3c5afa443 | ||
|
|
f1862f9b1a | ||
|
|
a68e58556c | ||
|
|
5e91f50c43 | ||
|
|
c1a8385c9c | ||
|
|
9978164438 | ||
|
|
eaa003775f | ||
|
|
e778462955 | ||
|
|
148fcb1bb8 | ||
|
|
757970bfc1 | ||
|
|
c677b4f6b7 | ||
|
|
20b0499b00 | ||
|
|
09f2456919 | ||
|
|
bcd84dedda | ||
|
|
2496c5d589 | ||
|
|
19c9319d2c | ||
|
|
6ef9ac6e53 | ||
|
|
502a5fc932 | ||
|
|
02ce95edaf | ||
|
|
5addb24e9b | ||
|
|
dc27e7618f | ||
|
|
3e1bbdd3d4 | ||
|
|
2ff786fd3d | ||
|
|
2a09d4244c | ||
|
|
beef6f0855 | ||
|
|
80c7119537 | ||
|
|
1df56335ee | ||
|
|
065d3fa837 | ||
|
|
1df5b5034b | ||
|
|
4a20ae6cb4 | ||
|
|
a87b87825d | ||
|
|
976e3160b8 | ||
|
|
4890454935 | ||
|
|
ca887518dd | ||
|
|
5b5ae1ef52 | ||
|
|
3eec137100 | ||
|
|
402a3797a4 | ||
|
|
0b871505a6 | ||
|
|
0485ff1899 | ||
|
|
69df004b9f | ||
|
|
8b9ff893b3 | ||
|
|
b3d5383073 | ||
|
|
9d5f328a09 | ||
|
|
f469ee3010 | ||
|
|
e30ac91f16 | ||
|
|
95183c365a | ||
|
|
0af5e60094 | ||
|
|
3c030740ea | ||
|
|
bb9ce98379 | ||
|
|
02fda265b8 | ||
|
|
501919df35 | ||
|
|
107909de30 | ||
|
|
052b667d6a | ||
|
|
e5ea607853 | ||
|
|
223cc8314a | ||
|
|
39e6658d01 | ||
|
|
8f051c86f7 | ||
|
|
11419cbbfd | ||
|
|
5b7e8a27fb | ||
|
|
718e4b8665 | ||
|
|
d387d4df92 | ||
|
|
1fa845de6e | ||
|
|
7126f9dffc | ||
|
|
b2b4bd82d7 | ||
|
|
03c7bcf9c6 | ||
|
|
ea355b4262 | ||
|
|
71a0abe211 | ||
|
|
d8c3b8e7f8 | ||
|
|
88baff5693 | ||
|
|
6d3b129666 | ||
|
|
8295a3be0d | ||
|
|
17a273d237 | ||
|
|
752bb88445 | ||
|
|
a263f3978b | ||
|
|
0009e5cc27 | ||
|
|
96ea865681 | ||
|
|
4a7498a0be | ||
|
|
e3f0a53f59 | ||
|
|
3734c95fd4 | ||
|
|
9ba7bf96ef | ||
|
|
bff5ff0cb8 | ||
|
|
acb86066ff | ||
|
|
263bbf897a | ||
|
|
97124f5fce | ||
|
|
6565b0cfbe | ||
|
|
b635b5a7e3 | ||
|
|
6d74c83ddb | ||
|
|
7cf5767949 | ||
|
|
fa366f0099 | ||
|
|
989013c974 | ||
|
|
c09e999916 | ||
|
|
61b75c82ce | ||
|
|
773a4ae1f3 | ||
|
|
38ac84ad71 | ||
|
|
054ca6a8fb | ||
|
|
3d9f865247 | ||
|
|
5cd6fba11a | ||
|
|
f7b18d97a7 | ||
|
|
45e6e2adee | ||
|
|
704c1539a2 | ||
|
|
00b293d2e9 | ||
|
|
52e9dc66f5 | ||
|
|
3c8f7d380f | ||
|
|
0a0ddb0eaf | ||
|
|
84b8c9c2bc | ||
|
|
257e1be95f | ||
|
|
6003289717 | ||
|
|
19d285149b | ||
|
|
33a2e1bd50 | ||
|
|
b8507371d5 | ||
|
|
bddded96ff | ||
|
|
24024706bf | ||
|
|
93fd1c7075 | ||
|
|
5b1b2621ab | ||
|
|
b50c3852ef | ||
|
|
1924d0740d | ||
|
|
74459ec403 | ||
|
|
c32a421ea7 | ||
|
|
4cfe8fe588 | ||
|
|
06c82973c6 | ||
|
|
f638cd08ea | ||
|
|
31b87130a2 | ||
|
|
ca99a27611 | ||
|
|
5eb1fde88c | ||
|
|
24679af2e8 | ||
|
|
474b035d99 | ||
|
|
4f45c52674 | ||
|
|
f059be8e4d | ||
|
|
03435641c8 | ||
|
|
58be1d7759 | ||
|
|
4e0edaf544 | ||
|
|
9c74103fbe | ||
|
|
551c6f02a2 | ||
|
|
358cf48506 | ||
|
|
331d4ad849 | ||
|
|
bd32cecf7a | ||
|
|
c84f2e48b0 | ||
|
|
370b7f8e12 | ||
|
|
4cb649853d | ||
|
|
a774d1fa10 | ||
|
|
5254e74719 | ||
|
|
14b5e85461 | ||
|
|
25f1cdbcb5 | ||
|
|
34df1a030b | ||
|
|
1d729b2b0f | ||
|
|
f73a7a6ed8 | ||
|
|
de9bf327c6 | ||
|
|
5d19c26d59 | ||
|
|
0415d1ccef | ||
|
|
d10de5b7f9 | ||
|
|
5265b3eee7 | ||
|
|
9abe9e7e54 | ||
|
|
3a89e88033 | ||
|
|
d5b63092ed | ||
|
|
6648b7d7da | ||
|
|
97c2c523a8 | ||
|
|
e682c230bd | ||
|
|
a3a4689af2 | ||
|
|
107412f2f2 | ||
|
|
6b2b484987 | ||
|
|
80877aa945 | ||
|
|
c52a2f2f7b |
@@ -39,6 +39,14 @@ jobs:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
|
||||
displayName: Set release version (stable)
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
|
||||
- script: './bump-version $(JellyfinVersion)'
|
||||
displayName: Bump internal version (stable)
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
|
||||
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
|
||||
displayName: 'Build Dockerfile'
|
||||
|
||||
@@ -80,6 +88,14 @@ jobs:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
|
||||
displayName: Set release version (stable)
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
|
||||
- script: './bump-version $(JellyfinVersion)'
|
||||
displayName: Bump internal version (stable)
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: 'Download OpenAPI Spec'
|
||||
inputs:
|
||||
@@ -127,6 +143,10 @@ jobs:
|
||||
displayName: Set release version (stable)
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
|
||||
- script: './bump-version $(JellyfinVersion)'
|
||||
displayName: Bump internal version (stable)
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
|
||||
- task: Docker@2
|
||||
displayName: 'Push Unstable Image'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../fedora/Makefile
|
||||
@@ -150,6 +150,7 @@
|
||||
- [ianjazz246](https://github.com/ianjazz246)
|
||||
- [peterspenler](https://github.com/peterspenler)
|
||||
- [MBR-0001](https://github.com/MBR-0001)
|
||||
- [jonas-resch](https://github.com/jonas-resch)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<PropertyGroup>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -18,11 +18,8 @@ using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.TV;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Dto;
|
||||
@@ -30,12 +27,7 @@ using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Book = MediaBrowser.Controller.Entities.Book;
|
||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||
using Genre = MediaBrowser.Controller.Entities.Genre;
|
||||
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
|
||||
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
|
||||
using Series = MediaBrowser.Controller.Entities.TV.Series;
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
@@ -395,6 +387,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||
}
|
||||
|
||||
writer.WriteFullEndElement();
|
||||
writer.Flush();
|
||||
xmlWriter.WriteElementString("Result", builder.ToString());
|
||||
}
|
||||
|
||||
@@ -484,6 +477,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||
}
|
||||
|
||||
writer.WriteFullEndElement();
|
||||
writer.Flush();
|
||||
xmlWriter.WriteElementString("Result", builder.ToString());
|
||||
}
|
||||
|
||||
@@ -537,7 +531,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||
User = user,
|
||||
Recursive = true,
|
||||
IsMissing = false,
|
||||
ExcludeItemTypes = new[] { nameof(Book) },
|
||||
ExcludeItemTypes = new[] { BaseItemKind.Book },
|
||||
IsFolder = isFolder,
|
||||
MediaTypes = mediaTypes,
|
||||
DtoOptions = GetDtoOptions()
|
||||
@@ -617,7 +611,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||
Limit = limit,
|
||||
StartIndex = startIndex,
|
||||
IsVirtualItem = false,
|
||||
ExcludeItemTypes = new[] { nameof(Book) },
|
||||
ExcludeItemTypes = new[] { BaseItemKind.Book },
|
||||
IsPlaceHolder = false,
|
||||
DtoOptions = GetDtoOptions(),
|
||||
OrderBy = GetOrderBy(sort, folder.IsPreSorted)
|
||||
@@ -642,7 +636,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
StartIndex = startIndex,
|
||||
Limit = limit,
|
||||
IncludeItemTypes = new[] { nameof(LiveTvChannel) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
|
||||
OrderBy = GetOrderBy(sort, false)
|
||||
};
|
||||
|
||||
@@ -673,39 +667,39 @@ namespace Emby.Dlna.ContentDirectory
|
||||
switch (stubType)
|
||||
{
|
||||
case StubType.Latest:
|
||||
return GetLatest(item, query, nameof(Audio));
|
||||
return GetLatest(item, query, BaseItemKind.Audio);
|
||||
case StubType.Playlists:
|
||||
return GetMusicPlaylists(query);
|
||||
case StubType.Albums:
|
||||
return GetChildrenOfItem(item, query, nameof(MusicAlbum));
|
||||
return GetChildrenOfItem(item, query, BaseItemKind.MusicAlbum);
|
||||
case StubType.Artists:
|
||||
return GetMusicArtists(item, query);
|
||||
case StubType.AlbumArtists:
|
||||
return GetMusicAlbumArtists(item, query);
|
||||
case StubType.FavoriteAlbums:
|
||||
return GetChildrenOfItem(item, query, nameof(MusicAlbum), true);
|
||||
return GetChildrenOfItem(item, query, BaseItemKind.MusicAlbum, true);
|
||||
case StubType.FavoriteArtists:
|
||||
return GetFavoriteArtists(item, query);
|
||||
case StubType.FavoriteSongs:
|
||||
return GetChildrenOfItem(item, query, nameof(Audio), true);
|
||||
return GetChildrenOfItem(item, query, BaseItemKind.Audio, true);
|
||||
case StubType.Songs:
|
||||
return GetChildrenOfItem(item, query, nameof(Audio));
|
||||
return GetChildrenOfItem(item, query, BaseItemKind.Audio);
|
||||
case StubType.Genres:
|
||||
return GetMusicGenres(item, query);
|
||||
}
|
||||
|
||||
var serverItems = new ServerItem[]
|
||||
{
|
||||
new (item, StubType.Latest),
|
||||
new (item, StubType.Playlists),
|
||||
new (item, StubType.Albums),
|
||||
new (item, StubType.AlbumArtists),
|
||||
new (item, StubType.Artists),
|
||||
new (item, StubType.Songs),
|
||||
new (item, StubType.Genres),
|
||||
new (item, StubType.FavoriteArtists),
|
||||
new (item, StubType.FavoriteAlbums),
|
||||
new (item, StubType.FavoriteSongs)
|
||||
new(item, StubType.Latest),
|
||||
new(item, StubType.Playlists),
|
||||
new(item, StubType.Albums),
|
||||
new(item, StubType.AlbumArtists),
|
||||
new(item, StubType.Artists),
|
||||
new(item, StubType.Songs),
|
||||
new(item, StubType.Genres),
|
||||
new(item, StubType.FavoriteArtists),
|
||||
new(item, StubType.FavoriteAlbums),
|
||||
new(item, StubType.FavoriteSongs)
|
||||
};
|
||||
|
||||
if (limit < serverItems.Length)
|
||||
@@ -744,25 +738,25 @@ namespace Emby.Dlna.ContentDirectory
|
||||
case StubType.ContinueWatching:
|
||||
return GetMovieContinueWatching(item, query);
|
||||
case StubType.Latest:
|
||||
return GetLatest(item, query, nameof(Movie));
|
||||
return GetLatest(item, query, BaseItemKind.Movie);
|
||||
case StubType.Movies:
|
||||
return GetChildrenOfItem(item, query, nameof(Movie));
|
||||
return GetChildrenOfItem(item, query, BaseItemKind.Movie);
|
||||
case StubType.Collections:
|
||||
return GetMovieCollections(query);
|
||||
case StubType.Favorites:
|
||||
return GetChildrenOfItem(item, query, nameof(Movie), true);
|
||||
return GetChildrenOfItem(item, query, BaseItemKind.Movie, true);
|
||||
case StubType.Genres:
|
||||
return GetGenres(item, query);
|
||||
}
|
||||
|
||||
var array = new ServerItem[]
|
||||
{
|
||||
new (item, StubType.ContinueWatching),
|
||||
new (item, StubType.Latest),
|
||||
new (item, StubType.Movies),
|
||||
new (item, StubType.Collections),
|
||||
new (item, StubType.Favorites),
|
||||
new (item, StubType.Genres)
|
||||
new(item, StubType.ContinueWatching),
|
||||
new(item, StubType.Latest),
|
||||
new(item, StubType.Movies),
|
||||
new(item, StubType.Collections),
|
||||
new(item, StubType.Favorites),
|
||||
new(item, StubType.Genres)
|
||||
};
|
||||
|
||||
if (limit < array.Length)
|
||||
@@ -829,26 +823,26 @@ namespace Emby.Dlna.ContentDirectory
|
||||
case StubType.NextUp:
|
||||
return GetNextUp(item, query);
|
||||
case StubType.Latest:
|
||||
return GetLatest(item, query, nameof(Episode));
|
||||
return GetLatest(item, query, BaseItemKind.Episode);
|
||||
case StubType.Series:
|
||||
return GetChildrenOfItem(item, query, nameof(Series));
|
||||
return GetChildrenOfItem(item, query, BaseItemKind.Series);
|
||||
case StubType.FavoriteSeries:
|
||||
return GetChildrenOfItem(item, query, nameof(Series), true);
|
||||
return GetChildrenOfItem(item, query, BaseItemKind.Series, true);
|
||||
case StubType.FavoriteEpisodes:
|
||||
return GetChildrenOfItem(item, query, nameof(Episode), true);
|
||||
return GetChildrenOfItem(item, query, BaseItemKind.Episode, true);
|
||||
case StubType.Genres:
|
||||
return GetGenres(item, query);
|
||||
}
|
||||
|
||||
var serverItems = new ServerItem[]
|
||||
{
|
||||
new (item, StubType.ContinueWatching),
|
||||
new (item, StubType.NextUp),
|
||||
new (item, StubType.Latest),
|
||||
new (item, StubType.Series),
|
||||
new (item, StubType.FavoriteSeries),
|
||||
new (item, StubType.FavoriteEpisodes),
|
||||
new (item, StubType.Genres)
|
||||
new(item, StubType.ContinueWatching),
|
||||
new(item, StubType.NextUp),
|
||||
new(item, StubType.Latest),
|
||||
new(item, StubType.Series),
|
||||
new(item, StubType.FavoriteSeries),
|
||||
new(item, StubType.FavoriteEpisodes),
|
||||
new(item, StubType.Genres)
|
||||
};
|
||||
|
||||
if (limit < serverItems.Length)
|
||||
@@ -896,7 +890,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||
private QueryResult<ServerItem> GetMovieCollections(InternalItemsQuery query)
|
||||
{
|
||||
query.Recursive = true;
|
||||
query.IncludeItemTypes = new[] { nameof(BoxSet) };
|
||||
query.IncludeItemTypes = new[] { BaseItemKind.BoxSet };
|
||||
|
||||
var result = _libraryManager.GetItemsResult(query);
|
||||
|
||||
@@ -911,7 +905,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||
/// <param name="itemType">The item type.</param>
|
||||
/// <param name="isFavorite">A value indicating whether to only fetch favorite items.</param>
|
||||
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
|
||||
private QueryResult<ServerItem> GetChildrenOfItem(BaseItem parent, InternalItemsQuery query, string itemType, bool isFavorite = false)
|
||||
private QueryResult<ServerItem> GetChildrenOfItem(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType, bool isFavorite = false)
|
||||
{
|
||||
query.Recursive = true;
|
||||
query.Parent = parent;
|
||||
@@ -1011,7 +1005,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||
private QueryResult<ServerItem> GetMusicPlaylists(InternalItemsQuery query)
|
||||
{
|
||||
query.Parent = null;
|
||||
query.IncludeItemTypes = new[] { nameof(Playlist) };
|
||||
query.IncludeItemTypes = new[] { BaseItemKind.Playlist };
|
||||
query.Recursive = true;
|
||||
|
||||
var result = _libraryManager.GetItemsResult(query);
|
||||
@@ -1050,7 +1044,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||
/// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
|
||||
/// <param name="itemType">The item type.</param>
|
||||
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
|
||||
private QueryResult<ServerItem> GetLatest(BaseItem parent, InternalItemsQuery query, string itemType)
|
||||
private QueryResult<ServerItem> GetLatest(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType)
|
||||
{
|
||||
query.OrderBy = Array.Empty<(string, SortOrder)>();
|
||||
|
||||
@@ -1084,7 +1078,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
Recursive = true,
|
||||
ArtistIds = new[] { item.Id },
|
||||
IncludeItemTypes = new[] { nameof(MusicAlbum) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
|
||||
Limit = limit,
|
||||
StartIndex = startIndex,
|
||||
DtoOptions = GetDtoOptions(),
|
||||
@@ -1113,8 +1107,8 @@ namespace Emby.Dlna.ContentDirectory
|
||||
GenreIds = new[] { item.Id },
|
||||
IncludeItemTypes = new[]
|
||||
{
|
||||
nameof(Movie),
|
||||
nameof(Series)
|
||||
BaseItemKind.Movie,
|
||||
BaseItemKind.Series
|
||||
},
|
||||
Limit = limit,
|
||||
StartIndex = startIndex,
|
||||
@@ -1142,7 +1136,7 @@ namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
Recursive = true,
|
||||
GenreIds = new[] { item.Id },
|
||||
IncludeItemTypes = new[] { nameof(MusicAlbum) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
|
||||
Limit = limit,
|
||||
StartIndex = startIndex,
|
||||
DtoOptions = GetDtoOptions(),
|
||||
|
||||
@@ -17,8 +17,7 @@ namespace Emby.Dlna.Didl
|
||||
public Filter(string filter)
|
||||
{
|
||||
_all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
_fields = (filter ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
_fields = filter.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
public bool Contains(string field)
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
@@ -84,8 +83,7 @@ namespace Emby.Dlna
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
var list = _profiles.Values.ToList();
|
||||
return list
|
||||
return _profiles.Values
|
||||
.OrderBy(i => i.Item1.Info.Type == DeviceProfileType.User ? 0 : 1)
|
||||
.ThenBy(i => i.Item1.Info.Name)
|
||||
.Select(i => i.Item2)
|
||||
@@ -227,11 +225,8 @@ namespace Emby.Dlna
|
||||
{
|
||||
try
|
||||
{
|
||||
var xmlFies = _fileSystem.GetFilePaths(path)
|
||||
return _fileSystem.GetFilePaths(path)
|
||||
.Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
return xmlFies
|
||||
.Select(i => ParseProfileFile(i, type))
|
||||
.Where(i => i != null)
|
||||
.ToList()!; // We just filtered out all the nulls
|
||||
@@ -253,11 +248,8 @@ namespace Emby.Dlna
|
||||
|
||||
try
|
||||
{
|
||||
DeviceProfile profile;
|
||||
|
||||
var tempProfile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path);
|
||||
|
||||
profile = ReserializeProfile(tempProfile);
|
||||
var profile = ReserializeProfile(tempProfile);
|
||||
|
||||
profile.Id = path.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
@@ -296,8 +288,7 @@ namespace Emby.Dlna
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
var list = _profiles.Values.ToList();
|
||||
return list
|
||||
return _profiles.Values
|
||||
.Select(i => i.Item1)
|
||||
.OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1)
|
||||
.ThenBy(i => i.Info.Name);
|
||||
@@ -350,7 +341,7 @@ namespace Emby.Dlna
|
||||
Directory.CreateDirectory(systemProfilesPath);
|
||||
|
||||
var fileOptions = AsyncFile.WriteOptions;
|
||||
fileOptions.Mode = FileMode.CreateNew;
|
||||
fileOptions.Mode = FileMode.Create;
|
||||
fileOptions.PreallocationSize = length;
|
||||
using (var fileStream = new FileStream(path, fileOptions))
|
||||
{
|
||||
|
||||
@@ -22,10 +22,14 @@
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ namespace Emby.Dlna.Main
|
||||
config);
|
||||
Current = this;
|
||||
|
||||
var netConfig = config.GetConfiguration<NetworkConfiguration>("network");
|
||||
var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
|
||||
_disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
|
||||
|
||||
if (_disabled && _config.GetDlnaConfiguration().EnableServer)
|
||||
@@ -262,7 +262,6 @@ namespace Emby.Dlna.Main
|
||||
{
|
||||
_publisher = new SsdpDevicePublisher(
|
||||
_communicationsServer,
|
||||
_networkManager,
|
||||
MediaBrowser.Common.System.OperatingSystem.Name,
|
||||
Environment.OSVersion.VersionString,
|
||||
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
||||
@@ -400,7 +399,6 @@ namespace Emby.Dlna.Main
|
||||
_imageProcessor,
|
||||
_deviceDiscovery,
|
||||
_httpClientFactory,
|
||||
_config,
|
||||
_userDataManager,
|
||||
_localization,
|
||||
_mediaSourceManager,
|
||||
|
||||
@@ -1179,6 +1179,7 @@ namespace Emby.Dlna.PlayTo
|
||||
return new Device(deviceProperties, httpClientFactory, logger);
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
private static DeviceIcon CreateIcon(XElement element)
|
||||
{
|
||||
if (element == null)
|
||||
@@ -1186,69 +1187,61 @@ namespace Emby.Dlna.PlayTo
|
||||
throw new ArgumentNullException(nameof(element));
|
||||
}
|
||||
|
||||
var mimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype"));
|
||||
var width = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("width"));
|
||||
var height = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("height"));
|
||||
var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth"));
|
||||
var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url"));
|
||||
|
||||
var widthValue = int.Parse(width, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||
var heightValue = int.Parse(height, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||
_ = int.TryParse(width, NumberStyles.Integer, CultureInfo.InvariantCulture, out var widthValue);
|
||||
_ = int.TryParse(height, NumberStyles.Integer, CultureInfo.InvariantCulture, out var heightValue);
|
||||
|
||||
return new DeviceIcon
|
||||
{
|
||||
Depth = depth,
|
||||
Depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth")) ?? string.Empty,
|
||||
Height = heightValue,
|
||||
MimeType = mimeType,
|
||||
Url = url,
|
||||
MimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype")) ?? string.Empty,
|
||||
Url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url")) ?? string.Empty,
|
||||
Width = widthValue
|
||||
};
|
||||
}
|
||||
|
||||
private static DeviceService Create(XElement element)
|
||||
{
|
||||
var type = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType"));
|
||||
var id = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId"));
|
||||
var scpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL"));
|
||||
var controlURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL"));
|
||||
var eventSubURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL"));
|
||||
|
||||
return new DeviceService
|
||||
=> new DeviceService()
|
||||
{
|
||||
ControlUrl = controlURL,
|
||||
EventSubUrl = eventSubURL,
|
||||
ScpdUrl = scpdUrl,
|
||||
ServiceId = id,
|
||||
ServiceType = type
|
||||
ControlUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL")) ?? string.Empty,
|
||||
EventSubUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL")) ?? string.Empty,
|
||||
ScpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL")) ?? string.Empty,
|
||||
ServiceId = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId")) ?? string.Empty,
|
||||
ServiceType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType")) ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateMediaInfo(UBaseObject mediaInfo, TransportState state)
|
||||
private void UpdateMediaInfo(UBaseObject? mediaInfo, TransportState state)
|
||||
{
|
||||
TransportState = state;
|
||||
|
||||
var previousMediaInfo = CurrentMediaInfo;
|
||||
CurrentMediaInfo = mediaInfo;
|
||||
|
||||
if (previousMediaInfo == null && mediaInfo != null)
|
||||
if (mediaInfo == null)
|
||||
{
|
||||
if (previousMediaInfo != null)
|
||||
{
|
||||
OnPlaybackStop(previousMediaInfo);
|
||||
}
|
||||
}
|
||||
else if (previousMediaInfo == null)
|
||||
{
|
||||
if (state != TransportState.Stopped)
|
||||
{
|
||||
OnPlaybackStart(mediaInfo);
|
||||
}
|
||||
}
|
||||
else if (mediaInfo != null && previousMediaInfo != null && !mediaInfo.Equals(previousMediaInfo))
|
||||
{
|
||||
OnMediaChanged(previousMediaInfo, mediaInfo);
|
||||
}
|
||||
else if (mediaInfo == null && previousMediaInfo != null)
|
||||
{
|
||||
OnPlaybackStop(previousMediaInfo);
|
||||
}
|
||||
else if (mediaInfo != null && mediaInfo.Equals(previousMediaInfo))
|
||||
else if (mediaInfo.Equals(previousMediaInfo))
|
||||
{
|
||||
OnPlaybackProgress(mediaInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnMediaChanged(previousMediaInfo, mediaInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlaybackStart(UBaseObject mediaInfo)
|
||||
|
||||
@@ -210,9 +210,9 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var duration = mediaSource == null ?
|
||||
(_device.Duration == null ? (long?)null : _device.Duration.Value.Ticks) :
|
||||
mediaSource.RunTimeTicks;
|
||||
var duration = mediaSource == null
|
||||
? _device.Duration?.Ticks
|
||||
: mediaSource.RunTimeTicks;
|
||||
|
||||
var playedToCompletion = positionTicks.HasValue && positionTicks.Value == 0;
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -35,7 +34,6 @@ namespace Emby.Dlna.PlayTo
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
||||
@@ -47,7 +45,7 @@ namespace Emby.Dlna.PlayTo
|
||||
private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
|
||||
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
|
||||
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
|
||||
{
|
||||
_logger = logger;
|
||||
_sessionManager = sessionManager;
|
||||
@@ -58,7 +56,6 @@ namespace Emby.Dlna.PlayTo
|
||||
_imageProcessor = imageProcessor;
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_config = config;
|
||||
_userDataManager = userDataManager;
|
||||
_localization = localization;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
|
||||
@@ -175,7 +175,7 @@ namespace Emby.Dlna.PlayTo
|
||||
var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ??
|
||||
(state.AllowedValues.Count > 0 ? state.AllowedValues[0] : value);
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue);
|
||||
return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType, sendValue);
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, "<{0}>{1}</{0}>", argument.Name, value);
|
||||
|
||||
@@ -167,8 +167,7 @@ namespace Emby.Dlna.Profiles
|
||||
|
||||
public void AddXmlRootAttribute(string name, string value)
|
||||
{
|
||||
var atts = XmlRootAttributes ?? System.Array.Empty<XmlAttribute>();
|
||||
var list = atts.ToList();
|
||||
var list = XmlRootAttributes.ToList();
|
||||
|
||||
list.Add(new XmlAttribute
|
||||
{
|
||||
|
||||
@@ -189,7 +189,7 @@ namespace Emby.Dlna.Server
|
||||
builder.Append("<icon>");
|
||||
|
||||
builder.Append("<mimetype>")
|
||||
.Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
|
||||
.Append(SecurityElement.Escape(icon.MimeType))
|
||||
.Append("</mimetype>");
|
||||
builder.Append("<width>")
|
||||
.Append(SecurityElement.Escape(icon.Width.ToString(CultureInfo.InvariantCulture)))
|
||||
@@ -198,7 +198,7 @@ namespace Emby.Dlna.Server
|
||||
.Append(SecurityElement.Escape(icon.Height.ToString(CultureInfo.InvariantCulture)))
|
||||
.Append("</height>");
|
||||
builder.Append("<depth>")
|
||||
.Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
|
||||
.Append(SecurityElement.Escape(icon.Depth))
|
||||
.Append("</depth>");
|
||||
builder.Append("<url>")
|
||||
.Append(BuildUrl(icon.Url))
|
||||
@@ -219,10 +219,10 @@ namespace Emby.Dlna.Server
|
||||
builder.Append("<service>");
|
||||
|
||||
builder.Append("<serviceType>")
|
||||
.Append(SecurityElement.Escape(service.ServiceType ?? string.Empty))
|
||||
.Append(SecurityElement.Escape(service.ServiceType))
|
||||
.Append("</serviceType>");
|
||||
builder.Append("<serviceId>")
|
||||
.Append(SecurityElement.Escape(service.ServiceId ?? string.Empty))
|
||||
.Append(SecurityElement.Escape(service.ServiceId))
|
||||
.Append("</serviceId>");
|
||||
builder.Append("<SCPDURL>")
|
||||
.Append(BuildUrl(service.ScpdUrl))
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace Emby.Dlna.Service
|
||||
builder.Append("<action>");
|
||||
|
||||
builder.Append("<name>")
|
||||
.Append(SecurityElement.Escape(item.Name ?? string.Empty))
|
||||
.Append(SecurityElement.Escape(item.Name))
|
||||
.Append("</name>");
|
||||
|
||||
builder.Append("<argumentList>");
|
||||
@@ -48,13 +48,13 @@ namespace Emby.Dlna.Service
|
||||
builder.Append("<argument>");
|
||||
|
||||
builder.Append("<name>")
|
||||
.Append(SecurityElement.Escape(argument.Name ?? string.Empty))
|
||||
.Append(SecurityElement.Escape(argument.Name))
|
||||
.Append("</name>");
|
||||
builder.Append("<direction>")
|
||||
.Append(SecurityElement.Escape(argument.Direction ?? string.Empty))
|
||||
.Append(SecurityElement.Escape(argument.Direction))
|
||||
.Append("</direction>");
|
||||
builder.Append("<relatedStateVariable>")
|
||||
.Append(SecurityElement.Escape(argument.RelatedStateVariable ?? string.Empty))
|
||||
.Append(SecurityElement.Escape(argument.RelatedStateVariable))
|
||||
.Append("</relatedStateVariable>");
|
||||
|
||||
builder.Append("</argument>");
|
||||
@@ -81,10 +81,10 @@ namespace Emby.Dlna.Service
|
||||
.Append("\">");
|
||||
|
||||
builder.Append("<name>")
|
||||
.Append(SecurityElement.Escape(item.Name ?? string.Empty))
|
||||
.Append(SecurityElement.Escape(item.Name))
|
||||
.Append("</name>");
|
||||
builder.Append("<dataType>")
|
||||
.Append(SecurityElement.Escape(item.DataType ?? string.Empty))
|
||||
.Append(SecurityElement.Escape(item.DataType))
|
||||
.Append("</dataType>");
|
||||
|
||||
if (item.AllowedValues.Count > 0)
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||
@@ -24,7 +28,7 @@
|
||||
<!-- Code analysers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
@@ -101,8 +102,7 @@ namespace Emby.Drawing
|
||||
public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
|
||||
{
|
||||
var file = await ProcessImage(options).ConfigureAwait(false);
|
||||
|
||||
using (var fileStream = AsyncFile.OpenRead(file.Item1))
|
||||
using (var fileStream = AsyncFile.OpenRead(file.Path))
|
||||
{
|
||||
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
|
||||
}
|
||||
@@ -117,7 +117,7 @@ namespace Emby.Drawing
|
||||
=> _transparentImageTypes.Contains(Path.GetExtension(path));
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<(string path, string? mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options)
|
||||
public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options)
|
||||
{
|
||||
ItemImageInfo originalImage = options.Image;
|
||||
BaseItem item = options.Item;
|
||||
@@ -130,20 +130,22 @@ namespace Emby.Drawing
|
||||
originalImageSize = new ImageDimensions(originalImage.Width, originalImage.Height);
|
||||
}
|
||||
|
||||
var mimeType = MimeTypes.GetMimeType(originalImagePath);
|
||||
if (!_imageEncoder.SupportsImageEncoding)
|
||||
{
|
||||
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
|
||||
return (originalImagePath, mimeType, dateModified);
|
||||
}
|
||||
|
||||
var supportedImageInfo = await GetSupportedImage(originalImagePath, dateModified).ConfigureAwait(false);
|
||||
originalImagePath = supportedImageInfo.path;
|
||||
originalImagePath = supportedImageInfo.Path;
|
||||
|
||||
if (!File.Exists(originalImagePath))
|
||||
// Original file doesn't exist, or original file is gif.
|
||||
if (!File.Exists(originalImagePath) || string.Equals(mimeType, MediaTypeNames.Image.Gif, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
|
||||
return (originalImagePath, mimeType, dateModified);
|
||||
}
|
||||
|
||||
dateModified = supportedImageInfo.dateModified;
|
||||
dateModified = supportedImageInfo.DateModified;
|
||||
bool requiresTransparency = _transparentImageTypes.Contains(Path.GetExtension(originalImagePath));
|
||||
|
||||
bool autoOrient = false;
|
||||
@@ -243,7 +245,7 @@ namespace Emby.Drawing
|
||||
return ImageFormat.Jpg;
|
||||
}
|
||||
|
||||
private string? GetMimeType(ImageFormat format, string path)
|
||||
private string GetMimeType(ImageFormat format, string path)
|
||||
=> format switch
|
||||
{
|
||||
ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
|
||||
@@ -437,7 +439,7 @@ namespace Emby.Drawing
|
||||
.ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
|
||||
private async Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
|
||||
{
|
||||
var inputFormat = Path.GetExtension(originalImagePath)
|
||||
.TrimStart('.')
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Emby.Naming.AudioBook
|
||||
public class AudioBookListResolver
|
||||
{
|
||||
private readonly NamingOptions _options;
|
||||
private readonly AudioBookResolver _audioBookResolver;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AudioBookListResolver"/> class.
|
||||
@@ -22,6 +23,7 @@ namespace Emby.Naming.AudioBook
|
||||
public AudioBookListResolver(NamingOptions options)
|
||||
{
|
||||
_options = options;
|
||||
_audioBookResolver = new AudioBookResolver(_options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -31,21 +33,18 @@ namespace Emby.Naming.AudioBook
|
||||
/// <returns>Returns IEnumerable of <see cref="AudioBookInfo"/>.</returns>
|
||||
public IEnumerable<AudioBookInfo> Resolve(IEnumerable<FileSystemMetadata> files)
|
||||
{
|
||||
var audioBookResolver = new AudioBookResolver(_options);
|
||||
|
||||
// File with empty fullname will be sorted out here.
|
||||
var audiobookFileInfos = files
|
||||
.Select(i => audioBookResolver.Resolve(i.FullName))
|
||||
.Select(i => _audioBookResolver.Resolve(i.FullName))
|
||||
.OfType<AudioBookFileInfo>()
|
||||
.ToList();
|
||||
|
||||
var stackResult = new StackResolver(_options)
|
||||
.ResolveAudioBooks(audiobookFileInfos);
|
||||
var stackResult = StackResolver.ResolveAudioBooks(audiobookFileInfos);
|
||||
|
||||
foreach (var stack in stackResult)
|
||||
{
|
||||
var stackFiles = stack.Files
|
||||
.Select(i => audioBookResolver.Resolve(i))
|
||||
.Select(i => _audioBookResolver.Resolve(i))
|
||||
.OfType<AudioBookFileInfo>()
|
||||
.ToList();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
using Jellyfin.Extensions;
|
||||
|
||||
namespace Emby.Naming.AudioBook
|
||||
{
|
||||
@@ -37,7 +37,7 @@ namespace Emby.Naming.AudioBook
|
||||
var extension = Path.GetExtension(path);
|
||||
|
||||
// Check supported extensions
|
||||
if (!_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
if (!_options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma warning disable CA1819
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Emby.Naming.Video;
|
||||
@@ -124,11 +125,11 @@ namespace Emby.Naming.Common
|
||||
token: "DSR")
|
||||
};
|
||||
|
||||
VideoFileStackingExpressions = new[]
|
||||
VideoFileStackingRules = new[]
|
||||
{
|
||||
"(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(?<ignore>.*?)(?<extension>\\.[^.]+)$",
|
||||
"(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$",
|
||||
"(?<title>.*?)(?<volume>[ ._-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$"
|
||||
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
|
||||
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false),
|
||||
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?<number>[a-d])(?:\.[^.]+)?$", false)
|
||||
};
|
||||
|
||||
CleanDateTimes = new[]
|
||||
@@ -403,6 +404,12 @@ namespace Emby.Naming.Common
|
||||
|
||||
VideoExtraRules = new[]
|
||||
{
|
||||
new ExtraRule(
|
||||
ExtraType.Trailer,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"trailers",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Trailer,
|
||||
ExtraRuleType.Filename,
|
||||
@@ -463,12 +470,24 @@ namespace Emby.Naming.Common
|
||||
" sample",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.ThemeVideo,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"backdrops",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.ThemeSong,
|
||||
ExtraRuleType.Filename,
|
||||
"theme",
|
||||
MediaType.Audio),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.ThemeSong,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"theme-music",
|
||||
MediaType.Audio),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Scene,
|
||||
ExtraRuleType.Suffix,
|
||||
@@ -563,7 +582,7 @@ namespace Emby.Naming.Common
|
||||
ExtraType.Unknown,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"extras",
|
||||
MediaType.Video),
|
||||
MediaType.Video)
|
||||
};
|
||||
|
||||
Format3DRules = new[]
|
||||
@@ -675,9 +694,29 @@ namespace Emby.Naming.Common
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
AllExtrasTypesFolderNames = new Dictionary<string, ExtraType>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["trailers"] = ExtraType.Trailer,
|
||||
["theme-music"] = ExtraType.ThemeSong,
|
||||
["backdrops"] = ExtraType.ThemeVideo,
|
||||
["extras"] = ExtraType.Unknown,
|
||||
["behind the scenes"] = ExtraType.BehindTheScenes,
|
||||
["deleted scenes"] = ExtraType.DeletedScene,
|
||||
["interviews"] = ExtraType.Interview,
|
||||
["scenes"] = ExtraType.Scene,
|
||||
["samples"] = ExtraType.Sample,
|
||||
["shorts"] = ExtraType.Clip,
|
||||
["featurettes"] = ExtraType.Clip
|
||||
};
|
||||
|
||||
Compile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the folder name to extra types mapping.
|
||||
/// </summary>
|
||||
public Dictionary<string, ExtraType> AllExtrasTypesFolderNames { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of audio file extensions.
|
||||
/// </summary>
|
||||
@@ -759,9 +798,9 @@ namespace Emby.Naming.Common
|
||||
public Format3DRule[] Format3DRules { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of raw video file-stacking expressions strings.
|
||||
/// Gets the file stacking rules.
|
||||
/// </summary>
|
||||
public string[] VideoFileStackingExpressions { get; set; }
|
||||
public FileStackRule[] VideoFileStackingRules { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of raw clean DateTimes regular expressions strings.
|
||||
@@ -783,11 +822,6 @@ namespace Emby.Naming.Common
|
||||
/// </summary>
|
||||
public ExtraRule[] VideoExtraRules { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets list of video file-stack regular expressions.
|
||||
/// </summary>
|
||||
public Regex[] VideoFileStackingRegexes { get; private set; } = Array.Empty<Regex>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets list of clean datetime regular expressions.
|
||||
/// </summary>
|
||||
@@ -813,7 +847,6 @@ namespace Emby.Naming.Common
|
||||
/// </summary>
|
||||
public void Compile()
|
||||
{
|
||||
VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray();
|
||||
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
|
||||
CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
|
||||
EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
|
||||
<!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
|
||||
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
|
||||
@@ -38,13 +42,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
using Jellyfin.Extensions;
|
||||
|
||||
namespace Emby.Naming.Subtitles
|
||||
{
|
||||
@@ -34,7 +35,7 @@ namespace Emby.Naming.Subtitles
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path);
|
||||
if (!_options.SubtitleFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
if (!_options.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -42,11 +43,11 @@ namespace Emby.Naming.Subtitles
|
||||
var flags = GetFlags(path);
|
||||
var info = new SubtitleInfo(
|
||||
path,
|
||||
_options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)),
|
||||
_options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)));
|
||||
_options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)),
|
||||
_options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparer.OrdinalIgnoreCase)
|
||||
&& !_options.SubtitleForcedFlags.Contains(i, StringComparer.OrdinalIgnoreCase))
|
||||
var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparison.OrdinalIgnoreCase)
|
||||
&& !_options.SubtitleForcedFlags.Contains(i, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
// Should have a name, language and file extension
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Video;
|
||||
using Jellyfin.Extensions;
|
||||
|
||||
namespace Emby.Naming.TV
|
||||
{
|
||||
@@ -48,7 +48,7 @@ namespace Emby.Naming.TV
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
// Check supported extensions
|
||||
if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
if (!_options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// It's not supported. Check stub extensions
|
||||
if (!StubResolver.TryResolveFile(path, _options, out stubType))
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace Emby.Naming.TV
|
||||
/// <param name="supportSpecialAliases">if set to <c>true</c> [support special aliases].</param>
|
||||
/// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param>
|
||||
/// <returns>System.Nullable{System.Int32}.</returns>
|
||||
private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPath(
|
||||
private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
|
||||
string path,
|
||||
bool supportSpecialAliases,
|
||||
bool supportNumericSeasonFolders)
|
||||
@@ -99,7 +99,7 @@ namespace Emby.Naming.TV
|
||||
if (filename.Contains(name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase));
|
||||
if (result.seasonNumber.HasValue)
|
||||
if (result.SeasonNumber.HasValue)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
@@ -142,7 +142,7 @@ namespace Emby.Naming.TV
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>System.Nullable{System.Int32}.</returns>
|
||||
private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPathSubstring(ReadOnlySpan<char> path)
|
||||
private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPathSubstring(ReadOnlySpan<char> path)
|
||||
{
|
||||
var numericStart = -1;
|
||||
var length = 0;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Globalization;
|
||||
using Emby.Naming.Common;
|
||||
|
||||
namespace Emby.Naming.TV
|
||||
@@ -51,7 +50,7 @@ namespace Emby.Naming.TV
|
||||
if (expression.IsNamed)
|
||||
{
|
||||
result.SeriesName = match.Groups["seriesname"].Value;
|
||||
result.Success = !string.IsNullOrEmpty(result.SeriesName) && !string.IsNullOrEmpty(match.Groups["seasonnumber"]?.Value);
|
||||
result.Success = !string.IsNullOrEmpty(result.SeriesName) && !match.Groups["seasonnumber"].ValueSpan.IsEmpty;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
@@ -9,45 +9,27 @@ namespace Emby.Naming.Video
|
||||
/// <summary>
|
||||
/// Resolve if file is extra for video.
|
||||
/// </summary>
|
||||
public class ExtraResolver
|
||||
public static class ExtraRuleResolver
|
||||
{
|
||||
private static readonly char[] _digits = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
|
||||
private readonly NamingOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExtraResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="options"><see cref="NamingOptions"/> object containing VideoExtraRules and passed to <see cref="AudioFileParser"/> and <see cref="VideoResolver"/>.</param>
|
||||
public ExtraResolver(NamingOptions options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
private static readonly char[] _digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve if file is extra.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to file.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
|
||||
public ExtraResult GetExtraInfo(string path)
|
||||
public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions)
|
||||
{
|
||||
var result = new ExtraResult();
|
||||
|
||||
for (var i = 0; i < _options.VideoExtraRules.Length; i++)
|
||||
for (var i = 0; i < namingOptions.VideoExtraRules.Length; i++)
|
||||
{
|
||||
var rule = _options.VideoExtraRules[i];
|
||||
if (rule.MediaType == MediaType.Audio)
|
||||
var rule = namingOptions.VideoExtraRules[i];
|
||||
if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions))
|
||||
|| (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions)))
|
||||
{
|
||||
if (!AudioFileParser.IsAudioFile(path, _options))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (rule.MediaType == MediaType.Video)
|
||||
{
|
||||
if (!VideoResolver.IsVideoFile(path, _options))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var pathSpan = path.AsSpan();
|
||||
@@ -76,9 +58,9 @@ namespace Emby.Naming.Video
|
||||
{
|
||||
var filename = Path.GetFileName(path);
|
||||
|
||||
var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
|
||||
var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
if (regex.IsMatch(filename))
|
||||
if (isMatch)
|
||||
{
|
||||
result.ExtraType = rule.ExtraType;
|
||||
result.Rule = rule;
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Extensions;
|
||||
|
||||
namespace Emby.Naming.Video
|
||||
{
|
||||
@@ -12,25 +12,30 @@ namespace Emby.Naming.Video
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileStack"/> class.
|
||||
/// </summary>
|
||||
public FileStack()
|
||||
/// <param name="name">The stack name.</param>
|
||||
/// <param name="isDirectory">Whether the stack files are directories.</param>
|
||||
/// <param name="files">The stack files.</param>
|
||||
public FileStack(string name, bool isDirectory, IReadOnlyList<string> files)
|
||||
{
|
||||
Files = new List<string>();
|
||||
Name = name;
|
||||
IsDirectoryStack = isDirectory;
|
||||
Files = files;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets name of file stack.
|
||||
/// Gets the name of file stack.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of paths in stack.
|
||||
/// Gets the list of paths in stack.
|
||||
/// </summary>
|
||||
public List<string> Files { get; set; }
|
||||
public IReadOnlyList<string> Files { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether stack is directory stack.
|
||||
/// Gets a value indicating whether stack is directory stack.
|
||||
/// </summary>
|
||||
public bool IsDirectoryStack { get; set; }
|
||||
public bool IsDirectoryStack { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Helper function to determine if path is in the stack.
|
||||
@@ -40,12 +45,12 @@ namespace Emby.Naming.Video
|
||||
/// <returns>True if file is in the stack.</returns>
|
||||
public bool ContainsFile(string file, bool isDirectory)
|
||||
{
|
||||
if (IsDirectoryStack == isDirectory)
|
||||
if (string.IsNullOrEmpty(file))
|
||||
{
|
||||
return Files.Contains(file, StringComparer.OrdinalIgnoreCase);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
return IsDirectoryStack == isDirectory && Files.Contains(file, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
48
Emby.Naming/Video/FileStackRule.cs
Normal file
48
Emby.Naming/Video/FileStackRule.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Emby.Naming.Video;
|
||||
|
||||
/// <summary>
|
||||
/// Regex based rule for file stacking (eg. disc1, disc2).
|
||||
/// </summary>
|
||||
public class FileStackRule
|
||||
{
|
||||
private readonly Regex _tokenRegex;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileStackRule"/> class.
|
||||
/// </summary>
|
||||
/// <param name="token">Token.</param>
|
||||
/// <param name="isNumerical">Whether the file stack rule uses numerical or alphabetical numbering.</param>
|
||||
public FileStackRule(string token, bool isNumerical)
|
||||
{
|
||||
_tokenRegex = new Regex(token, RegexOptions.IgnoreCase);
|
||||
IsNumerical = isNumerical;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the rule uses numerical or alphabetical numbering.
|
||||
/// </summary>
|
||||
public bool IsNumerical { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Match the input against the rule regex.
|
||||
/// </summary>
|
||||
/// <param name="input">The input.</param>
|
||||
/// <param name="result">The part type and number or <c>null</c>.</param>
|
||||
/// <returns>A value indicating whether the input matched the rule.</returns>
|
||||
public bool Match(string input, [NotNullWhen(true)] out (string StackName, string PartType, string PartNumber)? result)
|
||||
{
|
||||
result = null;
|
||||
var match = _tokenRegex.Match(input);
|
||||
if (!match.Success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var partType = match.Groups["parttype"].Success ? match.Groups["parttype"].Value : "unknown";
|
||||
result = (match.Groups["filename"].Value, partType, match.Groups["number"].Value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace Emby.Naming.Video
|
||||
public static class Format3DParser
|
||||
{
|
||||
// Static default result to save on allocation costs.
|
||||
private static readonly Format3DResult _defaultResult = new (false, null);
|
||||
private static readonly Format3DResult _defaultResult = new(false, null);
|
||||
|
||||
/// <summary>
|
||||
/// Parse 3D format related flags.
|
||||
|
||||
@@ -2,7 +2,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Emby.Naming.AudioBook;
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Model.IO;
|
||||
@@ -12,37 +11,28 @@ namespace Emby.Naming.Video
|
||||
/// <summary>
|
||||
/// Resolve <see cref="FileStack"/> from list of paths.
|
||||
/// </summary>
|
||||
public class StackResolver
|
||||
public static class StackResolver
|
||||
{
|
||||
private readonly NamingOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StackResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileStackingRegexes and passes options to <see cref="VideoResolver"/>.</param>
|
||||
public StackResolver(NamingOptions options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves only directories from paths.
|
||||
/// </summary>
|
||||
/// <param name="files">List of paths.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
|
||||
public IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files)
|
||||
public static IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files, NamingOptions namingOptions)
|
||||
{
|
||||
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }));
|
||||
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }), namingOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves only files from paths.
|
||||
/// </summary>
|
||||
/// <param name="files">List of paths.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <returns>Enumerable <see cref="FileStack"/> of files.</returns>
|
||||
public IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files)
|
||||
public static IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files, NamingOptions namingOptions)
|
||||
{
|
||||
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }));
|
||||
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }), namingOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -50,7 +40,7 @@ namespace Emby.Naming.Video
|
||||
/// </summary>
|
||||
/// <param name="files">List of paths.</param>
|
||||
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
|
||||
public IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
|
||||
public static IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
|
||||
{
|
||||
var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path));
|
||||
|
||||
@@ -60,19 +50,13 @@ namespace Emby.Naming.Video
|
||||
{
|
||||
foreach (var file in directory)
|
||||
{
|
||||
var stack = new FileStack { Name = Path.GetFileNameWithoutExtension(file.Path), IsDirectoryStack = false };
|
||||
stack.Files.Add(file.Path);
|
||||
var stack = new FileStack(Path.GetFileNameWithoutExtension(file.Path), false, new[] { file.Path });
|
||||
yield return stack;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false };
|
||||
foreach (var file in directory)
|
||||
{
|
||||
stack.Files.Add(file.Path);
|
||||
}
|
||||
|
||||
var stack = new FileStack(Path.GetFileName(directory.Key), false, directory.Select(f => f.Path).ToArray());
|
||||
yield return stack;
|
||||
}
|
||||
}
|
||||
@@ -82,158 +66,91 @@ namespace Emby.Naming.Video
|
||||
/// Resolves videos from paths.
|
||||
/// </summary>
|
||||
/// <param name="files">List of paths.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
|
||||
public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
|
||||
public static IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions)
|
||||
{
|
||||
var list = files
|
||||
.Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options))
|
||||
.OrderBy(i => i.FullName)
|
||||
.ToList();
|
||||
var potentialFiles = files
|
||||
.Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, namingOptions) || VideoResolver.IsStubFile(i.FullName, namingOptions))
|
||||
.OrderBy(i => i.FullName);
|
||||
|
||||
var expressions = _options.VideoFileStackingRegexes;
|
||||
|
||||
for (var i = 0; i < list.Count; i++)
|
||||
var potentialStacks = new Dictionary<string, StackMetadata>();
|
||||
foreach (var file in potentialFiles)
|
||||
{
|
||||
var offset = 0;
|
||||
|
||||
var file1 = list[i];
|
||||
|
||||
var expressionIndex = 0;
|
||||
while (expressionIndex < expressions.Length)
|
||||
var name = file.Name;
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
var exp = expressions[expressionIndex];
|
||||
var stack = new FileStack();
|
||||
name = Path.GetFileName(file.FullName);
|
||||
}
|
||||
|
||||
// (Title)(Volume)(Ignore)(Extension)
|
||||
var match1 = FindMatch(file1, exp, offset);
|
||||
|
||||
if (match1.Success)
|
||||
for (var i = 0; i < namingOptions.VideoFileStackingRules.Length; i++)
|
||||
{
|
||||
var rule = namingOptions.VideoFileStackingRules[i];
|
||||
if (!rule.Match(name, out var stackParsingResult))
|
||||
{
|
||||
var title1 = match1.Groups["title"].Value;
|
||||
var volume1 = match1.Groups["volume"].Value;
|
||||
var ignore1 = match1.Groups["ignore"].Value;
|
||||
var extension1 = match1.Groups["extension"].Value;
|
||||
continue;
|
||||
}
|
||||
|
||||
var j = i + 1;
|
||||
while (j < list.Count)
|
||||
var stackName = stackParsingResult.Value.StackName;
|
||||
var partNumber = stackParsingResult.Value.PartNumber;
|
||||
var partType = stackParsingResult.Value.PartType;
|
||||
|
||||
if (!potentialStacks.TryGetValue(stackName, out var stackResult))
|
||||
{
|
||||
stackResult = new StackMetadata(file.IsDirectory, rule.IsNumerical, partType);
|
||||
potentialStacks[stackName] = stackResult;
|
||||
}
|
||||
|
||||
if (stackResult.Parts.Count > 0)
|
||||
{
|
||||
if (stackResult.IsDirectory != file.IsDirectory
|
||||
|| !string.Equals(partType, stackResult.PartType, StringComparison.OrdinalIgnoreCase)
|
||||
|| stackResult.ContainsPart(partNumber))
|
||||
{
|
||||
var file2 = list[j];
|
||||
|
||||
if (file1.IsDirectory != file2.IsDirectory)
|
||||
{
|
||||
j++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// (Title)(Volume)(Ignore)(Extension)
|
||||
var match2 = FindMatch(file2, exp, offset);
|
||||
|
||||
if (match2.Success)
|
||||
{
|
||||
var title2 = match2.Groups[1].Value;
|
||||
var volume2 = match2.Groups[2].Value;
|
||||
var ignore2 = match2.Groups[3].Value;
|
||||
var extension2 = match2.Groups[4].Value;
|
||||
|
||||
if (string.Equals(title1, title2, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!string.Equals(volume1, volume2, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (stack.Files.Count == 0)
|
||||
{
|
||||
stack.Name = title1 + ignore1;
|
||||
stack.IsDirectoryStack = file1.IsDirectory;
|
||||
stack.Files.Add(file1.FullName);
|
||||
}
|
||||
|
||||
stack.Files.Add(file2.FullName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Sequel
|
||||
offset = 0;
|
||||
expressionIndex++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (!string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// False positive, try again with offset
|
||||
offset = match1.Groups[3].Index;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Extension mismatch
|
||||
offset = 0;
|
||||
expressionIndex++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Title mismatch
|
||||
offset = 0;
|
||||
expressionIndex++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No match 2, next expression
|
||||
offset = 0;
|
||||
expressionIndex++;
|
||||
break;
|
||||
}
|
||||
|
||||
j++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (j == list.Count)
|
||||
if (rule.IsNumerical != stackResult.IsNumerical)
|
||||
{
|
||||
expressionIndex = expressions.Length;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No match 1
|
||||
offset = 0;
|
||||
expressionIndex++;
|
||||
}
|
||||
|
||||
if (stack.Files.Count > 1)
|
||||
{
|
||||
yield return stack;
|
||||
i += stack.Files.Count - 1;
|
||||
break;
|
||||
}
|
||||
stackResult.Parts.Add(partNumber, file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetRegexInput(FileSystemMetadata file)
|
||||
{
|
||||
// For directories, dummy up an extension otherwise the expressions will fail
|
||||
var input = !file.IsDirectory
|
||||
? file.FullName
|
||||
: file.FullName + ".mkv";
|
||||
|
||||
return Path.GetFileName(input);
|
||||
}
|
||||
|
||||
private static Match FindMatch(FileSystemMetadata input, Regex regex, int offset)
|
||||
{
|
||||
var regexInput = GetRegexInput(input);
|
||||
|
||||
if (offset < 0 || offset >= regexInput.Length)
|
||||
foreach (var (fileName, stack) in potentialStacks)
|
||||
{
|
||||
return Match.Empty;
|
||||
if (stack.Parts.Count < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return new FileStack(fileName, stack.IsDirectory, stack.Parts.Select(kv => kv.Value.FullName).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
private class StackMetadata
|
||||
{
|
||||
public StackMetadata(bool isDirectory, bool isNumerical, string partType)
|
||||
{
|
||||
Parts = new Dictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase);
|
||||
IsDirectory = isDirectory;
|
||||
IsNumerical = isNumerical;
|
||||
PartType = partType;
|
||||
}
|
||||
|
||||
return regex.Match(regexInput, offset);
|
||||
public Dictionary<string, FileSystemMetadata> Parts { get; }
|
||||
|
||||
public bool IsDirectory { get; }
|
||||
|
||||
public bool IsNumerical { get; }
|
||||
|
||||
public string PartType { get; }
|
||||
|
||||
public bool ContainsPart(string partNumber) => Parts.ContainsKey(partNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
using Jellyfin.Extensions;
|
||||
|
||||
namespace Emby.Naming.Video
|
||||
{
|
||||
@@ -28,7 +28,7 @@ namespace Emby.Naming.Video
|
||||
|
||||
var extension = Path.GetExtension(path);
|
||||
|
||||
if (!options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace Emby.Naming.Video
|
||||
{
|
||||
@@ -17,7 +18,6 @@ namespace Emby.Naming.Video
|
||||
Name = name;
|
||||
|
||||
Files = Array.Empty<VideoFileInfo>();
|
||||
Extras = Array.Empty<VideoFileInfo>();
|
||||
AlternateVersions = Array.Empty<VideoFileInfo>();
|
||||
}
|
||||
|
||||
@@ -39,16 +39,15 @@ namespace Emby.Naming.Video
|
||||
/// <value>The files.</value>
|
||||
public IReadOnlyList<VideoFileInfo> Files { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the extras.
|
||||
/// </summary>
|
||||
/// <value>The extras.</value>
|
||||
public IReadOnlyList<VideoFileInfo> Extras { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the alternate versions.
|
||||
/// </summary>
|
||||
/// <value>The alternate versions.</value>
|
||||
public IReadOnlyList<VideoFileInfo> AlternateVersions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the extra type.
|
||||
/// </summary>
|
||||
public ExtraType? ExtraType { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace Emby.Naming.Video
|
||||
@@ -17,29 +16,41 @@ namespace Emby.Naming.Video
|
||||
/// <summary>
|
||||
/// Resolves alternative versions and extras from list of video files.
|
||||
/// </summary>
|
||||
/// <param name="files">List of related video files.</param>
|
||||
/// <param name="videoInfos">List of related video files.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
|
||||
/// <param name="parseName">Whether to parse the name or use the filename.</param>
|
||||
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
|
||||
public static IEnumerable<VideoInfo> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true)
|
||||
public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true)
|
||||
{
|
||||
var videoInfos = files
|
||||
.Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))
|
||||
.OfType<VideoFileInfo>()
|
||||
.ToList();
|
||||
|
||||
// Filter out all extras, otherwise they could cause stacks to not be resolved
|
||||
// See the unit test TestStackedWithTrailer
|
||||
var nonExtras = videoInfos
|
||||
.Where(i => i.ExtraType == null)
|
||||
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
|
||||
|
||||
var stackResult = new StackResolver(namingOptions)
|
||||
.Resolve(nonExtras).ToList();
|
||||
var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList();
|
||||
|
||||
var remainingFiles = videoInfos
|
||||
.Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory)))
|
||||
.ToList();
|
||||
var remainingFiles = new List<VideoFileInfo>();
|
||||
var standaloneMedia = new List<VideoFileInfo>();
|
||||
|
||||
for (var i = 0; i < videoInfos.Count; i++)
|
||||
{
|
||||
var current = videoInfos[i];
|
||||
if (stackResult.Any(s => s.ContainsFile(current.Path, current.IsDirectory)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current.ExtraType == null)
|
||||
{
|
||||
standaloneMedia.Add(current);
|
||||
}
|
||||
else
|
||||
{
|
||||
remainingFiles.Add(current);
|
||||
}
|
||||
}
|
||||
|
||||
var list = new List<VideoInfo>();
|
||||
|
||||
@@ -47,38 +58,20 @@ namespace Emby.Naming.Video
|
||||
{
|
||||
var info = new VideoInfo(stack.Name)
|
||||
{
|
||||
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions))
|
||||
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName))
|
||||
.OfType<VideoFileInfo>()
|
||||
.ToList()
|
||||
};
|
||||
|
||||
info.Year = info.Files[0].Year;
|
||||
|
||||
var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters);
|
||||
|
||||
if (extras.Count > 0)
|
||||
{
|
||||
info.Extras = extras;
|
||||
}
|
||||
|
||||
list.Add(info);
|
||||
}
|
||||
|
||||
var standaloneMedia = remainingFiles
|
||||
.Where(i => i.ExtraType == null)
|
||||
.ToList();
|
||||
|
||||
foreach (var media in standaloneMedia)
|
||||
{
|
||||
var info = new VideoInfo(media.Name) { Files = new[] { media } };
|
||||
|
||||
info.Year = info.Files[0].Year;
|
||||
|
||||
remainingFiles.Remove(media);
|
||||
var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
|
||||
|
||||
info.Extras = extras;
|
||||
|
||||
list.Add(info);
|
||||
}
|
||||
|
||||
@@ -87,58 +80,12 @@ namespace Emby.Naming.Video
|
||||
list = GetVideosGroupedByVersion(list, namingOptions);
|
||||
}
|
||||
|
||||
// If there's only one resolved video, use the folder name as well to find extras
|
||||
if (list.Count == 1)
|
||||
{
|
||||
var info = list[0];
|
||||
var videoPath = list[0].Files[0].Path;
|
||||
var parentPath = Path.GetDirectoryName(videoPath.AsSpan());
|
||||
|
||||
if (!parentPath.IsEmpty)
|
||||
{
|
||||
var folderName = Path.GetFileName(parentPath);
|
||||
if (!folderName.IsEmpty)
|
||||
{
|
||||
var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
|
||||
extras.AddRange(info.Extras);
|
||||
info.Extras = extras;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the extras that are just based on file name as well
|
||||
var extrasByFileName = remainingFiles
|
||||
.Where(i => i.ExtraRule != null && i.ExtraRule.RuleType == ExtraRuleType.Filename)
|
||||
.ToList();
|
||||
|
||||
remainingFiles = remainingFiles
|
||||
.Except(extrasByFileName)
|
||||
.ToList();
|
||||
|
||||
extrasByFileName.AddRange(info.Extras);
|
||||
info.Extras = extrasByFileName;
|
||||
}
|
||||
|
||||
// If there's only one video, accept all trailers
|
||||
// Be lenient because people use all kinds of mishmash conventions with trailers.
|
||||
if (list.Count == 1)
|
||||
{
|
||||
var trailers = remainingFiles
|
||||
.Where(i => i.ExtraType == ExtraType.Trailer)
|
||||
.ToList();
|
||||
|
||||
trailers.AddRange(list[0].Extras);
|
||||
list[0].Extras = trailers;
|
||||
|
||||
remainingFiles = remainingFiles
|
||||
.Except(trailers)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Whatever files are left, just add them
|
||||
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
|
||||
{
|
||||
Files = new[] { i },
|
||||
Year = i.Year
|
||||
Year = i.Year,
|
||||
ExtraType = i.ExtraType
|
||||
}));
|
||||
|
||||
return list;
|
||||
@@ -162,6 +109,11 @@ namespace Emby.Naming.Video
|
||||
for (var i = 0; i < videos.Count; i++)
|
||||
{
|
||||
var video = videos[i];
|
||||
if (video.ExtraType != null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
|
||||
{
|
||||
return videos;
|
||||
@@ -178,17 +130,14 @@ namespace Emby.Naming.Video
|
||||
|
||||
var alternateVersionsLen = videos.Count - 1;
|
||||
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
|
||||
var extras = new List<VideoFileInfo>(list[0].Extras);
|
||||
for (int i = 0; i < alternateVersionsLen; i++)
|
||||
{
|
||||
var video = videos[i + 1];
|
||||
alternateVersions[i] = video.Files[0];
|
||||
extras.AddRange(video.Extras);
|
||||
}
|
||||
|
||||
list[0].AlternateVersions = alternateVersions;
|
||||
list[0].Name = folderName.ToString();
|
||||
list[0].Extras = extras;
|
||||
|
||||
return list;
|
||||
}
|
||||
@@ -230,7 +179,7 @@ namespace Emby.Naming.Video
|
||||
var tmpTestFilename = testFilename.ToString();
|
||||
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
|
||||
{
|
||||
tmpTestFilename = cleanName.Trim().ToString();
|
||||
tmpTestFilename = cleanName.Trim();
|
||||
}
|
||||
|
||||
// The CleanStringParser should have removed common keywords etc.
|
||||
@@ -238,67 +187,5 @@ namespace Emby.Naming.Video
|
||||
|| testFilename[0] == '-'
|
||||
|| Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
|
||||
{
|
||||
return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
|
||||
}
|
||||
|
||||
private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName, ReadOnlySpan<char> trimmedBaseName)
|
||||
{
|
||||
if (baseName.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
|
||||
|| (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles].
|
||||
/// </summary>
|
||||
/// <param name="remainingFiles">The list of remaining filenames.</param>
|
||||
/// <param name="baseName">The base name to use for the comparison.</param>
|
||||
/// <param name="videoFlagDelimiters">The video flag delimiters.</param>
|
||||
/// <returns>A list of video extras for [baseName].</returns>
|
||||
private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> baseName, ReadOnlySpan<char> videoFlagDelimiters)
|
||||
{
|
||||
return ExtractExtras(remainingFiles, baseName, ReadOnlySpan<char>.Empty, videoFlagDelimiters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles].
|
||||
/// </summary>
|
||||
/// <param name="remainingFiles">The list of remaining filenames.</param>
|
||||
/// <param name="firstBaseName">The first base name to use for the comparison.</param>
|
||||
/// <param name="secondBaseName">The second base name to use for the comparison.</param>
|
||||
/// <param name="videoFlagDelimiters">The video flag delimiters.</param>
|
||||
/// <returns>A list of video extras for [firstBaseName] and [secondBaseName].</returns>
|
||||
private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> firstBaseName, ReadOnlySpan<char> secondBaseName, ReadOnlySpan<char> videoFlagDelimiters)
|
||||
{
|
||||
var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters);
|
||||
var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters);
|
||||
|
||||
var result = new List<VideoFileInfo>();
|
||||
for (var pos = remainingFiles.Count - 1; pos >= 0; pos--)
|
||||
{
|
||||
var file = remainingFiles[pos];
|
||||
if (file.ExtraType == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var filename = file.FileNameWithoutExtension;
|
||||
if (StartsWith(filename, firstBaseName, trimmedFirstBaseName)
|
||||
|| StartsWith(filename, secondBaseName, trimmedSecondBaseName))
|
||||
{
|
||||
result.Add(file);
|
||||
remainingFiles.RemoveAt(pos);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,11 @@ namespace Emby.Naming.Video
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="parseName">Whether to parse the name or use the filename.</param>
|
||||
/// <returns>VideoFileInfo.</returns>
|
||||
public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions)
|
||||
public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true)
|
||||
{
|
||||
return Resolve(path, true, namingOptions);
|
||||
return Resolve(path, true, namingOptions, parseName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -74,7 +75,7 @@ namespace Emby.Naming.Video
|
||||
|
||||
var format3DResult = Format3DParser.Parse(path, namingOptions);
|
||||
|
||||
var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path);
|
||||
var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions);
|
||||
|
||||
var name = Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
|
||||
@@ -24,63 +24,63 @@ namespace Emby.Notifications
|
||||
{
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.ApplicationUpdateInstalled.ToString()
|
||||
Type = nameof(NotificationType.ApplicationUpdateInstalled)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.InstallationFailed.ToString()
|
||||
Type = nameof(NotificationType.InstallationFailed)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.PluginInstalled.ToString()
|
||||
Type = nameof(NotificationType.PluginInstalled)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.PluginError.ToString()
|
||||
Type = nameof(NotificationType.PluginError)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.PluginUninstalled.ToString()
|
||||
Type = nameof(NotificationType.PluginUninstalled)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.PluginUpdateInstalled.ToString()
|
||||
Type = nameof(NotificationType.PluginUpdateInstalled)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.ServerRestartRequired.ToString()
|
||||
Type = nameof(NotificationType.ServerRestartRequired)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.TaskFailed.ToString()
|
||||
Type = nameof(NotificationType.TaskFailed)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.NewLibraryContent.ToString()
|
||||
Type = nameof(NotificationType.NewLibraryContent)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.AudioPlayback.ToString()
|
||||
Type = nameof(NotificationType.AudioPlayback)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.VideoPlayback.ToString()
|
||||
Type = nameof(NotificationType.VideoPlayback)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.AudioPlaybackStopped.ToString()
|
||||
Type = nameof(NotificationType.AudioPlaybackStopped)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.VideoPlaybackStopped.ToString()
|
||||
Type = nameof(NotificationType.VideoPlaybackStopped)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.UserLockedOut.ToString()
|
||||
Type = nameof(NotificationType.UserLockedOut)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.ApplicationUpdateAvailable.ToString()
|
||||
Type = nameof(NotificationType.ApplicationUpdateAvailable)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -98,7 +98,7 @@ namespace Emby.Notifications
|
||||
|
||||
private void Update(NotificationTypeInfo note)
|
||||
{
|
||||
note.Name = _localization.GetLocalizedString("NotificationOption" + note.Type) ?? note.Type;
|
||||
note.Name = _localization.GetLocalizedString("NotificationOption" + note.Type);
|
||||
|
||||
note.IsBasedOnUserEvent = note.Type.IndexOf("Playback", StringComparison.OrdinalIgnoreCase) != -1;
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<!-- Code analyzers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Events;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@@ -104,7 +105,7 @@ namespace Emby.Notifications
|
||||
|
||||
var type = entry.Type;
|
||||
|
||||
if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparer.OrdinalIgnoreCase))
|
||||
if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -60,7 +61,7 @@ namespace Emby.Photos
|
||||
item.SetImagePath(ImageType.Primary, item.Path);
|
||||
|
||||
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
|
||||
if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparer.OrdinalIgnoreCase))
|
||||
if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -301,7 +301,7 @@ namespace Emby.Server.Implementations.AppBase
|
||||
{
|
||||
return _configurations.GetOrAdd(
|
||||
key,
|
||||
(k, configurationManager) =>
|
||||
static (k, configurationManager) =>
|
||||
{
|
||||
var file = configurationManager.GetConfigurationFile(k);
|
||||
|
||||
@@ -371,7 +371,7 @@ namespace Emby.Server.Implementations.AppBase
|
||||
NewConfiguration = configuration
|
||||
});
|
||||
|
||||
_configurations.AddOrUpdate(key, configuration, (k, v) => configuration);
|
||||
_configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
|
||||
|
||||
var path = GetConfigurationFile(key);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.AppBase
|
||||
|
||||
@@ -19,6 +19,7 @@ using Emby.Dlna;
|
||||
using Emby.Dlna.Main;
|
||||
using Emby.Dlna.Ssdp;
|
||||
using Emby.Drawing;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Notifications;
|
||||
using Emby.Photos;
|
||||
using Emby.Server.Implementations.Archiving;
|
||||
@@ -119,7 +120,7 @@ namespace Emby.Server.Implementations
|
||||
/// <summary>
|
||||
/// The disposable parts.
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new ();
|
||||
private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
|
||||
|
||||
private readonly IFileSystem _fileSystemManager;
|
||||
private readonly IConfiguration _startupConfig;
|
||||
@@ -312,22 +313,6 @@ namespace Emby.Server.Implementations
|
||||
? Environment.MachineName
|
||||
: ConfigurationManager.Configuration.ServerName;
|
||||
|
||||
/// <summary>
|
||||
/// Temporary function to migration network settings out of system.xml and into network.xml.
|
||||
/// TODO: remove at the point when a fixed migration path has been decided upon.
|
||||
/// </summary>
|
||||
private void MigrateNetworkConfiguration()
|
||||
{
|
||||
string path = Path.Combine(ConfigurationManager.CommonApplicationPaths.ConfigurationDirectoryPath, "network.xml");
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
var networkSettings = new NetworkConfiguration();
|
||||
ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings);
|
||||
_xmlSerializer.SerializeToFile(networkSettings, path);
|
||||
Logger.LogDebug("Successfully migrated network settings.");
|
||||
}
|
||||
}
|
||||
|
||||
public string ExpandVirtualPath(string path)
|
||||
{
|
||||
var appPaths = ApplicationPaths;
|
||||
@@ -512,8 +497,6 @@ namespace Emby.Server.Implementations
|
||||
|
||||
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
|
||||
|
||||
// Have to migrate settings here as migration subsystem not yet initialised.
|
||||
MigrateNetworkConfiguration();
|
||||
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
|
||||
|
||||
// Initialize runtime stat collection
|
||||
@@ -596,6 +579,7 @@ namespace Emby.Server.Implementations
|
||||
serviceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
|
||||
serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
|
||||
serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
|
||||
serviceCollection.AddSingleton<NamingOptions>();
|
||||
|
||||
serviceCollection.AddSingleton<IMusicManager, MusicManager>();
|
||||
|
||||
@@ -1126,12 +1110,12 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetApiUrlForLocalAccess(bool allowHttps)
|
||||
public string GetApiUrlForLocalAccess(bool allowHttps = true)
|
||||
{
|
||||
// With an empty source, the port will be null
|
||||
string smart = NetManager.GetBindInterface(string.Empty, out _);
|
||||
var scheme = allowHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
|
||||
var port = allowHttps ? HttpsPort : HttpPort;
|
||||
var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
|
||||
int? port = !allowHttps ? HttpPort : null;
|
||||
return GetLocalApiUrl(smart.Trim('/'), scheme, port);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using System.IO;
|
||||
using MediaBrowser.Model.IO;
|
||||
using SharpCompress.Archives.SevenZip;
|
||||
using SharpCompress.Archives.Tar;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Readers.GZip;
|
||||
using SharpCompress.Readers.Zip;
|
||||
|
||||
namespace Emby.Server.Implementations.Archiving
|
||||
{
|
||||
@@ -14,55 +11,6 @@ namespace Emby.Server.Implementations.Archiving
|
||||
/// </summary>
|
||||
public class ZipClient : IZipClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts all.
|
||||
/// </summary>
|
||||
/// <param name="sourceFile">The source file.</param>
|
||||
/// <param name="targetPath">The target path.</param>
|
||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
||||
public void ExtractAll(string sourceFile, string targetPath, bool overwriteExistingFiles)
|
||||
{
|
||||
using var fileStream = File.OpenRead(sourceFile);
|
||||
ExtractAll(fileStream, targetPath, overwriteExistingFiles);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all.
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="targetPath">The target path.</param>
|
||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
||||
public void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles)
|
||||
{
|
||||
using var reader = ReaderFactory.Open(source);
|
||||
var options = new ExtractionOptions
|
||||
{
|
||||
ExtractFullPath = true
|
||||
};
|
||||
|
||||
if (overwriteExistingFiles)
|
||||
{
|
||||
options.Overwrite = true;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(targetPath);
|
||||
reader.WriteAllToDirectory(targetPath, options);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExtractAllFromZip(Stream source, string targetPath, bool overwriteExistingFiles)
|
||||
{
|
||||
using var reader = ZipReader.Open(source);
|
||||
var options = new ExtractionOptions
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = overwriteExistingFiles
|
||||
};
|
||||
|
||||
Directory.CreateDirectory(targetPath);
|
||||
reader.WriteAllToDirectory(targetPath, options);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles)
|
||||
{
|
||||
@@ -94,69 +42,5 @@ namespace Emby.Server.Implementations.Archiving
|
||||
reader.WriteEntryToFile(Path.Combine(targetPath, filename));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all from7z.
|
||||
/// </summary>
|
||||
/// <param name="sourceFile">The source file.</param>
|
||||
/// <param name="targetPath">The target path.</param>
|
||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
||||
public void ExtractAllFrom7z(string sourceFile, string targetPath, bool overwriteExistingFiles)
|
||||
{
|
||||
using var fileStream = File.OpenRead(sourceFile);
|
||||
ExtractAllFrom7z(fileStream, targetPath, overwriteExistingFiles);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all from7z.
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="targetPath">The target path.</param>
|
||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
||||
public void ExtractAllFrom7z(Stream source, string targetPath, bool overwriteExistingFiles)
|
||||
{
|
||||
using var archive = SevenZipArchive.Open(source);
|
||||
using var reader = archive.ExtractAllEntries();
|
||||
var options = new ExtractionOptions
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = overwriteExistingFiles
|
||||
};
|
||||
|
||||
Directory.CreateDirectory(targetPath);
|
||||
reader.WriteAllToDirectory(targetPath, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all from tar.
|
||||
/// </summary>
|
||||
/// <param name="sourceFile">The source file.</param>
|
||||
/// <param name="targetPath">The target path.</param>
|
||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
||||
public void ExtractAllFromTar(string sourceFile, string targetPath, bool overwriteExistingFiles)
|
||||
{
|
||||
using var fileStream = File.OpenRead(sourceFile);
|
||||
ExtractAllFromTar(fileStream, targetPath, overwriteExistingFiles);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all from tar.
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="targetPath">The target path.</param>
|
||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
||||
public void ExtractAllFromTar(Stream source, string targetPath, bool overwriteExistingFiles)
|
||||
{
|
||||
using var archive = TarArchive.Open(source);
|
||||
using var reader = archive.ExtractAllEntries();
|
||||
var options = new ExtractionOptions
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = overwriteExistingFiles
|
||||
};
|
||||
|
||||
Directory.CreateDirectory(targetPath);
|
||||
reader.WriteAllToDirectory(targetPath, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Progress;
|
||||
@@ -129,16 +130,14 @@ namespace Emby.Server.Implementations.Channels
|
||||
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
|
||||
if (internalChannel == null)
|
||||
{
|
||||
throw new ArgumentException();
|
||||
throw new ArgumentException(nameof(item.ChannelId));
|
||||
}
|
||||
|
||||
var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
|
||||
|
||||
var supportsDelete = channel as ISupportsDelete;
|
||||
|
||||
if (supportsDelete == null)
|
||||
if (channel is not ISupportsDelete supportsDelete)
|
||||
{
|
||||
throw new ArgumentException();
|
||||
throw new ArgumentException(nameof(channel));
|
||||
}
|
||||
|
||||
return supportsDelete.DeleteItem(item.ExternalId, CancellationToken.None);
|
||||
@@ -179,7 +178,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
try
|
||||
{
|
||||
return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes
|
||||
&& hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val;
|
||||
&& hasAttributes.Attributes.Contains("Recordings", StringComparison.OrdinalIgnoreCase)) == val;
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -541,7 +540,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
return _libraryManager.GetItemIds(
|
||||
new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { nameof(Channel) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.Channel },
|
||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
|
||||
}).Select(i => GetChannelFeatures(i)).ToArray();
|
||||
}
|
||||
@@ -1075,14 +1074,6 @@ namespace Emby.Server.Implementations.Channels
|
||||
forceUpdate = true;
|
||||
}
|
||||
|
||||
// was used for status
|
||||
// if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal))
|
||||
// {
|
||||
// item.ExternalEtag = info.Etag;
|
||||
// forceUpdate = true;
|
||||
// _logger.LogDebug("Forcing update due to ExternalEtag {0}", item.Name);
|
||||
// }
|
||||
|
||||
if (!internalChannelId.Equals(item.ChannelId))
|
||||
{
|
||||
forceUpdate = true;
|
||||
@@ -1143,7 +1134,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
|
||||
if (!info.IsLiveStream)
|
||||
{
|
||||
if (item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase))
|
||||
if (item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
item.Tags = item.Tags.Except(new[] { "livestream" }, StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
_logger.LogDebug("Forcing update due to Tags {0}", item.Name);
|
||||
@@ -1152,7 +1143,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase))
|
||||
if (!item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
item.Tags = item.Tags.Concat(new[] { "livestream" }).ToArray();
|
||||
_logger.LogDebug("Forcing update due to Tags {0}", item.Name);
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -51,7 +52,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
|
||||
var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { nameof(Channel) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.Channel },
|
||||
ExcludeItemIds = installedChannelIds.ToArray()
|
||||
});
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ namespace Emby.Server.Implementations.Collections
|
||||
|
||||
if (parentFolder == null)
|
||||
{
|
||||
throw new ArgumentException();
|
||||
throw new ArgumentException(nameof(parentFolder));
|
||||
}
|
||||
|
||||
var path = Path.Combine(parentFolder.Path, folderName);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using static MediaBrowser.Common.Cryptography.Constants;
|
||||
using static MediaBrowser.Model.Cryptography.Constants;
|
||||
|
||||
namespace Emby.Server.Implementations.Cryptography
|
||||
{
|
||||
@@ -12,10 +14,7 @@ namespace Emby.Server.Implementations.Cryptography
|
||||
/// </summary>
|
||||
public class CryptographyProvider : ICryptoProvider
|
||||
{
|
||||
// FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto
|
||||
// Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1
|
||||
// there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one
|
||||
// Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1
|
||||
// TODO: remove when not needed for backwards compat
|
||||
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
|
||||
{
|
||||
"MD5",
|
||||
@@ -35,60 +34,81 @@ namespace Emby.Server.Implementations.Cryptography
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DefaultHashMethod => "PBKDF2";
|
||||
public string DefaultHashMethod => "PBKDF2-SHA512";
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> GetSupportedHashMethods()
|
||||
=> _supportedHashMethods;
|
||||
|
||||
private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
|
||||
public PasswordHash CreatePasswordHash(ReadOnlySpan<char> password)
|
||||
{
|
||||
// downgrading for now as we need this library to be dotnetstandard compliant
|
||||
// with this downgrade we'll add a check to make sure we're on the downgrade method at the moment
|
||||
if (method != DefaultHashMethod)
|
||||
{
|
||||
throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}");
|
||||
}
|
||||
|
||||
using var r = new Rfc2898DeriveBytes(bytes, salt, iterations);
|
||||
return r.GetBytes(32);
|
||||
byte[] salt = GenerateSalt();
|
||||
return new PasswordHash(
|
||||
DefaultHashMethod,
|
||||
Rfc2898DeriveBytes.Pbkdf2(
|
||||
password,
|
||||
salt,
|
||||
DefaultIterations,
|
||||
HashAlgorithmName.SHA512,
|
||||
DefaultOutputLength),
|
||||
salt,
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) }
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt)
|
||||
public bool Verify(PasswordHash hash, ReadOnlySpan<char> password)
|
||||
{
|
||||
if (hashMethod == DefaultHashMethod)
|
||||
if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal))
|
||||
{
|
||||
return PBKDF2(hashMethod, bytes, salt, DefaultIterations);
|
||||
return hash.Hash.SequenceEqual(
|
||||
Rfc2898DeriveBytes.Pbkdf2(
|
||||
password,
|
||||
hash.Salt,
|
||||
int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
|
||||
HashAlgorithmName.SHA1,
|
||||
32));
|
||||
}
|
||||
|
||||
if (!_supportedHashMethods.Contains(hashMethod))
|
||||
if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal))
|
||||
{
|
||||
throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
|
||||
return hash.Hash.SequenceEqual(
|
||||
Rfc2898DeriveBytes.Pbkdf2(
|
||||
password,
|
||||
hash.Salt,
|
||||
int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
|
||||
HashAlgorithmName.SHA512,
|
||||
DefaultOutputLength));
|
||||
}
|
||||
|
||||
using var h = HashAlgorithm.Create(hashMethod) ?? throw new ResourceNotFoundException($"Unknown hash method: {hashMethod}.");
|
||||
if (salt.Length == 0)
|
||||
if (!_supportedHashMethods.Contains(hash.Id))
|
||||
{
|
||||
return h.ComputeHash(bytes);
|
||||
throw new CryptographicException($"Requested hash method is not supported: {hash.Id}");
|
||||
}
|
||||
|
||||
byte[] salted = new byte[bytes.Length + salt.Length];
|
||||
using var h = HashAlgorithm.Create(hash.Id) ?? throw new ResourceNotFoundException($"Unknown hash method: {hash.Id}.");
|
||||
var bytes = Encoding.UTF8.GetBytes(password.ToArray());
|
||||
if (hash.Salt.Length == 0)
|
||||
{
|
||||
return hash.Hash.SequenceEqual(h.ComputeHash(bytes));
|
||||
}
|
||||
|
||||
byte[] salted = new byte[bytes.Length + hash.Salt.Length];
|
||||
Array.Copy(bytes, salted, bytes.Length);
|
||||
Array.Copy(salt, 0, salted, bytes.Length, salt.Length);
|
||||
return h.ComputeHash(salted);
|
||||
hash.Salt.CopyTo(salted.AsSpan(bytes.Length));
|
||||
return hash.Hash.SequenceEqual(h.ComputeHash(salted));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
|
||||
=> PBKDF2(DefaultHashMethod, bytes, salt, DefaultIterations);
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte[] GenerateSalt()
|
||||
=> GenerateSalt(DefaultSaltLength);
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte[] GenerateSalt(int length)
|
||||
=> RandomNumberGenerator.GetBytes(length);
|
||||
{
|
||||
var salt = new byte[length];
|
||||
using var rng = RandomNumberGenerator.Create();
|
||||
rng.GetNonZeroBytes(salt);
|
||||
return salt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Jellyfin.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SQLitePCL.pretty;
|
||||
|
||||
@@ -160,21 +160,22 @@ namespace Emby.Server.Implementations.Data
|
||||
protected bool TableExists(ManagedConnection connection, string name)
|
||||
{
|
||||
return connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
|
||||
db =>
|
||||
{
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
|
||||
{
|
||||
if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
return true;
|
||||
if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}, ReadTransactionMode);
|
||||
return false;
|
||||
},
|
||||
ReadTransactionMode);
|
||||
}
|
||||
|
||||
protected List<string> GetColumnNames(IDatabaseConnection connection, string table)
|
||||
@@ -194,7 +195,7 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
|
||||
{
|
||||
if (existingColumnNames.Contains(columnName, StringComparer.OrdinalIgnoreCase))
|
||||
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ using SQLitePCL.pretty;
|
||||
|
||||
namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
public class ManagedConnection : IDisposable
|
||||
public sealed class ManagedConnection : IDisposable
|
||||
{
|
||||
private readonly SemaphoreSlim _writeLock;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -50,41 +50,42 @@ namespace Emby.Server.Implementations.Data
|
||||
var users = userDatasTableExists ? null : userManager.Users;
|
||||
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
db.ExecuteAll(string.Join(';', new[]
|
||||
db =>
|
||||
{
|
||||
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
|
||||
|
||||
"drop index if exists idx_userdata",
|
||||
"drop index if exists idx_userdata1",
|
||||
"drop index if exists idx_userdata2",
|
||||
"drop index if exists userdataindex1",
|
||||
"drop index if exists userdataindex",
|
||||
"drop index if exists userdataindex3",
|
||||
"drop index if exists userdataindex4",
|
||||
"create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
|
||||
"create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
|
||||
"create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
|
||||
"create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)"
|
||||
}));
|
||||
|
||||
if (userDataTableExists)
|
||||
{
|
||||
var existingColumnNames = GetColumnNames(db, "userdata");
|
||||
|
||||
AddColumn(db, "userdata", "InternalUserId", "int", existingColumnNames);
|
||||
AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames);
|
||||
AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
|
||||
|
||||
if (!userDatasTableExists)
|
||||
db.ExecuteAll(string.Join(';', new[]
|
||||
{
|
||||
ImportUserIds(db, users);
|
||||
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
|
||||
|
||||
db.ExecuteAll("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
|
||||
"drop index if exists idx_userdata",
|
||||
"drop index if exists idx_userdata1",
|
||||
"drop index if exists idx_userdata2",
|
||||
"drop index if exists userdataindex1",
|
||||
"drop index if exists userdataindex",
|
||||
"drop index if exists userdataindex3",
|
||||
"drop index if exists userdataindex4",
|
||||
"create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
|
||||
"create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
|
||||
"create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
|
||||
"create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)"
|
||||
}));
|
||||
|
||||
if (userDataTableExists)
|
||||
{
|
||||
var existingColumnNames = GetColumnNames(db, "userdata");
|
||||
|
||||
AddColumn(db, "userdata", "InternalUserId", "int", existingColumnNames);
|
||||
AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames);
|
||||
AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
|
||||
|
||||
if (!userDatasTableExists)
|
||||
{
|
||||
ImportUserIds(db, users);
|
||||
|
||||
db.ExecuteAll("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
|
||||
}
|
||||
}
|
||||
}
|
||||
}, TransactionMode);
|
||||
},
|
||||
TransactionMode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,10 +184,11 @@ namespace Emby.Server.Implementations.Data
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
SaveUserData(db, internalUserId, key, userData);
|
||||
}, TransactionMode);
|
||||
db =>
|
||||
{
|
||||
SaveUserData(db, internalUserId, key, userData);
|
||||
},
|
||||
TransactionMode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,13 +254,14 @@ namespace Emby.Server.Implementations.Data
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
foreach (var userItemData in userDataList)
|
||||
db =>
|
||||
{
|
||||
SaveUserData(db, internalUserId, userItemData.Key, userItemData);
|
||||
}
|
||||
}, TransactionMode);
|
||||
foreach (var userItemData in userDataList)
|
||||
{
|
||||
SaveUserData(db, internalUserId, userItemData.Key, userItemData);
|
||||
}
|
||||
},
|
||||
TransactionMode);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Emby.Server.Implementations.Devices
|
||||
{
|
||||
var value = File.ReadAllText(CachePath, Encoding.UTF8);
|
||||
|
||||
if (Guid.TryParse(value, out var guid))
|
||||
if (Guid.TryParse(value, out _))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
@@ -109,7 +109,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
}
|
||||
});
|
||||
|
||||
SetItemByNameInfo(item, dto, libraryItems, user);
|
||||
SetItemByNameInfo(item, dto, libraryItems);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,8 +153,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
new DtoOptions(false)
|
||||
{
|
||||
EnableImages = false
|
||||
}),
|
||||
user);
|
||||
}));
|
||||
}
|
||||
|
||||
return dto;
|
||||
@@ -294,7 +293,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
path = path.TrimStart('.');
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparer.OrdinalIgnoreCase))
|
||||
if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
fileExtensionContainer = path;
|
||||
}
|
||||
@@ -311,13 +310,13 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (taggedItems != null && options.ContainsField(ItemFields.ItemCounts))
|
||||
{
|
||||
SetItemByNameInfo(item, dto, taggedItems, user);
|
||||
SetItemByNameInfo(item, dto, taggedItems);
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IList<BaseItem> taggedItems, User user = null)
|
||||
private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IList<BaseItem> taggedItems)
|
||||
{
|
||||
if (item is MusicArtist)
|
||||
{
|
||||
@@ -370,6 +369,12 @@ namespace Emby.Server.Implementations.Dto
|
||||
if (item is MusicAlbum || item is Season || item is Playlist)
|
||||
{
|
||||
dto.ChildCount = dto.RecursiveItemCount;
|
||||
var folderChildCount = folder.LinkedChildren.Length;
|
||||
// The default is an empty array, so we can't reliably use the count when it's empty
|
||||
if (folderChildCount > 0)
|
||||
{
|
||||
dto.ChildCount ??= folderChildCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.ContainsField(ItemFields.ChildCount))
|
||||
@@ -464,7 +469,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
{
|
||||
var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { nameof(MusicAlbum) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
|
||||
Name = item.Album,
|
||||
Limit = 1
|
||||
});
|
||||
@@ -1398,44 +1403,27 @@ namespace Emby.Server.Implementations.Dto
|
||||
return null;
|
||||
}
|
||||
|
||||
ImageDimensions size;
|
||||
|
||||
var defaultAspectRatio = item.GetDefaultPrimaryImageAspectRatio();
|
||||
|
||||
if (defaultAspectRatio > 0)
|
||||
{
|
||||
return defaultAspectRatio;
|
||||
}
|
||||
|
||||
if (!imageInfo.IsLocalFile)
|
||||
{
|
||||
return null;
|
||||
return item.GetDefaultPrimaryImageAspectRatio();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
size = _imageProcessor.GetImageDimensions(item, imageInfo);
|
||||
|
||||
if (size.Width <= 0 || size.Height <= 0)
|
||||
var size = _imageProcessor.GetImageDimensions(item, imageInfo);
|
||||
var width = size.Width;
|
||||
var height = size.Height;
|
||||
if (width > 0 && height > 0)
|
||||
{
|
||||
return null;
|
||||
return (double)width / height;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to determine primary image aspect ratio for {0}", imageInfo.Path);
|
||||
return null;
|
||||
_logger.LogError(ex, "Failed to determine primary image aspect ratio for {ImagePath}", imageInfo.Path);
|
||||
}
|
||||
|
||||
var width = size.Width;
|
||||
var height = size.Height;
|
||||
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return (double)width / height;
|
||||
return item.GetDefaultPrimaryImageAspectRatio();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,10 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.1" />
|
||||
<PackageReference Include="Mono.Nat" Version="3.0.2" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.2" />
|
||||
<PackageReference Include="sharpcompress" Version="0.30.0" />
|
||||
<PackageReference Include="sharpcompress" Version="0.30.1" />
|
||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
|
||||
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
|
||||
</ItemGroup>
|
||||
@@ -49,10 +49,14 @@
|
||||
<NoWarn>AD0001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ using Jellyfin.Networking.Configuration;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Mono.Nat;
|
||||
|
||||
@@ -27,7 +26,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly ILogger<ExternalPortForwarding> _logger;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
||||
|
||||
private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>();
|
||||
|
||||
@@ -42,17 +40,14 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="appHost">The application host.</param>
|
||||
/// <param name="config">The configuration manager.</param>
|
||||
/// <param name="deviceDiscovery">The device discovery.</param>
|
||||
public ExternalPortForwarding(
|
||||
ILogger<ExternalPortForwarding> logger,
|
||||
IServerApplicationHost appHost,
|
||||
IServerConfigurationManager config,
|
||||
IDeviceDiscovery deviceDiscovery)
|
||||
IServerConfigurationManager config)
|
||||
{
|
||||
_logger = logger;
|
||||
_appHost = appHost;
|
||||
_config = config;
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
}
|
||||
|
||||
private string GetConfigIdentifier()
|
||||
|
||||
@@ -101,7 +101,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
}
|
||||
}
|
||||
|
||||
_lastProgressMessageTimes.AddOrUpdate(item.Id, key => DateTime.UtcNow, (key, existing) => DateTime.UtcNow);
|
||||
_lastProgressMessageTimes.AddOrUpdate(item.Id, _ => DateTime.UtcNow, (_, _) => DateTime.UtcNow);
|
||||
|
||||
var dict = new Dictionary<string, string>();
|
||||
dict["ItemId"] = item.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
@@ -144,7 +144,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
{
|
||||
OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
|
||||
|
||||
_lastProgressMessageTimes.TryRemove(e.Argument.Id, out DateTime removed);
|
||||
_lastProgressMessageTimes.TryRemove(e.Argument.Id, out _);
|
||||
}
|
||||
|
||||
private static bool EnableRefreshMessage(BaseItem item)
|
||||
@@ -423,7 +423,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
continue;
|
||||
}
|
||||
|
||||
var collectionFolders = _libraryManager.GetCollectionFolders(item, allUserRootChildren);
|
||||
foreach (var folder in allUserRootChildren)
|
||||
{
|
||||
list.Add(folder.Id.ToString("N", CultureInfo.InvariantCulture));
|
||||
@@ -465,6 +464,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
var changes = _changedItems.ToList();
|
||||
_changedItems.Clear();
|
||||
|
||||
var task = SendNotifications(changes, CancellationToken.None);
|
||||
SendNotifications(changes, CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
if (_updateTimer != null)
|
||||
{
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
{
|
||||
var session = await GetSession(requestContext).ConfigureAwait(false);
|
||||
|
||||
return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
|
||||
return session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
|
||||
}
|
||||
|
||||
public Task<User?> GetUser(object requestContext)
|
||||
|
||||
@@ -42,17 +42,14 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="socket">The socket.</param>
|
||||
/// <param name="remoteEndPoint">The remote end point.</param>
|
||||
/// <param name="query">The query.</param>
|
||||
public WebSocketConnection(
|
||||
ILogger<WebSocketConnection> logger,
|
||||
WebSocket socket,
|
||||
IPAddress? remoteEndPoint,
|
||||
IQueryCollection query)
|
||||
IPAddress? remoteEndPoint)
|
||||
{
|
||||
_logger = logger;
|
||||
_socket = socket;
|
||||
RemoteEndPoint = remoteEndPoint;
|
||||
QueryString = query;
|
||||
|
||||
_jsonOptions = JsonDefaults.Options;
|
||||
LastActivityDate = DateTime.Now;
|
||||
@@ -81,12 +78,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <inheritdoc />
|
||||
public DateTime LastKeepAliveDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the query string.
|
||||
/// </summary>
|
||||
/// <value>The query string.</value>
|
||||
public IQueryCollection QueryString { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state.
|
||||
/// </summary>
|
||||
@@ -180,7 +171,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
|
||||
WebSocketMessage<object>? stub;
|
||||
long bytesConsumed = 0;
|
||||
long bytesConsumed;
|
||||
try
|
||||
{
|
||||
stub = DeserializeWebSocketMessage(buffer, out bytesConsumed);
|
||||
@@ -236,7 +227,8 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
MessageId = Guid.NewGuid(),
|
||||
MessageType = SessionMessageType.KeepAlive
|
||||
}, CancellationToken.None);
|
||||
},
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -50,8 +51,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
using var connection = new WebSocketConnection(
|
||||
_loggerFactory.CreateLogger<WebSocketConnection>(),
|
||||
webSocket,
|
||||
context.Connection.RemoteIpAddress,
|
||||
context.Request.Query)
|
||||
context.GetNormalizedRemoteIp())
|
||||
{
|
||||
OnReceive = ProcessWebSocketMessageReceived
|
||||
};
|
||||
@@ -59,7 +59,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
var tasks = new Task[_webSocketListeners.Length];
|
||||
for (var i = 0; i < _webSocketListeners.Length; ++i)
|
||||
{
|
||||
tasks[i] = _webSocketListeners[i].ProcessWebSocketConnectedAsync(connection);
|
||||
tasks[i] = _webSocketListeners[i].ProcessWebSocketConnectedAsync(connection, context);
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -14,7 +12,7 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
public class FileRefresher : IDisposable
|
||||
public sealed class FileRefresher : IDisposable
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
@@ -22,7 +20,7 @@ namespace Emby.Server.Implementations.IO
|
||||
|
||||
private readonly List<string> _affectedPaths = new List<string>();
|
||||
private readonly object _timerLock = new object();
|
||||
private Timer _timer;
|
||||
private Timer? _timer;
|
||||
private bool _disposed;
|
||||
|
||||
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
|
||||
@@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.IO
|
||||
AddPath(path);
|
||||
}
|
||||
|
||||
public event EventHandler<EventArgs> Completed;
|
||||
public event EventHandler<EventArgs>? Completed;
|
||||
|
||||
public string Path { get; private set; }
|
||||
|
||||
@@ -111,7 +109,7 @@ namespace Emby.Server.Implementations.IO
|
||||
RestartTimer();
|
||||
}
|
||||
|
||||
private void OnTimerCallback(object state)
|
||||
private void OnTimerCallback(object? state)
|
||||
{
|
||||
List<string> paths;
|
||||
|
||||
@@ -127,7 +125,7 @@ namespace Emby.Server.Implementations.IO
|
||||
|
||||
try
|
||||
{
|
||||
ProcessPathChanges(paths.ToList());
|
||||
ProcessPathChanges(paths);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -137,12 +135,12 @@ namespace Emby.Server.Implementations.IO
|
||||
|
||||
private void ProcessPathChanges(List<string> paths)
|
||||
{
|
||||
var itemsToRefresh = paths
|
||||
IEnumerable<BaseItem> itemsToRefresh = paths
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Select(GetAffectedBaseItem)
|
||||
.Where(item => item != null)
|
||||
.GroupBy(x => x.Id)
|
||||
.Select(x => x.First());
|
||||
.GroupBy(x => x!.Id) // Removed null values in the previous .Where()
|
||||
.Select(x => x.First())!;
|
||||
|
||||
foreach (var item in itemsToRefresh)
|
||||
{
|
||||
@@ -176,15 +174,15 @@ namespace Emby.Server.Implementations.IO
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
private BaseItem GetAffectedBaseItem(string path)
|
||||
private BaseItem? GetAffectedBaseItem(string path)
|
||||
{
|
||||
BaseItem item = null;
|
||||
BaseItem? item = null;
|
||||
|
||||
while (item == null && !string.IsNullOrEmpty(path))
|
||||
{
|
||||
item = _libraryManager.FindByPath(path, null);
|
||||
|
||||
path = System.IO.Path.GetDirectoryName(path);
|
||||
path = System.IO.Path.GetDirectoryName(path) ?? string.Empty;
|
||||
}
|
||||
|
||||
if (item != null)
|
||||
@@ -219,8 +217,13 @@ namespace Emby.Server.Implementations.IO
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DisposeTimer();
|
||||
_disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ namespace Emby.Server.Implementations.IO
|
||||
// But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, or hand-editing metadata
|
||||
await Task.Delay(45000).ConfigureAwait(false);
|
||||
|
||||
_tempIgnoredPaths.TryRemove(path, out var val);
|
||||
_tempIgnoredPaths.TryRemove(path, out _);
|
||||
|
||||
if (refreshPath)
|
||||
{
|
||||
@@ -449,12 +449,12 @@ namespace Emby.Server.Implementations.IO
|
||||
}
|
||||
|
||||
var newRefresher = new FileRefresher(path, _configurationManager, _libraryManager, _logger);
|
||||
newRefresher.Completed += NewRefresher_Completed;
|
||||
newRefresher.Completed += OnNewRefresherCompleted;
|
||||
_activeRefreshers.Add(newRefresher);
|
||||
}
|
||||
}
|
||||
|
||||
private void NewRefresher_Completed(object sender, EventArgs e)
|
||||
private void OnNewRefresherCompleted(object sender, EventArgs e)
|
||||
{
|
||||
var refresher = (FileRefresher)sender;
|
||||
DisposeRefresher(refresher);
|
||||
@@ -481,6 +481,7 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
lock (_activeRefreshers)
|
||||
{
|
||||
refresher.Completed -= OnNewRefresherCompleted;
|
||||
refresher.Dispose();
|
||||
_activeRefreshers.Remove(refresher);
|
||||
}
|
||||
@@ -492,6 +493,7 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
foreach (var refresher in _activeRefreshers.ToList())
|
||||
{
|
||||
refresher.Completed -= OnNewRefresherCompleted;
|
||||
refresher.Dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -544,16 +544,6 @@ namespace Emby.Server.Implementations.IO
|
||||
/// <inheritdoc />
|
||||
public virtual bool AreEqual(string path1, string path2)
|
||||
{
|
||||
if (path1 == null && path2 == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (path1 == null || path2 == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return string.Equals(
|
||||
NormalizePath(path1),
|
||||
NormalizePath(path2),
|
||||
|
||||
@@ -17,11 +17,11 @@ namespace Emby.Server.Implementations.IO
|
||||
try
|
||||
{
|
||||
int read;
|
||||
while ((read = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
while ((read = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
|
||||
await destination.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (onStarted != null)
|
||||
{
|
||||
@@ -44,11 +44,11 @@ namespace Emby.Server.Implementations.IO
|
||||
if (emptyReadLimit <= 0)
|
||||
{
|
||||
int read;
|
||||
while ((read = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
while ((read = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
|
||||
await destination.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -60,7 +60,7 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
|
||||
var bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
@@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
eofCount = 0;
|
||||
|
||||
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
|
||||
await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,13 +88,13 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
int bytesRead;
|
||||
|
||||
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
while ((bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
{
|
||||
var bytesToWrite = Math.Min(bytesRead, copyLength);
|
||||
|
||||
if (bytesToWrite > 0)
|
||||
{
|
||||
await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
|
||||
await destination.WriteAsync(buffer.AsMemory(0, Convert.ToInt32(bytesToWrite)), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
copyLength -= bytesToWrite;
|
||||
@@ -137,9 +137,9 @@ namespace Emby.Server.Implementations.IO
|
||||
int bytesRead;
|
||||
int totalBytesRead = 0;
|
||||
|
||||
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
while ((bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
{
|
||||
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
|
||||
await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
totalBytesRead += bytesRead;
|
||||
}
|
||||
|
||||
@@ -28,35 +28,35 @@ namespace Emby.Server.Implementations.Images
|
||||
var view = (CollectionFolder)item;
|
||||
var viewType = view.CollectionType;
|
||||
|
||||
string[] includeItemTypes;
|
||||
BaseItemKind[] includeItemTypes;
|
||||
|
||||
if (string.Equals(viewType, CollectionType.Movies, StringComparison.Ordinal))
|
||||
{
|
||||
includeItemTypes = new string[] { "Movie" };
|
||||
includeItemTypes = new[] { BaseItemKind.Movie };
|
||||
}
|
||||
else if (string.Equals(viewType, CollectionType.TvShows, StringComparison.Ordinal))
|
||||
{
|
||||
includeItemTypes = new string[] { "Series" };
|
||||
includeItemTypes = new[] { BaseItemKind.Series };
|
||||
}
|
||||
else if (string.Equals(viewType, CollectionType.Music, StringComparison.Ordinal))
|
||||
{
|
||||
includeItemTypes = new string[] { "MusicAlbum" };
|
||||
includeItemTypes = new[] { BaseItemKind.MusicAlbum };
|
||||
}
|
||||
else if (string.Equals(viewType, CollectionType.Books, StringComparison.Ordinal))
|
||||
{
|
||||
includeItemTypes = new string[] { "Book", "AudioBook" };
|
||||
includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook };
|
||||
}
|
||||
else if (string.Equals(viewType, CollectionType.BoxSets, StringComparison.Ordinal))
|
||||
{
|
||||
includeItemTypes = new string[] { "BoxSet" };
|
||||
includeItemTypes = new[] { BaseItemKind.BoxSet };
|
||||
}
|
||||
else if (string.Equals(viewType, CollectionType.HomeVideos, StringComparison.Ordinal) || string.Equals(viewType, CollectionType.Photos, StringComparison.Ordinal))
|
||||
{
|
||||
includeItemTypes = new string[] { "Video", "Photo" };
|
||||
includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo };
|
||||
}
|
||||
else
|
||||
{
|
||||
includeItemTypes = new string[] { "Video", "Audio", "Photo", "Movie", "Series" };
|
||||
includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Audio, BaseItemKind.Photo, BaseItemKind.Movie, BaseItemKind.Series };
|
||||
}
|
||||
|
||||
var recursive = !string.Equals(CollectionType.Playlists, viewType, StringComparison.OrdinalIgnoreCase);
|
||||
@@ -68,9 +68,9 @@ namespace Emby.Server.Implementations.Images
|
||||
DtoOptions = new DtoOptions(false),
|
||||
ImageTypes = new ImageType[] { ImageType.Primary },
|
||||
Limit = 8,
|
||||
OrderBy = new ValueTuple<string, SortOrder>[]
|
||||
OrderBy = new[]
|
||||
{
|
||||
new ValueTuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending)
|
||||
(ItemSortBy.Random, SortOrder.Ascending)
|
||||
},
|
||||
IncludeItemTypes = includeItemTypes
|
||||
});
|
||||
|
||||
@@ -6,6 +6,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
@@ -34,14 +36,14 @@ namespace Emby.Server.Implementations.Images
|
||||
var view = (UserView)item;
|
||||
|
||||
var isUsingCollectionStrip = IsUsingCollectionStrip(view);
|
||||
var recursive = isUsingCollectionStrip && !new[] { CollectionType.BoxSets, CollectionType.Playlists }.Contains(view.ViewType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
|
||||
var recursive = isUsingCollectionStrip && !new[] { CollectionType.BoxSets, CollectionType.Playlists }.Contains(view.ViewType ?? string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var result = view.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
User = view.UserId.HasValue ? _userManager.GetUserById(view.UserId.Value) : null,
|
||||
CollapseBoxSetItems = false,
|
||||
Recursive = recursive,
|
||||
ExcludeItemTypes = new[] { "UserView", "CollectionFolder", "Person" },
|
||||
ExcludeItemTypes = new[] { BaseItemKind.UserView, BaseItemKind.CollectionFolder, BaseItemKind.Person },
|
||||
DtoOptions = new DtoOptions(false)
|
||||
});
|
||||
|
||||
|
||||
@@ -8,8 +8,6 @@ using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
@@ -43,7 +41,7 @@ namespace Emby.Server.Implementations.Images
|
||||
return _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
Genres = new[] { item.Name },
|
||||
IncludeItemTypes = new[] { nameof(Series), nameof(Movie) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.Series, BaseItemKind.Movie },
|
||||
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
|
||||
Limit = 4,
|
||||
Recursive = true,
|
||||
|
||||
@@ -44,9 +44,9 @@ namespace Emby.Server.Implementations.Images
|
||||
Genres = new[] { item.Name },
|
||||
IncludeItemTypes = new[]
|
||||
{
|
||||
nameof(MusicAlbum),
|
||||
nameof(MusicVideo),
|
||||
nameof(Audio)
|
||||
BaseItemKind.MusicAlbum,
|
||||
BaseItemKind.MusicVideo,
|
||||
BaseItemKind.Audio
|
||||
},
|
||||
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
|
||||
Limit = 4,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Emby.Naming.Audio;
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
@@ -13,17 +14,17 @@ namespace Emby.Server.Implementations.Library
|
||||
/// </summary>
|
||||
public class CoreResolutionIgnoreRule : IResolverIgnoreRule
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly IServerApplicationPaths _serverApplicationPaths;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="serverApplicationPaths">The server application paths.</param>
|
||||
public CoreResolutionIgnoreRule(ILibraryManager libraryManager, IServerApplicationPaths serverApplicationPaths)
|
||||
public CoreResolutionIgnoreRule(NamingOptions namingOptions, IServerApplicationPaths serverApplicationPaths)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_namingOptions = namingOptions;
|
||||
_serverApplicationPaths = serverApplicationPaths;
|
||||
}
|
||||
|
||||
@@ -53,20 +54,10 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
if (parent != null)
|
||||
{
|
||||
// Ignore trailer folders but allow it at the collection level
|
||||
if (string.Equals(filename, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase)
|
||||
&& !(parent is AggregateFolder)
|
||||
&& !(parent is UserRootFolder))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.Equals(filename, BaseItem.ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.Equals(filename, BaseItem.ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase))
|
||||
// Ignore extras folders but allow it at the collection level
|
||||
if (_namingOptions.AllExtrasTypesFolderNames.ContainsKey(filename)
|
||||
&& parent is not AggregateFolder
|
||||
&& parent is not UserRootFolder)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -78,7 +69,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
// Don't resolve these into audio files
|
||||
if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
|
||||
&& _libraryManager.IsAudioFile(filename))
|
||||
&& AudioFileParser.IsAudioFile(filename, _namingOptions))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -11,14 +11,12 @@ using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Naming.Audio;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.TV;
|
||||
using Emby.Naming.Video;
|
||||
using Emby.Server.Implementations.Library.Resolvers;
|
||||
using Emby.Server.Implementations.Library.Validators;
|
||||
using Emby.Server.Implementations.Playlists;
|
||||
using Emby.Server.Implementations.ScheduledTasks;
|
||||
using Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
@@ -79,6 +77,8 @@ namespace Emby.Server.Implementations.Library
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IItemRepository _itemRepository;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly ExtraResolver _extraResolver;
|
||||
|
||||
/// <summary>
|
||||
/// The _root folder sync lock.
|
||||
@@ -88,9 +88,6 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
|
||||
|
||||
private NamingOptions _namingOptions;
|
||||
private string[] _videoFileExtensions;
|
||||
|
||||
/// <summary>
|
||||
/// The _root folder.
|
||||
/// </summary>
|
||||
@@ -116,6 +113,7 @@ namespace Emby.Server.Implementations.Library
|
||||
/// <param name="itemRepository">The item repository.</param>
|
||||
/// <param name="imageProcessor">The image processor.</param>
|
||||
/// <param name="memoryCache">The memory cache.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public LibraryManager(
|
||||
IServerApplicationHost appHost,
|
||||
ILogger<LibraryManager> logger,
|
||||
@@ -130,7 +128,8 @@ namespace Emby.Server.Implementations.Library
|
||||
IMediaEncoder mediaEncoder,
|
||||
IItemRepository itemRepository,
|
||||
IImageProcessor imageProcessor,
|
||||
IMemoryCache memoryCache)
|
||||
IMemoryCache memoryCache,
|
||||
NamingOptions namingOptions)
|
||||
{
|
||||
_appHost = appHost;
|
||||
_logger = logger;
|
||||
@@ -146,6 +145,9 @@ namespace Emby.Server.Implementations.Library
|
||||
_itemRepository = itemRepository;
|
||||
_imageProcessor = imageProcessor;
|
||||
_memoryCache = memoryCache;
|
||||
_namingOptions = namingOptions;
|
||||
|
||||
_extraResolver = new ExtraResolver(namingOptions);
|
||||
|
||||
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
|
||||
|
||||
@@ -532,8 +534,8 @@ namespace Emby.Server.Implementations.Library
|
||||
return key.GetMD5();
|
||||
}
|
||||
|
||||
public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null)
|
||||
=> ResolvePath(fileInfo, new DirectoryService(_fileSystem), null, parent);
|
||||
public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null, IDirectoryService directoryService = null)
|
||||
=> ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent);
|
||||
|
||||
private BaseItem ResolvePath(
|
||||
FileSystemMetadata fileInfo,
|
||||
@@ -653,7 +655,7 @@ namespace Emby.Server.Implementations.Library
|
||||
return !args.ContainsFileSystemEntryByName(".ignore");
|
||||
}
|
||||
|
||||
public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, string collectionType)
|
||||
public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, string collectionType = null)
|
||||
{
|
||||
return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers);
|
||||
}
|
||||
@@ -676,7 +678,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
var result = resolver.ResolveMultiple(parent, fileList, collectionType, directoryService);
|
||||
|
||||
if (result != null && result.Items.Count > 0)
|
||||
if (result?.Items.Count > 0)
|
||||
{
|
||||
var items = new List<BaseItem>();
|
||||
items.AddRange(result.Items);
|
||||
@@ -964,7 +966,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
var existing = GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { nameof(MusicArtist) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.MusicArtist },
|
||||
Name = name,
|
||||
DtoOptions = options
|
||||
}).Cast<MusicArtist>()
|
||||
@@ -1374,7 +1376,7 @@ namespace Emby.Server.Implementations.Library
|
||||
return _itemRepository.GetItemIdsList(query);
|
||||
}
|
||||
|
||||
public QueryResult<(BaseItem, ItemCounts)> GetStudios(InternalItemsQuery query)
|
||||
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query)
|
||||
{
|
||||
if (query.User != null)
|
||||
{
|
||||
@@ -1385,7 +1387,7 @@ namespace Emby.Server.Implementations.Library
|
||||
return _itemRepository.GetStudios(query);
|
||||
}
|
||||
|
||||
public QueryResult<(BaseItem, ItemCounts)> GetGenres(InternalItemsQuery query)
|
||||
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query)
|
||||
{
|
||||
if (query.User != null)
|
||||
{
|
||||
@@ -1396,7 +1398,7 @@ namespace Emby.Server.Implementations.Library
|
||||
return _itemRepository.GetGenres(query);
|
||||
}
|
||||
|
||||
public QueryResult<(BaseItem, ItemCounts)> GetMusicGenres(InternalItemsQuery query)
|
||||
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query)
|
||||
{
|
||||
if (query.User != null)
|
||||
{
|
||||
@@ -1407,7 +1409,7 @@ namespace Emby.Server.Implementations.Library
|
||||
return _itemRepository.GetMusicGenres(query);
|
||||
}
|
||||
|
||||
public QueryResult<(BaseItem, ItemCounts)> GetAllArtists(InternalItemsQuery query)
|
||||
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query)
|
||||
{
|
||||
if (query.User != null)
|
||||
{
|
||||
@@ -1418,7 +1420,7 @@ namespace Emby.Server.Implementations.Library
|
||||
return _itemRepository.GetAllArtists(query);
|
||||
}
|
||||
|
||||
public QueryResult<(BaseItem, ItemCounts)> GetArtists(InternalItemsQuery query)
|
||||
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query)
|
||||
{
|
||||
if (query.User != null)
|
||||
{
|
||||
@@ -1459,7 +1461,7 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
public QueryResult<(BaseItem, ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
|
||||
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
|
||||
{
|
||||
if (query.User != null)
|
||||
{
|
||||
@@ -1758,7 +1760,7 @@ namespace Emby.Server.Implementations.Library
|
||||
return orderedItems ?? items;
|
||||
}
|
||||
|
||||
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ValueTuple<string, SortOrder>> orderBy)
|
||||
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<(string OrderBy, SortOrder SortOrder)> orderBy)
|
||||
{
|
||||
var isFirst = true;
|
||||
|
||||
@@ -2500,16 +2502,6 @@ namespace Emby.Server.Implementations.Library
|
||||
return RootFolder;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsVideoFile(string path)
|
||||
{
|
||||
return VideoResolver.IsVideoFile(path, GetNamingOptions());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsAudioFile(string path)
|
||||
=> AudioFileParser.IsAudioFile(path, GetNamingOptions());
|
||||
|
||||
/// <inheritdoc />
|
||||
public int? GetSeasonNumberFromPath(string path)
|
||||
=> SeasonPathParser.Parse(path, true, true).SeasonNumber;
|
||||
@@ -2525,7 +2517,7 @@ namespace Emby.Server.Implementations.Library
|
||||
isAbsoluteNaming = null;
|
||||
}
|
||||
|
||||
var resolver = new EpisodeResolver(GetNamingOptions());
|
||||
var resolver = new EpisodeResolver(_namingOptions);
|
||||
|
||||
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
|
||||
|
||||
@@ -2682,113 +2674,77 @@ namespace Emby.Server.Implementations.Library
|
||||
return changed;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NamingOptions GetNamingOptions()
|
||||
{
|
||||
if (_namingOptions == null)
|
||||
{
|
||||
_namingOptions = new NamingOptions();
|
||||
_videoFileExtensions = _namingOptions.VideoFileExtensions;
|
||||
}
|
||||
|
||||
return _namingOptions;
|
||||
}
|
||||
|
||||
public ItemLookupInfo ParseName(string name)
|
||||
{
|
||||
var namingOptions = GetNamingOptions();
|
||||
var namingOptions = _namingOptions;
|
||||
var result = VideoResolver.CleanDateTime(name, namingOptions);
|
||||
|
||||
return new ItemLookupInfo
|
||||
{
|
||||
Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName.ToString() : result.Name,
|
||||
Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName : result.Name,
|
||||
Year = result.Year
|
||||
};
|
||||
}
|
||||
|
||||
public IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
|
||||
public IEnumerable<BaseItem> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
|
||||
{
|
||||
var namingOptions = GetNamingOptions();
|
||||
|
||||
var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
|
||||
.Where(i => string.Equals(i.Name, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase))
|
||||
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
|
||||
.ToList();
|
||||
|
||||
var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
|
||||
|
||||
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (currentVideo != null)
|
||||
var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions);
|
||||
if (ownerVideoInfo == null)
|
||||
{
|
||||
files.AddRange(currentVideo.Extras.Where(i => i.ExtraType == ExtraType.Trailer).Select(i => _fileSystem.GetFileInfo(i.Path)));
|
||||
yield break;
|
||||
}
|
||||
|
||||
var resolvers = new IItemResolver[]
|
||||
var count = fileSystemChildren.Count;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
new GenericVideoResolver<Trailer>(this)
|
||||
};
|
||||
|
||||
return ResolvePaths(files, directoryService, null, new LibraryOptions(), null, resolvers)
|
||||
.OfType<Trailer>()
|
||||
.Select(video =>
|
||||
var current = fileSystemChildren[i];
|
||||
if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name))
|
||||
{
|
||||
// Try to retrieve it from the db. If we don't find it, use the resolved version
|
||||
if (GetItemById(video.Id) is Trailer dbItem)
|
||||
var filesInSubFolder = _fileSystem.GetFiles(current.FullName, null, false, false);
|
||||
foreach (var file in filesInSubFolder)
|
||||
{
|
||||
video = dbItem;
|
||||
if (!_extraResolver.TryGetExtraTypeForOwner(file.FullName, ownerVideoInfo, out var extraType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var extra = GetExtra(file, extraType.Value);
|
||||
if (extra != null)
|
||||
{
|
||||
yield return extra;
|
||||
}
|
||||
}
|
||||
|
||||
video.ParentId = Guid.Empty;
|
||||
video.OwnerId = owner.Id;
|
||||
video.ExtraType = ExtraType.Trailer;
|
||||
video.TrailerTypes = new[] { TrailerType.LocalTrailer };
|
||||
|
||||
return video;
|
||||
|
||||
// Sort them so that the list can be easily compared for changes
|
||||
}).OrderBy(i => i.Path);
|
||||
}
|
||||
|
||||
public IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
|
||||
{
|
||||
var namingOptions = GetNamingOptions();
|
||||
|
||||
var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
|
||||
.Where(i => BaseItem.AllExtrasTypesFolderNames.ContainsKey(i.Name ?? string.Empty))
|
||||
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
|
||||
.ToList();
|
||||
|
||||
var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
|
||||
|
||||
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (currentVideo != null)
|
||||
{
|
||||
files.AddRange(currentVideo.Extras.Where(i => i.ExtraType != ExtraType.Trailer).Select(i => _fileSystem.GetFileInfo(i.Path)));
|
||||
}
|
||||
else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo, out var extraType))
|
||||
{
|
||||
var extra = GetExtra(current, extraType.Value);
|
||||
if (extra != null)
|
||||
{
|
||||
yield return extra;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ResolvePaths(files, directoryService, null, new LibraryOptions(), null)
|
||||
.OfType<Video>()
|
||||
.Select(video =>
|
||||
BaseItem GetExtra(FileSystemMetadata file, ExtraType extraType)
|
||||
{
|
||||
var extra = ResolvePath(_fileSystem.GetFileInfo(file.FullName), directoryService, _extraResolver.GetResolversForExtraType(extraType));
|
||||
if (extra is not Video && extra is not Audio)
|
||||
{
|
||||
// Try to retrieve it from the db. If we don't find it, use the resolved version
|
||||
var dbItem = GetItemById(video.Id) as Video;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dbItem != null)
|
||||
{
|
||||
video = dbItem;
|
||||
}
|
||||
// Try to retrieve it from the db. If we don't find it, use the resolved version
|
||||
var itemById = GetItemById(extra.Id);
|
||||
if (itemById != null)
|
||||
{
|
||||
extra = itemById;
|
||||
}
|
||||
|
||||
video.ParentId = Guid.Empty;
|
||||
video.OwnerId = owner.Id;
|
||||
|
||||
SetExtraTypeFromFilename(video);
|
||||
|
||||
return video;
|
||||
|
||||
// Sort them so that the list can be easily compared for changes
|
||||
}).OrderBy(i => i.Path);
|
||||
extra.ExtraType = extraType;
|
||||
extra.ParentId = Guid.Empty;
|
||||
extra.OwnerId = owner.Id;
|
||||
return extra;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
|
||||
@@ -2838,15 +2794,6 @@ namespace Emby.Server.Implementations.Library
|
||||
return path;
|
||||
}
|
||||
|
||||
private void SetExtraTypeFromFilename(Video item)
|
||||
{
|
||||
var resolver = new ExtraResolver(GetNamingOptions());
|
||||
|
||||
var result = resolver.GetExtraInfo(item.Path);
|
||||
|
||||
item.ExtraType = result.ExtraType;
|
||||
}
|
||||
|
||||
public List<PersonInfo> GetPeople(InternalPeopleQuery query)
|
||||
{
|
||||
return _itemRepository.GetPeople(query);
|
||||
@@ -2953,11 +2900,12 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
|
||||
|
||||
var existingNameCount = 1; // first numbered name will be 2
|
||||
var virtualFolderPath = Path.Combine(rootFolderPath, name);
|
||||
while (Directory.Exists(virtualFolderPath))
|
||||
{
|
||||
name += "1";
|
||||
virtualFolderPath = Path.Combine(rootFolderPath, name);
|
||||
existingNameCount++;
|
||||
virtualFolderPath = Path.Combine(rootFolderPath, name + " " + existingNameCount);
|
||||
}
|
||||
|
||||
var mediaPathInfos = options.PathInfos;
|
||||
|
||||
@@ -66,11 +66,8 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
var delayMs = mediaSource.AnalyzeDurationMs ?? 0;
|
||||
delayMs = Math.Max(3000, delayMs);
|
||||
if (delayMs > 0)
|
||||
{
|
||||
_logger.LogInformation("Waiting {0}ms before probing the live stream", delayMs);
|
||||
await Task.Delay(delayMs, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
_logger.LogInformation("Waiting {0}ms before probing the live stream", delayMs);
|
||||
await Task.Delay(delayMs, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
mediaSource.AnalyzeDurationMs = 3000;
|
||||
|
||||
@@ -464,12 +464,11 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
try
|
||||
{
|
||||
var tuple = GetProvider(request.OpenToken);
|
||||
var provider = tuple.Item1;
|
||||
var (provider, keyId) = GetProvider(request.OpenToken);
|
||||
|
||||
var currentLiveStreams = _openStreams.Values.ToList();
|
||||
|
||||
liveStream = await provider.OpenMediaSource(tuple.Item2, currentLiveStreams, cancellationToken).ConfigureAwait(false);
|
||||
liveStream = await provider.OpenMediaSource(keyId, currentLiveStreams, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
mediaSource = liveStream.MediaSource;
|
||||
|
||||
@@ -829,7 +828,7 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
private (IMediaSourceProvider, string) GetProvider(string key)
|
||||
private (IMediaSourceProvider MediaSourceProvider, string KeyId) GetProvider(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
@@ -64,18 +65,18 @@ namespace Emby.Server.Implementations.Library
|
||||
stream = sortedStreams.FirstOrDefault(s => s.IsExternal || s.IsForced || s.IsDefault);
|
||||
|
||||
// if the audio language is not understood by the user, load their preferred subs, if there are any
|
||||
if (stream == null && !preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase))
|
||||
if (stream == null && !preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream = sortedStreams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase));
|
||||
stream = sortedStreams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
else if (mode == SubtitlePlaybackMode.Smart)
|
||||
{
|
||||
// if the audio language is not understood by the user, load their preferred subs, if there are any
|
||||
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase))
|
||||
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream = streams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)) ??
|
||||
streams.FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase));
|
||||
stream = streams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase)) ??
|
||||
streams.FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
else if (mode == SubtitlePlaybackMode.Always)
|
||||
@@ -136,9 +137,9 @@ namespace Emby.Server.Implementations.Library
|
||||
else if (mode == SubtitlePlaybackMode.Smart)
|
||||
{
|
||||
// Prefer smart logic over embedded metadata
|
||||
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase))
|
||||
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
filteredStreams = streams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase))
|
||||
filteredStreams = streams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace Emby.Server.Implementations.Library
|
||||
var genres = item
|
||||
.GetRecursiveChildren(user, new InternalItemsQuery(user)
|
||||
{
|
||||
IncludeItemTypes = new[] { nameof(Audio) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.Audio },
|
||||
DtoOptions = dtoOptions
|
||||
})
|
||||
.Cast<Audio>()
|
||||
@@ -89,7 +89,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
return _libraryManager.GetItemList(new InternalItemsQuery(user)
|
||||
{
|
||||
IncludeItemTypes = new[] { nameof(Audio) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.Audio },
|
||||
|
||||
GenreIds = genreIds.ToArray(),
|
||||
|
||||
@@ -103,7 +103,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
public List<BaseItem> GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions)
|
||||
{
|
||||
if (item is MusicGenre genre)
|
||||
if (item is MusicGenre)
|
||||
{
|
||||
return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library
|
||||
/// <param name="attribute">The attrib.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
/// <exception cref="ArgumentException"><paramref name="str" /> or <paramref name="attribute" /> is empty.</exception>
|
||||
public static string? GetAttributeValue(this string str, string attribute)
|
||||
public static string? GetAttributeValue(this ReadOnlySpan<char> str, ReadOnlySpan<char> attribute)
|
||||
{
|
||||
if (str.Length == 0)
|
||||
{
|
||||
@@ -28,17 +28,31 @@ namespace Emby.Server.Implementations.Library
|
||||
throw new ArgumentException("String can't be empty.", nameof(attribute));
|
||||
}
|
||||
|
||||
string srch = "[" + attribute + "=";
|
||||
int start = str.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
|
||||
if (start != -1)
|
||||
var attributeIndex = str.IndexOf(attribute, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// Must be at least 3 characters after the attribute =, ], any character.
|
||||
var maxIndex = str.Length - attribute.Length - 3;
|
||||
while (attributeIndex > -1 && attributeIndex < maxIndex)
|
||||
{
|
||||
start += srch.Length;
|
||||
int end = str.IndexOf(']', start);
|
||||
return str.Substring(start, end - start);
|
||||
var attributeEnd = attributeIndex + attribute.Length;
|
||||
if (attributeIndex > 0
|
||||
&& str[attributeIndex - 1] == '['
|
||||
&& (str[attributeEnd] == '=' || str[attributeEnd] == '-'))
|
||||
{
|
||||
var closingIndex = str[attributeEnd..].IndexOf(']');
|
||||
// Must be at least 1 character before the closing bracket.
|
||||
if (closingIndex > 1)
|
||||
{
|
||||
return str[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim().ToString();
|
||||
}
|
||||
}
|
||||
|
||||
str = str[attributeEnd..];
|
||||
attributeIndex = str.IndexOf(attribute, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// for imdbid we also accept pattern matching
|
||||
if (string.Equals(attribute, "imdbid", StringComparison.OrdinalIgnoreCase))
|
||||
if (attribute.Equals("imdbid", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var match = ProviderIdParsers.TryFindImdbId(str, out var imdbId);
|
||||
return match ? imdbId.ToString() : null;
|
||||
|
||||
@@ -6,7 +6,10 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Audio;
|
||||
using Emby.Naming.AudioBook;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Video;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
@@ -21,11 +24,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
/// </summary>
|
||||
public class AudioResolver : ItemResolver<MediaBrowser.Controller.Entities.Audio.Audio>, IMultiItemResolver
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
|
||||
public AudioResolver(ILibraryManager libraryManager)
|
||||
public AudioResolver(NamingOptions namingOptions)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_namingOptions = namingOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -40,7 +43,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
string collectionType,
|
||||
IDirectoryService directoryService)
|
||||
{
|
||||
var result = ResolveMultipleInternal(parent, files, collectionType, directoryService);
|
||||
var result = ResolveMultipleInternal(parent, files, collectionType);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
@@ -56,12 +59,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
private MultiItemResolverResult ResolveMultipleInternal(
|
||||
Folder parent,
|
||||
List<FileSystemMetadata> files,
|
||||
string collectionType,
|
||||
IDirectoryService directoryService)
|
||||
string collectionType)
|
||||
{
|
||||
if (string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ResolveMultipleAudio<AudioBook>(parent, files, directoryService, false, collectionType, true);
|
||||
return ResolveMultipleAudio(parent, files, true);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -87,14 +89,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
return null;
|
||||
}
|
||||
|
||||
var files = args.FileSystemChildren
|
||||
.Where(i => !_libraryManager.IgnoreFile(i, args.Parent))
|
||||
.ToList();
|
||||
|
||||
return FindAudio<AudioBook>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
|
||||
return FindAudioBook(args, false);
|
||||
}
|
||||
|
||||
if (_libraryManager.IsAudioFile(args.Path))
|
||||
if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
|
||||
{
|
||||
var extension = Path.GetExtension(args.Path);
|
||||
|
||||
@@ -107,7 +105,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
var isMixedCollectionType = string.IsNullOrEmpty(collectionType);
|
||||
|
||||
// For conflicting extensions, give priority to videos
|
||||
if (isMixedCollectionType && _libraryManager.IsVideoFile(args.Path))
|
||||
if (isMixedCollectionType && VideoResolver.IsVideoFile(args.Path, _namingOptions))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -141,29 +139,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
return null;
|
||||
}
|
||||
|
||||
private T FindAudio<T>(ItemResolveArgs args, string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, string collectionType, bool parseName)
|
||||
where T : MediaBrowser.Controller.Entities.Audio.Audio, new()
|
||||
private AudioBook FindAudioBook(ItemResolveArgs args, bool parseName)
|
||||
{
|
||||
// TODO: Allow GetMultiDiscMovie in here
|
||||
const bool supportsMultiVersion = false;
|
||||
var result = ResolveMultipleAudio(args.Parent, args.GetActualFileSystemChildren(), parseName);
|
||||
|
||||
var result = ResolveMultipleAudio<T>(parent, fileSystemEntries, directoryService, supportsMultiVersion, collectionType, parseName) ??
|
||||
new MultiItemResolverResult();
|
||||
|
||||
if (result.Items.Count == 1)
|
||||
if (result == null || result.Items.Count != 1 || result.Items[0] is not AudioBook item)
|
||||
{
|
||||
// If we were supporting this we'd be checking filesFromOtherItems
|
||||
var item = (T)result.Items[0];
|
||||
item.IsInMixedFolder = false;
|
||||
item.Name = Path.GetFileName(item.ContainingFolderPath);
|
||||
return item;
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
// If we were supporting this we'd be checking filesFromOtherItems
|
||||
item.IsInMixedFolder = false;
|
||||
item.Name = Path.GetFileName(item.ContainingFolderPath);
|
||||
return item;
|
||||
}
|
||||
|
||||
private MultiItemResolverResult ResolveMultipleAudio<T>(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, bool suppportMultiEditions, string collectionType, bool parseName)
|
||||
where T : MediaBrowser.Controller.Entities.Audio.Audio, new()
|
||||
private MultiItemResolverResult ResolveMultipleAudio(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, bool parseName)
|
||||
{
|
||||
var files = new List<FileSystemMetadata>();
|
||||
var items = new List<BaseItem>();
|
||||
@@ -176,15 +168,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
{
|
||||
leftOver.Add(child);
|
||||
}
|
||||
else if (!IsIgnored(child.Name))
|
||||
else
|
||||
{
|
||||
files.Add(child);
|
||||
}
|
||||
}
|
||||
|
||||
var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
|
||||
|
||||
var resolver = new AudioBookListResolver(namingOptions);
|
||||
var resolver = new AudioBookListResolver(_namingOptions);
|
||||
var resolverResult = resolver.Resolve(files).ToList();
|
||||
|
||||
var result = new MultiItemResolverResult
|
||||
@@ -210,7 +200,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
|
||||
var firstMedia = resolvedItem.Files[0];
|
||||
|
||||
var libraryItem = new T
|
||||
var libraryItem = new AudioBook
|
||||
{
|
||||
Path = firstMedia.Path,
|
||||
IsInMixedFolder = isInMixedFolder,
|
||||
@@ -230,12 +220,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool ContainsFile(List<AudioBookInfo> result, FileSystemMetadata file)
|
||||
private static bool ContainsFile(IEnumerable<AudioBookInfo> result, FileSystemMetadata file)
|
||||
{
|
||||
return result.Any(i => ContainsFile(i, file));
|
||||
}
|
||||
|
||||
private bool ContainsFile(AudioBookInfo result, FileSystemMetadata file)
|
||||
private static bool ContainsFile(AudioBookInfo result, FileSystemMetadata file)
|
||||
{
|
||||
return result.Files.Any(i => ContainsFile(i, file)) ||
|
||||
result.AlternateVersions.Any(i => ContainsFile(i, file)) ||
|
||||
@@ -246,10 +236,5 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
{
|
||||
return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsIgnored(string filename)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Naming.Audio;
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
@@ -22,20 +23,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
public class MusicAlbumResolver : ItemResolver<MusicAlbum>
|
||||
{
|
||||
private readonly ILogger<MusicAlbumResolver> _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="fileSystem">The file system.</param>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, IFileSystem fileSystem, ILibraryManager libraryManager)
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
_libraryManager = libraryManager;
|
||||
_namingOptions = namingOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -87,7 +85,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
/// <returns><c>true</c> if the provided path points to a music album, <c>false</c> otherwise.</returns>
|
||||
public bool IsMusicAlbum(string path, IDirectoryService directoryService)
|
||||
{
|
||||
return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService, _logger, _fileSystem, _libraryManager);
|
||||
return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -101,7 +99,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
// if (args.Parent is MusicArtist) return true; // saves us from testing children twice
|
||||
if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService, _logger, _fileSystem, _libraryManager))
|
||||
if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -114,15 +112,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
/// Determine if the supplied list contains what we should consider music.
|
||||
/// </summary>
|
||||
private bool ContainsMusic(
|
||||
IEnumerable<FileSystemMetadata> list,
|
||||
ICollection<FileSystemMetadata> list,
|
||||
bool allowSubfolders,
|
||||
IDirectoryService directoryService,
|
||||
ILogger<MusicAlbumResolver> logger,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager)
|
||||
IDirectoryService directoryService)
|
||||
{
|
||||
// check for audio files before digging down into directories
|
||||
var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && libraryManager.IsAudioFile(fileSystemInfo.FullName));
|
||||
var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && AudioFileParser.IsAudioFile(fileSystemInfo.FullName, _namingOptions));
|
||||
if (foundAudioFile)
|
||||
{
|
||||
// at least one audio file exists
|
||||
@@ -137,21 +132,20 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
|
||||
var discSubfolderCount = 0;
|
||||
|
||||
var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
|
||||
var parser = new AlbumParser(namingOptions);
|
||||
var parser = new AlbumParser(_namingOptions);
|
||||
|
||||
var directories = list.Where(fileSystemInfo => fileSystemInfo.IsDirectory);
|
||||
|
||||
var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
|
||||
{
|
||||
var path = fileSystemInfo.FullName;
|
||||
var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager);
|
||||
var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService);
|
||||
|
||||
if (hasMusic)
|
||||
{
|
||||
if (parser.IsMultiPart(path))
|
||||
{
|
||||
logger.LogDebug("Found multi-disc folder: {Path}", path);
|
||||
_logger.LogDebug("Found multi-disc folder: {Path}", path);
|
||||
Interlocked.Increment(ref discSubfolderCount);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
@@ -19,27 +18,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
public class MusicArtistResolver : ItemResolver<MusicArtist>
|
||||
{
|
||||
private readonly ILogger<MusicAlbumResolver> _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private NamingOptions _namingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger for the created <see cref="MusicAlbumResolver"/> instances.</param>
|
||||
/// <param name="fileSystem">The file system.</param>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="config">The configuration manager.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public MusicArtistResolver(
|
||||
ILogger<MusicAlbumResolver> logger,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IServerConfigurationManager config)
|
||||
NamingOptions namingOptions)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
_libraryManager = libraryManager;
|
||||
_config = config;
|
||||
_namingOptions = namingOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -89,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
|
||||
var directoryService = args.DirectoryService;
|
||||
|
||||
var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
|
||||
var albumResolver = new MusicAlbumResolver(_logger, _namingOptions);
|
||||
|
||||
// If we contain an album assume we are an artist folder
|
||||
var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
|
||||
|
||||
@@ -6,6 +6,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using DiscUtils.Udf;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Video;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -21,12 +22,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T>
|
||||
where T : Video, new()
|
||||
{
|
||||
protected BaseVideoResolver(ILibraryManager libraryManager)
|
||||
protected BaseVideoResolver(NamingOptions namingOptions)
|
||||
{
|
||||
LibraryManager = libraryManager;
|
||||
NamingOptions = namingOptions;
|
||||
}
|
||||
|
||||
protected ILibraryManager LibraryManager { get; }
|
||||
protected NamingOptions NamingOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the specified args.
|
||||
@@ -48,120 +49,71 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
|
||||
where TVideoType : Video, new()
|
||||
{
|
||||
var namingOptions = LibraryManager.GetNamingOptions();
|
||||
VideoFileInfo videoInfo = null;
|
||||
VideoType? videoType = null;
|
||||
|
||||
// If the path is a file check for a matching extensions
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
TVideoType video = null;
|
||||
VideoFileInfo videoInfo = null;
|
||||
|
||||
// Loop through each child file/folder and see if we find a video
|
||||
foreach (var child in args.FileSystemChildren)
|
||||
{
|
||||
var filename = child.Name;
|
||||
|
||||
if (child.IsDirectory)
|
||||
{
|
||||
if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
|
||||
{
|
||||
videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
|
||||
|
||||
if (videoInfo == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
video = new TVideoType
|
||||
{
|
||||
Path = args.Path,
|
||||
VideoType = VideoType.Dvd,
|
||||
ProductionYear = videoInfo.Year
|
||||
};
|
||||
break;
|
||||
videoType = VideoType.Dvd;
|
||||
}
|
||||
|
||||
if (IsBluRayDirectory(filename))
|
||||
else if (IsBluRayDirectory(filename))
|
||||
{
|
||||
videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
|
||||
|
||||
if (videoInfo == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
video = new TVideoType
|
||||
{
|
||||
Path = args.Path,
|
||||
VideoType = VideoType.BluRay,
|
||||
ProductionYear = videoInfo.Year
|
||||
};
|
||||
break;
|
||||
videoType = VideoType.BluRay;
|
||||
}
|
||||
}
|
||||
else if (IsDvdFile(filename))
|
||||
{
|
||||
videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
|
||||
|
||||
if (videoInfo == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
video = new TVideoType
|
||||
{
|
||||
Path = args.Path,
|
||||
VideoType = VideoType.Dvd,
|
||||
ProductionYear = videoInfo.Year
|
||||
};
|
||||
break;
|
||||
videoType = VideoType.Dvd;
|
||||
}
|
||||
|
||||
if (videoType == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
videoInfo = VideoResolver.ResolveDirectory(args.Path, NamingOptions, parseName);
|
||||
break;
|
||||
}
|
||||
|
||||
if (video != null)
|
||||
{
|
||||
video.Name = parseName ?
|
||||
videoInfo.Name :
|
||||
Path.GetFileName(args.Path);
|
||||
|
||||
Set3DFormat(video, videoInfo);
|
||||
}
|
||||
|
||||
return video;
|
||||
}
|
||||
else
|
||||
{
|
||||
var videoInfo = VideoResolver.Resolve(args.Path, false, namingOptions, false);
|
||||
|
||||
if (videoInfo == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (LibraryManager.IsVideoFile(args.Path) || videoInfo.IsStub)
|
||||
{
|
||||
var path = args.Path;
|
||||
|
||||
var video = new TVideoType
|
||||
{
|
||||
Path = path,
|
||||
IsInMixedFolder = true,
|
||||
ProductionYear = videoInfo.Year
|
||||
};
|
||||
|
||||
SetVideoType(video, videoInfo);
|
||||
|
||||
video.Name = parseName ?
|
||||
videoInfo.Name :
|
||||
Path.GetFileNameWithoutExtension(args.Path);
|
||||
|
||||
Set3DFormat(video, videoInfo);
|
||||
|
||||
return video;
|
||||
}
|
||||
videoInfo = VideoResolver.Resolve(args.Path, false, NamingOptions, parseName);
|
||||
}
|
||||
|
||||
return null;
|
||||
if (videoInfo == null || (!videoInfo.IsStub && !VideoResolver.IsVideoFile(args.Path, NamingOptions)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var video = new TVideoType
|
||||
{
|
||||
Name = videoInfo.Name,
|
||||
Path = args.Path,
|
||||
ProductionYear = videoInfo.Year,
|
||||
ExtraType = videoInfo.ExtraType
|
||||
};
|
||||
|
||||
if (videoType.HasValue)
|
||||
{
|
||||
video.VideoType = videoType.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetVideoType(video, videoInfo);
|
||||
}
|
||||
|
||||
Set3DFormat(video, videoInfo);
|
||||
|
||||
return video;
|
||||
}
|
||||
|
||||
protected void SetVideoType(Video video, VideoFileInfo videoInfo)
|
||||
@@ -206,8 +158,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
{
|
||||
// use disc-utils, both DVDs and BDs use UDF filesystem
|
||||
using (var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read))
|
||||
using (UdfReader udfReader = new UdfReader(videoFileStream))
|
||||
{
|
||||
UdfReader udfReader = new UdfReader(videoFileStream);
|
||||
if (udfReader.DirectoryExists("VIDEO_TS"))
|
||||
{
|
||||
video.IsoType = IsoType.Dvd;
|
||||
@@ -267,7 +219,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
|
||||
protected void Set3DFormat(Video video)
|
||||
{
|
||||
var result = Format3DParser.Parse(video.Path, LibraryManager.GetNamingOptions());
|
||||
var result = Format3DParser.Parse(video.Path, NamingOptions);
|
||||
|
||||
Set3DFormat(video, result.Is3D, result.Format3D);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Entities;
|
||||
@@ -32,7 +33,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
||||
|
||||
var extension = Path.GetExtension(args.Path);
|
||||
|
||||
if (extension != null && _validExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
if (extension != null && _validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// It's a book
|
||||
return new Book
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Video;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using static Emby.Naming.Video.ExtraRuleResolver;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves a Path into a Video or Video subclass.
|
||||
/// </summary>
|
||||
internal class ExtraResolver
|
||||
{
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly IItemResolver[] _trailerResolvers;
|
||||
private readonly IItemResolver[] _videoResolvers;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an new instance of the <see cref="ExtraResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="namingOptions">An instance of <see cref="NamingOptions"/>.</param>
|
||||
public ExtraResolver(NamingOptions namingOptions)
|
||||
{
|
||||
_namingOptions = namingOptions;
|
||||
_trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(namingOptions) };
|
||||
_videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(namingOptions) };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the resolvers for the extra type.
|
||||
/// </summary>
|
||||
/// <param name="extraType">The extra type.</param>
|
||||
/// <returns>The resolvers for the extra type.</returns>
|
||||
public IItemResolver[]? GetResolversForExtraType(ExtraType extraType) => extraType switch
|
||||
{
|
||||
ExtraType.Trailer => _trailerResolvers,
|
||||
// For audio we'll have to rely on the AudioResolver, which is a "built-in"
|
||||
ExtraType.ThemeSong => null,
|
||||
_ => _videoResolvers
|
||||
};
|
||||
|
||||
public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType)
|
||||
{
|
||||
var extraResult = GetExtraInfo(path, _namingOptions);
|
||||
if (extraResult.ExtraType == null)
|
||||
{
|
||||
extraType = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var cleanDateTimeResult = CleanDateTimeParser.Clean(Path.GetFileNameWithoutExtension(path), _namingOptions.CleanDateTimeRegexes);
|
||||
var name = cleanDateTimeResult.Name;
|
||||
var year = cleanDateTimeResult.Year;
|
||||
|
||||
var parentDir = ownerVideoFileInfo.IsDirectory ? ownerVideoFileInfo.Path : Path.GetDirectoryName(ownerVideoFileInfo.Path.AsSpan());
|
||||
|
||||
var trimmedFileNameWithoutExtension = TrimFilenameDelimiters(ownerVideoFileInfo.FileNameWithoutExtension, _namingOptions.VideoFlagDelimiters);
|
||||
var trimmedVideoInfoName = TrimFilenameDelimiters(ownerVideoFileInfo.Name, _namingOptions.VideoFlagDelimiters);
|
||||
var trimmedExtraFileName = TrimFilenameDelimiters(name, _namingOptions.VideoFlagDelimiters);
|
||||
|
||||
// first check filenames
|
||||
bool isValid = StartsWith(trimmedExtraFileName, trimmedFileNameWithoutExtension)
|
||||
|| (StartsWith(trimmedExtraFileName, trimmedVideoInfoName) && year == ownerVideoFileInfo.Year);
|
||||
|
||||
if (!isValid)
|
||||
{
|
||||
// When the extra rule type is DirectoryName we must go one level higher to get the "real" dir name
|
||||
var currentParentDir = extraResult.Rule?.RuleType == ExtraRuleType.DirectoryName
|
||||
? Path.GetDirectoryName(Path.GetDirectoryName(path.AsSpan()))
|
||||
: Path.GetDirectoryName(path.AsSpan());
|
||||
|
||||
isValid = !currentParentDir.IsEmpty && !parentDir.IsEmpty && currentParentDir.Equals(parentDir, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
extraType = extraResult.ExtraType;
|
||||
return isValid;
|
||||
}
|
||||
|
||||
private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
|
||||
{
|
||||
return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
|
||||
}
|
||||
|
||||
private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName)
|
||||
{
|
||||
return !baseName.IsEmpty && fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,23 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
#nullable disable
|
||||
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves a Path into an instance of the <see cref="Video"/> class.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of item to resolve.</typeparam>
|
||||
public class GenericVideoResolver<T> : BaseVideoResolver<T>
|
||||
where T : Video, new()
|
||||
{
|
||||
public GenericVideoResolver(ILibraryManager libraryManager)
|
||||
: base(libraryManager)
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GenericVideoResolver{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public GenericVideoResolver(NamingOptions namingOptions)
|
||||
: base(namingOptions)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
private static void SetProviderIdFromPath(BaseItem item)
|
||||
{
|
||||
// we need to only look at the name of this actual item (not parents)
|
||||
var justName = Path.GetFileName(item.Path);
|
||||
var justName = Path.GetFileName(item.Path.AsSpan());
|
||||
|
||||
var id = justName.GetAttributeValue("tmdbid");
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Video;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
@@ -38,10 +39,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MovieResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="imageProcessor">The image processor.</param>
|
||||
public MovieResolver(ILibraryManager libraryManager, IImageProcessor imageProcessor)
|
||||
: base(libraryManager)
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public MovieResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
|
||||
: base(namingOptions)
|
||||
{
|
||||
_imageProcessor = imageProcessor;
|
||||
}
|
||||
@@ -59,7 +60,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
string collectionType,
|
||||
IDirectoryService directoryService)
|
||||
{
|
||||
var result = ResolveMultipleInternal(parent, files, collectionType, directoryService);
|
||||
var result = ResolveMultipleInternal(parent, files, collectionType);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
@@ -89,26 +90,24 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
return null;
|
||||
}
|
||||
|
||||
var files = args.FileSystemChildren
|
||||
.Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
|
||||
.ToList();
|
||||
Video movie = null;
|
||||
var files = args.GetActualFileSystemChildren().ToList();
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
|
||||
movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
|
||||
}
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
|
||||
movie = FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(collectionType))
|
||||
{
|
||||
// Owned items will be caught by the plain video resolver
|
||||
// Owned items will be caught by the video extra resolver
|
||||
if (args.Parent == null)
|
||||
{
|
||||
// return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -117,23 +116,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
return null;
|
||||
}
|
||||
|
||||
{
|
||||
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
|
||||
}
|
||||
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
|
||||
}
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
|
||||
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
|
||||
}
|
||||
|
||||
return null;
|
||||
// ignore extras
|
||||
return movie?.ExtraType == null ? movie : null;
|
||||
}
|
||||
|
||||
// Handle owned items
|
||||
// Owned items will be caught by the video extra resolver
|
||||
if (args.Parent == null)
|
||||
{
|
||||
return base.Resolve(args);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (IsInvalid(args.Parent, collectionType))
|
||||
@@ -168,6 +166,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
item = ResolveVideo<Video>(args, false);
|
||||
}
|
||||
|
||||
// Ignore extras
|
||||
if (item?.ExtraType != null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
item.IsInMixedFolder = true;
|
||||
@@ -179,8 +183,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
private MultiItemResolverResult ResolveMultipleInternal(
|
||||
Folder parent,
|
||||
List<FileSystemMetadata> files,
|
||||
string collectionType,
|
||||
IDirectoryService directoryService)
|
||||
string collectionType)
|
||||
{
|
||||
if (IsInvalid(parent, collectionType))
|
||||
{
|
||||
@@ -189,13 +192,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ResolveVideos<MusicVideo>(parent, files, directoryService, true, collectionType, false);
|
||||
return ResolveVideos<MusicVideo>(parent, files, true, collectionType, false);
|
||||
}
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ResolveVideos<Video>(parent, files, directoryService, false, collectionType, false);
|
||||
return ResolveVideos<Video>(parent, files, false, collectionType, false);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(collectionType))
|
||||
@@ -203,7 +206,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
// Owned items should just use the plain video type
|
||||
if (parent == null)
|
||||
{
|
||||
return ResolveVideos<Video>(parent, files, directoryService, false, collectionType, false);
|
||||
return ResolveVideos<Video>(parent, files, false, collectionType, false);
|
||||
}
|
||||
|
||||
if (parent is Series || parent.GetParents().OfType<Series>().Any())
|
||||
@@ -211,12 +214,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
return null;
|
||||
}
|
||||
|
||||
return ResolveVideos<Movie>(parent, files, directoryService, false, collectionType, true);
|
||||
return ResolveVideos<Movie>(parent, files, false, collectionType, true);
|
||||
}
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ResolveVideos<Movie>(parent, files, directoryService, true, collectionType, true);
|
||||
return ResolveVideos<Movie>(parent, files, true, collectionType, true);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -225,21 +228,20 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
private MultiItemResolverResult ResolveVideos<T>(
|
||||
Folder parent,
|
||||
IEnumerable<FileSystemMetadata> fileSystemEntries,
|
||||
IDirectoryService directoryService,
|
||||
bool suppportMultiEditions,
|
||||
bool supportMultiEditions,
|
||||
string collectionType,
|
||||
bool parseName)
|
||||
where T : Video, new()
|
||||
{
|
||||
var files = new List<FileSystemMetadata>();
|
||||
var videos = new List<BaseItem>();
|
||||
var leftOver = new List<FileSystemMetadata>();
|
||||
var hasCollectionType = !string.IsNullOrEmpty(collectionType);
|
||||
|
||||
// Loop through each child file/folder and see if we find a video
|
||||
foreach (var child in fileSystemEntries)
|
||||
{
|
||||
// This is a hack but currently no better way to resolve a sometimes ambiguous situation
|
||||
if (string.IsNullOrEmpty(collectionType))
|
||||
if (!hasCollectionType)
|
||||
{
|
||||
if (string.Equals(child.Name, "tvshow.nfo", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(child.Name, "season.nfo", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -258,31 +260,39 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
}
|
||||
}
|
||||
|
||||
var namingOptions = LibraryManager.GetNamingOptions();
|
||||
var videoInfos = files
|
||||
.Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName))
|
||||
.Where(f => f != null)
|
||||
.ToList();
|
||||
|
||||
var resolverResult = VideoListResolver.Resolve(files, namingOptions, suppportMultiEditions).ToList();
|
||||
var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName);
|
||||
|
||||
var result = new MultiItemResolverResult
|
||||
{
|
||||
ExtraFiles = leftOver,
|
||||
Items = videos
|
||||
ExtraFiles = leftOver
|
||||
};
|
||||
|
||||
var isInMixedFolder = resolverResult.Count > 1 || (parent != null && parent.IsTopParent);
|
||||
var isInMixedFolder = resolverResult.Count > 1 || parent?.IsTopParent == true;
|
||||
|
||||
foreach (var video in resolverResult)
|
||||
{
|
||||
var firstVideo = video.Files[0];
|
||||
var path = firstVideo.Path;
|
||||
if (video.ExtraType != null)
|
||||
{
|
||||
result.ExtraFiles.Add(files.Find(f => string.Equals(f.FullName, path, StringComparison.OrdinalIgnoreCase)));
|
||||
continue;
|
||||
}
|
||||
|
||||
var additionalParts = video.Files.Count > 1 ? video.Files.Skip(1).Select(i => i.Path).ToArray() : Array.Empty<string>();
|
||||
|
||||
var videoItem = new T
|
||||
{
|
||||
Path = video.Files[0].Path,
|
||||
Path = path,
|
||||
IsInMixedFolder = isInMixedFolder,
|
||||
ProductionYear = video.Year,
|
||||
Name = parseName ?
|
||||
video.Name :
|
||||
Path.GetFileNameWithoutExtension(video.Files[0].Path),
|
||||
AdditionalParts = video.Files.Skip(1).Select(i => i.Path).ToArray(),
|
||||
Name = parseName ? video.Name : firstVideo.Name,
|
||||
AdditionalParts = additionalParts,
|
||||
LocalAlternateVersions = video.AlternateVersions.Select(i => i.Path).ToArray()
|
||||
};
|
||||
|
||||
@@ -300,21 +310,34 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
private static bool IsIgnored(string filename)
|
||||
{
|
||||
// Ignore samples
|
||||
Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase);
|
||||
Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
return m.Success;
|
||||
}
|
||||
|
||||
private bool ContainsFile(List<VideoInfo> result, FileSystemMetadata file)
|
||||
private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file)
|
||||
{
|
||||
return result.Any(i => ContainsFile(i, file));
|
||||
}
|
||||
for (var i = 0; i < result.Count; i++)
|
||||
{
|
||||
var current = result[i];
|
||||
for (var j = 0; j < current.Files.Count; j++)
|
||||
{
|
||||
if (ContainsFile(current.Files[j], file))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool ContainsFile(VideoInfo result, FileSystemMetadata file)
|
||||
{
|
||||
return result.Files.Any(i => ContainsFile(i, file)) ||
|
||||
result.AlternateVersions.Any(i => ContainsFile(i, file)) ||
|
||||
result.Extras.Any(i => ContainsFile(i, file));
|
||||
for (var j = 0; j < current.AlternateVersions.Count; j++)
|
||||
{
|
||||
if (ContainsFile(current.AlternateVersions[j], file))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ContainsFile(VideoFileInfo result, FileSystemMetadata file)
|
||||
@@ -343,9 +366,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
if (item is Movie || item is MusicVideo)
|
||||
{
|
||||
// We need to only look at the name of this actual item (not parents)
|
||||
var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path) : Path.GetFileName(item.ContainingFolderPath);
|
||||
var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path.AsSpan()) : Path.GetFileName(item.ContainingFolderPath.AsSpan());
|
||||
|
||||
if (!string.IsNullOrEmpty(justName))
|
||||
if (!justName.IsEmpty)
|
||||
{
|
||||
// check for tmdb id
|
||||
var tmdbid = justName.GetAttributeValue("tmdbid");
|
||||
@@ -359,7 +382,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
if (!string.IsNullOrEmpty(item.Path))
|
||||
{
|
||||
// check for imdb id - we use full media path, as we can assume, that this will match in any use case (wither id in parent dir or in file name)
|
||||
var imdbid = item.Path.GetAttributeValue("imdbid");
|
||||
var imdbid = item.Path.AsSpan().GetAttributeValue("imdbid");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(imdbid))
|
||||
{
|
||||
@@ -432,13 +455,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
// TODO: Allow GetMultiDiscMovie in here
|
||||
const bool SupportsMultiVersion = true;
|
||||
|
||||
var result = ResolveVideos<T>(parent, fileSystemEntries, directoryService, SupportsMultiVersion, collectionType, parseName) ??
|
||||
var result = ResolveVideos<T>(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ??
|
||||
new MultiItemResolverResult();
|
||||
|
||||
if (result.Items.Count == 1)
|
||||
{
|
||||
var videoPath = result.Items[0].Path;
|
||||
var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(LibraryManager, videoPath, i.Name));
|
||||
var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name));
|
||||
|
||||
if (!hasPhotos)
|
||||
{
|
||||
@@ -511,9 +534,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
return null;
|
||||
}
|
||||
|
||||
var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
|
||||
|
||||
var result = new StackResolver(namingOptions).ResolveDirectories(folderPaths).ToList();
|
||||
var result = StackResolver.ResolveDirectories(folderPaths, NamingOptions).ToList();
|
||||
|
||||
if (result.Count != 1)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -15,17 +16,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
public class PhotoAlbumResolver : GenericFolderResolver<PhotoAlbum>
|
||||
{
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PhotoAlbumResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="imageProcessor">The image processor.</param>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
public PhotoAlbumResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager)
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public PhotoAlbumResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
|
||||
{
|
||||
_imageProcessor = imageProcessor;
|
||||
_libraryManager = libraryManager;
|
||||
_namingOptions = namingOptions;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -73,7 +74,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
|
||||
foreach (var siblingFile in files)
|
||||
{
|
||||
if (PhotoResolver.IsOwnedByMedia(_libraryManager, siblingFile.FullName, filename))
|
||||
if (PhotoResolver.IsOwnedByMedia(_namingOptions, siblingFile.FullName, filename))
|
||||
{
|
||||
ownedByMedia = true;
|
||||
break;
|
||||
|
||||
@@ -6,6 +6,9 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Video;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -16,7 +19,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
public class PhotoResolver : ItemResolver<Photo>
|
||||
{
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
|
||||
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"folder",
|
||||
@@ -30,10 +34,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
"default"
|
||||
};
|
||||
|
||||
public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager)
|
||||
public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
|
||||
{
|
||||
_imageProcessor = imageProcessor;
|
||||
_libraryManager = libraryManager;
|
||||
_namingOptions = namingOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -60,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
if (IsOwnedByMedia(_libraryManager, file.FullName, filename))
|
||||
if (IsOwnedByMedia(_namingOptions, file.FullName, filename))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -77,17 +81,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static bool IsOwnedByMedia(ILibraryManager libraryManager, string file, string imageFilename)
|
||||
internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, string imageFilename)
|
||||
{
|
||||
if (libraryManager.IsVideoFile(file))
|
||||
{
|
||||
return IsOwnedByResolvedMedia(libraryManager, file, imageFilename);
|
||||
}
|
||||
|
||||
return false;
|
||||
return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename);
|
||||
}
|
||||
|
||||
internal static bool IsOwnedByResolvedMedia(ILibraryManager libraryManager, string file, string imageFilename)
|
||||
internal static bool IsOwnedByResolvedMedia(string file, string imageFilename)
|
||||
=> imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
|
||||
@@ -110,7 +109,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
}
|
||||
|
||||
string extension = Path.GetExtension(path).TrimStart('.');
|
||||
return imageProcessor.SupportedInputFormats.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
||||
return imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
@@ -57,10 +58,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
|
||||
// Check if this is a music playlist file
|
||||
// It should have the correct collection type and a supported file extension
|
||||
else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
|
||||
else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var extension = Path.GetExtension(args.Path);
|
||||
if (Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparer.OrdinalIgnoreCase))
|
||||
if (Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new Playlist
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Entities;
|
||||
@@ -17,9 +17,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
public EpisodeResolver(ILibraryManager libraryManager)
|
||||
: base(libraryManager)
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public EpisodeResolver(NamingOptions namingOptions)
|
||||
: base(namingOptions)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -44,34 +44,36 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
|
||||
// If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something
|
||||
// Also handle flat tv folders
|
||||
if ((season != null ||
|
||||
string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
|
||||
args.HasParent<Series>())
|
||||
&& (parent is Series || !BaseItem.AllExtrasTypesFolderNames.ContainsKey(parent.Name)))
|
||||
if (season != null ||
|
||||
string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
|
||||
args.HasParent<Series>())
|
||||
{
|
||||
var episode = ResolveVideo<Episode>(args, false);
|
||||
|
||||
if (episode != null)
|
||||
// Ignore extras
|
||||
if (episode == null || episode.ExtraType != null)
|
||||
{
|
||||
var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (series != null)
|
||||
{
|
||||
episode.SeriesId = series.Id;
|
||||
episode.SeriesName = series.Name;
|
||||
}
|
||||
var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault();
|
||||
|
||||
if (season != null)
|
||||
{
|
||||
episode.SeasonId = season.Id;
|
||||
episode.SeasonName = season.Name;
|
||||
}
|
||||
if (series != null)
|
||||
{
|
||||
episode.SeriesId = series.Id;
|
||||
episode.SeriesName = series.Name;
|
||||
}
|
||||
|
||||
// Assume season 1 if there's no season folder and a season number could not be determined
|
||||
if (season == null && !episode.ParentIndexNumber.HasValue && (episode.IndexNumber.HasValue || episode.PremiereDate.HasValue))
|
||||
{
|
||||
episode.ParentIndexNumber = 1;
|
||||
}
|
||||
if (season != null)
|
||||
{
|
||||
episode.SeasonId = season.Id;
|
||||
episode.SeasonName = season.Name;
|
||||
}
|
||||
|
||||
// Assume season 1 if there's no season folder and a season number could not be determined
|
||||
if (season == null && !episode.ParentIndexNumber.HasValue && (episode.IndexNumber.HasValue || episode.PremiereDate.HasValue))
|
||||
{
|
||||
episode.ParentIndexNumber = 1;
|
||||
}
|
||||
|
||||
return episode;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Globalization;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.TV;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -14,22 +15,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
/// </summary>
|
||||
public class SeasonResolver : GenericFolderResolver<Season>
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly ILogger<SeasonResolver> _logger;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SeasonResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="localization">The localization.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public SeasonResolver(
|
||||
ILibraryManager libraryManager,
|
||||
NamingOptions namingOptions,
|
||||
ILocalizationManager localization,
|
||||
ILogger<SeasonResolver> logger)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_namingOptions = namingOptions;
|
||||
_localization = localization;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -43,7 +44,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
{
|
||||
if (args.Parent is Series series && args.IsDirectory)
|
||||
{
|
||||
var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
|
||||
var namingOptions = _namingOptions;
|
||||
|
||||
var path = args.Path;
|
||||
|
||||
@@ -65,18 +66,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
|
||||
var episodeInfo = resolver.Resolve(testPath, true);
|
||||
|
||||
if (episodeInfo != null)
|
||||
if (episodeInfo?.EpisodeNumber != null && episodeInfo.SeasonNumber.HasValue)
|
||||
{
|
||||
if (episodeInfo.EpisodeNumber.HasValue && episodeInfo.SeasonNumber.HasValue)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Found folder underneath series with episode number: {0}. Season {1}. Episode {2}",
|
||||
path,
|
||||
episodeInfo.SeasonNumber.Value,
|
||||
episodeInfo.EpisodeNumber.Value);
|
||||
_logger.LogDebug(
|
||||
"Found folder underneath series with episode number: {0}. Season {1}. Episode {2}",
|
||||
path,
|
||||
episodeInfo.SeasonNumber.Value,
|
||||
episodeInfo.EpisodeNumber.Value);
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.TV;
|
||||
using Emby.Naming.Video;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
@@ -21,17 +23,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
public class SeriesResolver : GenericFolderResolver<Series>
|
||||
{
|
||||
private readonly ILogger<SeriesResolver> _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SeriesResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
public SeriesResolver(ILogger<SeriesResolver> logger, ILibraryManager libraryManager)
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public SeriesResolver(ILogger<SeriesResolver> logger, NamingOptions namingOptions)
|
||||
{
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_namingOptions = namingOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -54,12 +56,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
return null;
|
||||
}
|
||||
|
||||
var seriesInfo = Naming.TV.SeriesResolver.Resolve(_libraryManager.GetNamingOptions(), args.Path);
|
||||
var seriesInfo = Naming.TV.SeriesResolver.Resolve(_namingOptions, args.Path);
|
||||
|
||||
var collectionType = args.GetCollectionType();
|
||||
if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var configuredContentType = _libraryManager.GetConfiguredContentType(args.Path);
|
||||
// TODO refactor into separate class or something, this is copied from LibraryManager.GetConfiguredContentType
|
||||
var configuredContentType = args.GetConfiguredContentType();
|
||||
if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new Series
|
||||
@@ -91,7 +94,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
return null;
|
||||
}
|
||||
|
||||
if (IsSeriesFolder(args.Path, args.FileSystemChildren, _logger, _libraryManager, false))
|
||||
if (IsSeriesFolder(args.Path, args.FileSystemChildren, false))
|
||||
{
|
||||
return new Series
|
||||
{
|
||||
@@ -105,11 +108,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool IsSeriesFolder(
|
||||
private bool IsSeriesFolder(
|
||||
string path,
|
||||
IEnumerable<FileSystemMetadata> fileSystemChildren,
|
||||
ILogger<SeriesResolver> logger,
|
||||
ILibraryManager libraryManager,
|
||||
bool isTvContentType)
|
||||
{
|
||||
foreach (var child in fileSystemChildren)
|
||||
@@ -118,21 +119,21 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
{
|
||||
if (IsSeasonFolder(child.FullName, isTvContentType))
|
||||
{
|
||||
logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
|
||||
_logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string fullName = child.FullName;
|
||||
if (libraryManager.IsVideoFile(fullName))
|
||||
if (VideoResolver.IsVideoFile(path, _namingOptions))
|
||||
{
|
||||
if (isTvContentType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions();
|
||||
var namingOptions = _namingOptions;
|
||||
|
||||
var episodeResolver = new Naming.TV.EpisodeResolver(namingOptions);
|
||||
|
||||
@@ -145,7 +146,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogDebug("{Path} is not a series folder.", path);
|
||||
_logger.LogDebug("{Path} is not a series folder.", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -181,13 +182,42 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
/// <param name="path">The path.</param>
|
||||
private static void SetProviderIdFromPath(Series item, string path)
|
||||
{
|
||||
var justName = Path.GetFileName(path);
|
||||
var justName = Path.GetFileName(path.AsSpan());
|
||||
|
||||
var id = justName.GetAttributeValue("tvdbid");
|
||||
|
||||
if (!string.IsNullOrEmpty(id))
|
||||
var tvdbId = justName.GetAttributeValue("tvdbid");
|
||||
if (!string.IsNullOrEmpty(tvdbId))
|
||||
{
|
||||
item.SetProviderId(MetadataProvider.Tvdb, id);
|
||||
item.SetProviderId(MetadataProvider.Tvdb, tvdbId);
|
||||
}
|
||||
|
||||
var tvmazeId = justName.GetAttributeValue("tvmazeid");
|
||||
if (!string.IsNullOrEmpty(tvmazeId))
|
||||
{
|
||||
item.SetProviderId(MetadataProvider.TvMaze, tvmazeId);
|
||||
}
|
||||
|
||||
var tmdbId = justName.GetAttributeValue("tmdbid");
|
||||
if (!string.IsNullOrEmpty(tmdbId))
|
||||
{
|
||||
item.SetProviderId(MetadataProvider.Tmdb, tmdbId);
|
||||
}
|
||||
|
||||
var anidbId = justName.GetAttributeValue("anidbid");
|
||||
if (!string.IsNullOrEmpty(anidbId))
|
||||
{
|
||||
item.SetProviderId("AniDB", anidbId);
|
||||
}
|
||||
|
||||
var aniListId = justName.GetAttributeValue("anilistid");
|
||||
if (!string.IsNullOrEmpty(aniListId))
|
||||
{
|
||||
item.SetProviderId("AniList", aniListId);
|
||||
}
|
||||
|
||||
var aniSearchId = justName.GetAttributeValue("anisearchid");
|
||||
if (!string.IsNullOrEmpty(aniSearchId))
|
||||
{
|
||||
item.SetProviderId("AniSearch", aniSearchId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,9 @@ using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Search;
|
||||
using Genre = MediaBrowser.Controller.Entities.Genre;
|
||||
using Person = MediaBrowser.Controller.Entities.Person;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
@@ -59,9 +56,9 @@ namespace Emby.Server.Implementations.Library
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddIfMissing(List<string> list, string value)
|
||||
private static void AddIfMissing(List<BaseItemKind> list, BaseItemKind value)
|
||||
{
|
||||
if (!list.Contains(value, StringComparer.OrdinalIgnoreCase))
|
||||
if (!list.Contains(value))
|
||||
{
|
||||
list.Add(value);
|
||||
}
|
||||
@@ -86,63 +83,63 @@ namespace Emby.Server.Implementations.Library
|
||||
searchTerm = searchTerm.Trim().RemoveDiacritics();
|
||||
|
||||
var excludeItemTypes = query.ExcludeItemTypes.ToList();
|
||||
var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<string>()).ToList();
|
||||
var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<BaseItemKind>()).ToList();
|
||||
|
||||
excludeItemTypes.Add(nameof(Year));
|
||||
excludeItemTypes.Add(nameof(Folder));
|
||||
excludeItemTypes.Add(BaseItemKind.Year);
|
||||
excludeItemTypes.Add(BaseItemKind.Folder);
|
||||
|
||||
if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Genre", StringComparer.OrdinalIgnoreCase)))
|
||||
if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains(BaseItemKind.Genre)))
|
||||
{
|
||||
if (!query.IncludeMedia)
|
||||
{
|
||||
AddIfMissing(includeItemTypes, nameof(Genre));
|
||||
AddIfMissing(includeItemTypes, nameof(MusicGenre));
|
||||
AddIfMissing(includeItemTypes, BaseItemKind.Genre);
|
||||
AddIfMissing(includeItemTypes, BaseItemKind.MusicGenre);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddIfMissing(excludeItemTypes, nameof(Genre));
|
||||
AddIfMissing(excludeItemTypes, nameof(MusicGenre));
|
||||
AddIfMissing(excludeItemTypes, BaseItemKind.Genre);
|
||||
AddIfMissing(excludeItemTypes, BaseItemKind.MusicGenre);
|
||||
}
|
||||
|
||||
if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains("People", StringComparer.OrdinalIgnoreCase) || includeItemTypes.Contains("Person", StringComparer.OrdinalIgnoreCase)))
|
||||
if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains(BaseItemKind.Person)))
|
||||
{
|
||||
if (!query.IncludeMedia)
|
||||
{
|
||||
AddIfMissing(includeItemTypes, nameof(Person));
|
||||
AddIfMissing(includeItemTypes, BaseItemKind.Person);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddIfMissing(excludeItemTypes, nameof(Person));
|
||||
AddIfMissing(excludeItemTypes, BaseItemKind.Person);
|
||||
}
|
||||
|
||||
if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Studio", StringComparer.OrdinalIgnoreCase)))
|
||||
if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains(BaseItemKind.Studio)))
|
||||
{
|
||||
if (!query.IncludeMedia)
|
||||
{
|
||||
AddIfMissing(includeItemTypes, nameof(Studio));
|
||||
AddIfMissing(includeItemTypes, BaseItemKind.Studio);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddIfMissing(excludeItemTypes, nameof(Studio));
|
||||
AddIfMissing(excludeItemTypes, BaseItemKind.Studio);
|
||||
}
|
||||
|
||||
if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains("MusicArtist", StringComparer.OrdinalIgnoreCase)))
|
||||
if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains(BaseItemKind.MusicArtist)))
|
||||
{
|
||||
if (!query.IncludeMedia)
|
||||
{
|
||||
AddIfMissing(includeItemTypes, nameof(MusicArtist));
|
||||
AddIfMissing(includeItemTypes, BaseItemKind.MusicArtist);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddIfMissing(excludeItemTypes, nameof(MusicArtist));
|
||||
AddIfMissing(excludeItemTypes, BaseItemKind.MusicArtist);
|
||||
}
|
||||
|
||||
AddIfMissing(excludeItemTypes, nameof(CollectionFolder));
|
||||
AddIfMissing(excludeItemTypes, nameof(Folder));
|
||||
AddIfMissing(excludeItemTypes, BaseItemKind.CollectionFolder);
|
||||
AddIfMissing(excludeItemTypes, BaseItemKind.Folder);
|
||||
var mediaTypes = query.MediaTypes.ToList();
|
||||
|
||||
if (includeItemTypes.Count > 0)
|
||||
@@ -183,7 +180,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
List<BaseItem> mediaItems;
|
||||
|
||||
if (searchQuery.IncludeItemTypes.Length == 1 && string.Equals(searchQuery.IncludeItemTypes[0], "MusicArtist", StringComparison.OrdinalIgnoreCase))
|
||||
if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist)
|
||||
{
|
||||
if (!searchQuery.ParentId.Equals(Guid.Empty))
|
||||
{
|
||||
@@ -192,8 +189,8 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
searchQuery.ParentId = Guid.Empty;
|
||||
searchQuery.IncludeItemsByName = true;
|
||||
searchQuery.IncludeItemTypes = Array.Empty<string>();
|
||||
mediaItems = _libraryManager.GetAllArtists(searchQuery).Items.Select(i => i.Item1).ToList();
|
||||
searchQuery.IncludeItemTypes = Array.Empty<BaseItemKind>();
|
||||
mediaItems = _libraryManager.GetAllArtists(searchQuery).Items.Select(i => i.Item).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
var cacheKey = GetCacheKey(userId, item.Id);
|
||||
_userData.AddOrUpdate(cacheKey, userData, (k, v) => userData);
|
||||
_userData.AddOrUpdate(cacheKey, userData, (_, _) => userData);
|
||||
|
||||
UserDataSaved?.Invoke(this, new UserDataSaveEventArgs
|
||||
{
|
||||
@@ -125,7 +125,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
var cacheKey = GetCacheKey(userId, itemId);
|
||||
|
||||
return _userData.GetOrAdd(cacheKey, k => GetUserDataInternal(userId, keys));
|
||||
return _userData.GetOrAdd(cacheKey, _ => GetUserDataInternal(userId, keys));
|
||||
}
|
||||
|
||||
private UserItemData GetUserDataInternal(long internalUserId, List<string> keys)
|
||||
|
||||
@@ -8,11 +8,11 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Channels;
|
||||
@@ -20,8 +20,6 @@ using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Library;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Genre = MediaBrowser.Controller.Entities.Genre;
|
||||
using Person = MediaBrowser.Controller.Entities.Person;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
@@ -80,7 +78,7 @@ namespace Emby.Server.Implementations.Library
|
||||
continue;
|
||||
}
|
||||
|
||||
if (query.PresetViews.Contains(folderViewType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
|
||||
if (query.PresetViews.Contains(folderViewType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
list.Add(GetUserView(folder, folderViewType, string.Empty));
|
||||
}
|
||||
@@ -175,12 +173,12 @@ namespace Emby.Server.Implementations.Library
|
||||
string viewType,
|
||||
string localizationKey,
|
||||
string sortName,
|
||||
Jellyfin.Data.Entities.User user,
|
||||
User user,
|
||||
string[] presetViews)
|
||||
{
|
||||
if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
if (!presetViews.Contains(viewType, StringComparer.OrdinalIgnoreCase))
|
||||
if (!presetViews.Contains(viewType, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (Folder)parents[0];
|
||||
}
|
||||
@@ -300,11 +298,11 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
includeItemTypes = new string[] { "Movie" };
|
||||
includeItemTypes = new[] { BaseItemKind.Movie };
|
||||
}
|
||||
else if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
includeItemTypes = new string[] { "Episode" };
|
||||
includeItemTypes = new[] { BaseItemKind.Episode };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,13 +342,13 @@ namespace Emby.Server.Implementations.Library
|
||||
var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0
|
||||
? new[]
|
||||
{
|
||||
nameof(Person),
|
||||
nameof(Studio),
|
||||
nameof(Year),
|
||||
nameof(MusicGenre),
|
||||
nameof(Genre)
|
||||
BaseItemKind.Person,
|
||||
BaseItemKind.Studio,
|
||||
BaseItemKind.Year,
|
||||
BaseItemKind.MusicGenre,
|
||||
BaseItemKind.Genre
|
||||
}
|
||||
: Array.Empty<string>();
|
||||
: Array.Empty<BaseItemKind>();
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
@@ -361,7 +359,7 @@ namespace Emby.Server.Implementations.Library
|
||||
(ItemSortBy.SortName, SortOrder.Descending),
|
||||
(ItemSortBy.ProductionYear, SortOrder.Descending)
|
||||
},
|
||||
IsFolder = includeItemTypes.Length == 0 ? false : (bool?)null,
|
||||
IsFolder = includeItemTypes.Length == 0 ? false : null,
|
||||
ExcludeItemTypes = excludeItemTypes,
|
||||
IsVirtualItem = false,
|
||||
Limit = limit * 5,
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.Library.Validators
|
||||
|
||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { nameof(MusicArtist) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.MusicArtist },
|
||||
IsDeadArtist = true,
|
||||
IsLocked = false
|
||||
}).Cast<MusicArtist>().ToList();
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Collections;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Validators
|
||||
{
|
||||
/// <summary>
|
||||
/// Class CollectionPostScanTask.
|
||||
/// </summary>
|
||||
public class CollectionPostScanTask : ILibraryPostScanTask
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ICollectionManager _collectionManager;
|
||||
private readonly ILogger<CollectionPostScanTask> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="collectionManager">The collection manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public CollectionPostScanTask(
|
||||
ILibraryManager libraryManager,
|
||||
ICollectionManager collectionManager,
|
||||
ILogger<CollectionPostScanTask> logger)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_collectionManager = collectionManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the specified progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>();
|
||||
|
||||
foreach (var library in _libraryManager.RootFolder.Children)
|
||||
{
|
||||
if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var startIndex = 0;
|
||||
var pagesize = 1000;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var movies = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
MediaTypes = new string[] { MediaType.Video },
|
||||
IncludeItemTypes = new[] { BaseItemKind.Movie },
|
||||
IsVirtualItem = false,
|
||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
|
||||
Parent = library,
|
||||
StartIndex = startIndex,
|
||||
Limit = pagesize,
|
||||
Recursive = true
|
||||
});
|
||||
|
||||
foreach (var m in movies)
|
||||
{
|
||||
if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
|
||||
{
|
||||
if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
|
||||
{
|
||||
movieList.Add(movie.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (movies.Count < pagesize)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
startIndex += pagesize;
|
||||
}
|
||||
}
|
||||
|
||||
var numComplete = 0;
|
||||
var count = collectionNameMoviesMap.Count;
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
progress.Report(100);
|
||||
return;
|
||||
}
|
||||
|
||||
var boxSets = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.BoxSet },
|
||||
CollapseBoxSetItems = false,
|
||||
Recursive = true
|
||||
});
|
||||
|
||||
foreach (var (collectionName, movieIds) in collectionNameMoviesMap)
|
||||
{
|
||||
try
|
||||
{
|
||||
var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet;
|
||||
if (boxSet == null)
|
||||
{
|
||||
// won't automatically create collection if only one movie in it
|
||||
if (movieIds.Count >= 2)
|
||||
{
|
||||
boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
|
||||
{
|
||||
Name = collectionName,
|
||||
IsLocked = true
|
||||
});
|
||||
|
||||
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= count;
|
||||
percent *= 100;
|
||||
|
||||
progress.Report(percent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds);
|
||||
}
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
@@ -91,7 +92,7 @@ namespace Emby.Server.Implementations.Library.Validators
|
||||
|
||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { nameof(Person) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.Person },
|
||||
IsDeadPerson = true,
|
||||
IsLocked = false
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user