mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-07-12 01:03:26 +01: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'
|
vmImage: 'ubuntu-latest'
|
||||||
|
|
||||||
steps:
|
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'
|
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
|
||||||
displayName: 'Build Dockerfile'
|
displayName: 'Build Dockerfile'
|
||||||
|
|
||||||
@@ -80,6 +88,14 @@ jobs:
|
|||||||
vmImage: 'ubuntu-latest'
|
vmImage: 'ubuntu-latest'
|
||||||
|
|
||||||
steps:
|
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
|
- task: DownloadPipelineArtifact@2
|
||||||
displayName: 'Download OpenAPI Spec'
|
displayName: 'Download OpenAPI Spec'
|
||||||
inputs:
|
inputs:
|
||||||
@@ -127,6 +143,10 @@ jobs:
|
|||||||
displayName: Set release version (stable)
|
displayName: Set release version (stable)
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
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
|
- task: Docker@2
|
||||||
displayName: 'Push Unstable Image'
|
displayName: 'Push Unstable Image'
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
../fedora/Makefile
|
|
||||||
@@ -150,6 +150,7 @@
|
|||||||
- [ianjazz246](https://github.com/ianjazz246)
|
- [ianjazz246](https://github.com/ianjazz246)
|
||||||
- [peterspenler](https://github.com/peterspenler)
|
- [peterspenler](https://github.com/peterspenler)
|
||||||
- [MBR-0001](https://github.com/MBR-0001)
|
- [MBR-0001](https://github.com/MBR-0001)
|
||||||
|
- [jonas-resch](https://github.com/jonas-resch)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
|
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
<PropertyGroup>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,8 @@ using MediaBrowser.Controller.Drawing;
|
|||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Entities.Movies;
|
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Controller.Playlists;
|
|
||||||
using MediaBrowser.Controller.TV;
|
using MediaBrowser.Controller.TV;
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
@@ -30,12 +27,7 @@ using MediaBrowser.Model.Entities;
|
|||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Book = MediaBrowser.Controller.Entities.Book;
|
|
||||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
|
||||||
using Genre = MediaBrowser.Controller.Entities.Genre;
|
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
|
namespace Emby.Dlna.ContentDirectory
|
||||||
{
|
{
|
||||||
@@ -395,6 +387,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
}
|
}
|
||||||
|
|
||||||
writer.WriteFullEndElement();
|
writer.WriteFullEndElement();
|
||||||
|
writer.Flush();
|
||||||
xmlWriter.WriteElementString("Result", builder.ToString());
|
xmlWriter.WriteElementString("Result", builder.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,6 +477,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
}
|
}
|
||||||
|
|
||||||
writer.WriteFullEndElement();
|
writer.WriteFullEndElement();
|
||||||
|
writer.Flush();
|
||||||
xmlWriter.WriteElementString("Result", builder.ToString());
|
xmlWriter.WriteElementString("Result", builder.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,7 +531,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
User = user,
|
User = user,
|
||||||
Recursive = true,
|
Recursive = true,
|
||||||
IsMissing = false,
|
IsMissing = false,
|
||||||
ExcludeItemTypes = new[] { nameof(Book) },
|
ExcludeItemTypes = new[] { BaseItemKind.Book },
|
||||||
IsFolder = isFolder,
|
IsFolder = isFolder,
|
||||||
MediaTypes = mediaTypes,
|
MediaTypes = mediaTypes,
|
||||||
DtoOptions = GetDtoOptions()
|
DtoOptions = GetDtoOptions()
|
||||||
@@ -617,7 +611,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
Limit = limit,
|
Limit = limit,
|
||||||
StartIndex = startIndex,
|
StartIndex = startIndex,
|
||||||
IsVirtualItem = false,
|
IsVirtualItem = false,
|
||||||
ExcludeItemTypes = new[] { nameof(Book) },
|
ExcludeItemTypes = new[] { BaseItemKind.Book },
|
||||||
IsPlaceHolder = false,
|
IsPlaceHolder = false,
|
||||||
DtoOptions = GetDtoOptions(),
|
DtoOptions = GetDtoOptions(),
|
||||||
OrderBy = GetOrderBy(sort, folder.IsPreSorted)
|
OrderBy = GetOrderBy(sort, folder.IsPreSorted)
|
||||||
@@ -642,7 +636,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
{
|
{
|
||||||
StartIndex = startIndex,
|
StartIndex = startIndex,
|
||||||
Limit = limit,
|
Limit = limit,
|
||||||
IncludeItemTypes = new[] { nameof(LiveTvChannel) },
|
IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
|
||||||
OrderBy = GetOrderBy(sort, false)
|
OrderBy = GetOrderBy(sort, false)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -673,39 +667,39 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
switch (stubType)
|
switch (stubType)
|
||||||
{
|
{
|
||||||
case StubType.Latest:
|
case StubType.Latest:
|
||||||
return GetLatest(item, query, nameof(Audio));
|
return GetLatest(item, query, BaseItemKind.Audio);
|
||||||
case StubType.Playlists:
|
case StubType.Playlists:
|
||||||
return GetMusicPlaylists(query);
|
return GetMusicPlaylists(query);
|
||||||
case StubType.Albums:
|
case StubType.Albums:
|
||||||
return GetChildrenOfItem(item, query, nameof(MusicAlbum));
|
return GetChildrenOfItem(item, query, BaseItemKind.MusicAlbum);
|
||||||
case StubType.Artists:
|
case StubType.Artists:
|
||||||
return GetMusicArtists(item, query);
|
return GetMusicArtists(item, query);
|
||||||
case StubType.AlbumArtists:
|
case StubType.AlbumArtists:
|
||||||
return GetMusicAlbumArtists(item, query);
|
return GetMusicAlbumArtists(item, query);
|
||||||
case StubType.FavoriteAlbums:
|
case StubType.FavoriteAlbums:
|
||||||
return GetChildrenOfItem(item, query, nameof(MusicAlbum), true);
|
return GetChildrenOfItem(item, query, BaseItemKind.MusicAlbum, true);
|
||||||
case StubType.FavoriteArtists:
|
case StubType.FavoriteArtists:
|
||||||
return GetFavoriteArtists(item, query);
|
return GetFavoriteArtists(item, query);
|
||||||
case StubType.FavoriteSongs:
|
case StubType.FavoriteSongs:
|
||||||
return GetChildrenOfItem(item, query, nameof(Audio), true);
|
return GetChildrenOfItem(item, query, BaseItemKind.Audio, true);
|
||||||
case StubType.Songs:
|
case StubType.Songs:
|
||||||
return GetChildrenOfItem(item, query, nameof(Audio));
|
return GetChildrenOfItem(item, query, BaseItemKind.Audio);
|
||||||
case StubType.Genres:
|
case StubType.Genres:
|
||||||
return GetMusicGenres(item, query);
|
return GetMusicGenres(item, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
var serverItems = new ServerItem[]
|
var serverItems = new ServerItem[]
|
||||||
{
|
{
|
||||||
new (item, StubType.Latest),
|
new(item, StubType.Latest),
|
||||||
new (item, StubType.Playlists),
|
new(item, StubType.Playlists),
|
||||||
new (item, StubType.Albums),
|
new(item, StubType.Albums),
|
||||||
new (item, StubType.AlbumArtists),
|
new(item, StubType.AlbumArtists),
|
||||||
new (item, StubType.Artists),
|
new(item, StubType.Artists),
|
||||||
new (item, StubType.Songs),
|
new(item, StubType.Songs),
|
||||||
new (item, StubType.Genres),
|
new(item, StubType.Genres),
|
||||||
new (item, StubType.FavoriteArtists),
|
new(item, StubType.FavoriteArtists),
|
||||||
new (item, StubType.FavoriteAlbums),
|
new(item, StubType.FavoriteAlbums),
|
||||||
new (item, StubType.FavoriteSongs)
|
new(item, StubType.FavoriteSongs)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (limit < serverItems.Length)
|
if (limit < serverItems.Length)
|
||||||
@@ -744,25 +738,25 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
case StubType.ContinueWatching:
|
case StubType.ContinueWatching:
|
||||||
return GetMovieContinueWatching(item, query);
|
return GetMovieContinueWatching(item, query);
|
||||||
case StubType.Latest:
|
case StubType.Latest:
|
||||||
return GetLatest(item, query, nameof(Movie));
|
return GetLatest(item, query, BaseItemKind.Movie);
|
||||||
case StubType.Movies:
|
case StubType.Movies:
|
||||||
return GetChildrenOfItem(item, query, nameof(Movie));
|
return GetChildrenOfItem(item, query, BaseItemKind.Movie);
|
||||||
case StubType.Collections:
|
case StubType.Collections:
|
||||||
return GetMovieCollections(query);
|
return GetMovieCollections(query);
|
||||||
case StubType.Favorites:
|
case StubType.Favorites:
|
||||||
return GetChildrenOfItem(item, query, nameof(Movie), true);
|
return GetChildrenOfItem(item, query, BaseItemKind.Movie, true);
|
||||||
case StubType.Genres:
|
case StubType.Genres:
|
||||||
return GetGenres(item, query);
|
return GetGenres(item, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
var array = new ServerItem[]
|
var array = new ServerItem[]
|
||||||
{
|
{
|
||||||
new (item, StubType.ContinueWatching),
|
new(item, StubType.ContinueWatching),
|
||||||
new (item, StubType.Latest),
|
new(item, StubType.Latest),
|
||||||
new (item, StubType.Movies),
|
new(item, StubType.Movies),
|
||||||
new (item, StubType.Collections),
|
new(item, StubType.Collections),
|
||||||
new (item, StubType.Favorites),
|
new(item, StubType.Favorites),
|
||||||
new (item, StubType.Genres)
|
new(item, StubType.Genres)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (limit < array.Length)
|
if (limit < array.Length)
|
||||||
@@ -829,26 +823,26 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
case StubType.NextUp:
|
case StubType.NextUp:
|
||||||
return GetNextUp(item, query);
|
return GetNextUp(item, query);
|
||||||
case StubType.Latest:
|
case StubType.Latest:
|
||||||
return GetLatest(item, query, nameof(Episode));
|
return GetLatest(item, query, BaseItemKind.Episode);
|
||||||
case StubType.Series:
|
case StubType.Series:
|
||||||
return GetChildrenOfItem(item, query, nameof(Series));
|
return GetChildrenOfItem(item, query, BaseItemKind.Series);
|
||||||
case StubType.FavoriteSeries:
|
case StubType.FavoriteSeries:
|
||||||
return GetChildrenOfItem(item, query, nameof(Series), true);
|
return GetChildrenOfItem(item, query, BaseItemKind.Series, true);
|
||||||
case StubType.FavoriteEpisodes:
|
case StubType.FavoriteEpisodes:
|
||||||
return GetChildrenOfItem(item, query, nameof(Episode), true);
|
return GetChildrenOfItem(item, query, BaseItemKind.Episode, true);
|
||||||
case StubType.Genres:
|
case StubType.Genres:
|
||||||
return GetGenres(item, query);
|
return GetGenres(item, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
var serverItems = new ServerItem[]
|
var serverItems = new ServerItem[]
|
||||||
{
|
{
|
||||||
new (item, StubType.ContinueWatching),
|
new(item, StubType.ContinueWatching),
|
||||||
new (item, StubType.NextUp),
|
new(item, StubType.NextUp),
|
||||||
new (item, StubType.Latest),
|
new(item, StubType.Latest),
|
||||||
new (item, StubType.Series),
|
new(item, StubType.Series),
|
||||||
new (item, StubType.FavoriteSeries),
|
new(item, StubType.FavoriteSeries),
|
||||||
new (item, StubType.FavoriteEpisodes),
|
new(item, StubType.FavoriteEpisodes),
|
||||||
new (item, StubType.Genres)
|
new(item, StubType.Genres)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (limit < serverItems.Length)
|
if (limit < serverItems.Length)
|
||||||
@@ -896,7 +890,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
private QueryResult<ServerItem> GetMovieCollections(InternalItemsQuery query)
|
private QueryResult<ServerItem> GetMovieCollections(InternalItemsQuery query)
|
||||||
{
|
{
|
||||||
query.Recursive = true;
|
query.Recursive = true;
|
||||||
query.IncludeItemTypes = new[] { nameof(BoxSet) };
|
query.IncludeItemTypes = new[] { BaseItemKind.BoxSet };
|
||||||
|
|
||||||
var result = _libraryManager.GetItemsResult(query);
|
var result = _libraryManager.GetItemsResult(query);
|
||||||
|
|
||||||
@@ -911,7 +905,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
/// <param name="itemType">The item type.</param>
|
/// <param name="itemType">The item type.</param>
|
||||||
/// <param name="isFavorite">A value indicating whether to only fetch favorite items.</param>
|
/// <param name="isFavorite">A value indicating whether to only fetch favorite items.</param>
|
||||||
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
|
/// <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.Recursive = true;
|
||||||
query.Parent = parent;
|
query.Parent = parent;
|
||||||
@@ -1011,7 +1005,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
private QueryResult<ServerItem> GetMusicPlaylists(InternalItemsQuery query)
|
private QueryResult<ServerItem> GetMusicPlaylists(InternalItemsQuery query)
|
||||||
{
|
{
|
||||||
query.Parent = null;
|
query.Parent = null;
|
||||||
query.IncludeItemTypes = new[] { nameof(Playlist) };
|
query.IncludeItemTypes = new[] { BaseItemKind.Playlist };
|
||||||
query.Recursive = true;
|
query.Recursive = true;
|
||||||
|
|
||||||
var result = _libraryManager.GetItemsResult(query);
|
var result = _libraryManager.GetItemsResult(query);
|
||||||
@@ -1050,7 +1044,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
/// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
|
/// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
|
||||||
/// <param name="itemType">The item type.</param>
|
/// <param name="itemType">The item type.</param>
|
||||||
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
|
/// <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)>();
|
query.OrderBy = Array.Empty<(string, SortOrder)>();
|
||||||
|
|
||||||
@@ -1084,7 +1078,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
{
|
{
|
||||||
Recursive = true,
|
Recursive = true,
|
||||||
ArtistIds = new[] { item.Id },
|
ArtistIds = new[] { item.Id },
|
||||||
IncludeItemTypes = new[] { nameof(MusicAlbum) },
|
IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
|
||||||
Limit = limit,
|
Limit = limit,
|
||||||
StartIndex = startIndex,
|
StartIndex = startIndex,
|
||||||
DtoOptions = GetDtoOptions(),
|
DtoOptions = GetDtoOptions(),
|
||||||
@@ -1113,8 +1107,8 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
GenreIds = new[] { item.Id },
|
GenreIds = new[] { item.Id },
|
||||||
IncludeItemTypes = new[]
|
IncludeItemTypes = new[]
|
||||||
{
|
{
|
||||||
nameof(Movie),
|
BaseItemKind.Movie,
|
||||||
nameof(Series)
|
BaseItemKind.Series
|
||||||
},
|
},
|
||||||
Limit = limit,
|
Limit = limit,
|
||||||
StartIndex = startIndex,
|
StartIndex = startIndex,
|
||||||
@@ -1142,7 +1136,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
{
|
{
|
||||||
Recursive = true,
|
Recursive = true,
|
||||||
GenreIds = new[] { item.Id },
|
GenreIds = new[] { item.Id },
|
||||||
IncludeItemTypes = new[] { nameof(MusicAlbum) },
|
IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
|
||||||
Limit = limit,
|
Limit = limit,
|
||||||
StartIndex = startIndex,
|
StartIndex = startIndex,
|
||||||
DtoOptions = GetDtoOptions(),
|
DtoOptions = GetDtoOptions(),
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ namespace Emby.Dlna.Didl
|
|||||||
public Filter(string filter)
|
public Filter(string filter)
|
||||||
{
|
{
|
||||||
_all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase);
|
_all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase);
|
||||||
|
_fields = filter.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||||
_fields = (filter ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Contains(string field)
|
public bool Contains(string field)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ using System.Globalization;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -84,8 +83,7 @@ namespace Emby.Dlna
|
|||||||
{
|
{
|
||||||
lock (_profiles)
|
lock (_profiles)
|
||||||
{
|
{
|
||||||
var list = _profiles.Values.ToList();
|
return _profiles.Values
|
||||||
return list
|
|
||||||
.OrderBy(i => i.Item1.Info.Type == DeviceProfileType.User ? 0 : 1)
|
.OrderBy(i => i.Item1.Info.Type == DeviceProfileType.User ? 0 : 1)
|
||||||
.ThenBy(i => i.Item1.Info.Name)
|
.ThenBy(i => i.Item1.Info.Name)
|
||||||
.Select(i => i.Item2)
|
.Select(i => i.Item2)
|
||||||
@@ -227,11 +225,8 @@ namespace Emby.Dlna
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var xmlFies = _fileSystem.GetFilePaths(path)
|
return _fileSystem.GetFilePaths(path)
|
||||||
.Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
|
.Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return xmlFies
|
|
||||||
.Select(i => ParseProfileFile(i, type))
|
.Select(i => ParseProfileFile(i, type))
|
||||||
.Where(i => i != null)
|
.Where(i => i != null)
|
||||||
.ToList()!; // We just filtered out all the nulls
|
.ToList()!; // We just filtered out all the nulls
|
||||||
@@ -253,11 +248,8 @@ namespace Emby.Dlna
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
DeviceProfile profile;
|
|
||||||
|
|
||||||
var tempProfile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path);
|
var tempProfile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path);
|
||||||
|
var profile = ReserializeProfile(tempProfile);
|
||||||
profile = ReserializeProfile(tempProfile);
|
|
||||||
|
|
||||||
profile.Id = path.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
profile.Id = path.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
@@ -296,8 +288,7 @@ namespace Emby.Dlna
|
|||||||
{
|
{
|
||||||
lock (_profiles)
|
lock (_profiles)
|
||||||
{
|
{
|
||||||
var list = _profiles.Values.ToList();
|
return _profiles.Values
|
||||||
return list
|
|
||||||
.Select(i => i.Item1)
|
.Select(i => i.Item1)
|
||||||
.OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1)
|
.OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1)
|
||||||
.ThenBy(i => i.Info.Name);
|
.ThenBy(i => i.Info.Name);
|
||||||
@@ -350,7 +341,7 @@ namespace Emby.Dlna
|
|||||||
Directory.CreateDirectory(systemProfilesPath);
|
Directory.CreateDirectory(systemProfilesPath);
|
||||||
|
|
||||||
var fileOptions = AsyncFile.WriteOptions;
|
var fileOptions = AsyncFile.WriteOptions;
|
||||||
fileOptions.Mode = FileMode.CreateNew;
|
fileOptions.Mode = FileMode.Create;
|
||||||
fileOptions.PreallocationSize = length;
|
fileOptions.PreallocationSize = length;
|
||||||
using (var fileStream = new FileStream(path, fileOptions))
|
using (var fileStream = new FileStream(path, fileOptions))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,10 +22,14 @@
|
|||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<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" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ namespace Emby.Dlna.Main
|
|||||||
config);
|
config);
|
||||||
Current = this;
|
Current = this;
|
||||||
|
|
||||||
var netConfig = config.GetConfiguration<NetworkConfiguration>("network");
|
var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
|
||||||
_disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
|
_disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
|
||||||
|
|
||||||
if (_disabled && _config.GetDlnaConfiguration().EnableServer)
|
if (_disabled && _config.GetDlnaConfiguration().EnableServer)
|
||||||
@@ -262,7 +262,6 @@ namespace Emby.Dlna.Main
|
|||||||
{
|
{
|
||||||
_publisher = new SsdpDevicePublisher(
|
_publisher = new SsdpDevicePublisher(
|
||||||
_communicationsServer,
|
_communicationsServer,
|
||||||
_networkManager,
|
|
||||||
MediaBrowser.Common.System.OperatingSystem.Name,
|
MediaBrowser.Common.System.OperatingSystem.Name,
|
||||||
Environment.OSVersion.VersionString,
|
Environment.OSVersion.VersionString,
|
||||||
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
||||||
@@ -400,7 +399,6 @@ namespace Emby.Dlna.Main
|
|||||||
_imageProcessor,
|
_imageProcessor,
|
||||||
_deviceDiscovery,
|
_deviceDiscovery,
|
||||||
_httpClientFactory,
|
_httpClientFactory,
|
||||||
_config,
|
|
||||||
_userDataManager,
|
_userDataManager,
|
||||||
_localization,
|
_localization,
|
||||||
_mediaSourceManager,
|
_mediaSourceManager,
|
||||||
|
|||||||
@@ -1179,6 +1179,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return new Device(deviceProperties, httpClientFactory, logger);
|
return new Device(deviceProperties, httpClientFactory, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
private static DeviceIcon CreateIcon(XElement element)
|
private static DeviceIcon CreateIcon(XElement element)
|
||||||
{
|
{
|
||||||
if (element == null)
|
if (element == null)
|
||||||
@@ -1186,69 +1187,61 @@ namespace Emby.Dlna.PlayTo
|
|||||||
throw new ArgumentNullException(nameof(element));
|
throw new ArgumentNullException(nameof(element));
|
||||||
}
|
}
|
||||||
|
|
||||||
var mimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype"));
|
|
||||||
var width = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("width"));
|
var width = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("width"));
|
||||||
var height = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("height"));
|
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);
|
_ = int.TryParse(width, NumberStyles.Integer, CultureInfo.InvariantCulture, out var widthValue);
|
||||||
var heightValue = int.Parse(height, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
_ = int.TryParse(height, NumberStyles.Integer, CultureInfo.InvariantCulture, out var heightValue);
|
||||||
|
|
||||||
return new DeviceIcon
|
return new DeviceIcon
|
||||||
{
|
{
|
||||||
Depth = depth,
|
Depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth")) ?? string.Empty,
|
||||||
Height = heightValue,
|
Height = heightValue,
|
||||||
MimeType = mimeType,
|
MimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype")) ?? string.Empty,
|
||||||
Url = url,
|
Url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url")) ?? string.Empty,
|
||||||
Width = widthValue
|
Width = widthValue
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DeviceService Create(XElement element)
|
private static DeviceService Create(XElement element)
|
||||||
{
|
=> new DeviceService()
|
||||||
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
|
|
||||||
{
|
{
|
||||||
ControlUrl = controlURL,
|
ControlUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL")) ?? string.Empty,
|
||||||
EventSubUrl = eventSubURL,
|
EventSubUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL")) ?? string.Empty,
|
||||||
ScpdUrl = scpdUrl,
|
ScpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL")) ?? string.Empty,
|
||||||
ServiceId = id,
|
ServiceId = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId")) ?? string.Empty,
|
||||||
ServiceType = type
|
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;
|
TransportState = state;
|
||||||
|
|
||||||
var previousMediaInfo = CurrentMediaInfo;
|
var previousMediaInfo = CurrentMediaInfo;
|
||||||
CurrentMediaInfo = mediaInfo;
|
CurrentMediaInfo = mediaInfo;
|
||||||
|
|
||||||
if (previousMediaInfo == null && mediaInfo != null)
|
if (mediaInfo == null)
|
||||||
|
{
|
||||||
|
if (previousMediaInfo != null)
|
||||||
|
{
|
||||||
|
OnPlaybackStop(previousMediaInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (previousMediaInfo == null)
|
||||||
{
|
{
|
||||||
if (state != TransportState.Stopped)
|
if (state != TransportState.Stopped)
|
||||||
{
|
{
|
||||||
OnPlaybackStart(mediaInfo);
|
OnPlaybackStart(mediaInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (mediaInfo != null && previousMediaInfo != null && !mediaInfo.Equals(previousMediaInfo))
|
else if (mediaInfo.Equals(previousMediaInfo))
|
||||||
{
|
|
||||||
OnMediaChanged(previousMediaInfo, mediaInfo);
|
|
||||||
}
|
|
||||||
else if (mediaInfo == null && previousMediaInfo != null)
|
|
||||||
{
|
|
||||||
OnPlaybackStop(previousMediaInfo);
|
|
||||||
}
|
|
||||||
else if (mediaInfo != null && mediaInfo.Equals(previousMediaInfo))
|
|
||||||
{
|
{
|
||||||
OnPlaybackProgress(mediaInfo);
|
OnPlaybackProgress(mediaInfo);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OnMediaChanged(previousMediaInfo, mediaInfo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPlaybackStart(UBaseObject mediaInfo)
|
private void OnPlaybackStart(UBaseObject mediaInfo)
|
||||||
|
|||||||
@@ -210,9 +210,9 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false);
|
var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
var duration = mediaSource == null ?
|
var duration = mediaSource == null
|
||||||
(_device.Duration == null ? (long?)null : _device.Duration.Value.Ticks) :
|
? _device.Duration?.Ticks
|
||||||
mediaSource.RunTimeTicks;
|
: mediaSource.RunTimeTicks;
|
||||||
|
|
||||||
var playedToCompletion = positionTicks.HasValue && positionTicks.Value == 0;
|
var playedToCompletion = positionTicks.HasValue && positionTicks.Value == 0;
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ using System.Threading.Tasks;
|
|||||||
using Jellyfin.Data.Events;
|
using Jellyfin.Data.Events;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Controller.Dlna;
|
using MediaBrowser.Controller.Dlna;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
@@ -35,7 +34,6 @@ namespace Emby.Dlna.PlayTo
|
|||||||
private readonly IServerApplicationHost _appHost;
|
private readonly IServerApplicationHost _appHost;
|
||||||
private readonly IImageProcessor _imageProcessor;
|
private readonly IImageProcessor _imageProcessor;
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly IServerConfigurationManager _config;
|
|
||||||
private readonly IUserDataManager _userDataManager;
|
private readonly IUserDataManager _userDataManager;
|
||||||
private readonly ILocalizationManager _localization;
|
private readonly ILocalizationManager _localization;
|
||||||
|
|
||||||
@@ -47,7 +45,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
|
private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
|
||||||
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
|
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;
|
_logger = logger;
|
||||||
_sessionManager = sessionManager;
|
_sessionManager = sessionManager;
|
||||||
@@ -58,7 +56,6 @@ namespace Emby.Dlna.PlayTo
|
|||||||
_imageProcessor = imageProcessor;
|
_imageProcessor = imageProcessor;
|
||||||
_deviceDiscovery = deviceDiscovery;
|
_deviceDiscovery = deviceDiscovery;
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
_config = config;
|
|
||||||
_userDataManager = userDataManager;
|
_userDataManager = userDataManager;
|
||||||
_localization = localization;
|
_localization = localization;
|
||||||
_mediaSourceManager = mediaSourceManager;
|
_mediaSourceManager = mediaSourceManager;
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ??
|
var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ??
|
||||||
(state.AllowedValues.Count > 0 ? state.AllowedValues[0] : value);
|
(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);
|
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)
|
public void AddXmlRootAttribute(string name, string value)
|
||||||
{
|
{
|
||||||
var atts = XmlRootAttributes ?? System.Array.Empty<XmlAttribute>();
|
var list = XmlRootAttributes.ToList();
|
||||||
var list = atts.ToList();
|
|
||||||
|
|
||||||
list.Add(new XmlAttribute
|
list.Add(new XmlAttribute
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ namespace Emby.Dlna.Server
|
|||||||
builder.Append("<icon>");
|
builder.Append("<icon>");
|
||||||
|
|
||||||
builder.Append("<mimetype>")
|
builder.Append("<mimetype>")
|
||||||
.Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
|
.Append(SecurityElement.Escape(icon.MimeType))
|
||||||
.Append("</mimetype>");
|
.Append("</mimetype>");
|
||||||
builder.Append("<width>")
|
builder.Append("<width>")
|
||||||
.Append(SecurityElement.Escape(icon.Width.ToString(CultureInfo.InvariantCulture)))
|
.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(SecurityElement.Escape(icon.Height.ToString(CultureInfo.InvariantCulture)))
|
||||||
.Append("</height>");
|
.Append("</height>");
|
||||||
builder.Append("<depth>")
|
builder.Append("<depth>")
|
||||||
.Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
|
.Append(SecurityElement.Escape(icon.Depth))
|
||||||
.Append("</depth>");
|
.Append("</depth>");
|
||||||
builder.Append("<url>")
|
builder.Append("<url>")
|
||||||
.Append(BuildUrl(icon.Url))
|
.Append(BuildUrl(icon.Url))
|
||||||
@@ -219,10 +219,10 @@ namespace Emby.Dlna.Server
|
|||||||
builder.Append("<service>");
|
builder.Append("<service>");
|
||||||
|
|
||||||
builder.Append("<serviceType>")
|
builder.Append("<serviceType>")
|
||||||
.Append(SecurityElement.Escape(service.ServiceType ?? string.Empty))
|
.Append(SecurityElement.Escape(service.ServiceType))
|
||||||
.Append("</serviceType>");
|
.Append("</serviceType>");
|
||||||
builder.Append("<serviceId>")
|
builder.Append("<serviceId>")
|
||||||
.Append(SecurityElement.Escape(service.ServiceId ?? string.Empty))
|
.Append(SecurityElement.Escape(service.ServiceId))
|
||||||
.Append("</serviceId>");
|
.Append("</serviceId>");
|
||||||
builder.Append("<SCPDURL>")
|
builder.Append("<SCPDURL>")
|
||||||
.Append(BuildUrl(service.ScpdUrl))
|
.Append(BuildUrl(service.ScpdUrl))
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ namespace Emby.Dlna.Service
|
|||||||
builder.Append("<action>");
|
builder.Append("<action>");
|
||||||
|
|
||||||
builder.Append("<name>")
|
builder.Append("<name>")
|
||||||
.Append(SecurityElement.Escape(item.Name ?? string.Empty))
|
.Append(SecurityElement.Escape(item.Name))
|
||||||
.Append("</name>");
|
.Append("</name>");
|
||||||
|
|
||||||
builder.Append("<argumentList>");
|
builder.Append("<argumentList>");
|
||||||
@@ -48,13 +48,13 @@ namespace Emby.Dlna.Service
|
|||||||
builder.Append("<argument>");
|
builder.Append("<argument>");
|
||||||
|
|
||||||
builder.Append("<name>")
|
builder.Append("<name>")
|
||||||
.Append(SecurityElement.Escape(argument.Name ?? string.Empty))
|
.Append(SecurityElement.Escape(argument.Name))
|
||||||
.Append("</name>");
|
.Append("</name>");
|
||||||
builder.Append("<direction>")
|
builder.Append("<direction>")
|
||||||
.Append(SecurityElement.Escape(argument.Direction ?? string.Empty))
|
.Append(SecurityElement.Escape(argument.Direction))
|
||||||
.Append("</direction>");
|
.Append("</direction>");
|
||||||
builder.Append("<relatedStateVariable>")
|
builder.Append("<relatedStateVariable>")
|
||||||
.Append(SecurityElement.Escape(argument.RelatedStateVariable ?? string.Empty))
|
.Append(SecurityElement.Escape(argument.RelatedStateVariable))
|
||||||
.Append("</relatedStateVariable>");
|
.Append("</relatedStateVariable>");
|
||||||
|
|
||||||
builder.Append("</argument>");
|
builder.Append("</argument>");
|
||||||
@@ -81,10 +81,10 @@ namespace Emby.Dlna.Service
|
|||||||
.Append("\">");
|
.Append("\">");
|
||||||
|
|
||||||
builder.Append("<name>")
|
builder.Append("<name>")
|
||||||
.Append(SecurityElement.Escape(item.Name ?? string.Empty))
|
.Append(SecurityElement.Escape(item.Name))
|
||||||
.Append("</name>");
|
.Append("</name>");
|
||||||
builder.Append("<dataType>")
|
builder.Append("<dataType>")
|
||||||
.Append(SecurityElement.Escape(item.DataType ?? string.Empty))
|
.Append(SecurityElement.Escape(item.DataType))
|
||||||
.Append("</dataType>");
|
.Append("</dataType>");
|
||||||
|
|
||||||
if (item.AllowedValues.Count > 0)
|
if (item.AllowedValues.Count > 0)
|
||||||
|
|||||||
@@ -11,6 +11,10 @@
|
|||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||||
@@ -24,7 +28,7 @@
|
|||||||
<!-- Code analysers-->
|
<!-- Code analysers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<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" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Mime;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
@@ -101,8 +102,7 @@ namespace Emby.Drawing
|
|||||||
public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
|
public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
|
||||||
{
|
{
|
||||||
var file = await ProcessImage(options).ConfigureAwait(false);
|
var file = await ProcessImage(options).ConfigureAwait(false);
|
||||||
|
using (var fileStream = AsyncFile.OpenRead(file.Path))
|
||||||
using (var fileStream = AsyncFile.OpenRead(file.Item1))
|
|
||||||
{
|
{
|
||||||
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
|
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -117,7 +117,7 @@ namespace Emby.Drawing
|
|||||||
=> _transparentImageTypes.Contains(Path.GetExtension(path));
|
=> _transparentImageTypes.Contains(Path.GetExtension(path));
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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;
|
ItemImageInfo originalImage = options.Image;
|
||||||
BaseItem item = options.Item;
|
BaseItem item = options.Item;
|
||||||
@@ -130,20 +130,22 @@ namespace Emby.Drawing
|
|||||||
originalImageSize = new ImageDimensions(originalImage.Width, originalImage.Height);
|
originalImageSize = new ImageDimensions(originalImage.Width, originalImage.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mimeType = MimeTypes.GetMimeType(originalImagePath);
|
||||||
if (!_imageEncoder.SupportsImageEncoding)
|
if (!_imageEncoder.SupportsImageEncoding)
|
||||||
{
|
{
|
||||||
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
|
return (originalImagePath, mimeType, dateModified);
|
||||||
}
|
}
|
||||||
|
|
||||||
var supportedImageInfo = await GetSupportedImage(originalImagePath, dateModified).ConfigureAwait(false);
|
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 requiresTransparency = _transparentImageTypes.Contains(Path.GetExtension(originalImagePath));
|
||||||
|
|
||||||
bool autoOrient = false;
|
bool autoOrient = false;
|
||||||
@@ -243,7 +245,7 @@ namespace Emby.Drawing
|
|||||||
return ImageFormat.Jpg;
|
return ImageFormat.Jpg;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string? GetMimeType(ImageFormat format, string path)
|
private string GetMimeType(ImageFormat format, string path)
|
||||||
=> format switch
|
=> format switch
|
||||||
{
|
{
|
||||||
ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
|
ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
|
||||||
@@ -437,7 +439,7 @@ namespace Emby.Drawing
|
|||||||
.ToString("N", CultureInfo.InvariantCulture);
|
.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)
|
var inputFormat = Path.GetExtension(originalImagePath)
|
||||||
.TrimStart('.')
|
.TrimStart('.')
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ namespace Emby.Naming.AudioBook
|
|||||||
public class AudioBookListResolver
|
public class AudioBookListResolver
|
||||||
{
|
{
|
||||||
private readonly NamingOptions _options;
|
private readonly NamingOptions _options;
|
||||||
|
private readonly AudioBookResolver _audioBookResolver;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="AudioBookListResolver"/> class.
|
/// Initializes a new instance of the <see cref="AudioBookListResolver"/> class.
|
||||||
@@ -22,6 +23,7 @@ namespace Emby.Naming.AudioBook
|
|||||||
public AudioBookListResolver(NamingOptions options)
|
public AudioBookListResolver(NamingOptions options)
|
||||||
{
|
{
|
||||||
_options = options;
|
_options = options;
|
||||||
|
_audioBookResolver = new AudioBookResolver(_options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -31,21 +33,18 @@ namespace Emby.Naming.AudioBook
|
|||||||
/// <returns>Returns IEnumerable of <see cref="AudioBookInfo"/>.</returns>
|
/// <returns>Returns IEnumerable of <see cref="AudioBookInfo"/>.</returns>
|
||||||
public IEnumerable<AudioBookInfo> Resolve(IEnumerable<FileSystemMetadata> files)
|
public IEnumerable<AudioBookInfo> Resolve(IEnumerable<FileSystemMetadata> files)
|
||||||
{
|
{
|
||||||
var audioBookResolver = new AudioBookResolver(_options);
|
|
||||||
|
|
||||||
// File with empty fullname will be sorted out here.
|
// File with empty fullname will be sorted out here.
|
||||||
var audiobookFileInfos = files
|
var audiobookFileInfos = files
|
||||||
.Select(i => audioBookResolver.Resolve(i.FullName))
|
.Select(i => _audioBookResolver.Resolve(i.FullName))
|
||||||
.OfType<AudioBookFileInfo>()
|
.OfType<AudioBookFileInfo>()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var stackResult = new StackResolver(_options)
|
var stackResult = StackResolver.ResolveAudioBooks(audiobookFileInfos);
|
||||||
.ResolveAudioBooks(audiobookFileInfos);
|
|
||||||
|
|
||||||
foreach (var stack in stackResult)
|
foreach (var stack in stackResult)
|
||||||
{
|
{
|
||||||
var stackFiles = stack.Files
|
var stackFiles = stack.Files
|
||||||
.Select(i => audioBookResolver.Resolve(i))
|
.Select(i => _audioBookResolver.Resolve(i))
|
||||||
.OfType<AudioBookFileInfo>()
|
.OfType<AudioBookFileInfo>()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
|
|
||||||
namespace Emby.Naming.AudioBook
|
namespace Emby.Naming.AudioBook
|
||||||
{
|
{
|
||||||
@@ -37,7 +37,7 @@ namespace Emby.Naming.AudioBook
|
|||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path);
|
||||||
|
|
||||||
// Check supported extensions
|
// Check supported extensions
|
||||||
if (!_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
if (!_options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma warning disable CA1819
|
#pragma warning disable CA1819
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Emby.Naming.Video;
|
using Emby.Naming.Video;
|
||||||
@@ -124,11 +125,11 @@ namespace Emby.Naming.Common
|
|||||||
token: "DSR")
|
token: "DSR")
|
||||||
};
|
};
|
||||||
|
|
||||||
VideoFileStackingExpressions = new[]
|
VideoFileStackingRules = new[]
|
||||||
{
|
{
|
||||||
"(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(?<ignore>.*?)(?<extension>\\.[^.]+)$",
|
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
|
||||||
"(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$",
|
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false),
|
||||||
"(?<title>.*?)(?<volume>[ ._-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$"
|
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?<number>[a-d])(?:\.[^.]+)?$", false)
|
||||||
};
|
};
|
||||||
|
|
||||||
CleanDateTimes = new[]
|
CleanDateTimes = new[]
|
||||||
@@ -403,6 +404,12 @@ namespace Emby.Naming.Common
|
|||||||
|
|
||||||
VideoExtraRules = new[]
|
VideoExtraRules = new[]
|
||||||
{
|
{
|
||||||
|
new ExtraRule(
|
||||||
|
ExtraType.Trailer,
|
||||||
|
ExtraRuleType.DirectoryName,
|
||||||
|
"trailers",
|
||||||
|
MediaType.Video),
|
||||||
|
|
||||||
new ExtraRule(
|
new ExtraRule(
|
||||||
ExtraType.Trailer,
|
ExtraType.Trailer,
|
||||||
ExtraRuleType.Filename,
|
ExtraRuleType.Filename,
|
||||||
@@ -463,12 +470,24 @@ namespace Emby.Naming.Common
|
|||||||
" sample",
|
" sample",
|
||||||
MediaType.Video),
|
MediaType.Video),
|
||||||
|
|
||||||
|
new ExtraRule(
|
||||||
|
ExtraType.ThemeVideo,
|
||||||
|
ExtraRuleType.DirectoryName,
|
||||||
|
"backdrops",
|
||||||
|
MediaType.Video),
|
||||||
|
|
||||||
new ExtraRule(
|
new ExtraRule(
|
||||||
ExtraType.ThemeSong,
|
ExtraType.ThemeSong,
|
||||||
ExtraRuleType.Filename,
|
ExtraRuleType.Filename,
|
||||||
"theme",
|
"theme",
|
||||||
MediaType.Audio),
|
MediaType.Audio),
|
||||||
|
|
||||||
|
new ExtraRule(
|
||||||
|
ExtraType.ThemeSong,
|
||||||
|
ExtraRuleType.DirectoryName,
|
||||||
|
"theme-music",
|
||||||
|
MediaType.Audio),
|
||||||
|
|
||||||
new ExtraRule(
|
new ExtraRule(
|
||||||
ExtraType.Scene,
|
ExtraType.Scene,
|
||||||
ExtraRuleType.Suffix,
|
ExtraRuleType.Suffix,
|
||||||
@@ -563,7 +582,7 @@ namespace Emby.Naming.Common
|
|||||||
ExtraType.Unknown,
|
ExtraType.Unknown,
|
||||||
ExtraRuleType.DirectoryName,
|
ExtraRuleType.DirectoryName,
|
||||||
"extras",
|
"extras",
|
||||||
MediaType.Video),
|
MediaType.Video)
|
||||||
};
|
};
|
||||||
|
|
||||||
Format3DRules = new[]
|
Format3DRules = new[]
|
||||||
@@ -675,9 +694,29 @@ namespace Emby.Naming.Common
|
|||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.ToArray();
|
.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();
|
Compile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the folder name to extra types mapping.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, ExtraType> AllExtrasTypesFolderNames { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets list of audio file extensions.
|
/// Gets or sets list of audio file extensions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -759,9 +798,9 @@ namespace Emby.Naming.Common
|
|||||||
public Format3DRule[] Format3DRules { get; set; }
|
public Format3DRule[] Format3DRules { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets list of raw video file-stacking expressions strings.
|
/// Gets the file stacking rules.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string[] VideoFileStackingExpressions { get; set; }
|
public FileStackRule[] VideoFileStackingRules { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets list of raw clean DateTimes regular expressions strings.
|
/// Gets or sets list of raw clean DateTimes regular expressions strings.
|
||||||
@@ -783,11 +822,6 @@ namespace Emby.Naming.Common
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ExtraRule[] VideoExtraRules { get; set; }
|
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>
|
/// <summary>
|
||||||
/// Gets list of clean datetime regular expressions.
|
/// Gets list of clean datetime regular expressions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -813,7 +847,6 @@ namespace Emby.Naming.Common
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void Compile()
|
public void Compile()
|
||||||
{
|
{
|
||||||
VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray();
|
|
||||||
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
|
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
|
||||||
CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
|
CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
|
||||||
EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
|
EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
|
||||||
|
|||||||
@@ -15,6 +15,10 @@
|
|||||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
|
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
|
||||||
<!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
|
<!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
|
||||||
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
|
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
|
||||||
@@ -38,13 +42,13 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0" PrivateAssets="All" />
|
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<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" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
|
|
||||||
namespace Emby.Naming.Subtitles
|
namespace Emby.Naming.Subtitles
|
||||||
{
|
{
|
||||||
@@ -34,7 +35,7 @@ namespace Emby.Naming.Subtitles
|
|||||||
}
|
}
|
||||||
|
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path);
|
||||||
if (!_options.SubtitleFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
if (!_options.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -42,11 +43,11 @@ namespace Emby.Naming.Subtitles
|
|||||||
var flags = GetFlags(path);
|
var flags = GetFlags(path);
|
||||||
var info = new SubtitleInfo(
|
var info = new SubtitleInfo(
|
||||||
path,
|
path,
|
||||||
_options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)),
|
_options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)),
|
||||||
_options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)));
|
_options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)));
|
||||||
|
|
||||||
var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparer.OrdinalIgnoreCase)
|
var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparison.OrdinalIgnoreCase)
|
||||||
&& !_options.SubtitleForcedFlags.Contains(i, StringComparer.OrdinalIgnoreCase))
|
&& !_options.SubtitleForcedFlags.Contains(i, StringComparison.OrdinalIgnoreCase))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// Should have a name, language and file extension
|
// Should have a name, language and file extension
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
using Emby.Naming.Video;
|
using Emby.Naming.Video;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
|
|
||||||
namespace Emby.Naming.TV
|
namespace Emby.Naming.TV
|
||||||
{
|
{
|
||||||
@@ -48,7 +48,7 @@ namespace Emby.Naming.TV
|
|||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path);
|
||||||
// Check supported extensions
|
// Check supported extensions
|
||||||
if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
if (!_options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// It's not supported. Check stub extensions
|
// It's not supported. Check stub extensions
|
||||||
if (!StubResolver.TryResolveFile(path, _options, out stubType))
|
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="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>
|
/// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param>
|
||||||
/// <returns>System.Nullable{System.Int32}.</returns>
|
/// <returns>System.Nullable{System.Int32}.</returns>
|
||||||
private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPath(
|
private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
|
||||||
string path,
|
string path,
|
||||||
bool supportSpecialAliases,
|
bool supportSpecialAliases,
|
||||||
bool supportNumericSeasonFolders)
|
bool supportNumericSeasonFolders)
|
||||||
@@ -99,7 +99,7 @@ namespace Emby.Naming.TV
|
|||||||
if (filename.Contains(name, StringComparison.OrdinalIgnoreCase))
|
if (filename.Contains(name, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase));
|
var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase));
|
||||||
if (result.seasonNumber.HasValue)
|
if (result.SeasonNumber.HasValue)
|
||||||
{
|
{
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -142,7 +142,7 @@ namespace Emby.Naming.TV
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="path">The path.</param>
|
||||||
/// <returns>System.Nullable{System.Int32}.</returns>
|
/// <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 numericStart = -1;
|
||||||
var length = 0;
|
var length = 0;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using System.Globalization;
|
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
|
|
||||||
namespace Emby.Naming.TV
|
namespace Emby.Naming.TV
|
||||||
@@ -51,7 +50,7 @@ namespace Emby.Naming.TV
|
|||||||
if (expression.IsNamed)
|
if (expression.IsNamed)
|
||||||
{
|
{
|
||||||
result.SeriesName = match.Groups["seriesname"].Value;
|
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.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|||||||
@@ -9,45 +9,27 @@ namespace Emby.Naming.Video
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolve if file is extra for video.
|
/// Resolve if file is extra for video.
|
||||||
/// </summary>
|
/// </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 static readonly char[] _digits = { '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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to resolve if file is extra.
|
/// Attempts to resolve if file is extra.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">Path to file.</param>
|
/// <param name="path">Path to file.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
|
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
|
||||||
public ExtraResult GetExtraInfo(string path)
|
public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
var result = new ExtraResult();
|
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];
|
var rule = namingOptions.VideoExtraRules[i];
|
||||||
if (rule.MediaType == MediaType.Audio)
|
if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions))
|
||||||
|
|| (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions)))
|
||||||
{
|
{
|
||||||
if (!AudioFileParser.IsAudioFile(path, _options))
|
continue;
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (rule.MediaType == MediaType.Video)
|
|
||||||
{
|
|
||||||
if (!VideoResolver.IsVideoFile(path, _options))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var pathSpan = path.AsSpan();
|
var pathSpan = path.AsSpan();
|
||||||
@@ -76,9 +58,9 @@ namespace Emby.Naming.Video
|
|||||||
{
|
{
|
||||||
var filename = Path.GetFileName(path);
|
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.ExtraType = rule.ExtraType;
|
||||||
result.Rule = rule;
|
result.Rule = rule;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using Jellyfin.Extensions;
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
{
|
{
|
||||||
@@ -12,25 +12,30 @@ namespace Emby.Naming.Video
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="FileStack"/> class.
|
/// Initializes a new instance of the <see cref="FileStack"/> class.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Gets or sets name of file stack.
|
/// Gets the name of file stack.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets list of paths in stack.
|
/// Gets the list of paths in stack.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<string> Files { get; set; }
|
public IReadOnlyList<string> Files { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether stack is directory stack.
|
/// Gets a value indicating whether stack is directory stack.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsDirectoryStack { get; set; }
|
public bool IsDirectoryStack { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Helper function to determine if path is in the stack.
|
/// 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>
|
/// <returns>True if file is in the stack.</returns>
|
||||||
public bool ContainsFile(string file, bool isDirectory)
|
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
|
public static class Format3DParser
|
||||||
{
|
{
|
||||||
// Static default result to save on allocation costs.
|
// 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>
|
/// <summary>
|
||||||
/// Parse 3D format related flags.
|
/// Parse 3D format related flags.
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Emby.Naming.AudioBook;
|
using Emby.Naming.AudioBook;
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
@@ -12,37 +11,28 @@ namespace Emby.Naming.Video
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolve <see cref="FileStack"/> from list of paths.
|
/// Resolve <see cref="FileStack"/> from list of paths.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Resolves only directories from paths.
|
/// Resolves only directories from paths.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="files">List of paths.</param>
|
/// <param name="files">List of paths.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
|
/// <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>
|
/// <summary>
|
||||||
/// Resolves only files from paths.
|
/// Resolves only files from paths.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="files">List of paths.</param>
|
/// <param name="files">List of paths.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <returns>Enumerable <see cref="FileStack"/> of files.</returns>
|
/// <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>
|
/// <summary>
|
||||||
@@ -50,7 +40,7 @@ namespace Emby.Naming.Video
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="files">List of paths.</param>
|
/// <param name="files">List of paths.</param>
|
||||||
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
|
/// <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));
|
var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path));
|
||||||
|
|
||||||
@@ -60,19 +50,13 @@ namespace Emby.Naming.Video
|
|||||||
{
|
{
|
||||||
foreach (var file in directory)
|
foreach (var file in directory)
|
||||||
{
|
{
|
||||||
var stack = new FileStack { Name = Path.GetFileNameWithoutExtension(file.Path), IsDirectoryStack = false };
|
var stack = new FileStack(Path.GetFileNameWithoutExtension(file.Path), false, new[] { file.Path });
|
||||||
stack.Files.Add(file.Path);
|
|
||||||
yield return stack;
|
yield return stack;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false };
|
var stack = new FileStack(Path.GetFileName(directory.Key), false, directory.Select(f => f.Path).ToArray());
|
||||||
foreach (var file in directory)
|
|
||||||
{
|
|
||||||
stack.Files.Add(file.Path);
|
|
||||||
}
|
|
||||||
|
|
||||||
yield return stack;
|
yield return stack;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,158 +66,91 @@ namespace Emby.Naming.Video
|
|||||||
/// Resolves videos from paths.
|
/// Resolves videos from paths.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="files">List of paths.</param>
|
/// <param name="files">List of paths.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
|
/// <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
|
var potentialFiles = files
|
||||||
.Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options))
|
.Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, namingOptions) || VideoResolver.IsStubFile(i.FullName, namingOptions))
|
||||||
.OrderBy(i => i.FullName)
|
.OrderBy(i => i.FullName);
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var expressions = _options.VideoFileStackingRegexes;
|
var potentialStacks = new Dictionary<string, StackMetadata>();
|
||||||
|
foreach (var file in potentialFiles)
|
||||||
for (var i = 0; i < list.Count; i++)
|
|
||||||
{
|
{
|
||||||
var offset = 0;
|
var name = file.Name;
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
var file1 = list[i];
|
|
||||||
|
|
||||||
var expressionIndex = 0;
|
|
||||||
while (expressionIndex < expressions.Length)
|
|
||||||
{
|
{
|
||||||
var exp = expressions[expressionIndex];
|
name = Path.GetFileName(file.FullName);
|
||||||
var stack = new FileStack();
|
}
|
||||||
|
|
||||||
// (Title)(Volume)(Ignore)(Extension)
|
for (var i = 0; i < namingOptions.VideoFileStackingRules.Length; i++)
|
||||||
var match1 = FindMatch(file1, exp, offset);
|
{
|
||||||
|
var rule = namingOptions.VideoFileStackingRules[i];
|
||||||
if (match1.Success)
|
if (!rule.Match(name, out var stackParsingResult))
|
||||||
{
|
{
|
||||||
var title1 = match1.Groups["title"].Value;
|
continue;
|
||||||
var volume1 = match1.Groups["volume"].Value;
|
}
|
||||||
var ignore1 = match1.Groups["ignore"].Value;
|
|
||||||
var extension1 = match1.Groups["extension"].Value;
|
|
||||||
|
|
||||||
var j = i + 1;
|
var stackName = stackParsingResult.Value.StackName;
|
||||||
while (j < list.Count)
|
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];
|
continue;
|
||||||
|
|
||||||
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++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
stackResult.Parts.Add(partNumber, file);
|
||||||
{
|
break;
|
||||||
yield return stack;
|
|
||||||
i += stack.Files.Count - 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetRegexInput(FileSystemMetadata file)
|
foreach (var (fileName, stack) in potentialStacks)
|
||||||
{
|
|
||||||
// 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)
|
|
||||||
{
|
{
|
||||||
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;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
{
|
{
|
||||||
@@ -28,7 +28,7 @@ namespace Emby.Naming.Video
|
|||||||
|
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path);
|
||||||
|
|
||||||
if (!options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using MediaBrowser.Model.Entities;
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
{
|
{
|
||||||
@@ -17,7 +18,6 @@ namespace Emby.Naming.Video
|
|||||||
Name = name;
|
Name = name;
|
||||||
|
|
||||||
Files = Array.Empty<VideoFileInfo>();
|
Files = Array.Empty<VideoFileInfo>();
|
||||||
Extras = Array.Empty<VideoFileInfo>();
|
|
||||||
AlternateVersions = Array.Empty<VideoFileInfo>();
|
AlternateVersions = Array.Empty<VideoFileInfo>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,16 +39,15 @@ namespace Emby.Naming.Video
|
|||||||
/// <value>The files.</value>
|
/// <value>The files.</value>
|
||||||
public IReadOnlyList<VideoFileInfo> Files { get; set; }
|
public IReadOnlyList<VideoFileInfo> Files { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the extras.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The extras.</value>
|
|
||||||
public IReadOnlyList<VideoFileInfo> Extras { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the alternate versions.
|
/// Gets or sets the alternate versions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The alternate versions.</value>
|
/// <value>The alternate versions.</value>
|
||||||
public IReadOnlyList<VideoFileInfo> AlternateVersions { get; set; }
|
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.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
@@ -17,29 +16,41 @@ namespace Emby.Naming.Video
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves alternative versions and extras from list of video files.
|
/// Resolves alternative versions and extras from list of video files.
|
||||||
/// </summary>
|
/// </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="namingOptions">The naming options.</param>
|
||||||
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</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>
|
/// <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
|
// Filter out all extras, otherwise they could cause stacks to not be resolved
|
||||||
// See the unit test TestStackedWithTrailer
|
// See the unit test TestStackedWithTrailer
|
||||||
var nonExtras = videoInfos
|
var nonExtras = videoInfos
|
||||||
.Where(i => i.ExtraType == null)
|
.Where(i => i.ExtraType == null)
|
||||||
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
|
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
|
||||||
|
|
||||||
var stackResult = new StackResolver(namingOptions)
|
var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList();
|
||||||
.Resolve(nonExtras).ToList();
|
|
||||||
|
|
||||||
var remainingFiles = videoInfos
|
var remainingFiles = new List<VideoFileInfo>();
|
||||||
.Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory)))
|
var standaloneMedia = new List<VideoFileInfo>();
|
||||||
.ToList();
|
|
||||||
|
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>();
|
var list = new List<VideoInfo>();
|
||||||
|
|
||||||
@@ -47,38 +58,20 @@ namespace Emby.Naming.Video
|
|||||||
{
|
{
|
||||||
var info = new VideoInfo(stack.Name)
|
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>()
|
.OfType<VideoFileInfo>()
|
||||||
.ToList()
|
.ToList()
|
||||||
};
|
};
|
||||||
|
|
||||||
info.Year = info.Files[0].Year;
|
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);
|
list.Add(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
var standaloneMedia = remainingFiles
|
|
||||||
.Where(i => i.ExtraType == null)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
foreach (var media in standaloneMedia)
|
foreach (var media in standaloneMedia)
|
||||||
{
|
{
|
||||||
var info = new VideoInfo(media.Name) { Files = new[] { media } };
|
var info = new VideoInfo(media.Name) { Files = new[] { media } };
|
||||||
|
|
||||||
info.Year = info.Files[0].Year;
|
info.Year = info.Files[0].Year;
|
||||||
|
|
||||||
remainingFiles.Remove(media);
|
|
||||||
var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
|
|
||||||
|
|
||||||
info.Extras = extras;
|
|
||||||
|
|
||||||
list.Add(info);
|
list.Add(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,58 +80,12 @@ namespace Emby.Naming.Video
|
|||||||
list = GetVideosGroupedByVersion(list, namingOptions);
|
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
|
// Whatever files are left, just add them
|
||||||
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
|
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
|
||||||
{
|
{
|
||||||
Files = new[] { i },
|
Files = new[] { i },
|
||||||
Year = i.Year
|
Year = i.Year,
|
||||||
|
ExtraType = i.ExtraType
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
@@ -162,6 +109,11 @@ namespace Emby.Naming.Video
|
|||||||
for (var i = 0; i < videos.Count; i++)
|
for (var i = 0; i < videos.Count; i++)
|
||||||
{
|
{
|
||||||
var video = videos[i];
|
var video = videos[i];
|
||||||
|
if (video.ExtraType != null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
|
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
|
||||||
{
|
{
|
||||||
return videos;
|
return videos;
|
||||||
@@ -178,17 +130,14 @@ namespace Emby.Naming.Video
|
|||||||
|
|
||||||
var alternateVersionsLen = videos.Count - 1;
|
var alternateVersionsLen = videos.Count - 1;
|
||||||
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
|
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
|
||||||
var extras = new List<VideoFileInfo>(list[0].Extras);
|
|
||||||
for (int i = 0; i < alternateVersionsLen; i++)
|
for (int i = 0; i < alternateVersionsLen; i++)
|
||||||
{
|
{
|
||||||
var video = videos[i + 1];
|
var video = videos[i + 1];
|
||||||
alternateVersions[i] = video.Files[0];
|
alternateVersions[i] = video.Files[0];
|
||||||
extras.AddRange(video.Extras);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
list[0].AlternateVersions = alternateVersions;
|
list[0].AlternateVersions = alternateVersions;
|
||||||
list[0].Name = folderName.ToString();
|
list[0].Name = folderName.ToString();
|
||||||
list[0].Extras = extras;
|
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
@@ -230,7 +179,7 @@ namespace Emby.Naming.Video
|
|||||||
var tmpTestFilename = testFilename.ToString();
|
var tmpTestFilename = testFilename.ToString();
|
||||||
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
|
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
|
||||||
{
|
{
|
||||||
tmpTestFilename = cleanName.Trim().ToString();
|
tmpTestFilename = cleanName.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The CleanStringParser should have removed common keywords etc.
|
// The CleanStringParser should have removed common keywords etc.
|
||||||
@@ -238,67 +187,5 @@ namespace Emby.Naming.Video
|
|||||||
|| testFilename[0] == '-'
|
|| testFilename[0] == '-'
|
||||||
|| Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
|
|| 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>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="path">The path.</param>
|
||||||
/// <param name="namingOptions">The naming options.</param>
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
|
/// <param name="parseName">Whether to parse the name or use the filename.</param>
|
||||||
/// <returns>VideoFileInfo.</returns>
|
/// <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>
|
/// <summary>
|
||||||
@@ -74,7 +75,7 @@ namespace Emby.Naming.Video
|
|||||||
|
|
||||||
var format3DResult = Format3DParser.Parse(path, namingOptions);
|
var format3DResult = Format3DParser.Parse(path, namingOptions);
|
||||||
|
|
||||||
var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path);
|
var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions);
|
||||||
|
|
||||||
var name = Path.GetFileNameWithoutExtension(path);
|
var name = Path.GetFileNameWithoutExtension(path);
|
||||||
|
|
||||||
|
|||||||
@@ -24,63 +24,63 @@ namespace Emby.Notifications
|
|||||||
{
|
{
|
||||||
new NotificationTypeInfo
|
new NotificationTypeInfo
|
||||||
{
|
{
|
||||||
Type = NotificationType.ApplicationUpdateInstalled.ToString()
|
Type = nameof(NotificationType.ApplicationUpdateInstalled)
|
||||||
},
|
},
|
||||||
new NotificationTypeInfo
|
new NotificationTypeInfo
|
||||||
{
|
{
|
||||||
Type = NotificationType.InstallationFailed.ToString()
|
Type = nameof(NotificationType.InstallationFailed)
|
||||||
},
|
},
|
||||||
new NotificationTypeInfo
|
new NotificationTypeInfo
|
||||||
{
|
{
|
||||||
Type = NotificationType.PluginInstalled.ToString()
|
Type = nameof(NotificationType.PluginInstalled)
|
||||||
},
|
},
|
||||||
new NotificationTypeInfo
|
new NotificationTypeInfo
|
||||||
{
|
{
|
||||||
Type = NotificationType.PluginError.ToString()
|
Type = nameof(NotificationType.PluginError)
|
||||||
},
|
},
|
||||||
new NotificationTypeInfo
|
new NotificationTypeInfo
|
||||||
{
|
{
|
||||||
Type = NotificationType.PluginUninstalled.ToString()
|
Type = nameof(NotificationType.PluginUninstalled)
|
||||||
},
|
},
|
||||||
new NotificationTypeInfo
|
new NotificationTypeInfo
|
||||||
{
|
{
|
||||||
Type = NotificationType.PluginUpdateInstalled.ToString()
|
Type = nameof(NotificationType.PluginUpdateInstalled)
|
||||||
},
|
},
|
||||||
new NotificationTypeInfo
|
new NotificationTypeInfo
|
||||||
{
|
{
|
||||||
Type = NotificationType.ServerRestartRequired.ToString()
|
Type = nameof(NotificationType.ServerRestartRequired)
|
||||||
},
|
},
|
||||||
new NotificationTypeInfo
|
new NotificationTypeInfo
|
||||||
{
|
{
|
||||||
Type = NotificationType.TaskFailed.ToString()
|
Type = nameof(NotificationType.TaskFailed)
|
||||||
},
|
},
|
||||||
new NotificationTypeInfo
|
new NotificationTypeInfo
|
||||||
{
|
{
|
||||||
Type = NotificationType.NewLibraryContent.ToString()
|
Type = nameof(NotificationType.NewLibraryContent)
|
||||||
},
|
},
|
||||||
new NotificationTypeInfo
|
new NotificationTypeInfo
|
||||||
{
|
{
|
||||||
Type = NotificationType.AudioPlayback.ToString()
|
Type = nameof(NotificationType.AudioPlayback)
|
||||||
},
|
},
|
||||||
new NotificationTypeInfo
|
new NotificationTypeInfo
|
||||||
{
|
{
|
||||||
Type = NotificationType.VideoPlayback.ToString()
|
Type = nameof(NotificationType.VideoPlayback)
|
||||||
},
|
},
|
||||||
new NotificationTypeInfo
|
new NotificationTypeInfo
|
||||||
{
|
{
|
||||||
Type = NotificationType.AudioPlaybackStopped.ToString()
|
Type = nameof(NotificationType.AudioPlaybackStopped)
|
||||||
},
|
},
|
||||||
new NotificationTypeInfo
|
new NotificationTypeInfo
|
||||||
{
|
{
|
||||||
Type = NotificationType.VideoPlaybackStopped.ToString()
|
Type = nameof(NotificationType.VideoPlaybackStopped)
|
||||||
},
|
},
|
||||||
new NotificationTypeInfo
|
new NotificationTypeInfo
|
||||||
{
|
{
|
||||||
Type = NotificationType.UserLockedOut.ToString()
|
Type = nameof(NotificationType.UserLockedOut)
|
||||||
},
|
},
|
||||||
new NotificationTypeInfo
|
new NotificationTypeInfo
|
||||||
{
|
{
|
||||||
Type = NotificationType.ApplicationUpdateAvailable.ToString()
|
Type = nameof(NotificationType.ApplicationUpdateAvailable)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ namespace Emby.Notifications
|
|||||||
|
|
||||||
private void Update(NotificationTypeInfo note)
|
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;
|
note.IsBasedOnUserEvent = note.Type.IndexOf("Playback", StringComparison.OrdinalIgnoreCase) != -1;
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<!-- Code analyzers-->
|
<!-- Code analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<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" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Events;
|
using Jellyfin.Data.Events;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
@@ -104,7 +105,7 @@ namespace Emby.Notifications
|
|||||||
|
|
||||||
var type = entry.Type;
|
var type = entry.Type;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparer.OrdinalIgnoreCase))
|
if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<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="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
@@ -60,7 +61,7 @@ namespace Emby.Photos
|
|||||||
item.SetImagePath(ImageType.Primary, item.Path);
|
item.SetImagePath(ImageType.Primary, item.Path);
|
||||||
|
|
||||||
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
|
// 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
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
{
|
{
|
||||||
return _configurations.GetOrAdd(
|
return _configurations.GetOrAdd(
|
||||||
key,
|
key,
|
||||||
(k, configurationManager) =>
|
static (k, configurationManager) =>
|
||||||
{
|
{
|
||||||
var file = configurationManager.GetConfigurationFile(k);
|
var file = configurationManager.GetConfigurationFile(k);
|
||||||
|
|
||||||
@@ -371,7 +371,7 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
NewConfiguration = configuration
|
NewConfiguration = configuration
|
||||||
});
|
});
|
||||||
|
|
||||||
_configurations.AddOrUpdate(key, configuration, (k, v) => configuration);
|
_configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
|
||||||
|
|
||||||
var path = GetConfigurationFile(key);
|
var path = GetConfigurationFile(key);
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.AppBase
|
namespace Emby.Server.Implementations.AppBase
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ using Emby.Dlna;
|
|||||||
using Emby.Dlna.Main;
|
using Emby.Dlna.Main;
|
||||||
using Emby.Dlna.Ssdp;
|
using Emby.Dlna.Ssdp;
|
||||||
using Emby.Drawing;
|
using Emby.Drawing;
|
||||||
|
using Emby.Naming.Common;
|
||||||
using Emby.Notifications;
|
using Emby.Notifications;
|
||||||
using Emby.Photos;
|
using Emby.Photos;
|
||||||
using Emby.Server.Implementations.Archiving;
|
using Emby.Server.Implementations.Archiving;
|
||||||
@@ -119,7 +120,7 @@ namespace Emby.Server.Implementations
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The disposable parts.
|
/// The disposable parts.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new ();
|
private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
|
||||||
|
|
||||||
private readonly IFileSystem _fileSystemManager;
|
private readonly IFileSystem _fileSystemManager;
|
||||||
private readonly IConfiguration _startupConfig;
|
private readonly IConfiguration _startupConfig;
|
||||||
@@ -312,22 +313,6 @@ namespace Emby.Server.Implementations
|
|||||||
? Environment.MachineName
|
? Environment.MachineName
|
||||||
: ConfigurationManager.Configuration.ServerName;
|
: 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)
|
public string ExpandVirtualPath(string path)
|
||||||
{
|
{
|
||||||
var appPaths = ApplicationPaths;
|
var appPaths = ApplicationPaths;
|
||||||
@@ -512,8 +497,6 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
|
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
|
||||||
|
|
||||||
// Have to migrate settings here as migration subsystem not yet initialised.
|
|
||||||
MigrateNetworkConfiguration();
|
|
||||||
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
|
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
|
||||||
|
|
||||||
// Initialize runtime stat collection
|
// 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<IProviderManager>(provider.GetRequiredService<IProviderManager>));
|
||||||
serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
|
serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
|
||||||
serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
|
serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
|
||||||
|
serviceCollection.AddSingleton<NamingOptions>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IMusicManager, MusicManager>();
|
serviceCollection.AddSingleton<IMusicManager, MusicManager>();
|
||||||
|
|
||||||
@@ -1126,12 +1110,12 @@ namespace Emby.Server.Implementations
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string GetApiUrlForLocalAccess(bool allowHttps)
|
public string GetApiUrlForLocalAccess(bool allowHttps = true)
|
||||||
{
|
{
|
||||||
// With an empty source, the port will be null
|
// With an empty source, the port will be null
|
||||||
string smart = NetManager.GetBindInterface(string.Empty, out _);
|
string smart = NetManager.GetBindInterface(string.Empty, out _);
|
||||||
var scheme = allowHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
|
var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
|
||||||
var port = allowHttps ? HttpsPort : HttpPort;
|
int? port = !allowHttps ? HttpPort : null;
|
||||||
return GetLocalApiUrl(smart.Trim('/'), scheme, port);
|
return GetLocalApiUrl(smart.Trim('/'), scheme, port);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using SharpCompress.Archives.SevenZip;
|
|
||||||
using SharpCompress.Archives.Tar;
|
|
||||||
using SharpCompress.Common;
|
using SharpCompress.Common;
|
||||||
using SharpCompress.Readers;
|
using SharpCompress.Readers;
|
||||||
using SharpCompress.Readers.GZip;
|
using SharpCompress.Readers.GZip;
|
||||||
using SharpCompress.Readers.Zip;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Archiving
|
namespace Emby.Server.Implementations.Archiving
|
||||||
{
|
{
|
||||||
@@ -14,55 +11,6 @@ namespace Emby.Server.Implementations.Archiving
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ZipClient : IZipClient
|
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 />
|
/// <inheritdoc />
|
||||||
public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles)
|
public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles)
|
||||||
{
|
{
|
||||||
@@ -94,69 +42,5 @@ namespace Emby.Server.Implementations.Archiving
|
|||||||
reader.WriteEntryToFile(Path.Combine(targetPath, filename));
|
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 System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using Jellyfin.Extensions.Json;
|
using Jellyfin.Extensions.Json;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Progress;
|
using MediaBrowser.Common.Progress;
|
||||||
@@ -129,16 +130,14 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
|
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
|
||||||
if (internalChannel == null)
|
if (internalChannel == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentException();
|
throw new ArgumentException(nameof(item.ChannelId));
|
||||||
}
|
}
|
||||||
|
|
||||||
var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
|
var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
|
||||||
|
|
||||||
var supportsDelete = channel as ISupportsDelete;
|
if (channel is not ISupportsDelete supportsDelete)
|
||||||
|
|
||||||
if (supportsDelete == null)
|
|
||||||
{
|
{
|
||||||
throw new ArgumentException();
|
throw new ArgumentException(nameof(channel));
|
||||||
}
|
}
|
||||||
|
|
||||||
return supportsDelete.DeleteItem(item.ExternalId, CancellationToken.None);
|
return supportsDelete.DeleteItem(item.ExternalId, CancellationToken.None);
|
||||||
@@ -179,7 +178,7 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes
|
return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes
|
||||||
&& hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val;
|
&& hasAttributes.Attributes.Contains("Recordings", StringComparison.OrdinalIgnoreCase)) == val;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -541,7 +540,7 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
return _libraryManager.GetItemIds(
|
return _libraryManager.GetItemIds(
|
||||||
new InternalItemsQuery
|
new InternalItemsQuery
|
||||||
{
|
{
|
||||||
IncludeItemTypes = new[] { nameof(Channel) },
|
IncludeItemTypes = new[] { BaseItemKind.Channel },
|
||||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
|
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
|
||||||
}).Select(i => GetChannelFeatures(i)).ToArray();
|
}).Select(i => GetChannelFeatures(i)).ToArray();
|
||||||
}
|
}
|
||||||
@@ -1075,14 +1074,6 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
forceUpdate = true;
|
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))
|
if (!internalChannelId.Equals(item.ChannelId))
|
||||||
{
|
{
|
||||||
forceUpdate = true;
|
forceUpdate = true;
|
||||||
@@ -1143,7 +1134,7 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
|
|
||||||
if (!info.IsLiveStream)
|
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();
|
item.Tags = item.Tags.Except(new[] { "livestream" }, StringComparer.OrdinalIgnoreCase).ToArray();
|
||||||
_logger.LogDebug("Forcing update due to Tags {0}", item.Name);
|
_logger.LogDebug("Forcing update due to Tags {0}", item.Name);
|
||||||
@@ -1152,7 +1143,7 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase))
|
if (!item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
item.Tags = item.Tags.Concat(new[] { "livestream" }).ToArray();
|
item.Tags = item.Tags.Concat(new[] { "livestream" }).ToArray();
|
||||||
_logger.LogDebug("Forcing update due to Tags {0}", item.Name);
|
_logger.LogDebug("Forcing update due to Tags {0}", item.Name);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller.Channels;
|
using MediaBrowser.Controller.Channels;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
@@ -51,7 +52,7 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
|
|
||||||
var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
|
var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
{
|
{
|
||||||
IncludeItemTypes = new[] { nameof(Channel) },
|
IncludeItemTypes = new[] { BaseItemKind.Channel },
|
||||||
ExcludeItemIds = installedChannelIds.ToArray()
|
ExcludeItemIds = installedChannelIds.ToArray()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ namespace Emby.Server.Implementations.Collections
|
|||||||
|
|
||||||
if (parentFolder == null)
|
if (parentFolder == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentException();
|
throw new ArgumentException(nameof(parentFolder));
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = Path.Combine(parentFolder.Path, folderName);
|
var path = Path.Combine(parentFolder.Path, folderName);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Model.Cryptography;
|
using MediaBrowser.Model.Cryptography;
|
||||||
using static MediaBrowser.Common.Cryptography.Constants;
|
using static MediaBrowser.Model.Cryptography.Constants;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Cryptography
|
namespace Emby.Server.Implementations.Cryptography
|
||||||
{
|
{
|
||||||
@@ -12,10 +14,7 @@ namespace Emby.Server.Implementations.Cryptography
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class CryptographyProvider : ICryptoProvider
|
public class CryptographyProvider : ICryptoProvider
|
||||||
{
|
{
|
||||||
// FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto
|
// TODO: remove when not needed for backwards compat
|
||||||
// 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
|
|
||||||
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
|
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
|
||||||
{
|
{
|
||||||
"MD5",
|
"MD5",
|
||||||
@@ -35,60 +34,81 @@ namespace Emby.Server.Implementations.Cryptography
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string DefaultHashMethod => "PBKDF2";
|
public string DefaultHashMethod => "PBKDF2-SHA512";
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerable<string> GetSupportedHashMethods()
|
public PasswordHash CreatePasswordHash(ReadOnlySpan<char> password)
|
||||||
=> _supportedHashMethods;
|
|
||||||
|
|
||||||
private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
|
|
||||||
{
|
{
|
||||||
// downgrading for now as we need this library to be dotnetstandard compliant
|
byte[] salt = GenerateSalt();
|
||||||
// with this downgrade we'll add a check to make sure we're on the downgrade method at the moment
|
return new PasswordHash(
|
||||||
if (method != DefaultHashMethod)
|
DefaultHashMethod,
|
||||||
{
|
Rfc2898DeriveBytes.Pbkdf2(
|
||||||
throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}");
|
password,
|
||||||
}
|
salt,
|
||||||
|
DefaultIterations,
|
||||||
using var r = new Rfc2898DeriveBytes(bytes, salt, iterations);
|
HashAlgorithmName.SHA512,
|
||||||
return r.GetBytes(32);
|
DefaultOutputLength),
|
||||||
|
salt,
|
||||||
|
new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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 (!_supportedHashMethods.Contains(hash.Id))
|
||||||
if (salt.Length == 0)
|
|
||||||
{
|
{
|
||||||
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(bytes, salted, bytes.Length);
|
||||||
Array.Copy(salt, 0, salted, bytes.Length, salt.Length);
|
hash.Salt.CopyTo(salted.AsSpan(bytes.Length));
|
||||||
return h.ComputeHash(salted);
|
return hash.Hash.SequenceEqual(h.ComputeHash(salted));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
|
|
||||||
=> PBKDF2(DefaultHashMethod, bytes, salt, DefaultIterations);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public byte[] GenerateSalt()
|
public byte[] GenerateSalt()
|
||||||
=> GenerateSalt(DefaultSaltLength);
|
=> GenerateSalt(DefaultSaltLength);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public byte[] GenerateSalt(int length)
|
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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SQLitePCL.pretty;
|
using SQLitePCL.pretty;
|
||||||
|
|
||||||
@@ -160,21 +160,22 @@ namespace Emby.Server.Implementations.Data
|
|||||||
protected bool TableExists(ManagedConnection connection, string name)
|
protected bool TableExists(ManagedConnection connection, string name)
|
||||||
{
|
{
|
||||||
return connection.RunInTransaction(
|
return connection.RunInTransaction(
|
||||||
db =>
|
db =>
|
||||||
{
|
|
||||||
using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
|
|
||||||
{
|
{
|
||||||
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;
|
return false;
|
||||||
}, ReadTransactionMode);
|
},
|
||||||
|
ReadTransactionMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected List<string> GetColumnNames(IDatabaseConnection connection, string table)
|
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)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using SQLitePCL.pretty;
|
|||||||
|
|
||||||
namespace Emby.Server.Implementations.Data
|
namespace Emby.Server.Implementations.Data
|
||||||
{
|
{
|
||||||
public class ManagedConnection : IDisposable
|
public sealed class ManagedConnection : IDisposable
|
||||||
{
|
{
|
||||||
private readonly SemaphoreSlim _writeLock;
|
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;
|
var users = userDatasTableExists ? null : userManager.Users;
|
||||||
|
|
||||||
connection.RunInTransaction(
|
connection.RunInTransaction(
|
||||||
db =>
|
db =>
|
||||||
{
|
|
||||||
db.ExecuteAll(string.Join(';', new[]
|
|
||||||
{
|
{
|
||||||
"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(string.Join(';', new[]
|
||||||
|
|
||||||
"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);
|
"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())
|
using (var connection = GetConnection())
|
||||||
{
|
{
|
||||||
connection.RunInTransaction(
|
connection.RunInTransaction(
|
||||||
db =>
|
db =>
|
||||||
{
|
{
|
||||||
SaveUserData(db, internalUserId, key, userData);
|
SaveUserData(db, internalUserId, key, userData);
|
||||||
}, TransactionMode);
|
},
|
||||||
|
TransactionMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,13 +254,14 @@ namespace Emby.Server.Implementations.Data
|
|||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection())
|
||||||
{
|
{
|
||||||
connection.RunInTransaction(
|
connection.RunInTransaction(
|
||||||
db =>
|
db =>
|
||||||
{
|
|
||||||
foreach (var userItemData in userDataList)
|
|
||||||
{
|
{
|
||||||
SaveUserData(db, internalUserId, userItemData.Key, userItemData);
|
foreach (var userItemData in userDataList)
|
||||||
}
|
{
|
||||||
}, TransactionMode);
|
SaveUserData(db, internalUserId, userItemData.Key, userItemData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TransactionMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ namespace Emby.Server.Implementations.Devices
|
|||||||
{
|
{
|
||||||
var value = File.ReadAllText(CachePath, Encoding.UTF8);
|
var value = File.ReadAllText(CachePath, Encoding.UTF8);
|
||||||
|
|
||||||
if (Guid.TryParse(value, out var guid))
|
if (Guid.TryParse(value, out _))
|
||||||
{
|
{
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ using System.Collections.Generic;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Common;
|
using MediaBrowser.Common;
|
||||||
using MediaBrowser.Controller.Channels;
|
using MediaBrowser.Controller.Channels;
|
||||||
using MediaBrowser.Controller.Drawing;
|
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)
|
new DtoOptions(false)
|
||||||
{
|
{
|
||||||
EnableImages = false
|
EnableImages = false
|
||||||
}),
|
}));
|
||||||
user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dto;
|
return dto;
|
||||||
@@ -294,7 +293,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
path = path.TrimStart('.');
|
path = path.TrimStart('.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparer.OrdinalIgnoreCase))
|
if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
fileExtensionContainer = path;
|
fileExtensionContainer = path;
|
||||||
}
|
}
|
||||||
@@ -311,13 +310,13 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
|
|
||||||
if (taggedItems != null && options.ContainsField(ItemFields.ItemCounts))
|
if (taggedItems != null && options.ContainsField(ItemFields.ItemCounts))
|
||||||
{
|
{
|
||||||
SetItemByNameInfo(item, dto, taggedItems, user);
|
SetItemByNameInfo(item, dto, taggedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
return dto;
|
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)
|
if (item is MusicArtist)
|
||||||
{
|
{
|
||||||
@@ -370,6 +369,12 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
if (item is MusicAlbum || item is Season || item is Playlist)
|
if (item is MusicAlbum || item is Season || item is Playlist)
|
||||||
{
|
{
|
||||||
dto.ChildCount = dto.RecursiveItemCount;
|
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))
|
if (options.ContainsField(ItemFields.ChildCount))
|
||||||
@@ -464,7 +469,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
{
|
{
|
||||||
var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||||
{
|
{
|
||||||
IncludeItemTypes = new[] { nameof(MusicAlbum) },
|
IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
|
||||||
Name = item.Album,
|
Name = item.Album,
|
||||||
Limit = 1
|
Limit = 1
|
||||||
});
|
});
|
||||||
@@ -1398,44 +1403,27 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageDimensions size;
|
|
||||||
|
|
||||||
var defaultAspectRatio = item.GetDefaultPrimaryImageAspectRatio();
|
|
||||||
|
|
||||||
if (defaultAspectRatio > 0)
|
|
||||||
{
|
|
||||||
return defaultAspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!imageInfo.IsLocalFile)
|
if (!imageInfo.IsLocalFile)
|
||||||
{
|
{
|
||||||
return null;
|
return item.GetDefaultPrimaryImageAspectRatio();
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
size = _imageProcessor.GetImageDimensions(item, imageInfo);
|
var size = _imageProcessor.GetImageDimensions(item, imageInfo);
|
||||||
|
var width = size.Width;
|
||||||
if (size.Width <= 0 || size.Height <= 0)
|
var height = size.Height;
|
||||||
|
if (width > 0 && height > 0)
|
||||||
{
|
{
|
||||||
return null;
|
return (double)width / height;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Failed to determine primary image aspect ratio for {0}", imageInfo.Path);
|
_logger.LogError(ex, "Failed to determine primary image aspect ratio for {ImagePath}", imageInfo.Path);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var width = size.Width;
|
return item.GetDefaultPrimaryImageAspectRatio();
|
||||||
var height = size.Height;
|
|
||||||
|
|
||||||
if (width <= 0 || height <= 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (double)width / height;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,10 +29,10 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" 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.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="Mono.Nat" Version="3.0.2" />
|
||||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.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="SQLitePCL.pretty.netstandard" Version="3.1.0" />
|
||||||
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
|
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -49,10 +49,14 @@
|
|||||||
<NoWarn>AD0001</NoWarn>
|
<NoWarn>AD0001</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<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" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ using Jellyfin.Networking.Configuration;
|
|||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Plugins;
|
using MediaBrowser.Controller.Plugins;
|
||||||
using MediaBrowser.Model.Dlna;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Mono.Nat;
|
using Mono.Nat;
|
||||||
|
|
||||||
@@ -27,7 +26,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||||||
private readonly IServerApplicationHost _appHost;
|
private readonly IServerApplicationHost _appHost;
|
||||||
private readonly ILogger<ExternalPortForwarding> _logger;
|
private readonly ILogger<ExternalPortForwarding> _logger;
|
||||||
private readonly IServerConfigurationManager _config;
|
private readonly IServerConfigurationManager _config;
|
||||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>();
|
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="logger">The logger.</param>
|
||||||
/// <param name="appHost">The application host.</param>
|
/// <param name="appHost">The application host.</param>
|
||||||
/// <param name="config">The configuration manager.</param>
|
/// <param name="config">The configuration manager.</param>
|
||||||
/// <param name="deviceDiscovery">The device discovery.</param>
|
|
||||||
public ExternalPortForwarding(
|
public ExternalPortForwarding(
|
||||||
ILogger<ExternalPortForwarding> logger,
|
ILogger<ExternalPortForwarding> logger,
|
||||||
IServerApplicationHost appHost,
|
IServerApplicationHost appHost,
|
||||||
IServerConfigurationManager config,
|
IServerConfigurationManager config)
|
||||||
IDeviceDiscovery deviceDiscovery)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_appHost = appHost;
|
_appHost = appHost;
|
||||||
_config = config;
|
_config = config;
|
||||||
_deviceDiscovery = deviceDiscovery;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetConfigIdentifier()
|
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>();
|
var dict = new Dictionary<string, string>();
|
||||||
dict["ItemId"] = item.Id.ToString("N", CultureInfo.InvariantCulture);
|
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)));
|
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)
|
private static bool EnableRefreshMessage(BaseItem item)
|
||||||
@@ -423,7 +423,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var collectionFolders = _libraryManager.GetCollectionFolders(item, allUserRootChildren);
|
|
||||||
foreach (var folder in allUserRootChildren)
|
foreach (var folder in allUserRootChildren)
|
||||||
{
|
{
|
||||||
list.Add(folder.Id.ToString("N", CultureInfo.InvariantCulture));
|
list.Add(folder.Id.ToString("N", CultureInfo.InvariantCulture));
|
||||||
@@ -465,6 +464,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Dispose(true);
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||||||
var changes = _changedItems.ToList();
|
var changes = _changedItems.ToList();
|
||||||
_changedItems.Clear();
|
_changedItems.Clear();
|
||||||
|
|
||||||
var task = SendNotifications(changes, CancellationToken.None);
|
SendNotifications(changes, CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
|
||||||
if (_updateTimer != null)
|
if (_updateTimer != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller.Authentication;
|
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
{
|
{
|
||||||
var session = await GetSession(requestContext).ConfigureAwait(false);
|
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)
|
public Task<User?> GetUser(object requestContext)
|
||||||
|
|||||||
@@ -42,17 +42,14 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
/// <param name="logger">The logger.</param>
|
/// <param name="logger">The logger.</param>
|
||||||
/// <param name="socket">The socket.</param>
|
/// <param name="socket">The socket.</param>
|
||||||
/// <param name="remoteEndPoint">The remote end point.</param>
|
/// <param name="remoteEndPoint">The remote end point.</param>
|
||||||
/// <param name="query">The query.</param>
|
|
||||||
public WebSocketConnection(
|
public WebSocketConnection(
|
||||||
ILogger<WebSocketConnection> logger,
|
ILogger<WebSocketConnection> logger,
|
||||||
WebSocket socket,
|
WebSocket socket,
|
||||||
IPAddress? remoteEndPoint,
|
IPAddress? remoteEndPoint)
|
||||||
IQueryCollection query)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_socket = socket;
|
_socket = socket;
|
||||||
RemoteEndPoint = remoteEndPoint;
|
RemoteEndPoint = remoteEndPoint;
|
||||||
QueryString = query;
|
|
||||||
|
|
||||||
_jsonOptions = JsonDefaults.Options;
|
_jsonOptions = JsonDefaults.Options;
|
||||||
LastActivityDate = DateTime.Now;
|
LastActivityDate = DateTime.Now;
|
||||||
@@ -81,12 +78,6 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public DateTime LastKeepAliveDate { get; set; }
|
public DateTime LastKeepAliveDate { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the query string.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The query string.</value>
|
|
||||||
public IQueryCollection QueryString { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the state.
|
/// Gets the state.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -180,7 +171,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
}
|
}
|
||||||
|
|
||||||
WebSocketMessage<object>? stub;
|
WebSocketMessage<object>? stub;
|
||||||
long bytesConsumed = 0;
|
long bytesConsumed;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
stub = DeserializeWebSocketMessage(buffer, out bytesConsumed);
|
stub = DeserializeWebSocketMessage(buffer, out bytesConsumed);
|
||||||
@@ -236,7 +227,8 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
{
|
{
|
||||||
MessageId = Guid.NewGuid(),
|
MessageId = Guid.NewGuid(),
|
||||||
MessageType = SessionMessageType.KeepAlive
|
MessageType = SessionMessageType.KeepAlive
|
||||||
}, CancellationToken.None);
|
},
|
||||||
|
CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -50,8 +51,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
using var connection = new WebSocketConnection(
|
using var connection = new WebSocketConnection(
|
||||||
_loggerFactory.CreateLogger<WebSocketConnection>(),
|
_loggerFactory.CreateLogger<WebSocketConnection>(),
|
||||||
webSocket,
|
webSocket,
|
||||||
context.Connection.RemoteIpAddress,
|
context.GetNormalizedRemoteIp())
|
||||||
context.Request.Query)
|
|
||||||
{
|
{
|
||||||
OnReceive = ProcessWebSocketMessageReceived
|
OnReceive = ProcessWebSocketMessageReceived
|
||||||
};
|
};
|
||||||
@@ -59,7 +59,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
var tasks = new Task[_webSocketListeners.Length];
|
var tasks = new Task[_webSocketListeners.Length];
|
||||||
for (var i = 0; i < _webSocketListeners.Length; ++i)
|
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);
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@@ -14,7 +12,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace Emby.Server.Implementations.IO
|
namespace Emby.Server.Implementations.IO
|
||||||
{
|
{
|
||||||
public class FileRefresher : IDisposable
|
public sealed class FileRefresher : IDisposable
|
||||||
{
|
{
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
@@ -22,7 +20,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
|
|
||||||
private readonly List<string> _affectedPaths = new List<string>();
|
private readonly List<string> _affectedPaths = new List<string>();
|
||||||
private readonly object _timerLock = new object();
|
private readonly object _timerLock = new object();
|
||||||
private Timer _timer;
|
private Timer? _timer;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
|
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
|
||||||
@@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
AddPath(path);
|
AddPath(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public event EventHandler<EventArgs> Completed;
|
public event EventHandler<EventArgs>? Completed;
|
||||||
|
|
||||||
public string Path { get; private set; }
|
public string Path { get; private set; }
|
||||||
|
|
||||||
@@ -111,7 +109,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
RestartTimer();
|
RestartTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTimerCallback(object state)
|
private void OnTimerCallback(object? state)
|
||||||
{
|
{
|
||||||
List<string> paths;
|
List<string> paths;
|
||||||
|
|
||||||
@@ -127,7 +125,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ProcessPathChanges(paths.ToList());
|
ProcessPathChanges(paths);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -137,12 +135,12 @@ namespace Emby.Server.Implementations.IO
|
|||||||
|
|
||||||
private void ProcessPathChanges(List<string> paths)
|
private void ProcessPathChanges(List<string> paths)
|
||||||
{
|
{
|
||||||
var itemsToRefresh = paths
|
IEnumerable<BaseItem> itemsToRefresh = paths
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.Select(GetAffectedBaseItem)
|
.Select(GetAffectedBaseItem)
|
||||||
.Where(item => item != null)
|
.Where(item => item != null)
|
||||||
.GroupBy(x => x.Id)
|
.GroupBy(x => x!.Id) // Removed null values in the previous .Where()
|
||||||
.Select(x => x.First());
|
.Select(x => x.First())!;
|
||||||
|
|
||||||
foreach (var item in itemsToRefresh)
|
foreach (var item in itemsToRefresh)
|
||||||
{
|
{
|
||||||
@@ -176,15 +174,15 @@ namespace Emby.Server.Implementations.IO
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="path">The path.</param>
|
||||||
/// <returns>BaseItem.</returns>
|
/// <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))
|
while (item == null && !string.IsNullOrEmpty(path))
|
||||||
{
|
{
|
||||||
item = _libraryManager.FindByPath(path, null);
|
item = _libraryManager.FindByPath(path, null);
|
||||||
|
|
||||||
path = System.IO.Path.GetDirectoryName(path);
|
path = System.IO.Path.GetDirectoryName(path) ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item != null)
|
if (item != null)
|
||||||
@@ -219,8 +217,13 @@ namespace Emby.Server.Implementations.IO
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_disposed = true;
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
DisposeTimer();
|
DisposeTimer();
|
||||||
|
_disposed = true;
|
||||||
GC.SuppressFinalize(this);
|
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
|
// 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);
|
await Task.Delay(45000).ConfigureAwait(false);
|
||||||
|
|
||||||
_tempIgnoredPaths.TryRemove(path, out var val);
|
_tempIgnoredPaths.TryRemove(path, out _);
|
||||||
|
|
||||||
if (refreshPath)
|
if (refreshPath)
|
||||||
{
|
{
|
||||||
@@ -449,12 +449,12 @@ namespace Emby.Server.Implementations.IO
|
|||||||
}
|
}
|
||||||
|
|
||||||
var newRefresher = new FileRefresher(path, _configurationManager, _libraryManager, _logger);
|
var newRefresher = new FileRefresher(path, _configurationManager, _libraryManager, _logger);
|
||||||
newRefresher.Completed += NewRefresher_Completed;
|
newRefresher.Completed += OnNewRefresherCompleted;
|
||||||
_activeRefreshers.Add(newRefresher);
|
_activeRefreshers.Add(newRefresher);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void NewRefresher_Completed(object sender, EventArgs e)
|
private void OnNewRefresherCompleted(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var refresher = (FileRefresher)sender;
|
var refresher = (FileRefresher)sender;
|
||||||
DisposeRefresher(refresher);
|
DisposeRefresher(refresher);
|
||||||
@@ -481,6 +481,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
{
|
{
|
||||||
lock (_activeRefreshers)
|
lock (_activeRefreshers)
|
||||||
{
|
{
|
||||||
|
refresher.Completed -= OnNewRefresherCompleted;
|
||||||
refresher.Dispose();
|
refresher.Dispose();
|
||||||
_activeRefreshers.Remove(refresher);
|
_activeRefreshers.Remove(refresher);
|
||||||
}
|
}
|
||||||
@@ -492,6 +493,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
{
|
{
|
||||||
foreach (var refresher in _activeRefreshers.ToList())
|
foreach (var refresher in _activeRefreshers.ToList())
|
||||||
{
|
{
|
||||||
|
refresher.Completed -= OnNewRefresherCompleted;
|
||||||
refresher.Dispose();
|
refresher.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -544,16 +544,6 @@ namespace Emby.Server.Implementations.IO
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual bool AreEqual(string path1, string path2)
|
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(
|
return string.Equals(
|
||||||
NormalizePath(path1),
|
NormalizePath(path1),
|
||||||
NormalizePath(path2),
|
NormalizePath(path2),
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ namespace Emby.Server.Implementations.IO
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
int read;
|
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();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
|
await destination.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (onStarted != null)
|
if (onStarted != null)
|
||||||
{
|
{
|
||||||
@@ -44,11 +44,11 @@ namespace Emby.Server.Implementations.IO
|
|||||||
if (emptyReadLimit <= 0)
|
if (emptyReadLimit <= 0)
|
||||||
{
|
{
|
||||||
int read;
|
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();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
|
await destination.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -60,7 +60,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
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)
|
if (bytesRead == 0)
|
||||||
{
|
{
|
||||||
@@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
{
|
{
|
||||||
eofCount = 0;
|
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;
|
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);
|
var bytesToWrite = Math.Min(bytesRead, copyLength);
|
||||||
|
|
||||||
if (bytesToWrite > 0)
|
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;
|
copyLength -= bytesToWrite;
|
||||||
@@ -137,9 +137,9 @@ namespace Emby.Server.Implementations.IO
|
|||||||
int bytesRead;
|
int bytesRead;
|
||||||
int totalBytesRead = 0;
|
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;
|
totalBytesRead += bytesRead;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,35 +28,35 @@ namespace Emby.Server.Implementations.Images
|
|||||||
var view = (CollectionFolder)item;
|
var view = (CollectionFolder)item;
|
||||||
var viewType = view.CollectionType;
|
var viewType = view.CollectionType;
|
||||||
|
|
||||||
string[] includeItemTypes;
|
BaseItemKind[] includeItemTypes;
|
||||||
|
|
||||||
if (string.Equals(viewType, CollectionType.Movies, StringComparison.Ordinal))
|
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))
|
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))
|
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))
|
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))
|
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))
|
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
|
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);
|
var recursive = !string.Equals(CollectionType.Playlists, viewType, StringComparison.OrdinalIgnoreCase);
|
||||||
@@ -68,9 +68,9 @@ namespace Emby.Server.Implementations.Images
|
|||||||
DtoOptions = new DtoOptions(false),
|
DtoOptions = new DtoOptions(false),
|
||||||
ImageTypes = new ImageType[] { ImageType.Primary },
|
ImageTypes = new ImageType[] { ImageType.Primary },
|
||||||
Limit = 8,
|
Limit = 8,
|
||||||
OrderBy = new ValueTuple<string, SortOrder>[]
|
OrderBy = new[]
|
||||||
{
|
{
|
||||||
new ValueTuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending)
|
(ItemSortBy.Random, SortOrder.Ascending)
|
||||||
},
|
},
|
||||||
IncludeItemTypes = includeItemTypes
|
IncludeItemTypes = includeItemTypes
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
@@ -34,14 +36,14 @@ namespace Emby.Server.Implementations.Images
|
|||||||
var view = (UserView)item;
|
var view = (UserView)item;
|
||||||
|
|
||||||
var isUsingCollectionStrip = IsUsingCollectionStrip(view);
|
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
|
var result = view.GetItemList(new InternalItemsQuery
|
||||||
{
|
{
|
||||||
User = view.UserId.HasValue ? _userManager.GetUserById(view.UserId.Value) : null,
|
User = view.UserId.HasValue ? _userManager.GetUserById(view.UserId.Value) : null,
|
||||||
CollapseBoxSetItems = false,
|
CollapseBoxSetItems = false,
|
||||||
Recursive = recursive,
|
Recursive = recursive,
|
||||||
ExcludeItemTypes = new[] { "UserView", "CollectionFolder", "Person" },
|
ExcludeItemTypes = new[] { BaseItemKind.UserView, BaseItemKind.CollectionFolder, BaseItemKind.Person },
|
||||||
DtoOptions = new DtoOptions(false)
|
DtoOptions = new DtoOptions(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ using MediaBrowser.Common.Configuration;
|
|||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Movies;
|
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
@@ -43,7 +41,7 @@ namespace Emby.Server.Implementations.Images
|
|||||||
return _libraryManager.GetItemList(new InternalItemsQuery
|
return _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
{
|
{
|
||||||
Genres = new[] { item.Name },
|
Genres = new[] { item.Name },
|
||||||
IncludeItemTypes = new[] { nameof(Series), nameof(Movie) },
|
IncludeItemTypes = new[] { BaseItemKind.Series, BaseItemKind.Movie },
|
||||||
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
|
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
|
||||||
Limit = 4,
|
Limit = 4,
|
||||||
Recursive = true,
|
Recursive = true,
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ namespace Emby.Server.Implementations.Images
|
|||||||
Genres = new[] { item.Name },
|
Genres = new[] { item.Name },
|
||||||
IncludeItemTypes = new[]
|
IncludeItemTypes = new[]
|
||||||
{
|
{
|
||||||
nameof(MusicAlbum),
|
BaseItemKind.MusicAlbum,
|
||||||
nameof(MusicVideo),
|
BaseItemKind.MusicVideo,
|
||||||
nameof(Audio)
|
BaseItemKind.Audio
|
||||||
},
|
},
|
||||||
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
|
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
|
||||||
Limit = 4,
|
Limit = 4,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using Emby.Naming.Audio;
|
||||||
|
using Emby.Naming.Common;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.Resolvers;
|
using MediaBrowser.Controller.Resolvers;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
|
|
||||||
@@ -13,17 +14,17 @@ namespace Emby.Server.Implementations.Library
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class CoreResolutionIgnoreRule : IResolverIgnoreRule
|
public class CoreResolutionIgnoreRule : IResolverIgnoreRule
|
||||||
{
|
{
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly NamingOptions _namingOptions;
|
||||||
private readonly IServerApplicationPaths _serverApplicationPaths;
|
private readonly IServerApplicationPaths _serverApplicationPaths;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class.
|
/// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <param name="serverApplicationPaths">The server application paths.</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;
|
_serverApplicationPaths = serverApplicationPaths;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,20 +54,10 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
if (parent != null)
|
if (parent != null)
|
||||||
{
|
{
|
||||||
// Ignore trailer folders but allow it at the collection level
|
// Ignore extras folders but allow it at the collection level
|
||||||
if (string.Equals(filename, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase)
|
if (_namingOptions.AllExtrasTypesFolderNames.ContainsKey(filename)
|
||||||
&& !(parent is AggregateFolder)
|
&& parent is not AggregateFolder
|
||||||
&& !(parent is UserRootFolder))
|
&& parent is not UserRootFolder)
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(filename, BaseItem.ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(filename, BaseItem.ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -78,7 +69,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
// Don't resolve these into audio files
|
// Don't resolve these into audio files
|
||||||
if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
|
if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
|
||||||
&& _libraryManager.IsAudioFile(filename))
|
&& AudioFileParser.IsAudioFile(filename, _namingOptions))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,12 @@ using System.Net;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Naming.Audio;
|
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
using Emby.Naming.TV;
|
using Emby.Naming.TV;
|
||||||
using Emby.Naming.Video;
|
|
||||||
using Emby.Server.Implementations.Library.Resolvers;
|
using Emby.Server.Implementations.Library.Resolvers;
|
||||||
using Emby.Server.Implementations.Library.Validators;
|
using Emby.Server.Implementations.Library.Validators;
|
||||||
using Emby.Server.Implementations.Playlists;
|
using Emby.Server.Implementations.Playlists;
|
||||||
using Emby.Server.Implementations.ScheduledTasks;
|
using Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
@@ -79,6 +77,8 @@ namespace Emby.Server.Implementations.Library
|
|||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
private readonly IItemRepository _itemRepository;
|
private readonly IItemRepository _itemRepository;
|
||||||
private readonly IImageProcessor _imageProcessor;
|
private readonly IImageProcessor _imageProcessor;
|
||||||
|
private readonly NamingOptions _namingOptions;
|
||||||
|
private readonly ExtraResolver _extraResolver;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The _root folder sync lock.
|
/// The _root folder sync lock.
|
||||||
@@ -88,9 +88,6 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
|
private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
|
||||||
|
|
||||||
private NamingOptions _namingOptions;
|
|
||||||
private string[] _videoFileExtensions;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The _root folder.
|
/// The _root folder.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -116,6 +113,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
/// <param name="itemRepository">The item repository.</param>
|
/// <param name="itemRepository">The item repository.</param>
|
||||||
/// <param name="imageProcessor">The image processor.</param>
|
/// <param name="imageProcessor">The image processor.</param>
|
||||||
/// <param name="memoryCache">The memory cache.</param>
|
/// <param name="memoryCache">The memory cache.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
public LibraryManager(
|
public LibraryManager(
|
||||||
IServerApplicationHost appHost,
|
IServerApplicationHost appHost,
|
||||||
ILogger<LibraryManager> logger,
|
ILogger<LibraryManager> logger,
|
||||||
@@ -130,7 +128,8 @@ namespace Emby.Server.Implementations.Library
|
|||||||
IMediaEncoder mediaEncoder,
|
IMediaEncoder mediaEncoder,
|
||||||
IItemRepository itemRepository,
|
IItemRepository itemRepository,
|
||||||
IImageProcessor imageProcessor,
|
IImageProcessor imageProcessor,
|
||||||
IMemoryCache memoryCache)
|
IMemoryCache memoryCache,
|
||||||
|
NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
_appHost = appHost;
|
_appHost = appHost;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -146,6 +145,9 @@ namespace Emby.Server.Implementations.Library
|
|||||||
_itemRepository = itemRepository;
|
_itemRepository = itemRepository;
|
||||||
_imageProcessor = imageProcessor;
|
_imageProcessor = imageProcessor;
|
||||||
_memoryCache = memoryCache;
|
_memoryCache = memoryCache;
|
||||||
|
_namingOptions = namingOptions;
|
||||||
|
|
||||||
|
_extraResolver = new ExtraResolver(namingOptions);
|
||||||
|
|
||||||
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
|
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
|
||||||
|
|
||||||
@@ -532,8 +534,8 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return key.GetMD5();
|
return key.GetMD5();
|
||||||
}
|
}
|
||||||
|
|
||||||
public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null)
|
public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null, IDirectoryService directoryService = null)
|
||||||
=> ResolvePath(fileInfo, new DirectoryService(_fileSystem), null, parent);
|
=> ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent);
|
||||||
|
|
||||||
private BaseItem ResolvePath(
|
private BaseItem ResolvePath(
|
||||||
FileSystemMetadata fileInfo,
|
FileSystemMetadata fileInfo,
|
||||||
@@ -653,7 +655,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return !args.ContainsFileSystemEntryByName(".ignore");
|
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);
|
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);
|
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>();
|
var items = new List<BaseItem>();
|
||||||
items.AddRange(result.Items);
|
items.AddRange(result.Items);
|
||||||
@@ -964,7 +966,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
var existing = GetItemList(new InternalItemsQuery
|
var existing = GetItemList(new InternalItemsQuery
|
||||||
{
|
{
|
||||||
IncludeItemTypes = new[] { nameof(MusicArtist) },
|
IncludeItemTypes = new[] { BaseItemKind.MusicArtist },
|
||||||
Name = name,
|
Name = name,
|
||||||
DtoOptions = options
|
DtoOptions = options
|
||||||
}).Cast<MusicArtist>()
|
}).Cast<MusicArtist>()
|
||||||
@@ -1374,7 +1376,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return _itemRepository.GetItemIdsList(query);
|
return _itemRepository.GetItemIdsList(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
public QueryResult<(BaseItem, ItemCounts)> GetStudios(InternalItemsQuery query)
|
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query)
|
||||||
{
|
{
|
||||||
if (query.User != null)
|
if (query.User != null)
|
||||||
{
|
{
|
||||||
@@ -1385,7 +1387,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return _itemRepository.GetStudios(query);
|
return _itemRepository.GetStudios(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
public QueryResult<(BaseItem, ItemCounts)> GetGenres(InternalItemsQuery query)
|
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query)
|
||||||
{
|
{
|
||||||
if (query.User != null)
|
if (query.User != null)
|
||||||
{
|
{
|
||||||
@@ -1396,7 +1398,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return _itemRepository.GetGenres(query);
|
return _itemRepository.GetGenres(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
public QueryResult<(BaseItem, ItemCounts)> GetMusicGenres(InternalItemsQuery query)
|
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query)
|
||||||
{
|
{
|
||||||
if (query.User != null)
|
if (query.User != null)
|
||||||
{
|
{
|
||||||
@@ -1407,7 +1409,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return _itemRepository.GetMusicGenres(query);
|
return _itemRepository.GetMusicGenres(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
public QueryResult<(BaseItem, ItemCounts)> GetAllArtists(InternalItemsQuery query)
|
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query)
|
||||||
{
|
{
|
||||||
if (query.User != null)
|
if (query.User != null)
|
||||||
{
|
{
|
||||||
@@ -1418,7 +1420,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return _itemRepository.GetAllArtists(query);
|
return _itemRepository.GetAllArtists(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
public QueryResult<(BaseItem, ItemCounts)> GetArtists(InternalItemsQuery query)
|
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query)
|
||||||
{
|
{
|
||||||
if (query.User != null)
|
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)
|
if (query.User != null)
|
||||||
{
|
{
|
||||||
@@ -1758,7 +1760,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return orderedItems ?? items;
|
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;
|
var isFirst = true;
|
||||||
|
|
||||||
@@ -2500,16 +2502,6 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return RootFolder;
|
return RootFolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool IsVideoFile(string path)
|
|
||||||
{
|
|
||||||
return VideoResolver.IsVideoFile(path, GetNamingOptions());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool IsAudioFile(string path)
|
|
||||||
=> AudioFileParser.IsAudioFile(path, GetNamingOptions());
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int? GetSeasonNumberFromPath(string path)
|
public int? GetSeasonNumberFromPath(string path)
|
||||||
=> SeasonPathParser.Parse(path, true, true).SeasonNumber;
|
=> SeasonPathParser.Parse(path, true, true).SeasonNumber;
|
||||||
@@ -2525,7 +2517,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
isAbsoluteNaming = null;
|
isAbsoluteNaming = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var resolver = new EpisodeResolver(GetNamingOptions());
|
var resolver = new EpisodeResolver(_namingOptions);
|
||||||
|
|
||||||
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
|
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
|
||||||
|
|
||||||
@@ -2682,113 +2674,77 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public NamingOptions GetNamingOptions()
|
|
||||||
{
|
|
||||||
if (_namingOptions == null)
|
|
||||||
{
|
|
||||||
_namingOptions = new NamingOptions();
|
|
||||||
_videoFileExtensions = _namingOptions.VideoFileExtensions;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _namingOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ItemLookupInfo ParseName(string name)
|
public ItemLookupInfo ParseName(string name)
|
||||||
{
|
{
|
||||||
var namingOptions = GetNamingOptions();
|
var namingOptions = _namingOptions;
|
||||||
var result = VideoResolver.CleanDateTime(name, namingOptions);
|
var result = VideoResolver.CleanDateTime(name, namingOptions);
|
||||||
|
|
||||||
return new ItemLookupInfo
|
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
|
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 ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions);
|
||||||
|
if (ownerVideoInfo == null)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
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)
|
var current = fileSystemChildren[i];
|
||||||
};
|
if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name))
|
||||||
|
|
||||||
return ResolvePaths(files, directoryService, null, new LibraryOptions(), null, resolvers)
|
|
||||||
.OfType<Trailer>()
|
|
||||||
.Select(video =>
|
|
||||||
{
|
{
|
||||||
// Try to retrieve it from the db. If we don't find it, use the resolved version
|
var filesInSubFolder = _fileSystem.GetFiles(current.FullName, null, false, false);
|
||||||
if (GetItemById(video.Id) is Trailer dbItem)
|
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;
|
else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo, out var extraType))
|
||||||
video.OwnerId = owner.Id;
|
{
|
||||||
video.ExtraType = ExtraType.Trailer;
|
var extra = GetExtra(current, extraType.Value);
|
||||||
video.TrailerTypes = new[] { TrailerType.LocalTrailer };
|
if (extra != null)
|
||||||
|
{
|
||||||
return video;
|
yield return extra;
|
||||||
|
}
|
||||||
// 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)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResolvePaths(files, directoryService, null, new LibraryOptions(), null)
|
BaseItem GetExtra(FileSystemMetadata file, ExtraType extraType)
|
||||||
.OfType<Video>()
|
{
|
||||||
.Select(video =>
|
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
|
return null;
|
||||||
var dbItem = GetItemById(video.Id) as Video;
|
}
|
||||||
|
|
||||||
if (dbItem != null)
|
// Try to retrieve it from the db. If we don't find it, use the resolved version
|
||||||
{
|
var itemById = GetItemById(extra.Id);
|
||||||
video = dbItem;
|
if (itemById != null)
|
||||||
}
|
{
|
||||||
|
extra = itemById;
|
||||||
|
}
|
||||||
|
|
||||||
video.ParentId = Guid.Empty;
|
extra.ExtraType = extraType;
|
||||||
video.OwnerId = owner.Id;
|
extra.ParentId = Guid.Empty;
|
||||||
|
extra.OwnerId = owner.Id;
|
||||||
SetExtraTypeFromFilename(video);
|
return extra;
|
||||||
|
}
|
||||||
return video;
|
|
||||||
|
|
||||||
// Sort them so that the list can be easily compared for changes
|
|
||||||
}).OrderBy(i => i.Path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
|
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
|
||||||
@@ -2838,15 +2794,6 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return path;
|
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)
|
public List<PersonInfo> GetPeople(InternalPeopleQuery query)
|
||||||
{
|
{
|
||||||
return _itemRepository.GetPeople(query);
|
return _itemRepository.GetPeople(query);
|
||||||
@@ -2953,11 +2900,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
|
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
|
||||||
|
|
||||||
|
var existingNameCount = 1; // first numbered name will be 2
|
||||||
var virtualFolderPath = Path.Combine(rootFolderPath, name);
|
var virtualFolderPath = Path.Combine(rootFolderPath, name);
|
||||||
while (Directory.Exists(virtualFolderPath))
|
while (Directory.Exists(virtualFolderPath))
|
||||||
{
|
{
|
||||||
name += "1";
|
existingNameCount++;
|
||||||
virtualFolderPath = Path.Combine(rootFolderPath, name);
|
virtualFolderPath = Path.Combine(rootFolderPath, name + " " + existingNameCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaPathInfos = options.PathInfos;
|
var mediaPathInfos = options.PathInfos;
|
||||||
|
|||||||
@@ -66,11 +66,8 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
var delayMs = mediaSource.AnalyzeDurationMs ?? 0;
|
var delayMs = mediaSource.AnalyzeDurationMs ?? 0;
|
||||||
delayMs = Math.Max(3000, delayMs);
|
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;
|
mediaSource.AnalyzeDurationMs = 3000;
|
||||||
|
|||||||
@@ -464,12 +464,11 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var tuple = GetProvider(request.OpenToken);
|
var (provider, keyId) = GetProvider(request.OpenToken);
|
||||||
var provider = tuple.Item1;
|
|
||||||
|
|
||||||
var currentLiveStreams = _openStreams.Values.ToList();
|
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;
|
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))
|
if (string.IsNullOrEmpty(key))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library
|
namespace Emby.Server.Implementations.Library
|
||||||
@@ -64,18 +65,18 @@ namespace Emby.Server.Implementations.Library
|
|||||||
stream = sortedStreams.FirstOrDefault(s => s.IsExternal || s.IsForced || s.IsDefault);
|
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 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)
|
else if (mode == SubtitlePlaybackMode.Smart)
|
||||||
{
|
{
|
||||||
// if the audio language is not understood by the user, load their preferred subs, if there are any
|
// 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)) ??
|
stream = streams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase)) ??
|
||||||
streams.FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase));
|
streams.FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (mode == SubtitlePlaybackMode.Always)
|
else if (mode == SubtitlePlaybackMode.Always)
|
||||||
@@ -136,9 +137,9 @@ namespace Emby.Server.Implementations.Library
|
|||||||
else if (mode == SubtitlePlaybackMode.Smart)
|
else if (mode == SubtitlePlaybackMode.Smart)
|
||||||
{
|
{
|
||||||
// Prefer smart logic over embedded metadata
|
// 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();
|
.ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
var genres = item
|
var genres = item
|
||||||
.GetRecursiveChildren(user, new InternalItemsQuery(user)
|
.GetRecursiveChildren(user, new InternalItemsQuery(user)
|
||||||
{
|
{
|
||||||
IncludeItemTypes = new[] { nameof(Audio) },
|
IncludeItemTypes = new[] { BaseItemKind.Audio },
|
||||||
DtoOptions = dtoOptions
|
DtoOptions = dtoOptions
|
||||||
})
|
})
|
||||||
.Cast<Audio>()
|
.Cast<Audio>()
|
||||||
@@ -89,7 +89,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
return _libraryManager.GetItemList(new InternalItemsQuery(user)
|
return _libraryManager.GetItemList(new InternalItemsQuery(user)
|
||||||
{
|
{
|
||||||
IncludeItemTypes = new[] { nameof(Audio) },
|
IncludeItemTypes = new[] { BaseItemKind.Audio },
|
||||||
|
|
||||||
GenreIds = genreIds.ToArray(),
|
GenreIds = genreIds.ToArray(),
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
public List<BaseItem> GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions)
|
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);
|
return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
/// <param name="attribute">The attrib.</param>
|
/// <param name="attribute">The attrib.</param>
|
||||||
/// <returns>System.String.</returns>
|
/// <returns>System.String.</returns>
|
||||||
/// <exception cref="ArgumentException"><paramref name="str" /> or <paramref name="attribute" /> is empty.</exception>
|
/// <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)
|
if (str.Length == 0)
|
||||||
{
|
{
|
||||||
@@ -28,17 +28,31 @@ namespace Emby.Server.Implementations.Library
|
|||||||
throw new ArgumentException("String can't be empty.", nameof(attribute));
|
throw new ArgumentException("String can't be empty.", nameof(attribute));
|
||||||
}
|
}
|
||||||
|
|
||||||
string srch = "[" + attribute + "=";
|
var attributeIndex = str.IndexOf(attribute, StringComparison.OrdinalIgnoreCase);
|
||||||
int start = str.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
|
|
||||||
if (start != -1)
|
// 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;
|
var attributeEnd = attributeIndex + attribute.Length;
|
||||||
int end = str.IndexOf(']', start);
|
if (attributeIndex > 0
|
||||||
return str.Substring(start, end - start);
|
&& 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
|
// 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);
|
var match = ProviderIdParsers.TryFindImdbId(str, out var imdbId);
|
||||||
return match ? imdbId.ToString() : null;
|
return match ? imdbId.ToString() : null;
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Emby.Naming.Audio;
|
||||||
using Emby.Naming.AudioBook;
|
using Emby.Naming.AudioBook;
|
||||||
|
using Emby.Naming.Common;
|
||||||
|
using Emby.Naming.Video;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
@@ -21,11 +24,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class AudioResolver : ItemResolver<MediaBrowser.Controller.Entities.Audio.Audio>, IMultiItemResolver
|
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>
|
/// <summary>
|
||||||
@@ -40,7 +43,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
string collectionType,
|
string collectionType,
|
||||||
IDirectoryService directoryService)
|
IDirectoryService directoryService)
|
||||||
{
|
{
|
||||||
var result = ResolveMultipleInternal(parent, files, collectionType, directoryService);
|
var result = ResolveMultipleInternal(parent, files, collectionType);
|
||||||
|
|
||||||
if (result != null)
|
if (result != null)
|
||||||
{
|
{
|
||||||
@@ -56,12 +59,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
private MultiItemResolverResult ResolveMultipleInternal(
|
private MultiItemResolverResult ResolveMultipleInternal(
|
||||||
Folder parent,
|
Folder parent,
|
||||||
List<FileSystemMetadata> files,
|
List<FileSystemMetadata> files,
|
||||||
string collectionType,
|
string collectionType)
|
||||||
IDirectoryService directoryService)
|
|
||||||
{
|
{
|
||||||
if (string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return ResolveMultipleAudio<AudioBook>(parent, files, directoryService, false, collectionType, true);
|
return ResolveMultipleAudio(parent, files, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -87,14 +89,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var files = args.FileSystemChildren
|
return FindAudioBook(args, false);
|
||||||
.Where(i => !_libraryManager.IgnoreFile(i, args.Parent))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return FindAudio<AudioBook>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_libraryManager.IsAudioFile(args.Path))
|
if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(args.Path);
|
var extension = Path.GetExtension(args.Path);
|
||||||
|
|
||||||
@@ -107,7 +105,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
var isMixedCollectionType = string.IsNullOrEmpty(collectionType);
|
var isMixedCollectionType = string.IsNullOrEmpty(collectionType);
|
||||||
|
|
||||||
// For conflicting extensions, give priority to videos
|
// For conflicting extensions, give priority to videos
|
||||||
if (isMixedCollectionType && _libraryManager.IsVideoFile(args.Path))
|
if (isMixedCollectionType && VideoResolver.IsVideoFile(args.Path, _namingOptions))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -141,29 +139,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private T FindAudio<T>(ItemResolveArgs args, string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, string collectionType, bool parseName)
|
private AudioBook FindAudioBook(ItemResolveArgs args, bool parseName)
|
||||||
where T : MediaBrowser.Controller.Entities.Audio.Audio, new()
|
|
||||||
{
|
{
|
||||||
// TODO: Allow GetMultiDiscMovie in here
|
// 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) ??
|
if (result == null || result.Items.Count != 1 || result.Items[0] is not AudioBook item)
|
||||||
new MultiItemResolverResult();
|
|
||||||
|
|
||||||
if (result.Items.Count == 1)
|
|
||||||
{
|
{
|
||||||
// If we were supporting this we'd be checking filesFromOtherItems
|
return null;
|
||||||
var item = (T)result.Items[0];
|
|
||||||
item.IsInMixedFolder = false;
|
|
||||||
item.Name = Path.GetFileName(item.ContainingFolderPath);
|
|
||||||
return item;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
private MultiItemResolverResult ResolveMultipleAudio(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, bool parseName)
|
||||||
where T : MediaBrowser.Controller.Entities.Audio.Audio, new()
|
|
||||||
{
|
{
|
||||||
var files = new List<FileSystemMetadata>();
|
var files = new List<FileSystemMetadata>();
|
||||||
var items = new List<BaseItem>();
|
var items = new List<BaseItem>();
|
||||||
@@ -176,15 +168,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
{
|
{
|
||||||
leftOver.Add(child);
|
leftOver.Add(child);
|
||||||
}
|
}
|
||||||
else if (!IsIgnored(child.Name))
|
else
|
||||||
{
|
{
|
||||||
files.Add(child);
|
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 resolverResult = resolver.Resolve(files).ToList();
|
||||||
|
|
||||||
var result = new MultiItemResolverResult
|
var result = new MultiItemResolverResult
|
||||||
@@ -210,7 +200,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
|
|
||||||
var firstMedia = resolvedItem.Files[0];
|
var firstMedia = resolvedItem.Files[0];
|
||||||
|
|
||||||
var libraryItem = new T
|
var libraryItem = new AudioBook
|
||||||
{
|
{
|
||||||
Path = firstMedia.Path,
|
Path = firstMedia.Path,
|
||||||
IsInMixedFolder = isInMixedFolder,
|
IsInMixedFolder = isInMixedFolder,
|
||||||
@@ -230,12 +220,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
return result;
|
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));
|
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)) ||
|
return result.Files.Any(i => ContainsFile(i, file)) ||
|
||||||
result.AlternateVersions.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);
|
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;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Naming.Audio;
|
using Emby.Naming.Audio;
|
||||||
|
using Emby.Naming.Common;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
@@ -22,20 +23,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
public class MusicAlbumResolver : ItemResolver<MusicAlbum>
|
public class MusicAlbumResolver : ItemResolver<MusicAlbum>
|
||||||
{
|
{
|
||||||
private readonly ILogger<MusicAlbumResolver> _logger;
|
private readonly ILogger<MusicAlbumResolver> _logger;
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly NamingOptions _namingOptions;
|
||||||
private readonly ILibraryManager _libraryManager;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class.
|
/// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">The logger.</param>
|
/// <param name="logger">The logger.</param>
|
||||||
/// <param name="fileSystem">The file system.</param>
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions)
|
||||||
public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, IFileSystem fileSystem, ILibraryManager libraryManager)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_fileSystem = fileSystem;
|
_namingOptions = namingOptions;
|
||||||
_libraryManager = libraryManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// <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)
|
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>
|
/// <summary>
|
||||||
@@ -101,7 +99,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
if (args.IsDirectory)
|
if (args.IsDirectory)
|
||||||
{
|
{
|
||||||
// if (args.Parent is MusicArtist) return true; // saves us from testing children twice
|
// 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;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -114,15 +112,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
/// Determine if the supplied list contains what we should consider music.
|
/// Determine if the supplied list contains what we should consider music.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private bool ContainsMusic(
|
private bool ContainsMusic(
|
||||||
IEnumerable<FileSystemMetadata> list,
|
ICollection<FileSystemMetadata> list,
|
||||||
bool allowSubfolders,
|
bool allowSubfolders,
|
||||||
IDirectoryService directoryService,
|
IDirectoryService directoryService)
|
||||||
ILogger<MusicAlbumResolver> logger,
|
|
||||||
IFileSystem fileSystem,
|
|
||||||
ILibraryManager libraryManager)
|
|
||||||
{
|
{
|
||||||
// check for audio files before digging down into directories
|
// 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)
|
if (foundAudioFile)
|
||||||
{
|
{
|
||||||
// at least one audio file exists
|
// at least one audio file exists
|
||||||
@@ -137,21 +132,20 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
|
|
||||||
var discSubfolderCount = 0;
|
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 directories = list.Where(fileSystemInfo => fileSystemInfo.IsDirectory);
|
||||||
|
|
||||||
var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
|
var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
|
||||||
{
|
{
|
||||||
var path = fileSystemInfo.FullName;
|
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 (hasMusic)
|
||||||
{
|
{
|
||||||
if (parser.IsMultiPart(path))
|
if (parser.IsMultiPart(path))
|
||||||
{
|
{
|
||||||
logger.LogDebug("Found multi-disc folder: {Path}", path);
|
_logger.LogDebug("Found multi-disc folder: {Path}", path);
|
||||||
Interlocked.Increment(ref discSubfolderCount);
|
Interlocked.Increment(ref discSubfolderCount);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -3,12 +3,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using Emby.Naming.Common;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Resolvers;
|
using MediaBrowser.Controller.Resolvers;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||||
@@ -19,27 +18,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
public class MusicArtistResolver : ItemResolver<MusicArtist>
|
public class MusicArtistResolver : ItemResolver<MusicArtist>
|
||||||
{
|
{
|
||||||
private readonly ILogger<MusicAlbumResolver> _logger;
|
private readonly ILogger<MusicAlbumResolver> _logger;
|
||||||
private readonly IFileSystem _fileSystem;
|
private NamingOptions _namingOptions;
|
||||||
private readonly ILibraryManager _libraryManager;
|
|
||||||
private readonly IServerConfigurationManager _config;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
|
/// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">The logger for the created <see cref="MusicAlbumResolver"/> instances.</param>
|
/// <param name="logger">The logger for the created <see cref="MusicAlbumResolver"/> instances.</param>
|
||||||
/// <param name="fileSystem">The file system.</param>
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
|
||||||
/// <param name="config">The configuration manager.</param>
|
|
||||||
public MusicArtistResolver(
|
public MusicArtistResolver(
|
||||||
ILogger<MusicAlbumResolver> logger,
|
ILogger<MusicAlbumResolver> logger,
|
||||||
IFileSystem fileSystem,
|
NamingOptions namingOptions)
|
||||||
ILibraryManager libraryManager,
|
|
||||||
IServerConfigurationManager config)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_fileSystem = fileSystem;
|
_namingOptions = namingOptions;
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_config = config;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -89,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
|
|
||||||
var directoryService = args.DirectoryService;
|
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
|
// If we contain an album assume we are an artist folder
|
||||||
var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
|
var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using DiscUtils.Udf;
|
using DiscUtils.Udf;
|
||||||
|
using Emby.Naming.Common;
|
||||||
using Emby.Naming.Video;
|
using Emby.Naming.Video;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
@@ -21,12 +22,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T>
|
public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T>
|
||||||
where T : Video, new()
|
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>
|
/// <summary>
|
||||||
/// Resolves the specified args.
|
/// Resolves the specified args.
|
||||||
@@ -48,120 +49,71 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
|
protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
|
||||||
where TVideoType : Video, new()
|
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 the path is a file check for a matching extensions
|
||||||
if (args.IsDirectory)
|
if (args.IsDirectory)
|
||||||
{
|
{
|
||||||
TVideoType video = null;
|
|
||||||
VideoFileInfo videoInfo = null;
|
|
||||||
|
|
||||||
// Loop through each child file/folder and see if we find a video
|
// Loop through each child file/folder and see if we find a video
|
||||||
foreach (var child in args.FileSystemChildren)
|
foreach (var child in args.FileSystemChildren)
|
||||||
{
|
{
|
||||||
var filename = child.Name;
|
var filename = child.Name;
|
||||||
|
|
||||||
if (child.IsDirectory)
|
if (child.IsDirectory)
|
||||||
{
|
{
|
||||||
if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
|
if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
|
||||||
{
|
{
|
||||||
videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
|
videoType = VideoType.Dvd;
|
||||||
|
|
||||||
if (videoInfo == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
video = new TVideoType
|
|
||||||
{
|
|
||||||
Path = args.Path,
|
|
||||||
VideoType = VideoType.Dvd,
|
|
||||||
ProductionYear = videoInfo.Year
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
else if (IsBluRayDirectory(filename))
|
||||||
if (IsBluRayDirectory(filename))
|
|
||||||
{
|
{
|
||||||
videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
|
videoType = VideoType.BluRay;
|
||||||
|
|
||||||
if (videoInfo == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
video = new TVideoType
|
|
||||||
{
|
|
||||||
Path = args.Path,
|
|
||||||
VideoType = VideoType.BluRay,
|
|
||||||
ProductionYear = videoInfo.Year
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (IsDvdFile(filename))
|
else if (IsDvdFile(filename))
|
||||||
{
|
{
|
||||||
videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
|
videoType = VideoType.Dvd;
|
||||||
|
|
||||||
if (videoInfo == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
video = new TVideoType
|
|
||||||
{
|
|
||||||
Path = args.Path,
|
|
||||||
VideoType = VideoType.Dvd,
|
|
||||||
ProductionYear = videoInfo.Year
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
else
|
||||||
{
|
{
|
||||||
var videoInfo = VideoResolver.Resolve(args.Path, false, namingOptions, false);
|
videoInfo = VideoResolver.Resolve(args.Path, false, NamingOptions, parseName);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
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
|
// use disc-utils, both DVDs and BDs use UDF filesystem
|
||||||
using (var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read))
|
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"))
|
if (udfReader.DirectoryExists("VIDEO_TS"))
|
||||||
{
|
{
|
||||||
video.IsoType = IsoType.Dvd;
|
video.IsoType = IsoType.Dvd;
|
||||||
@@ -267,7 +219,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
|
|
||||||
protected void Set3DFormat(Video video)
|
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);
|
Set3DFormat(video, result.Is3D, result.Format3D);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
@@ -32,7 +33,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
|||||||
|
|
||||||
var extension = Path.GetExtension(args.Path);
|
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
|
// It's a book
|
||||||
return new 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
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
|
using Emby.Naming.Common;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library.Resolvers
|
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>
|
public class GenericVideoResolver<T> : BaseVideoResolver<T>
|
||||||
where T : Video, new()
|
where T : Video, new()
|
||||||
{
|
{
|
||||||
public GenericVideoResolver(ILibraryManager libraryManager)
|
/// <summary>
|
||||||
: base(libraryManager)
|
/// 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)
|
private static void SetProviderIdFromPath(BaseItem item)
|
||||||
{
|
{
|
||||||
// we need to only look at the name of this actual item (not parents)
|
// 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");
|
var id = justName.GetAttributeValue("tmdbid");
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using Emby.Naming.Common;
|
||||||
using Emby.Naming.Video;
|
using Emby.Naming.Video;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
@@ -38,10 +39,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="MovieResolver"/> class.
|
/// Initializes a new instance of the <see cref="MovieResolver"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
|
||||||
/// <param name="imageProcessor">The image processor.</param>
|
/// <param name="imageProcessor">The image processor.</param>
|
||||||
public MovieResolver(ILibraryManager libraryManager, IImageProcessor imageProcessor)
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
: base(libraryManager)
|
public MovieResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
|
||||||
|
: base(namingOptions)
|
||||||
{
|
{
|
||||||
_imageProcessor = imageProcessor;
|
_imageProcessor = imageProcessor;
|
||||||
}
|
}
|
||||||
@@ -59,7 +60,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
string collectionType,
|
string collectionType,
|
||||||
IDirectoryService directoryService)
|
IDirectoryService directoryService)
|
||||||
{
|
{
|
||||||
var result = ResolveMultipleInternal(parent, files, collectionType, directoryService);
|
var result = ResolveMultipleInternal(parent, files, collectionType);
|
||||||
|
|
||||||
if (result != null)
|
if (result != null)
|
||||||
{
|
{
|
||||||
@@ -89,26 +90,24 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var files = args.FileSystemChildren
|
Video movie = null;
|
||||||
.Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
|
var files = args.GetActualFileSystemChildren().ToList();
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
|
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))
|
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))
|
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)
|
if (args.Parent == null)
|
||||||
{
|
{
|
||||||
// return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,23 +116,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
|
||||||
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
|
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)
|
if (args.Parent == null)
|
||||||
{
|
{
|
||||||
return base.Resolve(args);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsInvalid(args.Parent, collectionType))
|
if (IsInvalid(args.Parent, collectionType))
|
||||||
@@ -168,6 +166,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
item = ResolveVideo<Video>(args, false);
|
item = ResolveVideo<Video>(args, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore extras
|
||||||
|
if (item?.ExtraType != null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (item != null)
|
if (item != null)
|
||||||
{
|
{
|
||||||
item.IsInMixedFolder = true;
|
item.IsInMixedFolder = true;
|
||||||
@@ -179,8 +183,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
private MultiItemResolverResult ResolveMultipleInternal(
|
private MultiItemResolverResult ResolveMultipleInternal(
|
||||||
Folder parent,
|
Folder parent,
|
||||||
List<FileSystemMetadata> files,
|
List<FileSystemMetadata> files,
|
||||||
string collectionType,
|
string collectionType)
|
||||||
IDirectoryService directoryService)
|
|
||||||
{
|
{
|
||||||
if (IsInvalid(parent, collectionType))
|
if (IsInvalid(parent, collectionType))
|
||||||
{
|
{
|
||||||
@@ -189,13 +192,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
|
|
||||||
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
|
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) ||
|
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(collectionType, CollectionType.Photos, 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))
|
if (string.IsNullOrEmpty(collectionType))
|
||||||
@@ -203,7 +206,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
// Owned items should just use the plain video type
|
// Owned items should just use the plain video type
|
||||||
if (parent == null)
|
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())
|
if (parent is Series || parent.GetParents().OfType<Series>().Any())
|
||||||
@@ -211,12 +214,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
return null;
|
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))
|
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;
|
return null;
|
||||||
@@ -225,21 +228,20 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
private MultiItemResolverResult ResolveVideos<T>(
|
private MultiItemResolverResult ResolveVideos<T>(
|
||||||
Folder parent,
|
Folder parent,
|
||||||
IEnumerable<FileSystemMetadata> fileSystemEntries,
|
IEnumerable<FileSystemMetadata> fileSystemEntries,
|
||||||
IDirectoryService directoryService,
|
bool supportMultiEditions,
|
||||||
bool suppportMultiEditions,
|
|
||||||
string collectionType,
|
string collectionType,
|
||||||
bool parseName)
|
bool parseName)
|
||||||
where T : Video, new()
|
where T : Video, new()
|
||||||
{
|
{
|
||||||
var files = new List<FileSystemMetadata>();
|
var files = new List<FileSystemMetadata>();
|
||||||
var videos = new List<BaseItem>();
|
|
||||||
var leftOver = new List<FileSystemMetadata>();
|
var leftOver = new List<FileSystemMetadata>();
|
||||||
|
var hasCollectionType = !string.IsNullOrEmpty(collectionType);
|
||||||
|
|
||||||
// Loop through each child file/folder and see if we find a video
|
// Loop through each child file/folder and see if we find a video
|
||||||
foreach (var child in fileSystemEntries)
|
foreach (var child in fileSystemEntries)
|
||||||
{
|
{
|
||||||
// This is a hack but currently no better way to resolve a sometimes ambiguous situation
|
// 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)
|
if (string.Equals(child.Name, "tvshow.nfo", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(child.Name, "season.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
|
var result = new MultiItemResolverResult
|
||||||
{
|
{
|
||||||
ExtraFiles = leftOver,
|
ExtraFiles = leftOver
|
||||||
Items = videos
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var isInMixedFolder = resolverResult.Count > 1 || (parent != null && parent.IsTopParent);
|
var isInMixedFolder = resolverResult.Count > 1 || parent?.IsTopParent == true;
|
||||||
|
|
||||||
foreach (var video in resolverResult)
|
foreach (var video in resolverResult)
|
||||||
{
|
{
|
||||||
var firstVideo = video.Files[0];
|
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
|
var videoItem = new T
|
||||||
{
|
{
|
||||||
Path = video.Files[0].Path,
|
Path = path,
|
||||||
IsInMixedFolder = isInMixedFolder,
|
IsInMixedFolder = isInMixedFolder,
|
||||||
ProductionYear = video.Year,
|
ProductionYear = video.Year,
|
||||||
Name = parseName ?
|
Name = parseName ? video.Name : firstVideo.Name,
|
||||||
video.Name :
|
AdditionalParts = additionalParts,
|
||||||
Path.GetFileNameWithoutExtension(video.Files[0].Path),
|
|
||||||
AdditionalParts = video.Files.Skip(1).Select(i => i.Path).ToArray(),
|
|
||||||
LocalAlternateVersions = video.AlternateVersions.Select(i => i.Path).ToArray()
|
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)
|
private static bool IsIgnored(string filename)
|
||||||
{
|
{
|
||||||
// Ignore samples
|
// 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;
|
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)
|
for (var j = 0; j < current.AlternateVersions.Count; j++)
|
||||||
{
|
{
|
||||||
return result.Files.Any(i => ContainsFile(i, file)) ||
|
if (ContainsFile(current.AlternateVersions[j], file))
|
||||||
result.AlternateVersions.Any(i => ContainsFile(i, file)) ||
|
{
|
||||||
result.Extras.Any(i => ContainsFile(i, file));
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ContainsFile(VideoFileInfo result, FileSystemMetadata file)
|
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)
|
if (item is Movie || item is MusicVideo)
|
||||||
{
|
{
|
||||||
// We need to only look at the name of this actual item (not parents)
|
// 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
|
// check for tmdb id
|
||||||
var tmdbid = justName.GetAttributeValue("tmdbid");
|
var tmdbid = justName.GetAttributeValue("tmdbid");
|
||||||
@@ -359,7 +382,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
if (!string.IsNullOrEmpty(item.Path))
|
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)
|
// 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))
|
if (!string.IsNullOrWhiteSpace(imdbid))
|
||||||
{
|
{
|
||||||
@@ -432,13 +455,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
// TODO: Allow GetMultiDiscMovie in here
|
// TODO: Allow GetMultiDiscMovie in here
|
||||||
const bool SupportsMultiVersion = true;
|
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();
|
new MultiItemResolverResult();
|
||||||
|
|
||||||
if (result.Items.Count == 1)
|
if (result.Items.Count == 1)
|
||||||
{
|
{
|
||||||
var videoPath = result.Items[0].Path;
|
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)
|
if (!hasPhotos)
|
||||||
{
|
{
|
||||||
@@ -511,9 +534,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
|
var result = StackResolver.ResolveDirectories(folderPaths, NamingOptions).ToList();
|
||||||
|
|
||||||
var result = new StackResolver(namingOptions).ResolveDirectories(folderPaths).ToList();
|
|
||||||
|
|
||||||
if (result.Count != 1)
|
if (result.Count != 1)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using Emby.Naming.Common;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
@@ -15,17 +16,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
public class PhotoAlbumResolver : GenericFolderResolver<PhotoAlbum>
|
public class PhotoAlbumResolver : GenericFolderResolver<PhotoAlbum>
|
||||||
{
|
{
|
||||||
private readonly IImageProcessor _imageProcessor;
|
private readonly IImageProcessor _imageProcessor;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly NamingOptions _namingOptions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="PhotoAlbumResolver"/> class.
|
/// Initializes a new instance of the <see cref="PhotoAlbumResolver"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="imageProcessor">The image processor.</param>
|
/// <param name="imageProcessor">The image processor.</param>
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
public PhotoAlbumResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager)
|
public PhotoAlbumResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
_imageProcessor = imageProcessor;
|
_imageProcessor = imageProcessor;
|
||||||
_libraryManager = libraryManager;
|
_namingOptions = namingOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -73,7 +74,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
|
|
||||||
foreach (var siblingFile in files)
|
foreach (var siblingFile in files)
|
||||||
{
|
{
|
||||||
if (PhotoResolver.IsOwnedByMedia(_libraryManager, siblingFile.FullName, filename))
|
if (PhotoResolver.IsOwnedByMedia(_namingOptions, siblingFile.FullName, filename))
|
||||||
{
|
{
|
||||||
ownedByMedia = true;
|
ownedByMedia = true;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Emby.Naming.Common;
|
||||||
|
using Emby.Naming.Video;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
@@ -16,7 +19,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
public class PhotoResolver : ItemResolver<Photo>
|
public class PhotoResolver : ItemResolver<Photo>
|
||||||
{
|
{
|
||||||
private readonly IImageProcessor _imageProcessor;
|
private readonly IImageProcessor _imageProcessor;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly NamingOptions _namingOptions;
|
||||||
|
|
||||||
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
"folder",
|
"folder",
|
||||||
@@ -30,10 +34,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
"default"
|
"default"
|
||||||
};
|
};
|
||||||
|
|
||||||
public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager)
|
public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
_imageProcessor = imageProcessor;
|
_imageProcessor = imageProcessor;
|
||||||
_libraryManager = libraryManager;
|
_namingOptions = namingOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -60,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
|
|
||||||
foreach (var file in files)
|
foreach (var file in files)
|
||||||
{
|
{
|
||||||
if (IsOwnedByMedia(_libraryManager, file.FullName, filename))
|
if (IsOwnedByMedia(_namingOptions, file.FullName, filename))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -77,17 +81,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
return null;
|
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 VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename);
|
||||||
{
|
|
||||||
return IsOwnedByResolvedMedia(libraryManager, file, imageFilename);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
=> imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
|
internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
|
||||||
@@ -110,7 +109,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
}
|
}
|
||||||
|
|
||||||
string extension = Path.GetExtension(path).TrimStart('.');
|
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;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Playlists;
|
using MediaBrowser.Controller.Playlists;
|
||||||
using MediaBrowser.Controller.Resolvers;
|
using MediaBrowser.Controller.Resolvers;
|
||||||
@@ -57,10 +58,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
|
|
||||||
// Check if this is a music playlist file
|
// Check if this is a music playlist file
|
||||||
// It should have the correct collection type and a supported file extension
|
// 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);
|
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
|
return new Playlist
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using MediaBrowser.Controller.Entities;
|
using Emby.Naming.Common;
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
@@ -17,9 +17,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
|
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
public EpisodeResolver(ILibraryManager libraryManager)
|
public EpisodeResolver(NamingOptions namingOptions)
|
||||||
: base(libraryManager)
|
: 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
|
// 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
|
// Also handle flat tv folders
|
||||||
if ((season != null ||
|
if (season != null ||
|
||||||
string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
|
string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
|
||||||
args.HasParent<Series>())
|
args.HasParent<Series>())
|
||||||
&& (parent is Series || !BaseItem.AllExtrasTypesFolderNames.ContainsKey(parent.Name)))
|
|
||||||
{
|
{
|
||||||
var episode = ResolveVideo<Episode>(args, false);
|
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)
|
var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault();
|
||||||
{
|
|
||||||
episode.SeriesId = series.Id;
|
|
||||||
episode.SeriesName = series.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (season != null)
|
if (series != null)
|
||||||
{
|
{
|
||||||
episode.SeasonId = season.Id;
|
episode.SeriesId = series.Id;
|
||||||
episode.SeasonName = season.Name;
|
episode.SeriesName = series.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assume season 1 if there's no season folder and a season number could not be determined
|
if (season != null)
|
||||||
if (season == null && !episode.ParentIndexNumber.HasValue && (episode.IndexNumber.HasValue || episode.PremiereDate.HasValue))
|
{
|
||||||
{
|
episode.SeasonId = season.Id;
|
||||||
episode.ParentIndexNumber = 1;
|
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;
|
return episode;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using Emby.Naming.Common;
|
||||||
using Emby.Naming.TV;
|
using Emby.Naming.TV;
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
@@ -14,22 +15,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class SeasonResolver : GenericFolderResolver<Season>
|
public class SeasonResolver : GenericFolderResolver<Season>
|
||||||
{
|
{
|
||||||
private readonly ILibraryManager _libraryManager;
|
|
||||||
private readonly ILocalizationManager _localization;
|
private readonly ILocalizationManager _localization;
|
||||||
private readonly ILogger<SeasonResolver> _logger;
|
private readonly ILogger<SeasonResolver> _logger;
|
||||||
|
private readonly NamingOptions _namingOptions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SeasonResolver"/> class.
|
/// Initializes a new instance of the <see cref="SeasonResolver"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <param name="localization">The localization.</param>
|
/// <param name="localization">The localization.</param>
|
||||||
/// <param name="logger">The logger.</param>
|
/// <param name="logger">The logger.</param>
|
||||||
public SeasonResolver(
|
public SeasonResolver(
|
||||||
ILibraryManager libraryManager,
|
NamingOptions namingOptions,
|
||||||
ILocalizationManager localization,
|
ILocalizationManager localization,
|
||||||
ILogger<SeasonResolver> logger)
|
ILogger<SeasonResolver> logger)
|
||||||
{
|
{
|
||||||
_libraryManager = libraryManager;
|
_namingOptions = namingOptions;
|
||||||
_localization = localization;
|
_localization = localization;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
@@ -43,7 +44,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
{
|
{
|
||||||
if (args.Parent is Series series && args.IsDirectory)
|
if (args.Parent is Series series && args.IsDirectory)
|
||||||
{
|
{
|
||||||
var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
|
var namingOptions = _namingOptions;
|
||||||
|
|
||||||
var path = args.Path;
|
var path = args.Path;
|
||||||
|
|
||||||
@@ -65,18 +66,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
|
|
||||||
var episodeInfo = resolver.Resolve(testPath, true);
|
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}",
|
||||||
_logger.LogDebug(
|
path,
|
||||||
"Found folder underneath series with episode number: {0}. Season {1}. Episode {2}",
|
episodeInfo.SeasonNumber.Value,
|
||||||
path,
|
episodeInfo.EpisodeNumber.Value);
|
||||||
episodeInfo.SeasonNumber.Value,
|
|
||||||
episodeInfo.EpisodeNumber.Value);
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using Emby.Naming.Common;
|
||||||
using Emby.Naming.TV;
|
using Emby.Naming.TV;
|
||||||
|
using Emby.Naming.Video;
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Resolvers;
|
using MediaBrowser.Controller.Resolvers;
|
||||||
@@ -21,17 +23,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
public class SeriesResolver : GenericFolderResolver<Series>
|
public class SeriesResolver : GenericFolderResolver<Series>
|
||||||
{
|
{
|
||||||
private readonly ILogger<SeriesResolver> _logger;
|
private readonly ILogger<SeriesResolver> _logger;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly NamingOptions _namingOptions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SeriesResolver"/> class.
|
/// Initializes a new instance of the <see cref="SeriesResolver"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">The logger.</param>
|
/// <param name="logger">The logger.</param>
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
public SeriesResolver(ILogger<SeriesResolver> logger, ILibraryManager libraryManager)
|
public SeriesResolver(ILogger<SeriesResolver> logger, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_libraryManager = libraryManager;
|
_namingOptions = namingOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -54,12 +56,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
return null;
|
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();
|
var collectionType = args.GetCollectionType();
|
||||||
if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
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))
|
if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return new Series
|
return new Series
|
||||||
@@ -91,7 +94,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsSeriesFolder(args.Path, args.FileSystemChildren, _logger, _libraryManager, false))
|
if (IsSeriesFolder(args.Path, args.FileSystemChildren, false))
|
||||||
{
|
{
|
||||||
return new Series
|
return new Series
|
||||||
{
|
{
|
||||||
@@ -105,11 +108,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsSeriesFolder(
|
private bool IsSeriesFolder(
|
||||||
string path,
|
string path,
|
||||||
IEnumerable<FileSystemMetadata> fileSystemChildren,
|
IEnumerable<FileSystemMetadata> fileSystemChildren,
|
||||||
ILogger<SeriesResolver> logger,
|
|
||||||
ILibraryManager libraryManager,
|
|
||||||
bool isTvContentType)
|
bool isTvContentType)
|
||||||
{
|
{
|
||||||
foreach (var child in fileSystemChildren)
|
foreach (var child in fileSystemChildren)
|
||||||
@@ -118,21 +119,21 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
{
|
{
|
||||||
if (IsSeasonFolder(child.FullName, isTvContentType))
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
string fullName = child.FullName;
|
string fullName = child.FullName;
|
||||||
if (libraryManager.IsVideoFile(fullName))
|
if (VideoResolver.IsVideoFile(path, _namingOptions))
|
||||||
{
|
{
|
||||||
if (isTvContentType)
|
if (isTvContentType)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions();
|
var namingOptions = _namingOptions;
|
||||||
|
|
||||||
var episodeResolver = new Naming.TV.EpisodeResolver(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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,13 +182,42 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
/// <param name="path">The path.</param>
|
/// <param name="path">The path.</param>
|
||||||
private static void SetProviderIdFromPath(Series item, string path)
|
private static void SetProviderIdFromPath(Series item, string path)
|
||||||
{
|
{
|
||||||
var justName = Path.GetFileName(path);
|
var justName = Path.GetFileName(path.AsSpan());
|
||||||
|
|
||||||
var id = justName.GetAttributeValue("tvdbid");
|
var tvdbId = justName.GetAttributeValue("tvdbid");
|
||||||
|
if (!string.IsNullOrEmpty(tvdbId))
|
||||||
if (!string.IsNullOrEmpty(id))
|
|
||||||
{
|
{
|
||||||
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 Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using MediaBrowser.Model.Search;
|
using MediaBrowser.Model.Search;
|
||||||
using Genre = MediaBrowser.Controller.Entities.Genre;
|
|
||||||
using Person = MediaBrowser.Controller.Entities.Person;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library
|
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);
|
list.Add(value);
|
||||||
}
|
}
|
||||||
@@ -86,63 +83,63 @@ namespace Emby.Server.Implementations.Library
|
|||||||
searchTerm = searchTerm.Trim().RemoveDiacritics();
|
searchTerm = searchTerm.Trim().RemoveDiacritics();
|
||||||
|
|
||||||
var excludeItemTypes = query.ExcludeItemTypes.ToList();
|
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(BaseItemKind.Year);
|
||||||
excludeItemTypes.Add(nameof(Folder));
|
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)
|
if (!query.IncludeMedia)
|
||||||
{
|
{
|
||||||
AddIfMissing(includeItemTypes, nameof(Genre));
|
AddIfMissing(includeItemTypes, BaseItemKind.Genre);
|
||||||
AddIfMissing(includeItemTypes, nameof(MusicGenre));
|
AddIfMissing(includeItemTypes, BaseItemKind.MusicGenre);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AddIfMissing(excludeItemTypes, nameof(Genre));
|
AddIfMissing(excludeItemTypes, BaseItemKind.Genre);
|
||||||
AddIfMissing(excludeItemTypes, nameof(MusicGenre));
|
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)
|
if (!query.IncludeMedia)
|
||||||
{
|
{
|
||||||
AddIfMissing(includeItemTypes, nameof(Person));
|
AddIfMissing(includeItemTypes, BaseItemKind.Person);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
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)
|
if (!query.IncludeMedia)
|
||||||
{
|
{
|
||||||
AddIfMissing(includeItemTypes, nameof(Studio));
|
AddIfMissing(includeItemTypes, BaseItemKind.Studio);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
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)
|
if (!query.IncludeMedia)
|
||||||
{
|
{
|
||||||
AddIfMissing(includeItemTypes, nameof(MusicArtist));
|
AddIfMissing(includeItemTypes, BaseItemKind.MusicArtist);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AddIfMissing(excludeItemTypes, nameof(MusicArtist));
|
AddIfMissing(excludeItemTypes, BaseItemKind.MusicArtist);
|
||||||
}
|
}
|
||||||
|
|
||||||
AddIfMissing(excludeItemTypes, nameof(CollectionFolder));
|
AddIfMissing(excludeItemTypes, BaseItemKind.CollectionFolder);
|
||||||
AddIfMissing(excludeItemTypes, nameof(Folder));
|
AddIfMissing(excludeItemTypes, BaseItemKind.Folder);
|
||||||
var mediaTypes = query.MediaTypes.ToList();
|
var mediaTypes = query.MediaTypes.ToList();
|
||||||
|
|
||||||
if (includeItemTypes.Count > 0)
|
if (includeItemTypes.Count > 0)
|
||||||
@@ -183,7 +180,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
List<BaseItem> mediaItems;
|
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))
|
if (!searchQuery.ParentId.Equals(Guid.Empty))
|
||||||
{
|
{
|
||||||
@@ -192,8 +189,8 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
searchQuery.ParentId = Guid.Empty;
|
searchQuery.ParentId = Guid.Empty;
|
||||||
searchQuery.IncludeItemsByName = true;
|
searchQuery.IncludeItemsByName = true;
|
||||||
searchQuery.IncludeItemTypes = Array.Empty<string>();
|
searchQuery.IncludeItemTypes = Array.Empty<BaseItemKind>();
|
||||||
mediaItems = _libraryManager.GetAllArtists(searchQuery).Items.Select(i => i.Item1).ToList();
|
mediaItems = _libraryManager.GetAllArtists(searchQuery).Items.Select(i => i.Item).ToList();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
|
|
||||||
var cacheKey = GetCacheKey(userId, item.Id);
|
var cacheKey = GetCacheKey(userId, item.Id);
|
||||||
_userData.AddOrUpdate(cacheKey, userData, (k, v) => userData);
|
_userData.AddOrUpdate(cacheKey, userData, (_, _) => userData);
|
||||||
|
|
||||||
UserDataSaved?.Invoke(this, new UserDataSaveEventArgs
|
UserDataSaved?.Invoke(this, new UserDataSaveEventArgs
|
||||||
{
|
{
|
||||||
@@ -125,7 +125,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
var cacheKey = GetCacheKey(userId, itemId);
|
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)
|
private UserItemData GetUserDataInternal(long internalUserId, List<string> keys)
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Channels;
|
using MediaBrowser.Controller.Channels;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
using MediaBrowser.Controller.LiveTv;
|
||||||
using MediaBrowser.Model.Channels;
|
using MediaBrowser.Model.Channels;
|
||||||
@@ -20,8 +20,6 @@ using MediaBrowser.Model.Entities;
|
|||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.Library;
|
using MediaBrowser.Model.Library;
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using Genre = MediaBrowser.Controller.Entities.Genre;
|
|
||||||
using Person = MediaBrowser.Controller.Entities.Person;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library
|
namespace Emby.Server.Implementations.Library
|
||||||
{
|
{
|
||||||
@@ -80,7 +78,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
continue;
|
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));
|
list.Add(GetUserView(folder, folderViewType, string.Empty));
|
||||||
}
|
}
|
||||||
@@ -175,12 +173,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
string viewType,
|
string viewType,
|
||||||
string localizationKey,
|
string localizationKey,
|
||||||
string sortName,
|
string sortName,
|
||||||
Jellyfin.Data.Entities.User user,
|
User user,
|
||||||
string[] presetViews)
|
string[] presetViews)
|
||||||
{
|
{
|
||||||
if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase)))
|
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];
|
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)))
|
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)))
|
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
|
var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0
|
||||||
? new[]
|
? new[]
|
||||||
{
|
{
|
||||||
nameof(Person),
|
BaseItemKind.Person,
|
||||||
nameof(Studio),
|
BaseItemKind.Studio,
|
||||||
nameof(Year),
|
BaseItemKind.Year,
|
||||||
nameof(MusicGenre),
|
BaseItemKind.MusicGenre,
|
||||||
nameof(Genre)
|
BaseItemKind.Genre
|
||||||
}
|
}
|
||||||
: Array.Empty<string>();
|
: Array.Empty<BaseItemKind>();
|
||||||
|
|
||||||
var query = new InternalItemsQuery(user)
|
var query = new InternalItemsQuery(user)
|
||||||
{
|
{
|
||||||
@@ -361,7 +359,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
(ItemSortBy.SortName, SortOrder.Descending),
|
(ItemSortBy.SortName, SortOrder.Descending),
|
||||||
(ItemSortBy.ProductionYear, SortOrder.Descending)
|
(ItemSortBy.ProductionYear, SortOrder.Descending)
|
||||||
},
|
},
|
||||||
IsFolder = includeItemTypes.Length == 0 ? false : (bool?)null,
|
IsFolder = includeItemTypes.Length == 0 ? false : null,
|
||||||
ExcludeItemTypes = excludeItemTypes,
|
ExcludeItemTypes = excludeItemTypes,
|
||||||
IsVirtualItem = false,
|
IsVirtualItem = false,
|
||||||
Limit = limit * 5,
|
Limit = limit * 5,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Globalization;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
@@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.Library.Validators
|
|||||||
|
|
||||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
{
|
{
|
||||||
IncludeItemTypes = new[] { nameof(MusicArtist) },
|
IncludeItemTypes = new[] { BaseItemKind.MusicArtist },
|
||||||
IsDeadArtist = true,
|
IsDeadArtist = true,
|
||||||
IsLocked = false
|
IsLocked = false
|
||||||
}).Cast<MusicArtist>().ToList();
|
}).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.Globalization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
@@ -91,7 +92,7 @@ namespace Emby.Server.Implementations.Library.Validators
|
|||||||
|
|
||||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
{
|
{
|
||||||
IncludeItemTypes = new[] { nameof(Person) },
|
IncludeItemTypes = new[] { BaseItemKind.Person },
|
||||||
IsDeadPerson = true,
|
IsDeadPerson = true,
|
||||||
IsLocked = false
|
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