mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-01-18 17:18:08 +00:00
Compare commits
289 Commits
explicit-l
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb32709259 | ||
|
|
c9b7c5bb56 | ||
|
|
42ff253339 | ||
|
|
c4ffc357a3 | ||
|
|
afcaec0a89 | ||
|
|
09edca8b7a | ||
|
|
f9fd34b11e | ||
|
|
aa666565d1 | ||
|
|
f83e8ee806 | ||
|
|
84ebed1eb7 | ||
|
|
1d7c6af520 | ||
|
|
226da3b371 | ||
|
|
8d052a6cb1 | ||
|
|
b9abf590c5 | ||
|
|
6a9bb060eb | ||
|
|
6a7600a8c6 | ||
|
|
e12131108e | ||
|
|
89b5b99873 | ||
|
|
50dcec1ff5 | ||
|
|
d089537bca | ||
|
|
e70355fbe1 | ||
|
|
140c459ac3 | ||
|
|
098e8c6fed | ||
|
|
1ba8e2c93c | ||
|
|
9e480f6efb | ||
|
|
0892847c2f | ||
|
|
c464ba83f2 | ||
|
|
62e51fd00a | ||
|
|
cf9051c277 | ||
|
|
d270957c82 | ||
|
|
0ee872999d | ||
|
|
c4f4dcc181 | ||
|
|
22ee5113d0 | ||
|
|
e233eee07b | ||
|
|
185849b68a | ||
|
|
e62b6f8339 | ||
|
|
9931537d87 | ||
|
|
82c4df5cde | ||
|
|
103f556c8d | ||
|
|
582a1d9866 | ||
|
|
244757c92c | ||
|
|
0ff869dfcd | ||
|
|
a1e0e4fd9d | ||
|
|
4138214ac3 | ||
|
|
9039077286 | ||
|
|
8a1129bbde | ||
|
|
706a8d2850 | ||
|
|
ba4dbcf5a1 | ||
|
|
bfae788a44 | ||
|
|
e02a2ae48f | ||
|
|
18dc32d735 | ||
|
|
85ff708597 | ||
|
|
23b48a0d0f | ||
|
|
d1055b0b36 | ||
|
|
e5fb071708 | ||
|
|
d28ee6d714 | ||
|
|
2f62a8bb39 | ||
|
|
75f1276119 | ||
|
|
6c8395ff87 | ||
|
|
82b2e7773f | ||
|
|
336958318d | ||
|
|
8a8f7956ef | ||
|
|
c728e97bda | ||
|
|
5c76dd26bc | ||
|
|
7af5ee1812 | ||
|
|
7f0e71578d | ||
|
|
45e881c93e | ||
|
|
b429306f05 | ||
|
|
88acd51ee2 | ||
|
|
3c802a7505 | ||
|
|
928a8458dd | ||
|
|
43797fee42 | ||
|
|
b9158c467a | ||
|
|
252ab45473 | ||
|
|
afc083e9fa | ||
|
|
f867ce3842 | ||
|
|
2a464c316d | ||
|
|
b9cf26db2f | ||
|
|
580585846b | ||
|
|
1af1c72e81 | ||
|
|
5557004375 | ||
|
|
5d50ff5f81 | ||
|
|
8e2ed40a8b | ||
|
|
8461268837 | ||
|
|
8a0b963d2c | ||
|
|
24acd94015 | ||
|
|
c30654c33c | ||
|
|
7bafd13564 | ||
|
|
0e73a56a45 | ||
|
|
f9fec33048 | ||
|
|
934a9c9e32 | ||
|
|
25115e95aa | ||
|
|
84f66dd54e | ||
|
|
d446fde009 | ||
|
|
ee676fd568 | ||
|
|
2cca942ce6 | ||
|
|
59d574edb7 | ||
|
|
c170b18155 | ||
|
|
45a49a4fb4 | ||
|
|
12a2f7c1a5 | ||
|
|
d0950c8f09 | ||
|
|
ebf8220f1d | ||
|
|
c4e8180b3c | ||
|
|
771b0a7eab | ||
|
|
4db0ab0f40 | ||
|
|
dd480f96cd | ||
|
|
6b6d54a07c | ||
|
|
428beda1c7 | ||
|
|
baa8d40940 | ||
|
|
c8bdee26b7 | ||
|
|
ef73ed6ef7 | ||
|
|
d70e0fe9cf | ||
|
|
053cc9406d | ||
|
|
0f85120c5e | ||
|
|
25aef7fabf | ||
|
|
492ea66841 | ||
|
|
8b2a8b94b6 | ||
|
|
f24e80701c | ||
|
|
0b3d6676d1 | ||
|
|
c3a8734adf | ||
|
|
8fd59d6f33 | ||
|
|
da3bff3edf | ||
|
|
45cb5a0008 | ||
|
|
ea097fb1a3 | ||
|
|
a25b48b151 | ||
|
|
873f1d9e83 | ||
|
|
294439bf74 | ||
|
|
6e74be0d46 | ||
|
|
deb81eae10 | ||
|
|
70dcf3f7b3 | ||
|
|
ebcfed83c4 | ||
|
|
5d46278584 | ||
|
|
4f020a947a | ||
|
|
3460d1de3c | ||
|
|
7d2e4cd817 | ||
|
|
8cd6ef37c4 | ||
|
|
e4daaf0d83 | ||
|
|
69c98af9f9 | ||
|
|
7425a493ee | ||
|
|
691c194152 | ||
|
|
2f8896c375 | ||
|
|
6c507b77ae | ||
|
|
6ed0ccd37c | ||
|
|
80e1e42947 | ||
|
|
6ace00eb6a | ||
|
|
a35ffbf17e | ||
|
|
8c02c3be93 | ||
|
|
45669c9b30 | ||
|
|
19c232809e | ||
|
|
301f65af48 | ||
|
|
082ba58e51 | ||
|
|
3b5bdc6bc2 | ||
|
|
b05e91dba1 | ||
|
|
c7703242e5 | ||
|
|
21042ad0c2 | ||
|
|
8904551a59 | ||
|
|
cf1ef22367 | ||
|
|
c08e81c52b | ||
|
|
23e66ae1ea | ||
|
|
37bbdf3fe7 | ||
|
|
f124223015 | ||
|
|
9587a9b13c | ||
|
|
67c67df507 | ||
|
|
569f8cfcfc | ||
|
|
aa4ddd139a | ||
|
|
8ac97f5471 | ||
|
|
efabfbc931 | ||
|
|
6b5dc115e8 | ||
|
|
2dc0af667e | ||
|
|
196c243a7d | ||
|
|
55dbff8f30 | ||
|
|
2af43e0131 | ||
|
|
faf1cea63e | ||
|
|
7e25089c08 | ||
|
|
8fa36a38e2 | ||
|
|
5b3f29946b | ||
|
|
c869b5b884 | ||
|
|
a08b6ac266 | ||
|
|
4e68a5a078 | ||
|
|
99c68ddd50 | ||
|
|
d7f628677e | ||
|
|
e51680cf56 | ||
|
|
2e7d7752e9 | ||
|
|
26ac2ccd74 | ||
|
|
de9e653b73 | ||
|
|
e34e7a1d0b | ||
|
|
5a30f108fe | ||
|
|
74c9629372 | ||
|
|
6c5f448787 | ||
|
|
f848b8f12c | ||
|
|
bcec5f2e44 | ||
|
|
7d05c875f3 | ||
|
|
c805c5e2b1 | ||
|
|
c2c4c0adbf | ||
|
|
5ea3910af9 | ||
|
|
06fb300cff | ||
|
|
626ab7e00a | ||
|
|
1d140645b0 | ||
|
|
5182aec13f | ||
|
|
52f0c3dd24 | ||
|
|
b8327dbc9f | ||
|
|
d1722936c0 | ||
|
|
931240a3f5 | ||
|
|
b216a27bfc | ||
|
|
8471a67bcd | ||
|
|
b8a409195f | ||
|
|
1da67e5e10 | ||
|
|
ed1ec7ca6b | ||
|
|
3d7a68beb1 | ||
|
|
32fc57cf17 | ||
|
|
0598c6eaf6 | ||
|
|
0d7b687da0 | ||
|
|
e69754fd3a | ||
|
|
ac81ddd39a | ||
|
|
217ea488df | ||
|
|
f693c9d39f | ||
|
|
96d72788a1 | ||
|
|
0d74a95bb8 | ||
|
|
a7d039b7c6 | ||
|
|
87b02b1316 | ||
|
|
871de372ff | ||
|
|
c9d93b0745 | ||
|
|
1ccd10863e | ||
|
|
4258df4485 | ||
|
|
63f06aad94 | ||
|
|
ffe82be7a7 | ||
|
|
23929a3e70 | ||
|
|
83d0dbdbcb | ||
|
|
573ce9ceaa | ||
|
|
f21fe9f95e | ||
|
|
f92eca3efb | ||
|
|
7d778d7bef | ||
|
|
21f65e2e27 | ||
|
|
28b0657608 | ||
|
|
a489942454 | ||
|
|
423c2654c0 | ||
|
|
4dc826644d | ||
|
|
0f21222a0c | ||
|
|
570b8b2eb9 | ||
|
|
08fd175f5a | ||
|
|
511b5d9c53 | ||
|
|
6514196e8d | ||
|
|
ed6cb30762 | ||
|
|
232c0399e2 | ||
|
|
dbb015441f | ||
|
|
4c1c160990 | ||
|
|
0931d6e4de | ||
|
|
3f2ebc4179 | ||
|
|
14e8194581 | ||
|
|
3c4dc16003 | ||
|
|
54d28d9842 | ||
|
|
adfa520057 | ||
|
|
5deb69b23f | ||
|
|
348b2992d7 | ||
|
|
9f8fb6d588 | ||
|
|
cee16d47cb | ||
|
|
9e53f46ad2 | ||
|
|
53dfcae1a6 | ||
|
|
81f1cc78b2 | ||
|
|
efd659412f | ||
|
|
c31ea251c4 | ||
|
|
285e7c6c4f | ||
|
|
c274336563 | ||
|
|
d5fd5dfe6a | ||
|
|
42ddcfa565 | ||
|
|
6fa69f9fe5 | ||
|
|
0b876365a1 | ||
|
|
cdc8325c7b | ||
|
|
a6a8e29916 | ||
|
|
6fd3847298 | ||
|
|
3ff516a430 | ||
|
|
d8591840f3 | ||
|
|
c5affbbf71 | ||
|
|
788f090f27 | ||
|
|
0e3b6652b3 | ||
|
|
d167d59c23 | ||
|
|
f58b4860f7 | ||
|
|
96b7fc0ac0 | ||
|
|
c8ad861590 | ||
|
|
1a1a24cfff | ||
|
|
d43db230fa | ||
|
|
0fb6d930e1 | ||
|
|
2508e8349b | ||
|
|
bd9a44ce7d | ||
|
|
da31d0c6a6 | ||
|
|
aebabb1580 | ||
|
|
d5402718b7 | ||
|
|
fd108ff528 | ||
|
|
22ce1f25d0 |
@@ -3,7 +3,7 @@
|
|||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"dotnet-ef": {
|
"dotnet-ef": {
|
||||||
"version": "9.0.11",
|
"version": "10.0.2",
|
||||||
"commands": [
|
"commands": [
|
||||||
"dotnet-ef"
|
"dotnet-ef"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -379,6 +379,9 @@ dotnet_diagnostic.CA1720.severity = suggestion
|
|||||||
# disable warning CA1724: Type names should not match namespaces
|
# disable warning CA1724: Type names should not match namespaces
|
||||||
dotnet_diagnostic.CA1724.severity = suggestion
|
dotnet_diagnostic.CA1724.severity = suggestion
|
||||||
|
|
||||||
|
# disable warning CA1873: Avoid potentially expensive logging
|
||||||
|
dotnet_diagnostic.CA1873.severity = suggestion
|
||||||
|
|
||||||
# disable warning CA1805: Do not initialize unnecessarily
|
# disable warning CA1805: Do not initialize unnecessarily
|
||||||
dotnet_diagnostic.CA1805.severity = suggestion
|
dotnet_diagnostic.CA1805.severity = suggestion
|
||||||
|
|
||||||
@@ -400,6 +403,10 @@ dotnet_diagnostic.CA1861.severity = suggestion
|
|||||||
# disable warning CA2000: Dispose objects before losing scope
|
# disable warning CA2000: Dispose objects before losing scope
|
||||||
dotnet_diagnostic.CA2000.severity = suggestion
|
dotnet_diagnostic.CA2000.severity = suggestion
|
||||||
|
|
||||||
|
# TODO: Reevaluate when false positives are fixed: https://github.com/dotnet/roslyn-analyzers/issues/7699
|
||||||
|
# disable warning CA2025: Do not pass 'IDisposable' instances into unawaited tasks
|
||||||
|
dotnet_diagnostic.CA2025.severity = suggestion
|
||||||
|
|
||||||
# disable warning CA2253: Named placeholders should not be numeric values
|
# disable warning CA2253: Named placeholders should not be numeric values
|
||||||
dotnet_diagnostic.CA2253.severity = suggestion
|
dotnet_diagnostic.CA2253.severity = suggestion
|
||||||
|
|
||||||
|
|||||||
15
.github/CODEOWNERS
vendored
15
.github/CODEOWNERS
vendored
@@ -1,4 +1,11 @@
|
|||||||
# Joshua must review all changes to deployment and build.sh
|
# Joshua must review all changes to bump_version and any files it touches
|
||||||
.ci/* @joshuaboniface
|
bump_version @joshuaboniface
|
||||||
deployment/* @joshuaboniface
|
.github/ISSUE_TEMPLATE @joshuaboniface
|
||||||
build.sh @joshuaboniface
|
MediaBrowser.Common/MediaBrowser.Common.csproj @joshuaboniface
|
||||||
|
Jellyfin.Data/Jellyfin.Data.csproj @joshuaboniface
|
||||||
|
MediaBrowser.Controller/MediaBrowser.Controller.csproj @joshuaboniface
|
||||||
|
MediaBrowser.Model/MediaBrowser.Model.csproj @joshuaboniface
|
||||||
|
Emby.Naming/Emby.Naming.csproj @joshuaboniface
|
||||||
|
src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @joshuaboniface
|
||||||
|
# Core must approve all changes within the repo config
|
||||||
|
.github/ @jellyfin/core
|
||||||
|
|||||||
17
.github/ISSUE_TEMPLATE/issue report.yml
vendored
17
.github/ISSUE_TEMPLATE/issue report.yml
vendored
@@ -87,7 +87,12 @@ body:
|
|||||||
label: Jellyfin Server version
|
label: Jellyfin Server version
|
||||||
description: What version of Jellyfin are you using?
|
description: What version of Jellyfin are you using?
|
||||||
options:
|
options:
|
||||||
- 10.10.0+
|
- 10.11.5
|
||||||
|
- 10.11.4
|
||||||
|
- 10.11.3
|
||||||
|
- 10.11.2
|
||||||
|
- 10.11.1
|
||||||
|
- 10.11.0
|
||||||
- Master
|
- Master
|
||||||
- Unstable
|
- Unstable
|
||||||
- Older*
|
- Older*
|
||||||
@@ -136,13 +141,14 @@ body:
|
|||||||
- **FFmpeg Version**: [e.g. 5.1.2-Jellyfin]
|
- **FFmpeg Version**: [e.g. 5.1.2-Jellyfin]
|
||||||
- **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
|
- **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
|
||||||
- **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
|
- **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
|
||||||
|
- **CPU Model**: [e.g. AMD Ryzen 5 9600X, Intel Core i7-8565U, etc.]
|
||||||
- **GPU Model**: [e.g. none, UHD630, GTX1050, etc.]
|
- **GPU Model**: [e.g. none, UHD630, GTX1050, etc.]
|
||||||
- **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
|
- **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
|
||||||
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
|
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
|
||||||
- **Base URL**: [e.g. none, yes: /example]
|
- **Base URL**: [e.g. none, yes: /example]
|
||||||
- **Networking**: [e.g. Host, Bridge/NAT]
|
- **Networking**: [e.g. Host, Bridge/NAT]
|
||||||
- **Jellyfin Data Storage**: [e.g. local SATA SSD, local HDD]
|
- **Jellyfin Data Storage & Filesystem**: [e.g. local SATA SSD - ext4, local HDD - NTFS]
|
||||||
- **Media Storage**: [e.g. Local HDD, SMB Share]
|
- **Media Storage & Filesystem**: [e.g. Local HDD - ext4, SMB Share]
|
||||||
- **External Integrations**: [e.g. Jellystat, Jellyseerr]
|
- **External Integrations**: [e.g. Jellystat, Jellyseerr]
|
||||||
value: |
|
value: |
|
||||||
- OS:
|
- OS:
|
||||||
@@ -153,13 +159,14 @@ body:
|
|||||||
- FFmpeg Version:
|
- FFmpeg Version:
|
||||||
- Playback Method:
|
- Playback Method:
|
||||||
- Hardware Acceleration:
|
- Hardware Acceleration:
|
||||||
|
- CPU Model:
|
||||||
- GPU Model:
|
- GPU Model:
|
||||||
- Plugins:
|
- Plugins:
|
||||||
- Reverse Proxy:
|
- Reverse Proxy:
|
||||||
- Base URL:
|
- Base URL:
|
||||||
- Networking:
|
- Networking:
|
||||||
- Jellyfin Data Storage:
|
- Jellyfin Data Storage & Filesystem:
|
||||||
- Media Storage:
|
- Media Storage & Filesystem:
|
||||||
- External Integrations:
|
- External Integrations:
|
||||||
render: markdown
|
render: markdown
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
13
.github/workflows/ci-codeql-analysis.yml
vendored
13
.github/workflows/ci-codeql-analysis.yml
vendored
@@ -21,17 +21,20 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: '9.0.x'
|
dotnet-version: '10.0.x'
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
|
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: +security-extended
|
queries: +security-extended
|
||||||
|
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
|
uses: github/codeql-action/autobuild@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
|
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||||
|
|||||||
18
.github/workflows/ci-compat.yml
vendored
18
.github/workflows/ci-compat.yml
vendored
@@ -17,16 +17,16 @@ jobs:
|
|||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: '9.0.x'
|
dotnet-version: '10.0.x'
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
dotnet build Jellyfin.Server -o ./out
|
dotnet build Jellyfin.Server -o ./out
|
||||||
|
|
||||||
- name: Upload Head
|
- name: Upload Head
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: abi-head
|
name: abi-head
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
@@ -47,9 +47,9 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: '9.0.x'
|
dotnet-version: '10.0.x'
|
||||||
|
|
||||||
- name: Checkout common ancestor
|
- name: Checkout common ancestor
|
||||||
env:
|
env:
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
dotnet build Jellyfin.Server -o ./out
|
dotnet build Jellyfin.Server -o ./out
|
||||||
|
|
||||||
- name: Upload Head
|
- name: Upload Head
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: abi-base
|
name: abi-base
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
@@ -85,13 +85,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download abi-head
|
- name: Download abi-head
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: abi-head
|
name: abi-head
|
||||||
path: abi-head
|
path: abi-head
|
||||||
|
|
||||||
- name: Download abi-base
|
- name: Download abi-base
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: abi-base
|
name: abi-base
|
||||||
path: abi-base
|
path: abi-base
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
{
|
{
|
||||||
echo 'body<<EOF'
|
echo 'body<<EOF'
|
||||||
for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll Jellyfin.MediaEncoding.Keyframes.dll Jellyfin.Database.Implementations.dll; do
|
for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll Jellyfin.MediaEncoding.Keyframes.dll Jellyfin.Database.Implementations.dll; do
|
||||||
COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 )"
|
COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 || true )"
|
||||||
if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then
|
if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then
|
||||||
printf "\n${file}\n${COMPAT_OUTPUT}\n"
|
printf "\n${file}\n${COMPAT_OUTPUT}\n"
|
||||||
fi
|
fi
|
||||||
|
|||||||
99
.github/workflows/ci-openapi.yml
vendored
99
.github/workflows/ci-openapi.yml
vendored
@@ -20,19 +20,21 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: '9.0.x'
|
dotnet-version: '10.0.x'
|
||||||
- name: Generate openapi.json
|
- name: Generate openapi.json
|
||||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||||
|
|
||||||
- name: Upload openapi.json
|
- name: Upload openapi.json
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: openapi-head
|
name: openapi-head
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json
|
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
|
||||||
|
|
||||||
openapi-base:
|
openapi-base:
|
||||||
name: OpenAPI - BASE
|
name: OpenAPI - BASE
|
||||||
@@ -46,6 +48,7 @@ jobs:
|
|||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Checkout common ancestor
|
- name: Checkout common ancestor
|
||||||
env:
|
env:
|
||||||
HEAD_REF: ${{ github.head_ref }}
|
HEAD_REF: ${{ github.head_ref }}
|
||||||
@@ -54,23 +57,25 @@ jobs:
|
|||||||
git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
|
git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
|
||||||
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
|
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
|
||||||
git checkout --progress --force $ANCESTOR_REF
|
git checkout --progress --force $ANCESTOR_REF
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: '9.0.x'
|
dotnet-version: '10.0.x'
|
||||||
- name: Generate openapi.json
|
- name: Generate openapi.json
|
||||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||||
|
|
||||||
- name: Upload openapi.json
|
- name: Upload openapi.json
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: openapi-base
|
name: openapi-base
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json
|
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
|
||||||
|
|
||||||
openapi-diff:
|
openapi-diff:
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
|
pull-requests: write
|
||||||
|
|
||||||
name: OpenAPI - Difference
|
name: OpenAPI - Difference
|
||||||
if: ${{ github.event_name == 'pull_request_target' }}
|
if: ${{ github.event_name == 'pull_request_target' }}
|
||||||
@@ -80,71 +85,27 @@ jobs:
|
|||||||
- openapi-base
|
- openapi-base
|
||||||
steps:
|
steps:
|
||||||
- name: Download openapi-head
|
- name: Download openapi-head
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: openapi-head
|
name: openapi-head
|
||||||
path: openapi-head
|
path: openapi-head
|
||||||
|
|
||||||
- name: Download openapi-base
|
- name: Download openapi-base
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: openapi-base
|
name: openapi-base
|
||||||
path: openapi-base
|
path: openapi-base
|
||||||
- name: Workaround openapi-diff issue
|
|
||||||
run: |
|
|
||||||
sed -i 's/"allOf"/"oneOf"/g' openapi-head/openapi.json
|
|
||||||
sed -i 's/"allOf"/"oneOf"/g' openapi-base/openapi.json
|
|
||||||
- name: Calculate OpenAPI difference
|
|
||||||
uses: docker://openapitools/openapi-diff
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
args: --fail-on-changed --markdown openapi-changes.md openapi-base/openapi.json openapi-head/openapi.json
|
|
||||||
- id: read-diff
|
|
||||||
name: Read openapi-diff output
|
|
||||||
run: |
|
|
||||||
# Read and fix markdown
|
|
||||||
body=$(cat openapi-changes.md)
|
|
||||||
# Write to workflow summary
|
|
||||||
echo "$body" >> $GITHUB_STEP_SUMMARY
|
|
||||||
# Set ApiChanged var
|
|
||||||
if [ "$body" != '' ]; then
|
|
||||||
echo "ApiChanged=1" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "ApiChanged=0" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
# Add header/footer for diff comment
|
|
||||||
echo '<!--openapi-diff-workflow-comment-->' > openapi-changes-reply.md
|
|
||||||
echo "<details>" >> openapi-changes-reply.md
|
|
||||||
echo "<summary>Changes in OpenAPI specification found. Expand to see details.</summary>" >> openapi-changes-reply.md
|
|
||||||
echo "" >> openapi-changes-reply.md
|
|
||||||
echo "$body" >> openapi-changes-reply.md
|
|
||||||
echo "" >> openapi-changes-reply.md
|
|
||||||
echo "</details>" >> openapi-changes-reply.md
|
|
||||||
- name: Find difference comment
|
|
||||||
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
|
|
||||||
id: find-comment
|
|
||||||
with:
|
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
|
||||||
direction: last
|
|
||||||
body-includes: openapi-diff-workflow-comment
|
|
||||||
- name: Reply or edit difference comment (changed)
|
|
||||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
|
||||||
if: ${{ steps.read-diff.outputs.ApiChanged == '1' }}
|
|
||||||
with:
|
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
|
||||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
|
||||||
edit-mode: replace
|
|
||||||
body-path: openapi-changes-reply.md
|
|
||||||
- name: Edit difference comment (unchanged)
|
|
||||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
|
||||||
if: ${{ steps.read-diff.outputs.ApiChanged == '0' && steps.find-comment.outputs.comment-id != '' }}
|
|
||||||
with:
|
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
|
||||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
|
||||||
edit-mode: replace
|
|
||||||
body: |
|
|
||||||
<!--openapi-diff-workflow-comment-->
|
|
||||||
|
|
||||||
No changes to OpenAPI specification found. See history of this comment for previous changes.
|
- name: Detect OpenAPI changes
|
||||||
|
id: openapi-diff
|
||||||
|
uses: jellyfin/openapi-diff-action@9274f6bda9d01ab091942a4a8334baa53692e8a4 # v1.0.0
|
||||||
|
with:
|
||||||
|
old-spec: openapi-base/openapi.json
|
||||||
|
new-spec: openapi-head/openapi.json
|
||||||
|
markdown: openapi-changelog.md
|
||||||
|
add-pr-comment: true
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|
||||||
publish-unstable:
|
publish-unstable:
|
||||||
name: OpenAPI - Publish Unstable Spec
|
name: OpenAPI - Publish Unstable Spec
|
||||||
@@ -158,7 +119,7 @@ jobs:
|
|||||||
run: |-
|
run: |-
|
||||||
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
|
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
|
||||||
- name: Download openapi-head
|
- name: Download openapi-head
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: openapi-head
|
name: openapi-head
|
||||||
path: openapi-head
|
path: openapi-head
|
||||||
@@ -178,7 +139,6 @@ jobs:
|
|||||||
username: "${{ secrets.REPO_USER }}"
|
username: "${{ secrets.REPO_USER }}"
|
||||||
key: "${{ secrets.REPO_KEY }}"
|
key: "${{ secrets.REPO_KEY }}"
|
||||||
debug: false
|
debug: false
|
||||||
script_stop: false
|
|
||||||
script: |
|
script: |
|
||||||
if ! test -d /run/workflows; then
|
if ! test -d /run/workflows; then
|
||||||
sudo mkdir -p /run/workflows
|
sudo mkdir -p /run/workflows
|
||||||
@@ -220,7 +180,7 @@ jobs:
|
|||||||
run: |-
|
run: |-
|
||||||
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||||
- name: Download openapi-head
|
- name: Download openapi-head
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: openapi-head
|
name: openapi-head
|
||||||
path: openapi-head
|
path: openapi-head
|
||||||
@@ -240,7 +200,6 @@ jobs:
|
|||||||
username: "${{ secrets.REPO_USER }}"
|
username: "${{ secrets.REPO_USER }}"
|
||||||
key: "${{ secrets.REPO_KEY }}"
|
key: "${{ secrets.REPO_KEY }}"
|
||||||
debug: false
|
debug: false
|
||||||
script_stop: false
|
|
||||||
script: |
|
script: |
|
||||||
if ! test -d /run/workflows; then
|
if ! test -d /run/workflows; then
|
||||||
sudo mkdir -p /run/workflows
|
sudo mkdir -p /run/workflows
|
||||||
|
|||||||
4
.github/workflows/ci-tests.yml
vendored
4
.github/workflows/ci-tests.yml
vendored
@@ -9,7 +9,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SDK_VERSION: "9.0.x"
|
SDK_VERSION: "10.0.x"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
run-tests:
|
run-tests:
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
- uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: ${{ env.SDK_VERSION }}
|
dotnet-version: ${{ env.SDK_VERSION }}
|
||||||
|
|
||||||
|
|||||||
3
.github/workflows/commands.yml
vendored
3
.github/workflows/commands.yml
vendored
@@ -43,13 +43,16 @@ jobs:
|
|||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
repository: jellyfin/jellyfin-triage-script
|
repository: jellyfin/jellyfin-triage-script
|
||||||
|
|
||||||
- name: install python
|
- name: install python
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: '3.14'
|
python-version: '3.14'
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
|
|
||||||
- name: install python packages
|
- name: install python packages
|
||||||
run: pip install -r rename/requirements.txt
|
run: pip install -r rename/requirements.txt
|
||||||
|
|
||||||
- name: run rename script
|
- name: run rename script
|
||||||
run: python3 rename.py
|
run: python3 rename.py
|
||||||
working-directory: ./rename
|
working-directory: ./rename
|
||||||
|
|||||||
3
.github/workflows/issue-template-check.yml
vendored
3
.github/workflows/issue-template-check.yml
vendored
@@ -13,13 +13,16 @@ jobs:
|
|||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
repository: jellyfin/jellyfin-triage-script
|
repository: jellyfin/jellyfin-triage-script
|
||||||
|
|
||||||
- name: install python
|
- name: install python
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: '3.14'
|
python-version: '3.14'
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
|
|
||||||
- name: install python packages
|
- name: install python packages
|
||||||
run: pip install -r main-repo-triage/requirements.txt
|
run: pip install -r main-repo-triage/requirements.txt
|
||||||
|
|
||||||
- name: check and comment issue
|
- name: check and comment issue
|
||||||
working-directory: ./main-repo-triage
|
working-directory: ./main-repo-triage
|
||||||
run: python3 single_issue_gha.py
|
run: python3 single_issue_gha.py
|
||||||
|
|||||||
1
.github/workflows/project-automation.yml
vendored
1
.github/workflows/project-automation.yml
vendored
@@ -21,6 +21,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
project: Current Release
|
project: Current Release
|
||||||
action: delete
|
action: delete
|
||||||
|
column: In progress
|
||||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
- name: Add to 'Release Next' project
|
- name: Add to 'Release Next' project
|
||||||
|
|||||||
6
.vscode/launch.json
vendored
6
.vscode/launch.json
vendored
@@ -6,7 +6,7 @@
|
|||||||
"type": "coreclr",
|
"type": "coreclr",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "build",
|
"preLaunchTask": "build",
|
||||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
|
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net10.0/jellyfin.dll",
|
||||||
"args": [],
|
"args": [],
|
||||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||||
"console": "internalConsole",
|
"console": "internalConsole",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"type": "coreclr",
|
"type": "coreclr",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "build",
|
"preLaunchTask": "build",
|
||||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
|
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net10.0/jellyfin.dll",
|
||||||
"args": ["--nowebclient"],
|
"args": ["--nowebclient"],
|
||||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||||
"console": "internalConsole",
|
"console": "internalConsole",
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"type": "coreclr",
|
"type": "coreclr",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "build",
|
"preLaunchTask": "build",
|
||||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
|
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net10.0/jellyfin.dll",
|
||||||
"args": ["--nowebclient", "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"],
|
"args": ["--nowebclient", "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"],
|
||||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||||
"console": "internalConsole",
|
"console": "internalConsole",
|
||||||
|
|||||||
@@ -206,7 +206,11 @@
|
|||||||
- [theshoeshiner](https://github.com/theshoeshiner)
|
- [theshoeshiner](https://github.com/theshoeshiner)
|
||||||
- [TokerX](https://github.com/TokerX)
|
- [TokerX](https://github.com/TokerX)
|
||||||
- [GeneMarks](https://github.com/GeneMarks)
|
- [GeneMarks](https://github.com/GeneMarks)
|
||||||
|
- [Kirill Nikiforov](https://github.com/allmazz)
|
||||||
|
- [bjorntp](https://github.com/bjorntp)
|
||||||
- [martenumberto](https://github.com/martenumberto)
|
- [martenumberto](https://github.com/martenumberto)
|
||||||
|
- [ZeusCraft10](https://github.com/ZeusCraft10)
|
||||||
|
- [MarcoCoreDuo](https://github.com/MarcoCoreDuo)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
|
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
|
||||||
<ItemGroup Label="Package Dependencies">
|
<ItemGroup Label="Package Dependencies">
|
||||||
<PackageVersion Include="AsyncKeyedLock" Version="7.1.8" />
|
<PackageVersion Include="AsyncKeyedLock" Version="8.0.0" />
|
||||||
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
|
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
|
||||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
||||||
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
||||||
@@ -25,33 +25,28 @@
|
|||||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||||
<PackageVersion Include="libse" Version="4.0.12" />
|
<PackageVersion Include="libse" Version="4.0.12" />
|
||||||
<PackageVersion Include="LrcParser" Version="2025.623.0" />
|
<PackageVersion Include="LrcParser" Version="2025.623.0" />
|
||||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="7.0.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.0.0" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
||||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.11" />
|
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.11" />
|
|
||||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.11" />
|
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.11" />
|
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.11" />
|
|
||||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.11" />
|
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||||
<PackageVersion Include="MimeTypes" Version="2.5.2" />
|
<PackageVersion Include="MimeTypes" Version="2.5.2" />
|
||||||
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
|
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
|
||||||
@@ -63,10 +58,10 @@
|
|||||||
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
|
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
|
||||||
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
||||||
<PackageVersion Include="Polly" Version="8.6.5" />
|
<PackageVersion Include="Polly" Version="8.6.5" />
|
||||||
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
|
<PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||||
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
|
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
|
||||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
<PackageVersion Include="Serilog.Settings.Configuration" Version="10.0.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
|
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
|
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
|
||||||
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
|
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||||
@@ -74,26 +69,22 @@
|
|||||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||||
<PackageVersion Include="SharpFuzz" Version="2.2.0" />
|
<PackageVersion Include="SharpFuzz" Version="2.2.0" />
|
||||||
<!-- Pinned to 3.116.1 because https://github.com/jellyfin/jellyfin/pull/14255 -->
|
<!-- Pinned to 3.116.1 because https://github.com/jellyfin/jellyfin/pull/14255 -->
|
||||||
<PackageVersion Include="SkiaSharp" Version="3.116.1" />
|
<PackageVersion Include="SkiaSharp" Version="[3.116.1]" />
|
||||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.116.1" />
|
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="[3.116.1]" />
|
||||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" />
|
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="[3.116.1]" />
|
||||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||||
<PackageVersion Include="Svg.Skia" Version="3.2.1" />
|
<PackageVersion Include="Svg.Skia" Version="3.2.1" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
|
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.9.0" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.3.2" />
|
||||||
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
<PackageVersion Include="System.Text.Json" Version="10.0.2" />
|
||||||
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
|
|
||||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.11" />
|
|
||||||
<PackageVersion Include="System.Text.Json" Version="9.0.11" />
|
|
||||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.11" />
|
|
||||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||||
<PackageVersion Include="z440.atl.core" Version="7.9.0" />
|
<PackageVersion Include="z440.atl.core" Version="7.10.0" />
|
||||||
<PackageVersion Include="TMDbLib" Version="2.3.0" />
|
<PackageVersion Include="TMDbLib" Version="2.3.0" />
|
||||||
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
|
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
|
||||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
|
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
|
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.61" />
|
||||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
75
Emby.Naming/Book/BookFileNameParser.cs
Normal file
75
Emby.Naming/Book/BookFileNameParser.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Emby.Naming.Book
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Helper class to retrieve basic metadata from a book filename.
|
||||||
|
/// </summary>
|
||||||
|
public static class BookFileNameParser
|
||||||
|
{
|
||||||
|
private const string NameMatchGroup = "name";
|
||||||
|
private const string IndexMatchGroup = "index";
|
||||||
|
private const string YearMatchGroup = "year";
|
||||||
|
private const string SeriesNameMatchGroup = "seriesName";
|
||||||
|
|
||||||
|
private static readonly Regex[] _nameMatches =
|
||||||
|
[
|
||||||
|
// seriesName (seriesYear) #index (of count) (year) where only seriesName and index are required
|
||||||
|
new Regex(@"^(?<seriesName>.+?)((\s\((?<seriesYear>[0-9]{4})\))?)\s#(?<index>[0-9]+)((\s\(of\s(?<count>[0-9]+)\))?)((\s\((?<year>[0-9]{4})\))?)$"),
|
||||||
|
new Regex(@"^(?<name>.+?)\s\((?<seriesName>.+?),\s#(?<index>[0-9]+)\)((\s\((?<year>[0-9]{4})\))?)$"),
|
||||||
|
new Regex(@"^(?<index>[0-9]+)\s\-\s(?<name>.+?)((\s\((?<year>[0-9]{4})\))?)$"),
|
||||||
|
new Regex(@"(?<name>.*)\((?<year>[0-9]{4})\)"),
|
||||||
|
// last resort matches the whole string as the name
|
||||||
|
new Regex(@"(?<name>.*)")
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a filename name to retrieve the book name, series name, index, and year.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">Book filename to parse for information.</param>
|
||||||
|
/// <returns>Returns <see cref="BookFileNameParserResult"/> object.</returns>
|
||||||
|
public static BookFileNameParserResult Parse(string? name)
|
||||||
|
{
|
||||||
|
var result = new BookFileNameParserResult();
|
||||||
|
|
||||||
|
if (name == null)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var regex in _nameMatches)
|
||||||
|
{
|
||||||
|
var match = regex.Match(name);
|
||||||
|
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match.Groups.TryGetValue(NameMatchGroup, out Group? nameGroup) && nameGroup.Success)
|
||||||
|
{
|
||||||
|
result.Name = nameGroup.Value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match.Groups.TryGetValue(IndexMatchGroup, out Group? indexGroup) && indexGroup.Success && int.TryParse(indexGroup.Value, out var index))
|
||||||
|
{
|
||||||
|
result.Index = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match.Groups.TryGetValue(YearMatchGroup, out Group? yearGroup) && yearGroup.Success && int.TryParse(yearGroup.Value, out var year))
|
||||||
|
{
|
||||||
|
result.Year = year;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match.Groups.TryGetValue(SeriesNameMatchGroup, out Group? seriesGroup) && seriesGroup.Success)
|
||||||
|
{
|
||||||
|
result.SeriesName = seriesGroup.Value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Emby.Naming/Book/BookFileNameParserResult.cs
Normal file
41
Emby.Naming/Book/BookFileNameParserResult.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Emby.Naming.Book
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Data object used to pass metadata parsed from a book filename.
|
||||||
|
/// </summary>
|
||||||
|
public class BookFileNameParserResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="BookFileNameParserResult"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public BookFileNameParserResult()
|
||||||
|
{
|
||||||
|
Name = null;
|
||||||
|
Index = null;
|
||||||
|
Year = null;
|
||||||
|
SeriesName = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of the book.
|
||||||
|
/// </summary>
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the book index.
|
||||||
|
/// </summary>
|
||||||
|
public int? Index { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the publication year.
|
||||||
|
/// </summary>
|
||||||
|
public int? Year { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the series name.
|
||||||
|
/// </summary>
|
||||||
|
public string? SeriesName { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Naming</PackageId>
|
<PackageId>Jellyfin.Naming</PackageId>
|
||||||
<VersionPrefix>10.11.4</VersionPrefix>
|
<VersionPrefix>10.12.0</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ namespace Emby.Naming.TV
|
|||||||
[GeneratedRegex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))")]
|
[GeneratedRegex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))")]
|
||||||
private static partial Regex SeriesNameRegex();
|
private static partial Regex SeriesNameRegex();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Regex that matches titles with year in parentheses. Captures the title (which may be
|
||||||
|
/// numeric) before the year, i.e. turns "1923 (2022)" into "1923".
|
||||||
|
/// </summary>
|
||||||
|
[GeneratedRegex(@"(?<title>.+?)\s*\(\d{4}\)")]
|
||||||
|
private static partial Regex TitleWithYearRegex();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolve information about series from path.
|
/// Resolve information about series from path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -27,6 +34,20 @@ namespace Emby.Naming.TV
|
|||||||
{
|
{
|
||||||
string seriesName = Path.GetFileName(path);
|
string seriesName = Path.GetFileName(path);
|
||||||
|
|
||||||
|
// First check if the filename matches a title with year pattern (handles numeric titles)
|
||||||
|
if (!string.IsNullOrEmpty(seriesName))
|
||||||
|
{
|
||||||
|
var titleWithYearMatch = TitleWithYearRegex().Match(seriesName);
|
||||||
|
if (titleWithYearMatch.Success)
|
||||||
|
{
|
||||||
|
seriesName = titleWithYearMatch.Groups["title"].Value.Trim();
|
||||||
|
return new SeriesInfo(path)
|
||||||
|
{
|
||||||
|
Name = seriesName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SeriesPathParserResult result = SeriesPathParser.Parse(options, path);
|
SeriesPathParserResult result = SeriesPathParser.Parse(options, path);
|
||||||
if (result.Success)
|
if (result.Success)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
@@ -136,19 +137,27 @@ namespace Emby.Naming.Video
|
|||||||
|
|
||||||
if (videos.Count > 1)
|
if (videos.Count > 1)
|
||||||
{
|
{
|
||||||
var groups = videos.GroupBy(x => ResolutionRegex().IsMatch(x.Files[0].FileNameWithoutExtension)).ToList();
|
var groups = videos
|
||||||
|
.Select(x => (filename: x.Files[0].FileNameWithoutExtension.ToString(), value: x))
|
||||||
|
.Select(x => (x.filename, resolutionMatch: ResolutionRegex().Match(x.filename), x.value))
|
||||||
|
.GroupBy(x => x.resolutionMatch.Success)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
videos.Clear();
|
videos.Clear();
|
||||||
|
|
||||||
|
StringComparer comparer = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering);
|
||||||
foreach (var group in groups)
|
foreach (var group in groups)
|
||||||
{
|
{
|
||||||
if (group.Key)
|
if (group.Key)
|
||||||
{
|
{
|
||||||
videos.InsertRange(0, group
|
videos.InsertRange(0, group
|
||||||
.OrderByDescending(x => ResolutionRegex().Match(x.Files[0].FileNameWithoutExtension.ToString()).Value, new AlphanumericComparator())
|
.OrderByDescending(x => x.resolutionMatch.Value, comparer)
|
||||||
.ThenBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
|
.ThenBy(x => x.filename, comparer)
|
||||||
|
.Select(x => x.value));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
videos.AddRange(group.OrderBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
|
videos.AddRange(group.OrderBy(x => x.filename, comparer).Select(x => x.value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -39,22 +39,24 @@ namespace Emby.Server.Implementations.Cryptography
|
|||||||
{
|
{
|
||||||
if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal))
|
if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
|
var iterations = GetIterationsParameter(hash);
|
||||||
return hash.Hash.SequenceEqual(
|
return hash.Hash.SequenceEqual(
|
||||||
Rfc2898DeriveBytes.Pbkdf2(
|
Rfc2898DeriveBytes.Pbkdf2(
|
||||||
password,
|
password,
|
||||||
hash.Salt,
|
hash.Salt,
|
||||||
int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
|
iterations,
|
||||||
HashAlgorithmName.SHA1,
|
HashAlgorithmName.SHA1,
|
||||||
32));
|
32));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal))
|
if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
|
var iterations = GetIterationsParameter(hash);
|
||||||
return hash.Hash.SequenceEqual(
|
return hash.Hash.SequenceEqual(
|
||||||
Rfc2898DeriveBytes.Pbkdf2(
|
Rfc2898DeriveBytes.Pbkdf2(
|
||||||
password,
|
password,
|
||||||
hash.Salt,
|
hash.Salt,
|
||||||
int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
|
iterations,
|
||||||
HashAlgorithmName.SHA512,
|
HashAlgorithmName.SHA512,
|
||||||
DefaultOutputLength));
|
DefaultOutputLength));
|
||||||
}
|
}
|
||||||
@@ -62,6 +64,27 @@ namespace Emby.Server.Implementations.Cryptography
|
|||||||
throw new NotSupportedException($"Can't verify hash with id: {hash.Id}");
|
throw new NotSupportedException($"Can't verify hash with id: {hash.Id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts and validates the iterations parameter from a password hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hash">The password hash containing parameters.</param>
|
||||||
|
/// <returns>The number of iterations.</returns>
|
||||||
|
/// <exception cref="FormatException">Thrown when iterations parameter is missing or invalid.</exception>
|
||||||
|
private static int GetIterationsParameter(PasswordHash hash)
|
||||||
|
{
|
||||||
|
if (!hash.Parameters.TryGetValue("iterations", out var iterationsStr))
|
||||||
|
{
|
||||||
|
throw new FormatException($"Password hash with id '{hash.Id}' is missing required 'iterations' parameter.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!int.TryParse(iterationsStr, CultureInfo.InvariantCulture, out var iterations))
|
||||||
|
{
|
||||||
|
throw new FormatException($"Password hash with id '{hash.Id}' has invalid 'iterations' parameter: '{iterationsStr}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return iterations;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public byte[] GenerateSalt()
|
public byte[] GenerateSalt()
|
||||||
=> GenerateSalt(DefaultSaltLength);
|
=> GenerateSalt(DefaultSaltLength);
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
<PackageReference Include="Microsoft.Data.Sqlite" />
|
<PackageReference Include="Microsoft.Data.Sqlite" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||||
<PackageReference Include="prometheus-net.DotNetRuntime" />
|
<PackageReference Include="prometheus-net.DotNetRuntime" />
|
||||||
@@ -39,7 +38,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -352,6 +352,12 @@ namespace Emby.Server.Implementations.IO
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var fileInfo = _fileSystem.GetFileSystemInfo(path);
|
||||||
|
if (DotIgnoreIgnoreRule.IsIgnored(fileInfo, null))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too
|
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too
|
||||||
foreach (var i in _tempIgnoredPaths.Keys)
|
foreach (var i in _tempIgnoredPaths.Keys)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -98,5 +98,11 @@ namespace Emby.Server.Implementations.Images
|
|||||||
|
|
||||||
return base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex);
|
return base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override bool HasChangedByDate(BaseItem item, ItemImageInfo image)
|
||||||
|
{
|
||||||
|
var age = DateTime.UtcNow - image.DateModified;
|
||||||
|
return age.TotalDays > 7;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
// Unix hidden files
|
// Unix hidden files
|
||||||
"**/.*",
|
"**/.*",
|
||||||
|
"**/.*/**",
|
||||||
|
|
||||||
// Mac - if you ever remove the above.
|
// Mac - if you ever remove the above.
|
||||||
// "**/._*",
|
// "**/._*",
|
||||||
|
|||||||
@@ -2144,7 +2144,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
item.ValidateImages();
|
item.ValidateImages();
|
||||||
|
|
||||||
_itemRepository.SaveImages(item);
|
await _itemRepository.SaveImagesAsync(item).ConfigureAwait(false);
|
||||||
|
|
||||||
RegisterItem(item);
|
RegisterItem(item);
|
||||||
}
|
}
|
||||||
@@ -2202,6 +2202,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||||
=> UpdateItemsAsync([item], parent, updateReason, cancellationToken);
|
=> UpdateItemsAsync([item], parent, updateReason, cancellationToken);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _itemRepository.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
|
public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
|
||||||
{
|
{
|
||||||
if (item.IsFileProtocol)
|
if (item.IsFileProtocol)
|
||||||
@@ -3195,19 +3201,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
|
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
|
||||||
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
|
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
|
||||||
|
|
||||||
var shortcutFilename = Path.GetFileNameWithoutExtension(path);
|
CreateShortcut(virtualFolderPath, pathInfo);
|
||||||
|
|
||||||
var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
|
|
||||||
|
|
||||||
while (File.Exists(lnk))
|
|
||||||
{
|
|
||||||
shortcutFilename += "1";
|
|
||||||
lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
|
|
||||||
}
|
|
||||||
|
|
||||||
_fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
|
|
||||||
|
|
||||||
RemoveContentTypeOverrides(path);
|
|
||||||
|
|
||||||
if (saveLibraryOptions)
|
if (saveLibraryOptions)
|
||||||
{
|
{
|
||||||
@@ -3372,5 +3366,24 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
return item is UserRootFolder || item.IsVisibleStandalone(user);
|
return item is UserRootFolder || item.IsVisibleStandalone(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo)
|
||||||
|
{
|
||||||
|
var path = pathInfo.Path;
|
||||||
|
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
|
||||||
|
|
||||||
|
var shortcutFilename = Path.GetFileNameWithoutExtension(path);
|
||||||
|
|
||||||
|
var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
|
||||||
|
|
||||||
|
while (File.Exists(lnk))
|
||||||
|
{
|
||||||
|
shortcutFilename += "1";
|
||||||
|
lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
|
||||||
|
}
|
||||||
|
|
||||||
|
_fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
|
||||||
|
RemoveContentTypeOverrides(path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Emby.Naming.Book;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Resolvers;
|
using MediaBrowser.Controller.Resolvers;
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library.Resolvers.Books
|
namespace Emby.Server.Implementations.Library.Resolvers.Books
|
||||||
{
|
{
|
||||||
@@ -35,17 +35,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
|||||||
|
|
||||||
var extension = Path.GetExtension(args.Path.AsSpan());
|
var extension = Path.GetExtension(args.Path.AsSpan());
|
||||||
|
|
||||||
if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
if (!_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// It's a book
|
return null;
|
||||||
return new Book
|
|
||||||
{
|
|
||||||
Path = args.Path,
|
|
||||||
IsInMixedFolder = true
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
var result = BookFileNameParser.Parse(Path.GetFileNameWithoutExtension(args.Path));
|
||||||
|
|
||||||
|
return new Book
|
||||||
|
{
|
||||||
|
Path = args.Path,
|
||||||
|
Name = result.Name ?? string.Empty,
|
||||||
|
IndexNumber = result.Index,
|
||||||
|
ProductionYear = result.Year,
|
||||||
|
SeriesName = result.SeriesName ?? Path.GetFileName(Path.GetDirectoryName(args.Path)),
|
||||||
|
IsInMixedFolder = true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private Book GetBook(ItemResolveArgs args)
|
private Book GetBook(ItemResolveArgs args)
|
||||||
@@ -59,15 +64,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
|||||||
StringComparison.OrdinalIgnoreCase);
|
StringComparison.OrdinalIgnoreCase);
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
// Don't return a Book if there is more (or less) than one document in the directory
|
// directory is only considered a book when it contains exactly one supported file
|
||||||
|
// other library structures with multiple books to a directory will get picked up as individual files
|
||||||
if (bookFiles.Count != 1)
|
if (bookFiles.Count != 1)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var result = BookFileNameParser.Parse(Path.GetFileName(args.Path));
|
||||||
|
|
||||||
return new Book
|
return new Book
|
||||||
{
|
{
|
||||||
Path = bookFiles[0].FullName
|
Path = bookFiles[0].FullName,
|
||||||
|
Name = result.Name ?? string.Empty,
|
||||||
|
IndexNumber = result.Index,
|
||||||
|
ProductionYear = result.Year,
|
||||||
|
SeriesName = result.SeriesName ?? string.Empty,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
results = results.GetRange(query.StartIndex.Value, totalRecordCount - query.StartIndex.Value);
|
results = results.GetRange(query.StartIndex.Value, totalRecordCount - query.StartIndex.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.Limit.HasValue)
|
if (query.Limit.HasValue && query.Limit.Value > 0)
|
||||||
{
|
{
|
||||||
results = results.GetRange(0, Math.Min(query.Limit.Value, results.Count));
|
results = results.GetRange(0, Math.Min(query.Limit.Value, results.Count));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
"Albums": "ألبومات",
|
"Albums": "ألبومات",
|
||||||
"AppDeviceValues": "تطبيق: {0}, جهاز: {1}",
|
"AppDeviceValues": "تطبيق: {0}, جهاز: {1}",
|
||||||
"Application": "تطبيق",
|
"Application": "تطبيق",
|
||||||
"Artists": "الفنانون",
|
"Artists": "فنانون",
|
||||||
"AuthenticationSucceededWithUserName": "نجحت عملية التوثيق بـ {0}",
|
"AuthenticationSucceededWithUserName": "نجحت عملية التوثيق بـ {0}",
|
||||||
"Books": "الكتب",
|
"Books": "الكتب",
|
||||||
"CameraImageUploadedFrom": "رُفعت صورة الكاميرا الجديدة من {0}",
|
"CameraImageUploadedFrom": "رُفعت صورة الكاميرا الجديدة من {0}",
|
||||||
"Channels": "القنوات",
|
"Channels": "القنوات",
|
||||||
"ChapterNameValue": "الفصل {0}",
|
"ChapterNameValue": "الفصل {0}",
|
||||||
"Collections": "المجموعات",
|
"Collections": "مجموعات",
|
||||||
"DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
|
"DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
|
||||||
"DeviceOnlineWithName": "{0} متصل",
|
"DeviceOnlineWithName": "{0} متصل",
|
||||||
"FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فاشلة من {0}",
|
"FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فاشلة من {0}",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"Folders": "المجلدات",
|
"Folders": "المجلدات",
|
||||||
"Genres": "التصنيفات",
|
"Genres": "التصنيفات",
|
||||||
"HeaderAlbumArtists": "فناني الألبوم",
|
"HeaderAlbumArtists": "فناني الألبوم",
|
||||||
"HeaderContinueWatching": "إستئناف المشاهدة",
|
"HeaderContinueWatching": "متابعة المشاهدة",
|
||||||
"HeaderFavoriteAlbums": "الألبومات المفضلة",
|
"HeaderFavoriteAlbums": "الألبومات المفضلة",
|
||||||
"HeaderFavoriteArtists": "الفنانون المفضلون",
|
"HeaderFavoriteArtists": "الفنانون المفضلون",
|
||||||
"HeaderFavoriteEpisodes": "الحلقات المفضلة",
|
"HeaderFavoriteEpisodes": "الحلقات المفضلة",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"Collections": "Калекцыі",
|
"Collections": "Калекцыі",
|
||||||
"Default": "Па змаўчанні",
|
"Default": "Па змаўчанні",
|
||||||
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
|
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
|
||||||
"Folders": "Тэчкі",
|
"Folders": "Папкі",
|
||||||
"Favorites": "Абранае",
|
"Favorites": "Абранае",
|
||||||
"External": "Знешні",
|
"External": "Знешні",
|
||||||
"Genres": "Жанры",
|
"Genres": "Жанры",
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
"ServerNameNeedsToBeRestarted": "{0} патрабуе перазапуску",
|
"ServerNameNeedsToBeRestarted": "{0} патрабуе перазапуску",
|
||||||
"Shows": "Шоу",
|
"Shows": "Шоу",
|
||||||
"StartupEmbyServerIsLoading": "Jellyfin Server загружаецца. Калі ласка, паўтарыце спробу крыху пазней.",
|
"StartupEmbyServerIsLoading": "Jellyfin Server загружаецца. Калі ласка, паўтарыце спробу крыху пазней.",
|
||||||
"SubtitleDownloadFailureFromForItem": "Не атрымалася спампаваць субтытры з {0} для {1}",
|
"SubtitleDownloadFailureFromForItem": "Субцітры для {1} не ўдалося спампаваць з {0}",
|
||||||
"TvShows": "Тэлепраграма",
|
"TvShows": "Тэлепраграма",
|
||||||
"Undefined": "Нявызначана",
|
"Undefined": "Нявызначана",
|
||||||
"UserLockedOutWithName": "Карыстальнік {0} быў заблакіраваны",
|
"UserLockedOutWithName": "Карыстальнік {0} быў заблакіраваны",
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
"UserStartedPlayingItemWithValues": "{0} прайграваецца {1} на {2}",
|
"UserStartedPlayingItemWithValues": "{0} прайграваецца {1} на {2}",
|
||||||
"UserStoppedPlayingItemWithValues": "{0} скончыў прайграванне {1} на {2}",
|
"UserStoppedPlayingItemWithValues": "{0} скончыў прайграванне {1} на {2}",
|
||||||
"ValueHasBeenAddedToLibrary": "{0} быў дададзены ў вашу медыятэку",
|
"ValueHasBeenAddedToLibrary": "{0} быў дададзены ў вашу медыятэку",
|
||||||
"ValueSpecialEpisodeName": "Спецэпізод - {0}",
|
"ValueSpecialEpisodeName": "Спецвыпуск - {0}",
|
||||||
"VersionNumber": "Версія {0}",
|
"VersionNumber": "Версія {0}",
|
||||||
"TasksMaintenanceCategory": "Абслугоўванне",
|
"TasksMaintenanceCategory": "Абслугоўванне",
|
||||||
"TasksLibraryCategory": "Бібліятэка",
|
"TasksLibraryCategory": "Бібліятэка",
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
"TaskCleanCacheDescription": "Выдаляе файлы кэша, якія больш не патрэбныя сістэме.",
|
"TaskCleanCacheDescription": "Выдаляе файлы кэша, якія больш не патрэбныя сістэме.",
|
||||||
"TaskRefreshChapterImages": "Вынуць выявы раздзелаў",
|
"TaskRefreshChapterImages": "Вынуць выявы раздзелаў",
|
||||||
"TaskRefreshLibrary": "Сканаваць бібліятэку",
|
"TaskRefreshLibrary": "Сканаваць бібліятэку",
|
||||||
"TaskRefreshLibraryDescription": "Скануе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метададзеныя.",
|
"TaskRefreshLibraryDescription": "Сканіруе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метаданыя.",
|
||||||
"TaskCleanLogs": "Ачысціць журнал",
|
"TaskCleanLogs": "Ачысціць журнал",
|
||||||
"TaskRefreshPeople": "Абнавіць выканаўцаў",
|
"TaskRefreshPeople": "Абнавіць выканаўцаў",
|
||||||
"TaskRefreshPeopleDescription": "Абнаўленне метаданых для акцёраў і рэжысёраў у вашай медыятэцы.",
|
"TaskRefreshPeopleDescription": "Абнаўленне метаданых для акцёраў і рэжысёраў у вашай медыятэцы.",
|
||||||
@@ -137,5 +137,5 @@
|
|||||||
"TaskExtractMediaSegments": "Сканіраванне медыя-сегмента",
|
"TaskExtractMediaSegments": "Сканіраванне медыя-сегмента",
|
||||||
"TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay",
|
"TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay",
|
||||||
"CleanupUserDataTask": "Задача па ачыстцы дадзеных карыстальніка",
|
"CleanupUserDataTask": "Задача па ачыстцы дадзеных карыстальніка",
|
||||||
"CleanupUserDataTaskDescription": "Ачысьціць усе дадзеныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён."
|
"CleanupUserDataTaskDescription": "Ачышчае ўсе даныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"CameraImageUploadedFrom": "Mae delwedd camera newydd wedi'i lanlwytho o {0}",
|
"CameraImageUploadedFrom": "Mae delwedd camera newydd wedi'i lanlwytho o {0}",
|
||||||
"Books": "Llyfrau",
|
"Books": "Llyfrau",
|
||||||
"AuthenticationSucceededWithUserName": "{0} wedi’i ddilysu’n llwyddiannus",
|
"AuthenticationSucceededWithUserName": "{0} wedi’i ddilysu’n llwyddiannus",
|
||||||
"Artists": "Artistiaid",
|
"Artists": "Crewyr",
|
||||||
"AppDeviceValues": "Ap: {0}, Dyfais: {1}",
|
"AppDeviceValues": "Ap: {0}, Dyfais: {1}",
|
||||||
"Albums": "Albwmau",
|
"Albums": "Albwmau",
|
||||||
"Genres": "Genres",
|
"Genres": "Genres",
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
"NotificationOptionAudioPlayback": "Dechreuwyd chwarae sain",
|
"NotificationOptionAudioPlayback": "Dechreuwyd chwarae sain",
|
||||||
"MessageServerConfigurationUpdated": "Mae gosodiadau gweinydd wedi'i ddiweddaru",
|
"MessageServerConfigurationUpdated": "Mae gosodiadau gweinydd wedi'i ddiweddaru",
|
||||||
"MessageNamedServerConfigurationUpdatedWithValue": "Mae adran gosodiadau gweinydd {0} wedi'i diweddaru",
|
"MessageNamedServerConfigurationUpdatedWithValue": "Mae adran gosodiadau gweinydd {0} wedi'i diweddaru",
|
||||||
"FailedLoginAttemptWithUserName": "Cais mewngofnodi wedi methu gan {0}",
|
"FailedLoginAttemptWithUserName": "Cais mewngofnodi wedi methu o {0}",
|
||||||
"ValueHasBeenAddedToLibrary": "{0} wedi'i hychwanegu at eich llyfrgell gyfryngau",
|
"ValueHasBeenAddedToLibrary": "{0} wedi'i hychwanegu at eich llyfrgell gyfryngau",
|
||||||
"UserStoppedPlayingItemWithValues": "{0} wedi gorffen chwarae {1} ar {2}",
|
"UserStoppedPlayingItemWithValues": "{0} wedi gorffen chwarae {1} ar {2}",
|
||||||
"UserStartedPlayingItemWithValues": "{0} yn chwarae {1} ar {2}",
|
"UserStartedPlayingItemWithValues": "{0} yn chwarae {1} ar {2}",
|
||||||
@@ -123,5 +123,14 @@
|
|||||||
"TaskRefreshChapterImages": "Echdynnu Lluniau Pennod",
|
"TaskRefreshChapterImages": "Echdynnu Lluniau Pennod",
|
||||||
"TaskCleanCacheDescription": "Dileu ffeiliau cache nad oes eu hangen ar y system mwyach.",
|
"TaskCleanCacheDescription": "Dileu ffeiliau cache nad oes eu hangen ar y system mwyach.",
|
||||||
"TaskCleanCache": "Gwaghau Ffolder Cache",
|
"TaskCleanCache": "Gwaghau Ffolder Cache",
|
||||||
"HearingImpaired": "Nam ar y clyw"
|
"HearingImpaired": "Nam ar y clyw",
|
||||||
|
"TaskAudioNormalization": "Gwastatau Sain",
|
||||||
|
"TaskAudioNormalizationDescription": "Yn sganio ffeiliau am ddata gwastatau sain.",
|
||||||
|
"TaskRefreshTrickplayImages": "Creuwch lluniau Trickplay",
|
||||||
|
"TaskRefreshTrickplayImagesDescription": "Creu rhagolygon Trickplay ar gyfer fideos mewn llyfrgelloedd gweithredol.",
|
||||||
|
"TaskDownloadMissingLyrics": "Lawrlwytho geiriau coll",
|
||||||
|
"TaskDownloadMissingLyricsDescription": "Lawrlwytho geiriau caneuon",
|
||||||
|
"TaskCleanCollectionsAndPlaylists": "Glanhau casgliadau a rhestrau chwarae",
|
||||||
|
"TaskCleanCollectionsAndPlaylistsDescription": "Dileu eitemau o gasgliadau a rhestrau chwarae sydd ddim yn bodoli bellach.",
|
||||||
|
"TaskExtractMediaSegments": "Sganio Darnau Cyfryngau"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
"Channels": "Kanäle",
|
"Channels": "Kanäle",
|
||||||
"ChapterNameValue": "Kapitel {0}",
|
"ChapterNameValue": "Kapitel {0}",
|
||||||
"Collections": "Sammlungen",
|
"Collections": "Sammlungen",
|
||||||
"DeviceOfflineWithName": "{0} hat die Verbindung getrennt",
|
"DeviceOfflineWithName": "{0} ist offline",
|
||||||
"DeviceOnlineWithName": "{0} ist verbunden",
|
"DeviceOnlineWithName": "{0} ist online",
|
||||||
"FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
|
"FailedLoginAttemptWithUserName": "Anmeldung von {0} fehlgeschlagen",
|
||||||
"Favorites": "Favoriten",
|
"Favorites": "Favoriten",
|
||||||
"Folders": "Verzeichnisse",
|
"Folders": "Verzeichnisse",
|
||||||
"Genres": "Genres",
|
"Genres": "Genres",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"HeaderFavoriteArtists": "Lieblingsinterpreten",
|
"HeaderFavoriteArtists": "Lieblingsinterpreten",
|
||||||
"HeaderFavoriteEpisodes": "Lieblingsepisoden",
|
"HeaderFavoriteEpisodes": "Lieblingsepisoden",
|
||||||
"HeaderFavoriteShows": "Lieblingsserien",
|
"HeaderFavoriteShows": "Lieblingsserien",
|
||||||
"HeaderFavoriteSongs": "Lieblingslieder",
|
"HeaderFavoriteSongs": "Lieblingssongs",
|
||||||
"HeaderLiveTV": "Live TV",
|
"HeaderLiveTV": "Live TV",
|
||||||
"HeaderNextUp": "Als Nächstes",
|
"HeaderNextUp": "Als Nächstes",
|
||||||
"HeaderRecordingGroups": "Aufnahme-Gruppen",
|
"HeaderRecordingGroups": "Aufnahme-Gruppen",
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
"NewVersionIsAvailable": "Eine neue Jellyfin-Serverversion steht zum Download bereit.",
|
"NewVersionIsAvailable": "Eine neue Jellyfin-Serverversion steht zum Download bereit.",
|
||||||
"NotificationOptionApplicationUpdateAvailable": "Anwendungsaktualisierung verfügbar",
|
"NotificationOptionApplicationUpdateAvailable": "Anwendungsaktualisierung verfügbar",
|
||||||
"NotificationOptionApplicationUpdateInstalled": "Anwendungsaktualisierung installiert",
|
"NotificationOptionApplicationUpdateInstalled": "Anwendungsaktualisierung installiert",
|
||||||
"NotificationOptionAudioPlayback": "Audiowiedergabe gestartet",
|
"NotificationOptionAudioPlayback": "Audio wird abgespielt",
|
||||||
"NotificationOptionAudioPlaybackStopped": "Audiowiedergabe gestoppt",
|
"NotificationOptionAudioPlaybackStopped": "Audiowiedergabe gestoppt",
|
||||||
"NotificationOptionCameraImageUploaded": "Foto hochgeladen",
|
"NotificationOptionCameraImageUploaded": "Foto hochgeladen",
|
||||||
"NotificationOptionInstallationFailed": "Installation fehlgeschlagen",
|
"NotificationOptionInstallationFailed": "Installation fehlgeschlagen",
|
||||||
@@ -57,8 +57,8 @@
|
|||||||
"NotificationOptionPluginUpdateInstalled": "Pluginaktualisierung installiert",
|
"NotificationOptionPluginUpdateInstalled": "Pluginaktualisierung installiert",
|
||||||
"NotificationOptionServerRestartRequired": "Serverneustart notwendig",
|
"NotificationOptionServerRestartRequired": "Serverneustart notwendig",
|
||||||
"NotificationOptionTaskFailed": "Geplante Aufgabe fehlgeschlagen",
|
"NotificationOptionTaskFailed": "Geplante Aufgabe fehlgeschlagen",
|
||||||
"NotificationOptionUserLockedOut": "Benutzer ausgeschlossen",
|
"NotificationOptionUserLockedOut": "Benutzer gesperrt",
|
||||||
"NotificationOptionVideoPlayback": "Videowiedergabe gestartet",
|
"NotificationOptionVideoPlayback": "Video wird abgespielt",
|
||||||
"NotificationOptionVideoPlaybackStopped": "Videowiedergabe gestoppt",
|
"NotificationOptionVideoPlaybackStopped": "Videowiedergabe gestoppt",
|
||||||
"Photos": "Fotos",
|
"Photos": "Fotos",
|
||||||
"Playlists": "Wiedergabelisten",
|
"Playlists": "Wiedergabelisten",
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
"UserCreatedWithName": "Benutzer {0} wurde erstellt",
|
"UserCreatedWithName": "Benutzer {0} wurde erstellt",
|
||||||
"UserDeletedWithName": "Benutzer {0} wurde gelöscht",
|
"UserDeletedWithName": "Benutzer {0} wurde gelöscht",
|
||||||
"UserDownloadingItemWithValues": "{0} lädt {1} herunter",
|
"UserDownloadingItemWithValues": "{0} lädt {1} herunter",
|
||||||
"UserLockedOutWithName": "Benutzer {0} wurde ausgeschlossen",
|
"UserLockedOutWithName": "Benutzer {0} wurde gesperrt",
|
||||||
"UserOfflineFromDevice": "{0} wurde getrennt von {1}",
|
"UserOfflineFromDevice": "{0} wurde getrennt von {1}",
|
||||||
"UserOnlineFromDevice": "{0} ist online von {1}",
|
"UserOnlineFromDevice": "{0} ist online von {1}",
|
||||||
"UserPasswordChangedWithName": "Das Passwort für Benutzer {0} wurde geändert",
|
"UserPasswordChangedWithName": "Das Passwort für Benutzer {0} wurde geändert",
|
||||||
@@ -97,25 +97,25 @@
|
|||||||
"TaskRefreshChannelsDescription": "Aktualisiert Internet-Kanal-Informationen.",
|
"TaskRefreshChannelsDescription": "Aktualisiert Internet-Kanal-Informationen.",
|
||||||
"TaskRefreshChannels": "Kanäle aktualisieren",
|
"TaskRefreshChannels": "Kanäle aktualisieren",
|
||||||
"TaskCleanTranscodeDescription": "Löscht Transkodierungsdateien, die älter als einen Tag sind.",
|
"TaskCleanTranscodeDescription": "Löscht Transkodierungsdateien, die älter als einen Tag sind.",
|
||||||
"TaskCleanTranscode": "Transkodierungs-Verzeichnis aufräumen",
|
"TaskCleanTranscode": "Transkodierungsverzeichnis leeren",
|
||||||
"TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche für automatische Updates konfiguriert sind und installiert diese.",
|
"TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche für automatische Updates konfiguriert sind und installiert diese.",
|
||||||
"TaskUpdatePlugins": "Plugins aktualisieren",
|
"TaskUpdatePlugins": "Plugins aktualisieren",
|
||||||
"TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
|
"TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
|
||||||
"TaskRefreshPeople": "Personen aktualisieren",
|
"TaskRefreshPeople": "Personen aktualisieren",
|
||||||
"TaskCleanLogsDescription": "Lösche Log-Dateien, die älter als {0} Tage sind.",
|
"TaskCleanLogsDescription": "Lösche Log-Dateien, die älter als {0} Tage sind.",
|
||||||
"TaskCleanLogs": "Log-Verzeichnis aufräumen",
|
"TaskCleanLogs": "Protokollverzeichnis leeren",
|
||||||
"TaskRefreshLibraryDescription": "Durchsucht alle Bibliotheken nach neu hinzugefügten Dateien und aktualisiert Metadaten.",
|
"TaskRefreshLibraryDescription": "Durchsucht deine Medienbibliothek nach neuen Dateien und aktualisiert Metadaten.",
|
||||||
"TaskRefreshLibrary": "Medien-Bibliothek scannen",
|
"TaskRefreshLibrary": "Medien-Bibliothek scannen",
|
||||||
"TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videos, die Kapitel besitzen.",
|
"TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videokapitel.",
|
||||||
"TaskRefreshChapterImages": "Kapitel-Bilder extrahieren",
|
"TaskRefreshChapterImages": "Kapitelvorschauen erstellen",
|
||||||
"TaskCleanCacheDescription": "Löscht vom System nicht mehr benötigte Zwischenspeicherdateien.",
|
"TaskCleanCacheDescription": "Löscht Cache-Dateien, die vom System nicht mehr benötigt werden.",
|
||||||
"TaskCleanCache": "Zwischenspeicher-Verzeichnis aufräumen",
|
"TaskCleanCache": "Cache-Verzeichnis leeren",
|
||||||
"TasksChannelsCategory": "Internet-Kanäle",
|
"TasksChannelsCategory": "Internet-Kanäle",
|
||||||
"TasksApplicationCategory": "Anwendung",
|
"TasksApplicationCategory": "Anwendung",
|
||||||
"TasksLibraryCategory": "Bibliothek",
|
"TasksLibraryCategory": "Bibliothek",
|
||||||
"TasksMaintenanceCategory": "Wartung",
|
"TasksMaintenanceCategory": "Wartung",
|
||||||
"TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.",
|
"TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.",
|
||||||
"TaskCleanActivityLog": "Aktivitätsprotokolle aufräumen",
|
"TaskCleanActivityLog": "Aktivitätsverlauf bereinigen",
|
||||||
"Undefined": "Undefiniert",
|
"Undefined": "Undefiniert",
|
||||||
"Forced": "Erzwungen",
|
"Forced": "Erzwungen",
|
||||||
"Default": "Standard",
|
"Default": "Standard",
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
"ScheduledTaskFailedWithName": "{0} falló",
|
"ScheduledTaskFailedWithName": "{0} falló",
|
||||||
"ScheduledTaskStartedWithName": "{0} iniciado",
|
"ScheduledTaskStartedWithName": "{0} iniciado",
|
||||||
"ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
|
"ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
|
||||||
"Shows": "Programas",
|
"Shows": "Series",
|
||||||
"Songs": "Canciones",
|
"Songs": "Canciones",
|
||||||
"StartupEmbyServerIsLoading": "El servidor Jellyfin se está cargando. Vuelve a intentarlo en breve.",
|
"StartupEmbyServerIsLoading": "El servidor Jellyfin se está cargando. Vuelve a intentarlo en breve.",
|
||||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"ItemAddedWithName": "{0} fue agregado a la biblioteca",
|
"ItemAddedWithName": "{0} fue agregado a la biblioteca",
|
||||||
"ItemRemovedWithName": "{0} fue removido de la biblioteca",
|
"ItemRemovedWithName": "{0} fue removido de la biblioteca",
|
||||||
"LabelIpAddressValue": "Dirección IP: {0}",
|
"LabelIpAddressValue": "Dirección IP: {0}",
|
||||||
"LabelRunningTimeValue": "Tiempo de reproducción: {0}",
|
"LabelRunningTimeValue": "Tiempo corriendo: {0}",
|
||||||
"Latest": "Recientes",
|
"Latest": "Recientes",
|
||||||
"MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
|
"MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
|
||||||
"MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",
|
"MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
"NotificationOptionApplicationUpdateAvailable": "Rakenduse uuendus on saadaval",
|
"NotificationOptionApplicationUpdateAvailable": "Rakenduse uuendus on saadaval",
|
||||||
"NewVersionIsAvailable": "Jellyfin serveri uus versioon on allalaadimiseks saadaval.",
|
"NewVersionIsAvailable": "Jellyfin serveri uus versioon on allalaadimiseks saadaval.",
|
||||||
"NameSeasonUnknown": "Tundmatu hooaeg",
|
"NameSeasonUnknown": "Tundmatu hooaeg",
|
||||||
"NameSeasonNumber": "Hooaeg {0}",
|
"NameSeasonNumber": "{0}. hooaeg",
|
||||||
"NameInstallFailed": "{0} paigaldamine nurjus",
|
"NameInstallFailed": "{0} paigaldamine nurjus",
|
||||||
"MusicVideos": "Muusikavideod",
|
"MusicVideos": "Muusikavideod",
|
||||||
"Music": "Muusika",
|
"Music": "Muusika",
|
||||||
@@ -137,5 +137,5 @@
|
|||||||
"TaskExtractMediaSegmentsDescription": "Eraldab või võtab meediasegmendid MediaSegment'i lubavatest pluginatest.",
|
"TaskExtractMediaSegmentsDescription": "Eraldab või võtab meediasegmendid MediaSegment'i lubavatest pluginatest.",
|
||||||
"TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht",
|
"TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht",
|
||||||
"CleanupUserDataTask": "Puhasta kasutajaandmed",
|
"CleanupUserDataTask": "Puhasta kasutajaandmed",
|
||||||
"CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mis pole enam vähemalt 90 päeva saadaval olnud."
|
"CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Sammlungen",
|
"Collections": "Sammlungen",
|
||||||
"DeviceOfflineWithName": "{0} wurde getrennt",
|
"DeviceOfflineWithName": "{0} wurde getrennt",
|
||||||
"DeviceOnlineWithName": "{0} ist verbunden",
|
"DeviceOnlineWithName": "{0} ist verbunden",
|
||||||
"FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
|
"FailedLoginAttemptWithUserName": "Fählgschlagene Ameldeversuech vo {0}",
|
||||||
"Favorites": "Favorite",
|
"Favorites": "Favorite",
|
||||||
"Folders": "Ordner",
|
"Folders": "Ordner",
|
||||||
"Genres": "Genre",
|
"Genres": "Genre",
|
||||||
|
|||||||
@@ -129,5 +129,12 @@
|
|||||||
"TaskAudioNormalization": "श्रव्य सामान्यीकरण",
|
"TaskAudioNormalization": "श्रव्य सामान्यीकरण",
|
||||||
"TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें",
|
"TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें",
|
||||||
"TaskDownloadMissingLyrics": "लापता गानों के बोल डाउनलोड करेँ",
|
"TaskDownloadMissingLyrics": "लापता गानों के बोल डाउनलोड करेँ",
|
||||||
"TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है"
|
"TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है",
|
||||||
|
"TaskExtractMediaSegments": "मीडिया सेगमेंट स्कैन",
|
||||||
|
"TaskExtractMediaSegmentsDescription": "मीडियासेगमेंट सक्षम प्लगइन्स से मीडिया सेगमेंट निकालता है या प्राप्त करता है।",
|
||||||
|
"TaskMoveTrickplayImages": "ट्रिकप्ले छवि स्थान माइग्रेट करें",
|
||||||
|
"TaskMoveTrickplayImagesDescription": "लाइब्रेरी सेटिंग्स के अनुसार मौजूदा ट्रिकप्ले फ़ाइलों को स्थानांतरित करता है।",
|
||||||
|
"TaskCleanCollectionsAndPlaylistsDescription": "संग्रहों और प्लेलिस्टों से उन आइटमों को हटाता है जो अब मौजूद नहीं हैं।",
|
||||||
|
"TaskCleanCollectionsAndPlaylists": "संग्रह और प्लेलिस्ट साफ़ करें",
|
||||||
|
"CleanupUserDataTask": "यूज़र डेटा की सफाई करता है।"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,5 +136,7 @@
|
|||||||
"TaskMoveTrickplayImages": "트릭플레이 이미지 위치 마이그레이션",
|
"TaskMoveTrickplayImages": "트릭플레이 이미지 위치 마이그레이션",
|
||||||
"TaskMoveTrickplayImagesDescription": "추출된 트릭플레이 이미지를 라이브러리 설정에 따라 이동합니다.",
|
"TaskMoveTrickplayImagesDescription": "추출된 트릭플레이 이미지를 라이브러리 설정에 따라 이동합니다.",
|
||||||
"TaskDownloadMissingLyrics": "누락된 가사 다운로드",
|
"TaskDownloadMissingLyrics": "누락된 가사 다운로드",
|
||||||
"TaskDownloadMissingLyricsDescription": "가사 다운로드"
|
"TaskDownloadMissingLyricsDescription": "가사 다운로드",
|
||||||
|
"CleanupUserDataTask": "사용자 데이터 정리 작업",
|
||||||
|
"CleanupUserDataTaskDescription": "최소 90일 이상 존재하지 않는 미디어에 대한 사용자 데이터(시청 상태, 즐겨찾기 등)를 정리합니다."
|
||||||
}
|
}
|
||||||
|
|||||||
9
Emby.Server.Implementations/Localization/Core/mi.json
Normal file
9
Emby.Server.Implementations/Localization/Core/mi.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Albums": "Pukaemi",
|
||||||
|
"AppDeviceValues": "Taupānga: {0}, Pūrere: {1}",
|
||||||
|
"Application": "Taupānga",
|
||||||
|
"Artists": "Kaiwaiata",
|
||||||
|
"AuthenticationSucceededWithUserName": "{0} has been successfully authenticated",
|
||||||
|
"Books": "Ngā pukapuka",
|
||||||
|
"CameraImageUploadedFrom": "Kua tuku ake he whakaahua kāmera hou mai i {0}"
|
||||||
|
}
|
||||||
@@ -2,12 +2,12 @@
|
|||||||
"AppDeviceValues": "അപ്ലിക്കേഷൻ: {0}, ഉപകരണം: {1}",
|
"AppDeviceValues": "അപ്ലിക്കേഷൻ: {0}, ഉപകരണം: {1}",
|
||||||
"Application": "അപ്ലിക്കേഷൻ",
|
"Application": "അപ്ലിക്കേഷൻ",
|
||||||
"AuthenticationSucceededWithUserName": "{0} വിജയകരമായി പ്രാമാണീകരിച്ചു",
|
"AuthenticationSucceededWithUserName": "{0} വിജയകരമായി പ്രാമാണീകരിച്ചു",
|
||||||
"CameraImageUploadedFrom": "Camera 0 from എന്നതിൽ നിന്ന് ഒരു പുതിയ ക്യാമറ ചിത്രം അപ്ലോഡുചെയ്തു",
|
"CameraImageUploadedFrom": "{0} എന്നതിൽ നിന്ന് ഒരു പുതിയ ക്യാമറ ചിത്രം അപ്ലോഡുചെയ്തു",
|
||||||
"ChapterNameValue": "അധ്യായം {0}",
|
"ChapterNameValue": "അധ്യായം {0}",
|
||||||
"DeviceOfflineWithName": "{0} വിച്ഛേദിച്ചു",
|
"DeviceOfflineWithName": "{0} വിച്ഛേദിച്ചു",
|
||||||
"DeviceOnlineWithName": "{0} ബന്ധിപ്പിച്ചു",
|
"DeviceOnlineWithName": "{0} ബന്ധിപ്പിച്ചു",
|
||||||
"FailedLoginAttemptWithUserName": "{0}ൽ നിന്നുള്ള പ്രവേശന ശ്രമം പരാജയപ്പെട്ടു",
|
"FailedLoginAttemptWithUserName": "{0}ൽ നിന്നുള്ള പ്രവേശന ശ്രമം പരാജയപ്പെട്ടു",
|
||||||
"Forced": "നിർബന്ധിച്ചു",
|
"Forced": "നിർബന്ധിതമായി",
|
||||||
"HeaderFavoriteAlbums": "പ്രിയപ്പെട്ട ആൽബങ്ങൾ",
|
"HeaderFavoriteAlbums": "പ്രിയപ്പെട്ട ആൽബങ്ങൾ",
|
||||||
"HeaderFavoriteArtists": "പ്രിയപ്പെട്ട കലാകാരന്മാർ",
|
"HeaderFavoriteArtists": "പ്രിയപ്പെട്ട കലാകാരന്മാർ",
|
||||||
"HeaderFavoriteEpisodes": "പ്രിയപ്പെട്ട എപ്പിസോഡുകൾ",
|
"HeaderFavoriteEpisodes": "പ്രിയപ്പെട്ട എപ്പിസോഡുകൾ",
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
"Artists": "കലാകാരന്മാർ",
|
"Artists": "കലാകാരന്മാർ",
|
||||||
"Shows": "ഷോകൾ",
|
"Shows": "ഷോകൾ",
|
||||||
"Default": "സ്ഥിരസ്ഥിതി",
|
"Default": "സ്ഥിരസ്ഥിതി",
|
||||||
"Favorites": "പ്രിയങ്കരങ്ങൾ",
|
"Favorites": "പ്രിയപ്പെട്ടവ",
|
||||||
"Books": "പുസ്തകങ്ങൾ",
|
"Books": "പുസ്തകങ്ങൾ",
|
||||||
"Genres": "വിഭാഗങ്ങൾ",
|
"Genres": "വിഭാഗങ്ങൾ",
|
||||||
"Channels": "ചാനലുകൾ",
|
"Channels": "ചാനലുകൾ",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"HeaderNextUp": "Дараа нь",
|
"HeaderNextUp": "Дараа нь",
|
||||||
"HeaderContinueWatching": "Үргэлжлүүлэн үзэх",
|
"HeaderContinueWatching": "Үргэлжлүүлэн үзэх",
|
||||||
"Songs": "Дуунууд",
|
"Songs": "Дуунууд",
|
||||||
"Playlists": "Playlist-ууд",
|
"Playlists": "Тоглуулах жагсаалтууд",
|
||||||
"Movies": "Кинонууд",
|
"Movies": "Кинонууд",
|
||||||
"Latest": "Сүүлийн үеийн",
|
"Latest": "Сүүлийн үеийн",
|
||||||
"Genres": "Төрлүүд",
|
"Genres": "Төрлүүд",
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
"Forced": "Хүчээр",
|
"Forced": "Хүчээр",
|
||||||
"HeaderAlbumArtists": "Цомгийн уран бүтээлчид",
|
"HeaderAlbumArtists": "Цомгийн уран бүтээлчид",
|
||||||
"HeaderFavoriteAlbums": "Дуртай цомгууд",
|
"HeaderFavoriteAlbums": "Дуртай цомгууд",
|
||||||
"HeaderLiveTV": "Шууд",
|
"HeaderLiveTV": "Шууд ТВ",
|
||||||
"HeaderRecordingGroups": "Бичлэгийн бүлгүүд",
|
"HeaderRecordingGroups": "Бичлэгийн бүлгүүд",
|
||||||
"HearingImpaired": "Сонсголын бэрхшээлтэй",
|
"HearingImpaired": "Сонсголын бэрхшээлтэй",
|
||||||
"HomeVideos": "Үндсэн дүрсүүд",
|
"HomeVideos": "Үндсэн дүрсүүд",
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
"ScheduledTaskStartedWithName": "{0}-г эхлүүлэв",
|
"ScheduledTaskStartedWithName": "{0}-г эхлүүлэв",
|
||||||
"ServerNameNeedsToBeRestarted": "{0}-г дахин асаана уу",
|
"ServerNameNeedsToBeRestarted": "{0}-г дахин асаана уу",
|
||||||
"Shows": "Шоу",
|
"Shows": "Шоу",
|
||||||
"Sync": "Дахин",
|
"Sync": "Синхрончлох",
|
||||||
"System": "Систем",
|
"System": "Систем",
|
||||||
"TvShows": "ТВ нэвтрүүлгүүд",
|
"TvShows": "ТВ нэвтрүүлгүүд",
|
||||||
"Undefined": "Танисангүй",
|
"Undefined": "Танисангүй",
|
||||||
|
|||||||
@@ -132,5 +132,10 @@
|
|||||||
"TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा",
|
"TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा",
|
||||||
"TaskAudioNormalization": "ऑडिओ सामान्यीकरण",
|
"TaskAudioNormalization": "ऑडिओ सामान्यीकरण",
|
||||||
"TaskAudioNormalizationDescription": "ऑडिओ सामान्यीकरणाचा डाटा स्कॅन करतो.",
|
"TaskAudioNormalizationDescription": "ऑडिओ सामान्यीकरणाचा डाटा स्कॅन करतो.",
|
||||||
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो"
|
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो",
|
||||||
|
"TaskExtractMediaSegmentsDescription": "सक्रिय असलेल्या प्लगिनमधून मीडिया विभाग प्राप्त करते.",
|
||||||
|
"TaskMoveTrickplayImagesDescription": "लायब्ररीच्या सेटिंग्जप्रमाणे आधीपासून अस्तित्वात असलेल्या ट्रिकप्ले फाइल्सचे स्थान बदलते.",
|
||||||
|
"TaskCleanCollectionsAndPlaylistsDescription": "जे संग्रह आणि प्लेलिस्ट आता अस्तित्वात नाहीत, त्यांमधील घटक हटवते.",
|
||||||
|
"CleanupUserDataTask": "वापरकर्ता डेटाची स्वच्छता प्रक्रिया",
|
||||||
|
"CleanupUserDataTaskDescription": "९० दिवसांहून अधिक काळ अनुपस्थित असलेल्या माध्यमांवरील सर्व वापरकर्ता माहिती (जसे पाहण्याची स्थिती, आवडी इ.) हटवते."
|
||||||
}
|
}
|
||||||
|
|||||||
1
Emby.Server.Implementations/Localization/Core/oc.json
Normal file
1
Emby.Server.Implementations/Localization/Core/oc.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -134,6 +134,8 @@
|
|||||||
"TaskCleanCollectionsAndPlaylistsDescription": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਵਿੱਚੋਂ ਉਹ ਆਈਟਮ ਹਟਾਉਂਦਾ ਹੈ ਜੋ ਹੁਣ ਮੌਜੂਦ ਨਹੀਂ ਹਨ।",
|
"TaskCleanCollectionsAndPlaylistsDescription": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਵਿੱਚੋਂ ਉਹ ਆਈਟਮ ਹਟਾਉਂਦਾ ਹੈ ਜੋ ਹੁਣ ਮੌਜੂਦ ਨਹੀਂ ਹਨ।",
|
||||||
"TaskCleanCollectionsAndPlaylists": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਨੂੰ ਸਾਫ ਕਰੋ",
|
"TaskCleanCollectionsAndPlaylists": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਨੂੰ ਸਾਫ ਕਰੋ",
|
||||||
"TaskAudioNormalization": "ਆਵਾਜ਼ ਸਧਾਰਣੀਕਰਨ",
|
"TaskAudioNormalization": "ਆਵਾਜ਼ ਸਧਾਰਣੀਕਰਨ",
|
||||||
"TaskRefreshTrickplayImagesDescription": "ਚਲ ਰਹੀ ਲਾਇਬ੍ਰੇਰੀਆਂ ਵਿੱਚ ਵੀਡੀਓਜ਼ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ।",
|
"TaskRefreshTrickplayImagesDescription": "ਵੀਡੀਓ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ (ਜੇ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਚੁਣਿਆ ਗਿਆ ਹੈ)।",
|
||||||
"TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।"
|
"TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।",
|
||||||
|
"CleanupUserDataTaskDescription": "ਘੱਟੋ-ਘੱਟ 90 ਦਿਨਾਂ ਤੋਂ ਮੌਜੂਦ ਨਾ ਹੋਣ ਵਾਲੇ ਮੀਡੀਆ ਤੋਂ ਸਾਰੇ ਉਪਭੋਗਤਾ ਡੇਟਾ (ਵਾਚ ਸਟੇਟ, ਮਨਪਸੰਦ ਸਟੇਟਸ ਆਦਿ) ਨੂੰ ਸਾਫ਼ ਕਰਦਾ ਹੈ।",
|
||||||
|
"CleanupUserDataTask": "ਯੂਜ਼ਰ ਡਾਟਾ ਸਾਫ਼ ਕਰਨ ਦਾ ਕੰਮ"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,8 +125,8 @@
|
|||||||
"TaskKeyframeExtractorDescription": "Wyodrębnia klatki kluczowe z plików wideo w celu utworzenia bardziej precyzyjnych list odtwarzania HLS. To zadanie może trwać przez długi czas.",
|
"TaskKeyframeExtractorDescription": "Wyodrębnia klatki kluczowe z plików wideo w celu utworzenia bardziej precyzyjnych list odtwarzania HLS. To zadanie może trwać przez długi czas.",
|
||||||
"TaskKeyframeExtractor": "Ekstraktor klatek kluczowych",
|
"TaskKeyframeExtractor": "Ekstraktor klatek kluczowych",
|
||||||
"HearingImpaired": "Niedosłyszący",
|
"HearingImpaired": "Niedosłyszący",
|
||||||
"TaskRefreshTrickplayImages": "Generuj obrazy trickplay",
|
"TaskRefreshTrickplayImages": "Generuj obrazy Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Tworzy podglądy trickplay dla filmów we włączonych bibliotekach.",
|
"TaskRefreshTrickplayImagesDescription": "Tworzy podglądy Trickplay dla filmów we włączonych bibliotekach.",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Usuwa elementy z kolekcji i list odtwarzania, które już nie istnieją.",
|
"TaskCleanCollectionsAndPlaylistsDescription": "Usuwa elementy z kolekcji i list odtwarzania, które już nie istnieją.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Oczyść kolekcje i listy odtwarzania",
|
"TaskCleanCollectionsAndPlaylists": "Oczyść kolekcje i listy odtwarzania",
|
||||||
"TaskAudioNormalization": "Normalizacja dźwięku",
|
"TaskAudioNormalization": "Normalizacja dźwięku",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"Collections": "Barrels",
|
"Collections": "Barrels",
|
||||||
"ItemAddedWithName": "{0} is now with yer treasure",
|
"ItemAddedWithName": "{0} is now with yer treasure",
|
||||||
"Default": "Normal-like",
|
"Default": "Normal-like",
|
||||||
"FailedLoginAttemptWithUserName": "Ye failed to get in, try from {0}",
|
"FailedLoginAttemptWithUserName": "Ye failed to enter from {0}",
|
||||||
"Favorites": "Finest Loot",
|
"Favorites": "Finest Loot",
|
||||||
"ItemRemovedWithName": "{0} was taken from yer treasure",
|
"ItemRemovedWithName": "{0} was taken from yer treasure",
|
||||||
"LabelIpAddressValue": "Ship's coordinates: {0}",
|
"LabelIpAddressValue": "Ship's coordinates: {0}",
|
||||||
@@ -113,5 +113,10 @@
|
|||||||
"TaskCleanCache": "Sweep the Cache Chest",
|
"TaskCleanCache": "Sweep the Cache Chest",
|
||||||
"TaskRefreshChapterImages": "Claim chapter portraits",
|
"TaskRefreshChapterImages": "Claim chapter portraits",
|
||||||
"TaskRefreshChapterImagesDescription": "Paints wee portraits fer videos that own chapters.",
|
"TaskRefreshChapterImagesDescription": "Paints wee portraits fer videos that own chapters.",
|
||||||
"TaskRefreshLibrary": "Scan the Treasure Trove"
|
"TaskRefreshLibrary": "Scan the Treasure Trove",
|
||||||
|
"TasksChannelsCategory": "Channels o' thy Internet",
|
||||||
|
"TaskRefreshTrickplayImages": "Summon the picture tricks",
|
||||||
|
"TaskRefreshTrickplayImagesDescription": "Summons picture trick previews for videos in ye enabled book roost",
|
||||||
|
"TaskUpdatePlugins": "Resummon yer Plugins",
|
||||||
|
"TaskCleanTranscode": "Swab Ye Transcode Directory"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"Artists": "Artistas",
|
"Artists": "Artistas",
|
||||||
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
|
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
|
||||||
"Books": "Livros",
|
"Books": "Livros",
|
||||||
"CameraImageUploadedFrom": "Uma nova imagem de câmara foi enviada a partir de {0}",
|
"CameraImageUploadedFrom": "Uma nova imagem da câmara foi enviada a partir de {0}",
|
||||||
"Channels": "Canais",
|
"Channels": "Canais",
|
||||||
"ChapterNameValue": "Capítulo {0}",
|
"ChapterNameValue": "Capítulo {0}",
|
||||||
"Collections": "Coleções",
|
"Collections": "Coleções",
|
||||||
@@ -125,8 +125,8 @@
|
|||||||
"TaskKeyframeExtractor": "Extrator de Quadros-chave",
|
"TaskKeyframeExtractor": "Extrator de Quadros-chave",
|
||||||
"External": "Externo",
|
"External": "Externo",
|
||||||
"HearingImpaired": "Surdo",
|
"HearingImpaired": "Surdo",
|
||||||
"TaskRefreshTrickplayImages": "Gerar imagens de truques",
|
"TaskRefreshTrickplayImages": "Gerar Imagens de Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Cria vizualizações de truques para videos nas librarias ativas.",
|
"TaskRefreshTrickplayImagesDescription": "Cria ficheiros de trickplay para vídeos nas bibliotecas ativas.",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
|
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
|
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
|
||||||
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
|
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
|
||||||
|
|||||||
1
Emby.Server.Implementations/Localization/Core/sw.json
Normal file
1
Emby.Server.Implementations/Localization/Core/sw.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -135,5 +135,7 @@
|
|||||||
"TaskExtractMediaSegments": "การสแกนส่วนของสื่อมีเดีย",
|
"TaskExtractMediaSegments": "การสแกนส่วนของสื่อมีเดีย",
|
||||||
"TaskMoveTrickplayImagesDescription": "ย้ายไฟล์ Trickplay ตามการตั้งค่าของไลบรารี",
|
"TaskMoveTrickplayImagesDescription": "ย้ายไฟล์ Trickplay ตามการตั้งค่าของไลบรารี",
|
||||||
"TaskExtractMediaSegmentsDescription": "แยกหรือดึงส่วนของสื่อจากปลั๊กอินที่เปิดใช้งาน MediaSegment",
|
"TaskExtractMediaSegmentsDescription": "แยกหรือดึงส่วนของสื่อจากปลั๊กอินที่เปิดใช้งาน MediaSegment",
|
||||||
"TaskMoveTrickplayImages": "ย้ายตำแหน่งเก็บภาพตัวอย่าง Trickplay"
|
"TaskMoveTrickplayImages": "ย้ายตำแหน่งเก็บภาพตัวอย่าง Trickplay",
|
||||||
|
"CleanupUserDataTask": "ส่วนงานล้างข้อมูลผู้ใช้",
|
||||||
|
"CleanupUserDataTaskDescription": "ล้างข้อมูลผู้ใช้ทั้งหมด (สถานะการรับชม สถานะรายการโปรด ฯลฯ) จากสื่อที่ไม่ได้ใช้งานแล้วอย่างน้อย 90 วัน"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
"TasksMaintenanceCategory": "Bảo Trì",
|
"TasksMaintenanceCategory": "Bảo Trì",
|
||||||
"VersionNumber": "Phiên Bản {0}",
|
"VersionNumber": "Phiên Bản {0}",
|
||||||
"ValueHasBeenAddedToLibrary": "{0} đã được thêm vào thư viện của bạn",
|
"ValueHasBeenAddedToLibrary": "{0} đã được thêm vào thư viện của bạn",
|
||||||
"UserStoppedPlayingItemWithValues": "{0} đã phát xong {1} trên {2}",
|
"UserStoppedPlayingItemWithValues": "{0} đã kết thúc phát {1} trên {2}",
|
||||||
"UserStartedPlayingItemWithValues": "{0} đang phát {1} trên {2}",
|
"UserStartedPlayingItemWithValues": "{0} đang phát {1} trên {2}",
|
||||||
"UserPolicyUpdatedWithName": "Chính sách người dùng đã được cập nhật cho {0}",
|
"UserPolicyUpdatedWithName": "Chính sách người dùng đã được cập nhật cho {0}",
|
||||||
"UserPasswordChangedWithName": "Mật khẩu đã được thay đổi cho người dùng {0}",
|
"UserPasswordChangedWithName": "Mật khẩu đã được thay đổi cho người dùng {0}",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"HeaderFavoriteShows": "最愛的節目",
|
"HeaderFavoriteShows": "最愛的節目",
|
||||||
"HeaderFavoriteSongs": "最愛的歌曲",
|
"HeaderFavoriteSongs": "最愛的歌曲",
|
||||||
"HeaderLiveTV": "電視直播",
|
"HeaderLiveTV": "電視直播",
|
||||||
"HeaderNextUp": "接著播放",
|
"HeaderNextUp": "繼續觀看",
|
||||||
"HeaderRecordingGroups": "錄製組",
|
"HeaderRecordingGroups": "錄製組",
|
||||||
"HomeVideos": "家庭影片",
|
"HomeVideos": "家庭影片",
|
||||||
"Inherit": "繼承",
|
"Inherit": "繼承",
|
||||||
@@ -127,8 +127,8 @@
|
|||||||
"HearingImpaired": "聽力障礙",
|
"HearingImpaired": "聽力障礙",
|
||||||
"TaskRefreshTrickplayImages": "建立 Trickplay 圖像",
|
"TaskRefreshTrickplayImages": "建立 Trickplay 圖像",
|
||||||
"TaskRefreshTrickplayImagesDescription": "為已啟用 Trickplay 的媒體庫內的影片建立 Trickplay 預覽圖。",
|
"TaskRefreshTrickplayImagesDescription": "為已啟用 Trickplay 的媒體庫內的影片建立 Trickplay 預覽圖。",
|
||||||
"TaskExtractMediaSegments": "掃描媒體段落",
|
"TaskExtractMediaSegments": "掃描媒體分段資訊",
|
||||||
"TaskExtractMediaSegmentsDescription": "從MediaSegment中被允許的插件獲取媒體段落。",
|
"TaskExtractMediaSegmentsDescription": "從允許MediaSegment 功能的插件中獲取媒體片段。",
|
||||||
"TaskDownloadMissingLyrics": "下載欠缺歌詞",
|
"TaskDownloadMissingLyrics": "下載欠缺歌詞",
|
||||||
"TaskDownloadMissingLyricsDescription": "下載歌詞",
|
"TaskDownloadMissingLyricsDescription": "下載歌詞",
|
||||||
"TaskCleanCollectionsAndPlaylists": "整理媒體與播放清單",
|
"TaskCleanCollectionsAndPlaylists": "整理媒體與播放清單",
|
||||||
@@ -137,5 +137,6 @@
|
|||||||
"TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。",
|
"TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。",
|
||||||
"TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。",
|
"TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。",
|
||||||
"TaskMoveTrickplayImages": "轉移 Trickplay 影像位置",
|
"TaskMoveTrickplayImages": "轉移 Trickplay 影像位置",
|
||||||
"CleanupUserDataTask": "用戶資料清理工作"
|
"CleanupUserDataTask": "用戶資料清理工作",
|
||||||
|
"CleanupUserDataTaskDescription": "從用戶數據中清除已經被刪除超過 90 日的媒體相關資料。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ namespace Emby.Server.Implementations.Localization
|
|||||||
|
|
||||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, CultureDto?> _cultureCache = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private List<CultureDto> _cultures = [];
|
private List<CultureDto> _cultures = [];
|
||||||
|
|
||||||
private FrozenDictionary<string, string> _iso6392BtoT = null!;
|
private FrozenDictionary<string, string> _iso6392BtoT = null!;
|
||||||
@@ -161,6 +162,7 @@ namespace Emby.Server.Implementations.Localization
|
|||||||
list.Add(new CultureDto(name, displayname, twoCharName, threeLetterNames));
|
list.Add(new CultureDto(name, displayname, twoCharName, threeLetterNames));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_cultureCache.Clear();
|
||||||
_cultures = list;
|
_cultures = list;
|
||||||
_iso6392BtoT = iso6392BtoTdict.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
_iso6392BtoT = iso6392BtoTdict.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
@@ -169,20 +171,31 @@ namespace Emby.Server.Implementations.Localization
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public CultureDto? FindLanguageInfo(string language)
|
public CultureDto? FindLanguageInfo(string language)
|
||||||
{
|
{
|
||||||
// TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs
|
if (string.IsNullOrEmpty(language))
|
||||||
for (var i = 0; i < _cultures.Count; i++)
|
|
||||||
{
|
{
|
||||||
var culture = _cultures[i];
|
return null;
|
||||||
if (language.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| language.Equals(culture.Name, StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| culture.ThreeLetterISOLanguageNames.Contains(language, StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| language.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return culture;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return default;
|
return _cultureCache.GetOrAdd(
|
||||||
|
language,
|
||||||
|
static (lang, cultures) =>
|
||||||
|
{
|
||||||
|
// TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs
|
||||||
|
for (var i = 0; i < cultures.Count; i++)
|
||||||
|
{
|
||||||
|
var culture = cultures[i];
|
||||||
|
if (lang.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| lang.Equals(culture.Name, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| culture.ThreeLetterISOLanguageNames.Contains(lang, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| lang.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return culture;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
_cultures);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -311,15 +324,19 @@ namespace Emby.Server.Implementations.Localization
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Fall back to server default language for ratings check
|
// Fall back to server default language for ratings check
|
||||||
// If it has no ratings, use the US ratings
|
var ratingsDictionary = GetParentalRatingsDictionary();
|
||||||
var ratingsDictionary = GetParentalRatingsDictionary() ?? GetParentalRatingsDictionary("us");
|
|
||||||
if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value))
|
if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value))
|
||||||
{
|
{
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we don't find anything, check all ratings systems
|
// If we don't find anything, check all ratings systems, starting with US
|
||||||
|
if (_allParentalRatings.TryGetValue("us", out var usRatings) && usRatings.TryGetValue(rating, out var usValue))
|
||||||
|
{
|
||||||
|
return usValue;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var dictionary in _allParentalRatings.Values)
|
foreach (var dictionary in _allParentalRatings.Values)
|
||||||
{
|
{
|
||||||
if (dictionary.TryGetValue(rating, out var value))
|
if (dictionary.TryGetValue(rating, out var value))
|
||||||
|
|||||||
@@ -347,8 +347,8 @@ pli||pi|Pali|pali
|
|||||||
pol||pl|Polish|polonais
|
pol||pl|Polish|polonais
|
||||||
pon|||Pohnpeian|pohnpei
|
pon|||Pohnpeian|pohnpei
|
||||||
por||pt|Portuguese|portugais
|
por||pt|Portuguese|portugais
|
||||||
pop||pt-pt|Portuguese (Portugal)|portugais (pt-pt)
|
por||pt-pt|Portuguese (Portugal)|portugais (pt-pt)
|
||||||
pob||pt-br|Portuguese (Brazil)|portugais (pt-br)
|
por||pt-br|Portuguese (Brazil)|portugais (pt-br)
|
||||||
pra|||Prakrit languages|prâkrit, langues
|
pra|||Prakrit languages|prâkrit, langues
|
||||||
pro|||Provençal, Old (to 1500)|provençal ancien (jusqu'à 1500)
|
pro|||Provençal, Old (to 1500)|provençal ancien (jusqu'à 1500)
|
||||||
pus||ps|Pushto; Pashto|pachto
|
pus||ps|Pushto; Pashto|pachto
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Sorting;
|
using MediaBrowser.Controller.Sorting;
|
||||||
using MediaBrowser.Model.Querying;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Sorting
|
namespace Emby.Server.Implementations.Sorting
|
||||||
{
|
{
|
||||||
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Sorting
|
|||||||
ArgumentNullException.ThrowIfNull(x);
|
ArgumentNullException.ThrowIfNull(x);
|
||||||
ArgumentNullException.ThrowIfNull(y);
|
ArgumentNullException.ThrowIfNull(y);
|
||||||
|
|
||||||
return AlphanumericComparator.CompareValues(x.Studios.FirstOrDefault(), y.Studios.FirstOrDefault());
|
return CultureInfo.InvariantCulture.CompareInfo.Compare(x.Studios.FirstOrDefault(), y.Studios.FirstOrDefault(), CompareOptions.NumericOrdering);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ namespace Emby.Server.Implementations.TV
|
|||||||
items = items.Skip(query.StartIndex.Value);
|
items = items.Skip(query.StartIndex.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.Limit.HasValue)
|
if (query.Limit.HasValue && query.Limit.Value > 0)
|
||||||
{
|
{
|
||||||
items = items.Take(query.Limit.Value);
|
items = items.Take(query.Limit.Value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,11 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
_logger.LogError(ex, "The URL configured for the plugin repository manifest URL is not valid: {Manifest}", manifest);
|
_logger.LogError(ex, "The URL configured for the plugin repository manifest URL is not valid: {Manifest}", manifest);
|
||||||
return Array.Empty<PackageInfo>();
|
return Array.Empty<PackageInfo>();
|
||||||
}
|
}
|
||||||
|
catch (NotSupportedException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "The URL scheme configured for the plugin repository is not supported: {Manifest}", manifest);
|
||||||
|
return Array.Empty<PackageInfo>();
|
||||||
|
}
|
||||||
catch (HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
|
_logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
|
||||||
@@ -557,7 +562,7 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
}
|
}
|
||||||
|
|
||||||
stream.Position = 0;
|
stream.Position = 0;
|
||||||
ZipFile.ExtractToDirectory(stream, targetDir, true);
|
await ZipFile.ExtractToDirectoryAsync(stream, targetDir, true, cancellationToken);
|
||||||
|
|
||||||
// Ensure we create one or populate existing ones with missing data.
|
// Ensure we create one or populate existing ones with missing data.
|
||||||
await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
|
await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Data.Queries;
|
using Jellyfin.Data.Queries;
|
||||||
|
using Jellyfin.Database.Implementations.Enums;
|
||||||
using MediaBrowser.Common.Api;
|
using MediaBrowser.Common.Api;
|
||||||
using MediaBrowser.Model.Activity;
|
using MediaBrowser.Model.Activity;
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers;
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
@@ -32,10 +35,20 @@ public class ActivityLogController : BaseJellyfinApiController
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets activity log entries.
|
/// Gets activity log entries.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
/// <param name="startIndex">The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
/// <param name="limit">The maximum number of records to return.</param>
|
||||||
/// <param name="minDate">Optional. The minimum date. Format = ISO.</param>
|
/// <param name="minDate">The minimum date.</param>
|
||||||
/// <param name="hasUserId">Optional. Filter log entries if it has user id, or not.</param>
|
/// <param name="maxDate">The maximum date.</param>
|
||||||
|
/// <param name="hasUserId">Filter log entries if it has user id, or not.</param>
|
||||||
|
/// <param name="name">Filter by name.</param>
|
||||||
|
/// <param name="overview">Filter by overview.</param>
|
||||||
|
/// <param name="shortOverview">Filter by short overview.</param>
|
||||||
|
/// <param name="type">Filter by type.</param>
|
||||||
|
/// <param name="itemId">Filter by item id.</param>
|
||||||
|
/// <param name="username">Filter by username.</param>
|
||||||
|
/// <param name="severity">Filter by log severity.</param>
|
||||||
|
/// <param name="sortBy">Specify one or more sort orders. Format: SortBy=Name,Type.</param>
|
||||||
|
/// <param name="sortOrder">Sort Order..</param>
|
||||||
/// <response code="200">Activity log returned.</response>
|
/// <response code="200">Activity log returned.</response>
|
||||||
/// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
|
/// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
|
||||||
[HttpGet("Entries")]
|
[HttpGet("Entries")]
|
||||||
@@ -44,14 +57,62 @@ public class ActivityLogController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? startIndex,
|
[FromQuery] int? startIndex,
|
||||||
[FromQuery] int? limit,
|
[FromQuery] int? limit,
|
||||||
[FromQuery] DateTime? minDate,
|
[FromQuery] DateTime? minDate,
|
||||||
[FromQuery] bool? hasUserId)
|
[FromQuery] DateTime? maxDate,
|
||||||
|
[FromQuery] bool? hasUserId,
|
||||||
|
[FromQuery] string? name,
|
||||||
|
[FromQuery] string? overview,
|
||||||
|
[FromQuery] string? shortOverview,
|
||||||
|
[FromQuery] string? type,
|
||||||
|
[FromQuery] Guid? itemId,
|
||||||
|
[FromQuery] string? username,
|
||||||
|
[FromQuery] LogLevel? severity,
|
||||||
|
[FromQuery] ActivityLogSortBy[]? sortBy,
|
||||||
|
[FromQuery] SortOrder[]? sortOrder)
|
||||||
{
|
{
|
||||||
return await _activityManager.GetPagedResultAsync(new ActivityLogQuery
|
var query = new ActivityLogQuery
|
||||||
{
|
{
|
||||||
Skip = startIndex,
|
Skip = startIndex,
|
||||||
Limit = limit,
|
Limit = limit,
|
||||||
MinDate = minDate,
|
MinDate = minDate,
|
||||||
HasUserId = hasUserId
|
MaxDate = maxDate,
|
||||||
}).ConfigureAwait(false);
|
HasUserId = hasUserId,
|
||||||
|
Name = name,
|
||||||
|
Overview = overview,
|
||||||
|
ShortOverview = shortOverview,
|
||||||
|
Type = type,
|
||||||
|
ItemId = itemId,
|
||||||
|
Username = username,
|
||||||
|
Severity = severity,
|
||||||
|
OrderBy = GetOrderBy(sortBy ?? [], sortOrder ?? []),
|
||||||
|
};
|
||||||
|
|
||||||
|
return await _activityManager.GetPagedResultAsync(query).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (ActivityLogSortBy SortBy, SortOrder SortOrder)[] GetOrderBy(
|
||||||
|
IReadOnlyList<ActivityLogSortBy> sortBy,
|
||||||
|
IReadOnlyList<SortOrder> requestedSortOrder)
|
||||||
|
{
|
||||||
|
if (sortBy.Count == 0)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new (ActivityLogSortBy, SortOrder)[sortBy.Count];
|
||||||
|
var i = 0;
|
||||||
|
for (; i < requestedSortOrder.Count; i++)
|
||||||
|
{
|
||||||
|
result[i] = (sortBy[i], requestedSortOrder[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add remaining elements with the first specified SortOrder
|
||||||
|
// or the default one if no SortOrders are specified
|
||||||
|
var order = requestedSortOrder.Count > 0 ? requestedSortOrder[0] : SortOrder.Ascending;
|
||||||
|
for (; i < sortBy.Count; i++)
|
||||||
|
{
|
||||||
|
result[i] = (sortBy[i], order);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,7 +122,6 @@ public class ArtistsController : BaseJellyfinApiController
|
|||||||
{
|
{
|
||||||
userId = RequestHelpers.GetUserId(User, userId);
|
userId = RequestHelpers.GetUserId(User, userId);
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
|
||||||
User? user = null;
|
User? user = null;
|
||||||
@@ -326,7 +325,6 @@ public class ArtistsController : BaseJellyfinApiController
|
|||||||
{
|
{
|
||||||
userId = RequestHelpers.GetUserId(User, userId);
|
userId = RequestHelpers.GetUserId(User, userId);
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
|
||||||
User? user = null;
|
User? user = null;
|
||||||
@@ -467,7 +465,7 @@ public class ArtistsController : BaseJellyfinApiController
|
|||||||
public ActionResult<BaseItemDto> GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId)
|
public ActionResult<BaseItemDto> GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId)
|
||||||
{
|
{
|
||||||
userId = RequestHelpers.GetUserId(User, userId);
|
userId = RequestHelpers.GetUserId(User, userId);
|
||||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
var dtoOptions = new DtoOptions();
|
||||||
|
|
||||||
var item = _libraryManager.GetArtist(name, dtoOptions);
|
var item = _libraryManager.GetArtist(name, dtoOptions);
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
||||||
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
||||||
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
||||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
|
||||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||||
@@ -107,7 +106,6 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
[FromQuery] bool? breakOnNonKeyFrames,
|
|
||||||
[FromQuery] int? audioSampleRate,
|
[FromQuery] int? audioSampleRate,
|
||||||
[FromQuery] int? maxAudioBitDepth,
|
[FromQuery] int? maxAudioBitDepth,
|
||||||
[FromQuery] int? audioBitRate,
|
[FromQuery] int? audioBitRate,
|
||||||
@@ -159,7 +157,6 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
||||||
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
||||||
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
||||||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
|
||||||
AudioSampleRate = audioSampleRate,
|
AudioSampleRate = audioSampleRate,
|
||||||
MaxAudioChannels = maxAudioChannels,
|
MaxAudioChannels = maxAudioChannels,
|
||||||
AudioBitRate = audioBitRate,
|
AudioBitRate = audioBitRate,
|
||||||
@@ -217,7 +214,6 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
||||||
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
||||||
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
||||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
|
||||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||||
@@ -274,7 +270,6 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
[FromQuery] bool? breakOnNonKeyFrames,
|
|
||||||
[FromQuery] int? audioSampleRate,
|
[FromQuery] int? audioSampleRate,
|
||||||
[FromQuery] int? maxAudioBitDepth,
|
[FromQuery] int? maxAudioBitDepth,
|
||||||
[FromQuery] int? audioBitRate,
|
[FromQuery] int? audioBitRate,
|
||||||
@@ -326,7 +321,6 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
||||||
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
||||||
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
||||||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
|
||||||
AudioSampleRate = audioSampleRate,
|
AudioSampleRate = audioSampleRate,
|
||||||
MaxAudioChannels = maxAudioChannels,
|
MaxAudioChannels = maxAudioChannels,
|
||||||
AudioBitRate = audioBitRate,
|
AudioBitRate = audioBitRate,
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ public class CollectionController : BaseJellyfinApiController
|
|||||||
UserIds = new[] { userId }
|
UserIds = new[] { userId }
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
var dtoOptions = new DtoOptions();
|
||||||
|
|
||||||
var dto = _dtoService.GetBaseItemDto(item, dtoOptions);
|
var dto = _dtoService.GetBaseItemDto(item, dtoOptions);
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
||||||
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
||||||
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
||||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
|
||||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||||
@@ -182,7 +181,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
[FromQuery] bool? breakOnNonKeyFrames,
|
|
||||||
[FromQuery] int? audioSampleRate,
|
[FromQuery] int? audioSampleRate,
|
||||||
[FromQuery] int? maxAudioBitDepth,
|
[FromQuery] int? maxAudioBitDepth,
|
||||||
[FromQuery] int? audioBitRate,
|
[FromQuery] int? audioBitRate,
|
||||||
@@ -238,7 +236,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
||||||
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
||||||
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
||||||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
|
||||||
AudioSampleRate = audioSampleRate,
|
AudioSampleRate = audioSampleRate,
|
||||||
MaxAudioChannels = maxAudioChannels,
|
MaxAudioChannels = maxAudioChannels,
|
||||||
AudioBitRate = audioBitRate,
|
AudioBitRate = audioBitRate,
|
||||||
@@ -364,7 +361,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
||||||
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
||||||
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
||||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
|
||||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||||
@@ -425,7 +421,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
[FromQuery] bool? breakOnNonKeyFrames,
|
|
||||||
[FromQuery] int? audioSampleRate,
|
[FromQuery] int? audioSampleRate,
|
||||||
[FromQuery] int? maxAudioBitDepth,
|
[FromQuery] int? maxAudioBitDepth,
|
||||||
[FromQuery] int? audioBitRate,
|
[FromQuery] int? audioBitRate,
|
||||||
@@ -481,7 +476,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
||||||
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
||||||
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
||||||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
|
||||||
AudioSampleRate = audioSampleRate,
|
AudioSampleRate = audioSampleRate,
|
||||||
MaxAudioChannels = maxAudioChannels,
|
MaxAudioChannels = maxAudioChannels,
|
||||||
AudioBitRate = audioBitRate,
|
AudioBitRate = audioBitRate,
|
||||||
@@ -543,7 +537,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
||||||
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
||||||
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
||||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
|
||||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||||
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
|
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
|
||||||
@@ -601,7 +594,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
[FromQuery] bool? breakOnNonKeyFrames,
|
|
||||||
[FromQuery] int? audioSampleRate,
|
[FromQuery] int? audioSampleRate,
|
||||||
[FromQuery] int? maxAudioBitDepth,
|
[FromQuery] int? maxAudioBitDepth,
|
||||||
[FromQuery] int? maxStreamingBitrate,
|
[FromQuery] int? maxStreamingBitrate,
|
||||||
@@ -654,7 +646,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
||||||
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
||||||
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
||||||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
|
||||||
AudioSampleRate = audioSampleRate,
|
AudioSampleRate = audioSampleRate,
|
||||||
MaxAudioChannels = maxAudioChannels,
|
MaxAudioChannels = maxAudioChannels,
|
||||||
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
|
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
|
||||||
@@ -713,7 +704,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
||||||
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
||||||
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
||||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
|
||||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||||
@@ -771,7 +761,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
[FromQuery] bool? breakOnNonKeyFrames,
|
|
||||||
[FromQuery] int? audioSampleRate,
|
[FromQuery] int? audioSampleRate,
|
||||||
[FromQuery] int? maxAudioBitDepth,
|
[FromQuery] int? maxAudioBitDepth,
|
||||||
[FromQuery] int? audioBitRate,
|
[FromQuery] int? audioBitRate,
|
||||||
@@ -826,7 +815,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
||||||
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
||||||
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
||||||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
|
||||||
AudioSampleRate = audioSampleRate,
|
AudioSampleRate = audioSampleRate,
|
||||||
MaxAudioChannels = maxAudioChannels,
|
MaxAudioChannels = maxAudioChannels,
|
||||||
AudioBitRate = audioBitRate,
|
AudioBitRate = audioBitRate,
|
||||||
@@ -887,7 +875,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
||||||
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
||||||
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
||||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
|
||||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||||
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
|
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
|
||||||
@@ -943,7 +930,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
[FromQuery] bool? breakOnNonKeyFrames,
|
|
||||||
[FromQuery] int? audioSampleRate,
|
[FromQuery] int? audioSampleRate,
|
||||||
[FromQuery] int? maxAudioBitDepth,
|
[FromQuery] int? maxAudioBitDepth,
|
||||||
[FromQuery] int? maxStreamingBitrate,
|
[FromQuery] int? maxStreamingBitrate,
|
||||||
@@ -996,7 +982,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
||||||
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
||||||
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
||||||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
|
||||||
AudioSampleRate = audioSampleRate,
|
AudioSampleRate = audioSampleRate,
|
||||||
MaxAudioChannels = maxAudioChannels,
|
MaxAudioChannels = maxAudioChannels,
|
||||||
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
|
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
|
||||||
@@ -1060,7 +1045,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
||||||
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
||||||
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
||||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
|
||||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||||
@@ -1124,7 +1108,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
[FromQuery] bool? breakOnNonKeyFrames,
|
|
||||||
[FromQuery] int? audioSampleRate,
|
[FromQuery] int? audioSampleRate,
|
||||||
[FromQuery] int? maxAudioBitDepth,
|
[FromQuery] int? maxAudioBitDepth,
|
||||||
[FromQuery] int? audioBitRate,
|
[FromQuery] int? audioBitRate,
|
||||||
@@ -1181,7 +1164,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
||||||
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
||||||
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
||||||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
|
||||||
AudioSampleRate = audioSampleRate,
|
AudioSampleRate = audioSampleRate,
|
||||||
MaxAudioChannels = maxAudioChannels,
|
MaxAudioChannels = maxAudioChannels,
|
||||||
AudioBitRate = audioBitRate,
|
AudioBitRate = audioBitRate,
|
||||||
@@ -1247,7 +1229,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
||||||
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
||||||
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
||||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
|
||||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||||
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
|
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
|
||||||
@@ -1309,7 +1290,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
[FromQuery] bool? breakOnNonKeyFrames,
|
|
||||||
[FromQuery] int? audioSampleRate,
|
[FromQuery] int? audioSampleRate,
|
||||||
[FromQuery] int? maxAudioBitDepth,
|
[FromQuery] int? maxAudioBitDepth,
|
||||||
[FromQuery] int? maxStreamingBitrate,
|
[FromQuery] int? maxStreamingBitrate,
|
||||||
@@ -1364,7 +1344,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
||||||
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
||||||
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
||||||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
|
||||||
AudioSampleRate = audioSampleRate,
|
AudioSampleRate = audioSampleRate,
|
||||||
MaxAudioChannels = maxAudioChannels,
|
MaxAudioChannels = maxAudioChannels,
|
||||||
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
|
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
|
||||||
@@ -1586,16 +1565,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
||||||
var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
|
var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
|
||||||
|
|
||||||
if (state.BaseRequest.BreakOnNonKeyFrames)
|
|
||||||
{
|
|
||||||
// FIXME: this is actually a workaround, as ideally it really should be the client which decides whether non-keyframe
|
|
||||||
// breakpoints are supported; but current implementation always uses "ffmpeg input seeking" which is liable
|
|
||||||
// to produce a missing part of video stream before first keyframe is encountered, which may lead to
|
|
||||||
// awkward cases like a few starting HLS segments having no video whatsoever, which breaks hls.js
|
|
||||||
_logger.LogInformation("Current HLS implementation doesn't support non-keyframe breaks but one is requested, ignoring that request");
|
|
||||||
state.BaseRequest.BreakOnNonKeyFrames = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
|
var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
|
||||||
|
|
||||||
var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
|
var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
|
||||||
@@ -1746,11 +1715,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
||||||
var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs;
|
var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs;
|
||||||
|
|
||||||
if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
|
|
||||||
{
|
|
||||||
return copyArgs + " -copypriorss:a:0 0";
|
|
||||||
}
|
|
||||||
|
|
||||||
return copyArgs;
|
return copyArgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1839,8 +1803,9 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
{
|
{
|
||||||
if (isActualOutputVideoCodecHevc)
|
if (isActualOutputVideoCodecHevc)
|
||||||
{
|
{
|
||||||
// Prefer dvh1 to dvhe
|
// Use hvc1 for 8.4. This is what Dolby uses for its official sample streams. Tagging with dvh1 would break some players with strict tag checking like Apple Safari.
|
||||||
args += " -tag:v:0 dvh1 -strict -2";
|
var codecTag = state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG ? "hvc1" : "dvh1";
|
||||||
|
args += $" -tag:v:0 {codecTag} -strict -2";
|
||||||
}
|
}
|
||||||
else if (isActualOutputVideoCodecAv1)
|
else if (isActualOutputVideoCodecAv1)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ public class GenresController : BaseJellyfinApiController
|
|||||||
{
|
{
|
||||||
userId = RequestHelpers.GetUserId(User, userId);
|
userId = RequestHelpers.GetUserId(User, userId);
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
|
||||||
|
|
||||||
User? user = userId.IsNullOrEmpty()
|
User? user = userId.IsNullOrEmpty()
|
||||||
@@ -159,8 +158,7 @@ public class GenresController : BaseJellyfinApiController
|
|||||||
public ActionResult<BaseItemDto> GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
|
public ActionResult<BaseItemDto> GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
|
||||||
{
|
{
|
||||||
userId = RequestHelpers.GetUserId(User, userId);
|
userId = RequestHelpers.GetUserId(User, userId);
|
||||||
var dtoOptions = new DtoOptions()
|
var dtoOptions = new DtoOptions();
|
||||||
.AddClientFields(User);
|
|
||||||
|
|
||||||
Genre? item;
|
Genre? item;
|
||||||
if (genreName.Contains(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase))
|
if (genreName.Contains(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ public class InstantMixController : BaseJellyfinApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||||
return GetResult(items, user, limit, dtoOptions);
|
return GetResult(items, user, limit, dtoOptions);
|
||||||
@@ -134,7 +133,6 @@ public class InstantMixController : BaseJellyfinApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||||
return GetResult(items, user, limit, dtoOptions);
|
return GetResult(items, user, limit, dtoOptions);
|
||||||
@@ -178,7 +176,6 @@ public class InstantMixController : BaseJellyfinApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||||
return GetResult(items, user, limit, dtoOptions);
|
return GetResult(items, user, limit, dtoOptions);
|
||||||
@@ -214,7 +211,6 @@ public class InstantMixController : BaseJellyfinApiController
|
|||||||
? null
|
? null
|
||||||
: _userManager.GetUserById(userId.Value);
|
: _userManager.GetUserById(userId.Value);
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions);
|
var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions);
|
||||||
return GetResult(items, user, limit, dtoOptions);
|
return GetResult(items, user, limit, dtoOptions);
|
||||||
@@ -258,7 +254,6 @@ public class InstantMixController : BaseJellyfinApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||||
return GetResult(items, user, limit, dtoOptions);
|
return GetResult(items, user, limit, dtoOptions);
|
||||||
@@ -302,7 +297,6 @@ public class InstantMixController : BaseJellyfinApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||||
return GetResult(items, user, limit, dtoOptions);
|
return GetResult(items, user, limit, dtoOptions);
|
||||||
@@ -385,7 +379,6 @@ public class InstantMixController : BaseJellyfinApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||||
return GetResult(items, user, limit, dtoOptions);
|
return GetResult(items, user, limit, dtoOptions);
|
||||||
|
|||||||
@@ -180,11 +180,14 @@ public class ItemUpdateController : BaseJellyfinApiController
|
|||||||
info.ContentTypeOptions = GetContentTypeOptions(true).ToArray();
|
info.ContentTypeOptions = GetContentTypeOptions(true).ToArray();
|
||||||
info.ContentType = configuredContentType;
|
info.ContentType = configuredContentType;
|
||||||
|
|
||||||
if (inheritedContentType is null || inheritedContentType == CollectionType.tvshows)
|
if (inheritedContentType is null
|
||||||
|
|| inheritedContentType == CollectionType.tvshows
|
||||||
|
|| inheritedContentType == CollectionType.movies)
|
||||||
{
|
{
|
||||||
info.ContentTypeOptions = info.ContentTypeOptions
|
info.ContentTypeOptions = info.ContentTypeOptions
|
||||||
.Where(i => string.IsNullOrWhiteSpace(i.Value)
|
.Where(i => string.IsNullOrWhiteSpace(i.Value)
|
||||||
|| string.Equals(i.Value, "TvShows", StringComparison.OrdinalIgnoreCase))
|
|| string.Equals(i.Value, "TvShows", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(i.Value, "Movies", StringComparison.OrdinalIgnoreCase))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,7 +421,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
|||||||
{
|
{
|
||||||
if (item is IHasAlbumArtist hasAlbumArtists)
|
if (item is IHasAlbumArtist hasAlbumArtists)
|
||||||
{
|
{
|
||||||
hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name);
|
hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name.Trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,7 +429,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
|||||||
{
|
{
|
||||||
if (item is IHasArtist hasArtists)
|
if (item is IHasArtist hasArtists)
|
||||||
{
|
{
|
||||||
hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name);
|
hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name.Trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -268,7 +268,6 @@ public class ItemsController : BaseJellyfinApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
|
||||||
if (includeItemTypes.Length == 1
|
if (includeItemTypes.Length == 1
|
||||||
@@ -849,7 +848,6 @@ public class ItemsController : BaseJellyfinApiController
|
|||||||
|
|
||||||
var parentIdGuid = parentId ?? Guid.Empty;
|
var parentIdGuid = parentId ?? Guid.Empty;
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
|
||||||
var ancestorIds = Array.Empty<Guid>();
|
var ancestorIds = Array.Empty<Guid>();
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ public class LibraryController : BaseJellyfinApiController
|
|||||||
item = parent;
|
item = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
var dtoOptions = new DtoOptions();
|
||||||
var items = themeItems
|
var items = themeItems
|
||||||
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
|
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
@@ -261,7 +261,7 @@ public class LibraryController : BaseJellyfinApiController
|
|||||||
item = parent;
|
item = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
var dtoOptions = new DtoOptions();
|
||||||
var items = themeItems
|
var items = themeItems
|
||||||
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
|
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
@@ -497,7 +497,7 @@ public class LibraryController : BaseJellyfinApiController
|
|||||||
|
|
||||||
var baseItemDtos = new List<BaseItemDto>();
|
var baseItemDtos = new List<BaseItemDto>();
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
var dtoOptions = new DtoOptions();
|
||||||
BaseItem? parent = item.GetParent();
|
BaseItem? parent = item.GetParent();
|
||||||
|
|
||||||
while (parent is not null)
|
while (parent is not null)
|
||||||
@@ -557,7 +557,7 @@ public class LibraryController : BaseJellyfinApiController
|
|||||||
items = items.Where(i => i.IsHidden == val).ToList();
|
items = items.Where(i => i.IsHidden == val).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
var dtoOptions = new DtoOptions();
|
||||||
var resultArray = _dtoService.GetBaseItemDtos(items, dtoOptions);
|
var resultArray = _dtoService.GetBaseItemDtos(items, dtoOptions);
|
||||||
return new QueryResult<BaseItemDto>(resultArray);
|
return new QueryResult<BaseItemDto>(resultArray);
|
||||||
}
|
}
|
||||||
@@ -759,8 +759,7 @@ public class LibraryController : BaseJellyfinApiController
|
|||||||
return new QueryResult<BaseItemDto>();
|
return new QueryResult<BaseItemDto>();
|
||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields };
|
||||||
.AddClientFields(User);
|
|
||||||
|
|
||||||
var program = item as IHasProgramAttributes;
|
var program = item as IHasProgramAttributes;
|
||||||
bool? isMovie = item is Movie || (program is not null && program.IsMovie) || item is Trailer;
|
bool? isMovie = item is Movie || (program is not null && program.IsMovie) || item is Trailer;
|
||||||
|
|||||||
@@ -342,6 +342,17 @@ public class LibraryStructureController : BaseJellyfinApiController
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LibraryOptions options = item.GetLibraryOptions();
|
||||||
|
foreach (var mediaPath in request.LibraryOptions!.PathInfos)
|
||||||
|
{
|
||||||
|
if (options.PathInfos.Any(i => i.Path == mediaPath.Path))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_libraryManager.CreateShortcut(item.Path, mediaPath);
|
||||||
|
}
|
||||||
|
|
||||||
item.UpdateLibraryOptions(request.LibraryOptions);
|
item.UpdateLibraryOptions(request.LibraryOptions);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,7 +170,6 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
{
|
{
|
||||||
userId = RequestHelpers.GetUserId(User, userId);
|
userId = RequestHelpers.GetUserId(User, userId);
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
|
||||||
var channelResult = _liveTvManager.GetInternalChannels(
|
var channelResult = _liveTvManager.GetInternalChannels(
|
||||||
@@ -242,8 +241,7 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions()
|
var dtoOptions = new DtoOptions();
|
||||||
.AddClientFields(User);
|
|
||||||
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
|
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,7 +295,6 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
{
|
{
|
||||||
userId = RequestHelpers.GetUserId(User, userId);
|
userId = RequestHelpers.GetUserId(User, userId);
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
|
||||||
return await _liveTvManager.GetRecordingsAsync(
|
return await _liveTvManager.GetRecordingsAsync(
|
||||||
@@ -444,8 +441,7 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions()
|
var dtoOptions = new DtoOptions();
|
||||||
.AddClientFields(User);
|
|
||||||
|
|
||||||
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
|
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
|
||||||
}
|
}
|
||||||
@@ -635,7 +631,6 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
|
return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -690,7 +685,6 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions { Fields = body.Fields ?? [] }
|
var dtoOptions = new DtoOptions { Fields = body.Fields ?? [] }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes ?? []);
|
.AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes ?? []);
|
||||||
return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
|
return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -760,7 +754,6 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
};
|
};
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
return await _liveTvManager.GetRecommendedProgramsAsync(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
|
return await _liveTvManager.GetRecommendedProgramsAsync(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,8 +74,7 @@ public class MoviesController : BaseJellyfinApiController
|
|||||||
var user = userId.IsNullOrEmpty()
|
var user = userId.IsNullOrEmpty()
|
||||||
? null
|
? null
|
||||||
: _userManager.GetUserById(userId.Value);
|
: _userManager.GetUserById(userId.Value);
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields };
|
||||||
.AddClientFields(User);
|
|
||||||
|
|
||||||
var categories = new List<RecommendationDto>();
|
var categories = new List<RecommendationDto>();
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ public class MusicGenresController : BaseJellyfinApiController
|
|||||||
{
|
{
|
||||||
userId = RequestHelpers.GetUserId(User, userId);
|
userId = RequestHelpers.GetUserId(User, userId);
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
|
||||||
|
|
||||||
User? user = userId.IsNullOrEmpty()
|
User? user = userId.IsNullOrEmpty()
|
||||||
@@ -148,7 +147,7 @@ public class MusicGenresController : BaseJellyfinApiController
|
|||||||
public ActionResult<BaseItemDto> GetMusicGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
|
public ActionResult<BaseItemDto> GetMusicGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
|
||||||
{
|
{
|
||||||
userId = RequestHelpers.GetUserId(User, userId);
|
userId = RequestHelpers.GetUserId(User, userId);
|
||||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
var dtoOptions = new DtoOptions();
|
||||||
|
|
||||||
MusicGenre? item;
|
MusicGenre? item;
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ public class PersonsController : BaseJellyfinApiController
|
|||||||
{
|
{
|
||||||
userId = RequestHelpers.GetUserId(User, userId);
|
userId = RequestHelpers.GetUserId(User, userId);
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
|
||||||
User? user = userId.IsNullOrEmpty()
|
User? user = userId.IsNullOrEmpty()
|
||||||
@@ -121,8 +120,7 @@ public class PersonsController : BaseJellyfinApiController
|
|||||||
public ActionResult<BaseItemDto> GetPerson([FromRoute, Required] string name, [FromQuery] Guid? userId)
|
public ActionResult<BaseItemDto> GetPerson([FromRoute, Required] string name, [FromQuery] Guid? userId)
|
||||||
{
|
{
|
||||||
userId = RequestHelpers.GetUserId(User, userId);
|
userId = RequestHelpers.GetUserId(User, userId);
|
||||||
var dtoOptions = new DtoOptions()
|
var dtoOptions = new DtoOptions();
|
||||||
.AddClientFields(User);
|
|
||||||
|
|
||||||
var item = _libraryManager.GetPerson(name);
|
var item = _libraryManager.GetPerson(name);
|
||||||
if (item is null)
|
if (item is null)
|
||||||
|
|||||||
@@ -548,7 +548,6 @@ public class PlaylistsController : BaseJellyfinApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
|
||||||
var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
|
var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ public class StudiosController : BaseJellyfinApiController
|
|||||||
{
|
{
|
||||||
userId = RequestHelpers.GetUserId(User, userId);
|
userId = RequestHelpers.GetUserId(User, userId);
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
|
||||||
User? user = userId.IsNullOrEmpty()
|
User? user = userId.IsNullOrEmpty()
|
||||||
@@ -142,7 +141,7 @@ public class StudiosController : BaseJellyfinApiController
|
|||||||
public ActionResult<BaseItemDto> GetStudio([FromRoute, Required] string name, [FromQuery] Guid? userId)
|
public ActionResult<BaseItemDto> GetStudio([FromRoute, Required] string name, [FromQuery] Guid? userId)
|
||||||
{
|
{
|
||||||
userId = RequestHelpers.GetUserId(User, userId);
|
userId = RequestHelpers.GetUserId(User, userId);
|
||||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
var dtoOptions = new DtoOptions();
|
||||||
|
|
||||||
var item = _libraryManager.GetStudio(name);
|
var item = _libraryManager.GetStudio(name);
|
||||||
if (!userId.IsNullOrEmpty())
|
if (!userId.IsNullOrEmpty())
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ public class SuggestionsController : BaseJellyfinApiController
|
|||||||
user = _userManager.GetUserById(requestUserId);
|
user = _userManager.GetUserById(requestUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
var dtoOptions = new DtoOptions();
|
||||||
var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
|
var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
|
||||||
{
|
{
|
||||||
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) },
|
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) },
|
||||||
|
|||||||
@@ -99,7 +99,6 @@ public class TvShowsController : BaseJellyfinApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
var options = new DtoOptions { Fields = fields }
|
var options = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
|
||||||
var result = _tvSeriesManager.GetNextUp(
|
var result = _tvSeriesManager.GetNextUp(
|
||||||
@@ -161,7 +160,6 @@ public class TvShowsController : BaseJellyfinApiController
|
|||||||
var parentIdGuid = parentId ?? Guid.Empty;
|
var parentIdGuid = parentId ?? Guid.Empty;
|
||||||
|
|
||||||
var options = new DtoOptions { Fields = fields }
|
var options = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
|
||||||
var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
||||||
@@ -231,7 +229,6 @@ public class TvShowsController : BaseJellyfinApiController
|
|||||||
List<BaseItem> episodes;
|
List<BaseItem> episodes;
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
var shouldIncludeMissingEpisodes = (user is not null && user.DisplayMissingEpisodes) || User.GetIsApiKey();
|
var shouldIncludeMissingEpisodes = (user is not null && user.DisplayMissingEpisodes) || User.GetIsApiKey();
|
||||||
|
|
||||||
@@ -360,7 +357,6 @@ public class TvShowsController : BaseJellyfinApiController
|
|||||||
});
|
});
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
|
||||||
var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user);
|
var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user);
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ public class UniversalAudioController : BaseJellyfinApiController
|
|||||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||||
/// <param name="enableRemoteMedia">Optional. Whether to enable remote media.</param>
|
/// <param name="enableRemoteMedia">Optional. Whether to enable remote media.</param>
|
||||||
/// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
|
/// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
|
||||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
|
||||||
/// <param name="enableRedirection">Whether to enable redirection. Defaults to true.</param>
|
/// <param name="enableRedirection">Whether to enable redirection. Defaults to true.</param>
|
||||||
/// <response code="200">Audio stream returned.</response>
|
/// <response code="200">Audio stream returned.</response>
|
||||||
/// <response code="302">Redirected to remote audio stream.</response>
|
/// <response code="302">Redirected to remote audio stream.</response>
|
||||||
@@ -114,7 +113,6 @@ public class UniversalAudioController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? maxAudioBitDepth,
|
[FromQuery] int? maxAudioBitDepth,
|
||||||
[FromQuery] bool? enableRemoteMedia,
|
[FromQuery] bool? enableRemoteMedia,
|
||||||
[FromQuery] bool enableAudioVbrEncoding = true,
|
[FromQuery] bool enableAudioVbrEncoding = true,
|
||||||
[FromQuery] bool breakOnNonKeyFrames = false,
|
|
||||||
[FromQuery] bool enableRedirection = true)
|
[FromQuery] bool enableRedirection = true)
|
||||||
{
|
{
|
||||||
userId = RequestHelpers.GetUserId(User, userId);
|
userId = RequestHelpers.GetUserId(User, userId);
|
||||||
@@ -127,7 +125,7 @@ public class UniversalAudioController : BaseJellyfinApiController
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
|
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
|
||||||
|
|
||||||
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
|
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
|
||||||
|
|
||||||
@@ -208,7 +206,6 @@ public class UniversalAudioController : BaseJellyfinApiController
|
|||||||
EnableAutoStreamCopy = true,
|
EnableAutoStreamCopy = true,
|
||||||
AllowAudioStreamCopy = true,
|
AllowAudioStreamCopy = true,
|
||||||
AllowVideoStreamCopy = true,
|
AllowVideoStreamCopy = true,
|
||||||
BreakOnNonKeyFrames = breakOnNonKeyFrames,
|
|
||||||
AudioSampleRate = maxAudioSampleRate,
|
AudioSampleRate = maxAudioSampleRate,
|
||||||
MaxAudioChannels = maxAudioChannels,
|
MaxAudioChannels = maxAudioChannels,
|
||||||
MaxAudioBitDepth = maxAudioBitDepth,
|
MaxAudioBitDepth = maxAudioBitDepth,
|
||||||
@@ -242,7 +239,6 @@ public class UniversalAudioController : BaseJellyfinApiController
|
|||||||
EnableAutoStreamCopy = true,
|
EnableAutoStreamCopy = true,
|
||||||
AllowAudioStreamCopy = true,
|
AllowAudioStreamCopy = true,
|
||||||
AllowVideoStreamCopy = true,
|
AllowVideoStreamCopy = true,
|
||||||
BreakOnNonKeyFrames = breakOnNonKeyFrames,
|
|
||||||
AudioSampleRate = maxAudioSampleRate,
|
AudioSampleRate = maxAudioSampleRate,
|
||||||
MaxAudioChannels = maxAudioChannels,
|
MaxAudioChannels = maxAudioChannels,
|
||||||
AudioBitRate = isStatic ? null : (audioBitRate ?? maxStreamingBitrate),
|
AudioBitRate = isStatic ? null : (audioBitRate ?? maxStreamingBitrate),
|
||||||
@@ -263,7 +259,6 @@ public class UniversalAudioController : BaseJellyfinApiController
|
|||||||
string? transcodingContainer,
|
string? transcodingContainer,
|
||||||
string? audioCodec,
|
string? audioCodec,
|
||||||
MediaStreamProtocol? transcodingProtocol,
|
MediaStreamProtocol? transcodingProtocol,
|
||||||
bool? breakOnNonKeyFrames,
|
|
||||||
int? transcodingAudioChannels,
|
int? transcodingAudioChannels,
|
||||||
int? maxAudioSampleRate,
|
int? maxAudioSampleRate,
|
||||||
int? maxAudioBitDepth,
|
int? maxAudioBitDepth,
|
||||||
@@ -298,7 +293,6 @@ public class UniversalAudioController : BaseJellyfinApiController
|
|||||||
Container = transcodingContainer ?? "mp3",
|
Container = transcodingContainer ?? "mp3",
|
||||||
AudioCodec = audioCodec ?? "mp3",
|
AudioCodec = audioCodec ?? "mp3",
|
||||||
Protocol = transcodingProtocol ?? MediaStreamProtocol.http,
|
Protocol = transcodingProtocol ?? MediaStreamProtocol.http,
|
||||||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
|
||||||
MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
|
MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using Jellyfin.Extensions;
|
|||||||
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.TV;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
@@ -94,7 +95,7 @@ public class UserLibraryController : BaseJellyfinApiController
|
|||||||
|
|
||||||
await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
|
await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
var dtoOptions = new DtoOptions();
|
||||||
|
|
||||||
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
|
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
|
||||||
}
|
}
|
||||||
@@ -133,7 +134,7 @@ public class UserLibraryController : BaseJellyfinApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
var item = _libraryManager.GetUserRootFolder();
|
var item = _libraryManager.GetUserRootFolder();
|
||||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
var dtoOptions = new DtoOptions();
|
||||||
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
|
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +181,7 @@ public class UserLibraryController : BaseJellyfinApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
|
var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
|
||||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
var dtoOptions = new DtoOptions();
|
||||||
var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();
|
var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();
|
||||||
|
|
||||||
return new QueryResult<BaseItemDto>(dtos);
|
return new QueryResult<BaseItemDto>(dtos);
|
||||||
@@ -422,7 +423,7 @@ public class UserLibraryController : BaseJellyfinApiController
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
var dtoOptions = new DtoOptions();
|
||||||
if (item is IHasTrailers hasTrailers)
|
if (item is IHasTrailers hasTrailers)
|
||||||
{
|
{
|
||||||
var trailers = hasTrailers.LocalTrailers;
|
var trailers = hasTrailers.LocalTrailers;
|
||||||
@@ -478,7 +479,7 @@ public class UserLibraryController : BaseJellyfinApiController
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
var dtoOptions = new DtoOptions();
|
||||||
|
|
||||||
return Ok(item
|
return Ok(item
|
||||||
.GetExtras()
|
.GetExtras()
|
||||||
@@ -549,7 +550,6 @@ public class UserLibraryController : BaseJellyfinApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
|
||||||
var list = _userViewManager.GetLatestItems(
|
var list = _userViewManager.GetLatestItems(
|
||||||
@@ -569,7 +569,7 @@ public class UserLibraryController : BaseJellyfinApiController
|
|||||||
var item = i.Item2[0];
|
var item = i.Item2[0];
|
||||||
var childCount = 0;
|
var childCount = 0;
|
||||||
|
|
||||||
if (i.Item1 is not null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum))
|
if (i.Item1 is not null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum || i.Item1 is Series ))
|
||||||
{
|
{
|
||||||
item = i.Item1;
|
item = i.Item1;
|
||||||
childCount = i.Item2.Count;
|
childCount = i.Item2.Count;
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ public class UserViewsController : BaseJellyfinApiController
|
|||||||
|
|
||||||
var folders = _userViewManager.GetUserViews(query);
|
var folders = _userViewManager.GetUserViews(query);
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
var dtoOptions = new DtoOptions();
|
||||||
dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.PrimaryImageAspectRatio, ItemFields.DisplayPreferencesId];
|
dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.PrimaryImageAspectRatio, ItemFields.DisplayPreferencesId];
|
||||||
|
|
||||||
var dtos = Array.ConvertAll(folders, i => _dtoService.GetBaseItemDto(i, dtoOptions, user));
|
var dtos = Array.ConvertAll(folders, i => _dtoService.GetBaseItemDto(i, dtoOptions, user));
|
||||||
|
|||||||
@@ -111,7 +111,6 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions();
|
var dtoOptions = new DtoOptions();
|
||||||
dtoOptions = dtoOptions.AddClientFields(User);
|
|
||||||
|
|
||||||
BaseItemDto[] items;
|
BaseItemDto[] items;
|
||||||
if (item is Video video)
|
if (item is Video video)
|
||||||
@@ -271,7 +270,6 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
||||||
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
||||||
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
||||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
|
||||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||||
@@ -330,7 +328,6 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
[FromQuery] bool? breakOnNonKeyFrames,
|
|
||||||
[FromQuery] int? audioSampleRate,
|
[FromQuery] int? audioSampleRate,
|
||||||
[FromQuery] int? maxAudioBitDepth,
|
[FromQuery] int? maxAudioBitDepth,
|
||||||
[FromQuery] int? audioBitRate,
|
[FromQuery] int? audioBitRate,
|
||||||
@@ -387,7 +384,6 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
||||||
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
||||||
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
||||||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
|
||||||
AudioSampleRate = audioSampleRate,
|
AudioSampleRate = audioSampleRate,
|
||||||
MaxAudioChannels = maxAudioChannels,
|
MaxAudioChannels = maxAudioChannels,
|
||||||
AudioBitRate = audioBitRate,
|
AudioBitRate = audioBitRate,
|
||||||
@@ -512,7 +508,6 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
||||||
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
||||||
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
||||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
|
||||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||||
@@ -571,7 +566,6 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
[FromQuery] bool? breakOnNonKeyFrames,
|
|
||||||
[FromQuery] int? audioSampleRate,
|
[FromQuery] int? audioSampleRate,
|
||||||
[FromQuery] int? maxAudioBitDepth,
|
[FromQuery] int? maxAudioBitDepth,
|
||||||
[FromQuery] int? audioBitRate,
|
[FromQuery] int? audioBitRate,
|
||||||
@@ -625,7 +619,6 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
enableAutoStreamCopy,
|
enableAutoStreamCopy,
|
||||||
allowVideoStreamCopy,
|
allowVideoStreamCopy,
|
||||||
allowAudioStreamCopy,
|
allowAudioStreamCopy,
|
||||||
breakOnNonKeyFrames,
|
|
||||||
audioSampleRate,
|
audioSampleRate,
|
||||||
maxAudioBitDepth,
|
maxAudioBitDepth,
|
||||||
audioBitRate,
|
audioBitRate,
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ public class YearsController : BaseJellyfinApiController
|
|||||||
{
|
{
|
||||||
userId = RequestHelpers.GetUserId(User, userId);
|
userId = RequestHelpers.GetUserId(User, userId);
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
|
||||||
User? user = userId.IsNullOrEmpty()
|
User? user = userId.IsNullOrEmpty()
|
||||||
@@ -182,8 +181,7 @@ public class YearsController : BaseJellyfinApiController
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions()
|
var dtoOptions = new DtoOptions();
|
||||||
.AddClientFields(User);
|
|
||||||
|
|
||||||
if (!userId.IsNullOrEmpty())
|
if (!userId.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Security.Claims;
|
|
||||||
using Jellyfin.Extensions;
|
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Querying;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Extensions;
|
namespace Jellyfin.Api.Extensions;
|
||||||
|
|
||||||
@@ -13,55 +9,6 @@ namespace Jellyfin.Api.Extensions;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class DtoExtensions
|
public static class DtoExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Add additional fields depending on client.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Use in place of GetDtoOptions.
|
|
||||||
/// Legacy order: 2.
|
|
||||||
/// </remarks>
|
|
||||||
/// <param name="dtoOptions">DtoOptions object.</param>
|
|
||||||
/// <param name="user">Current claims principal.</param>
|
|
||||||
/// <returns>Modified DtoOptions object.</returns>
|
|
||||||
internal static DtoOptions AddClientFields(
|
|
||||||
this DtoOptions dtoOptions, ClaimsPrincipal user)
|
|
||||||
{
|
|
||||||
string? client = user.GetClient();
|
|
||||||
|
|
||||||
// No client in claim
|
|
||||||
if (string.IsNullOrEmpty(client))
|
|
||||||
{
|
|
||||||
return dtoOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dtoOptions.ContainsField(ItemFields.RecursiveItemCount))
|
|
||||||
{
|
|
||||||
if (client.Contains("kodi", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
client.Contains("wmc", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
client.Contains("media center", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
client.Contains("classic", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.RecursiveItemCount];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dtoOptions.ContainsField(ItemFields.ChildCount))
|
|
||||||
{
|
|
||||||
if (client.Contains("kodi", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
client.Contains("wmc", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
client.Contains("media center", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
client.Contains("classic", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
client.Contains("roku", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
client.Contains("samsung", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
client.Contains("androidtv", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.ChildCount];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dtoOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add additional DtoOptions.
|
/// Add additional DtoOptions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ public class DynamicHlsHelper
|
|||||||
// from universal audio service, need to override the AudioCodec when the actual request differs from original query
|
// from universal audio service, need to override the AudioCodec when the actual request differs from original query
|
||||||
if (!string.Equals(state.OutputAudioCodec, _httpContextAccessor.HttpContext.Request.Query["AudioCodec"].ToString(), StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(state.OutputAudioCodec, _httpContextAccessor.HttpContext.Request.Query["AudioCodec"].ToString(), StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(_httpContextAccessor.HttpContext.Request.QueryString.ToString());
|
var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString);
|
||||||
newQuery["AudioCodec"] = state.OutputAudioCodec;
|
newQuery["AudioCodec"] = state.OutputAudioCodec;
|
||||||
queryString = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(string.Empty, newQuery);
|
queryString = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(string.Empty, newQuery);
|
||||||
}
|
}
|
||||||
@@ -173,10 +173,21 @@ public class DynamicHlsHelper
|
|||||||
queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
|
queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main stream
|
// Video rotation metadata is only supported in fMP4 remuxing
|
||||||
var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
|
if (state.VideoStream is not null
|
||||||
|
&& state.VideoRequest is not null
|
||||||
|
&& (state.VideoStream?.Rotation ?? 0) != 0
|
||||||
|
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||||
|
&& !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
|
||||||
|
&& !string.Equals(state.Request.SegmentContainer, "mp4", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
queryString += "&AllowVideoStreamCopy=false";
|
||||||
|
}
|
||||||
|
|
||||||
playlistUrl += queryString;
|
// Main stream
|
||||||
|
var baseUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
|
||||||
|
var playlistUrl = baseUrl + queryString;
|
||||||
|
var playlistQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString);
|
||||||
|
|
||||||
var subtitleStreams = state.MediaSource
|
var subtitleStreams = state.MediaSource
|
||||||
.MediaStreams
|
.MediaStreams
|
||||||
@@ -198,37 +209,36 @@ public class DynamicHlsHelper
|
|||||||
AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
|
AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Video rotation metadata is only supported in fMP4 remuxing
|
|
||||||
if (state.VideoStream is not null
|
|
||||||
&& state.VideoRequest is not null
|
|
||||||
&& (state.VideoStream?.Rotation ?? 0) != 0
|
|
||||||
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
|
||||||
&& !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
|
|
||||||
&& !string.Equals(state.Request.SegmentContainer, "mp4", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
playlistUrl += "&AllowVideoStreamCopy=false";
|
|
||||||
}
|
|
||||||
|
|
||||||
var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
|
var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
|
||||||
|
|
||||||
if (state.VideoStream is not null && state.VideoRequest is not null)
|
if (state.VideoStream is not null && state.VideoRequest is not null)
|
||||||
{
|
{
|
||||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||||
|
|
||||||
// Provide SDR HEVC entrance for backward compatibility.
|
// Provide AV1 and HEVC SDR entrances for backward compatibility.
|
||||||
if (encodingOptions.AllowHevcEncoding
|
foreach (var sdrVideoCodec in new[] { "av1", "hevc" })
|
||||||
&& !encodingOptions.AllowAv1Encoding
|
|
||||||
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
|
||||||
&& state.VideoStream.VideoRange == VideoRange.HDR
|
|
||||||
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
var requestedVideoProfiles = state.GetRequestedProfiles("hevc");
|
var isAv1EncodingAllowed = encodingOptions.AllowAv1Encoding
|
||||||
if (requestedVideoProfiles is not null && requestedVideoProfiles.Length > 0)
|
&& string.Equals(sdrVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var isHevcEncodingAllowed = encodingOptions.AllowHevcEncoding
|
||||||
|
&& string.Equals(sdrVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var isEncodingAllowed = isAv1EncodingAllowed || isHevcEncodingAllowed;
|
||||||
|
|
||||||
|
if (isEncodingAllowed
|
||||||
|
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||||
|
&& state.VideoStream.VideoRange == VideoRange.HDR)
|
||||||
{
|
{
|
||||||
// Force HEVC Main Profile and disable video stream copy.
|
// Force AV1 and HEVC Main Profile and disable video stream copy.
|
||||||
state.OutputVideoCodec = "hevc";
|
state.OutputVideoCodec = sdrVideoCodec;
|
||||||
var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main");
|
|
||||||
sdrVideoUrl += "&AllowVideoStreamCopy=false";
|
var sdrPlaylistQuery = playlistQuery;
|
||||||
|
sdrPlaylistQuery["VideoCodec"] = sdrVideoCodec;
|
||||||
|
sdrPlaylistQuery[sdrVideoCodec + "-profile"] = "main";
|
||||||
|
sdrPlaylistQuery["AllowVideoStreamCopy"] = "false";
|
||||||
|
|
||||||
|
var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylistQuery);
|
||||||
|
|
||||||
// HACK: Use the same bitrate so that the client can choose by other attributes, such as color range.
|
// HACK: Use the same bitrate so that the client can choose by other attributes, such as color range.
|
||||||
AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
|
AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
|
||||||
@@ -238,12 +248,30 @@ public class DynamicHlsHelper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Provide H.264 SDR entrance for backward compatibility.
|
||||||
|
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||||
|
&& state.VideoStream.VideoRange == VideoRange.HDR)
|
||||||
|
{
|
||||||
|
// Force H.264 and disable video stream copy.
|
||||||
|
state.OutputVideoCodec = "h264";
|
||||||
|
|
||||||
|
var sdrPlaylistQuery = playlistQuery;
|
||||||
|
sdrPlaylistQuery["VideoCodec"] = "h264";
|
||||||
|
sdrPlaylistQuery["AllowVideoStreamCopy"] = "false";
|
||||||
|
|
||||||
|
var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylistQuery);
|
||||||
|
|
||||||
|
// HACK: Use the same bitrate so that the client can choose by other attributes, such as color range.
|
||||||
|
AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
|
||||||
|
|
||||||
|
// Restore the video codec
|
||||||
|
state.OutputVideoCodec = "copy";
|
||||||
|
}
|
||||||
|
|
||||||
// Provide Level 5.0 entrance for backward compatibility.
|
// Provide Level 5.0 entrance for backward compatibility.
|
||||||
// e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
|
// e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
|
||||||
// but in fact it is capable of playing videos up to Level 6.1.
|
// but in fact it is capable of playing videos up to Level 6.1.
|
||||||
if (encodingOptions.AllowHevcEncoding
|
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||||
&& !encodingOptions.AllowAv1Encoding
|
|
||||||
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
|
||||||
&& state.VideoStream.Level.HasValue
|
&& state.VideoStream.Level.HasValue
|
||||||
&& state.VideoStream.Level > 150
|
&& state.VideoStream.Level > 150
|
||||||
&& state.VideoStream.VideoRange == VideoRange.SDR
|
&& state.VideoStream.VideoRange == VideoRange.SDR
|
||||||
@@ -273,12 +301,15 @@ public class DynamicHlsHelper
|
|||||||
var variation = GetBitrateVariation(totalBitrate);
|
var variation = GetBitrateVariation(totalBitrate);
|
||||||
|
|
||||||
var newBitrate = totalBitrate - variation;
|
var newBitrate = totalBitrate - variation;
|
||||||
var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
|
var variantQuery = playlistQuery;
|
||||||
|
variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture);
|
||||||
|
var variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery);
|
||||||
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
|
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
|
||||||
|
|
||||||
variation *= 2;
|
variation *= 2;
|
||||||
newBitrate = totalBitrate - variation;
|
newBitrate = totalBitrate - variation;
|
||||||
variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
|
variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture);
|
||||||
|
variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery);
|
||||||
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
|
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -723,7 +754,9 @@ public class DynamicHlsHelper
|
|||||||
{
|
{
|
||||||
if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
string? profile = state.GetRequestedProfiles("aac").FirstOrDefault();
|
string? profile = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
|
||||||
|
? state.AudioStream?.Profile : state.GetRequestedProfiles("aac").FirstOrDefault();
|
||||||
|
|
||||||
return HlsCodecStringHelpers.GetAACString(profile);
|
return HlsCodecStringHelpers.GetAACString(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -757,6 +790,19 @@ public class DynamicHlsHelper
|
|||||||
return HlsCodecStringHelpers.GetOPUSString();
|
return HlsCodecStringHelpers.GetOPUSString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(state.ActualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return HlsCodecStringHelpers.GetTRUEHDString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(state.ActualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// lavc only support encoding DTS core profile
|
||||||
|
string? profile = EncodingHelper.IsCopyCodec(state.OutputAudioCodec) ? state.AudioStream?.Profile : "DTS";
|
||||||
|
|
||||||
|
return HlsCodecStringHelpers.GetDTSString(profile);
|
||||||
|
}
|
||||||
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -863,23 +909,6 @@ public class DynamicHlsHelper
|
|||||||
return variation;
|
return variation;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ReplaceVideoBitrate(string url, int oldValue, int newValue)
|
|
||||||
{
|
|
||||||
return url.Replace(
|
|
||||||
"videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
|
|
||||||
"videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
|
|
||||||
StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string ReplaceProfile(string url, string codec, string oldValue, string newValue)
|
|
||||||
{
|
|
||||||
string profileStr = codec + "-profile=";
|
|
||||||
return url.Replace(
|
|
||||||
profileStr + oldValue,
|
|
||||||
profileStr + newValue,
|
|
||||||
StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
|
private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
|
||||||
{
|
{
|
||||||
var oldPlaylist = playlist.ToString();
|
var oldPlaylist = playlist.ToString();
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ public static class HlsCodecStringHelpers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const string OPUS = "Opus";
|
public const string OPUS = "Opus";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Codec name for TRUEHD.
|
||||||
|
/// </summary>
|
||||||
|
public const string TRUEHD = "mlpa";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a MP3 codec string.
|
/// Gets a MP3 codec string.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -59,7 +64,7 @@ public static class HlsCodecStringHelpers
|
|||||||
{
|
{
|
||||||
StringBuilder result = new StringBuilder("mp4a", 9);
|
StringBuilder result = new StringBuilder("mp4a", 9);
|
||||||
|
|
||||||
if (string.Equals(profile, "HE", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(profile, "HE-AAC", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
result.Append(".40.5");
|
result.Append(".40.5");
|
||||||
}
|
}
|
||||||
@@ -117,6 +122,46 @@ public static class HlsCodecStringHelpers
|
|||||||
return OPUS;
|
return OPUS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an TRUEHD codec string.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>TRUEHD codec string.</returns>
|
||||||
|
public static string GetTRUEHDString()
|
||||||
|
{
|
||||||
|
return TRUEHD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an DTS codec string.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="profile">DTS profile.</param>
|
||||||
|
/// <returns>DTS codec string.</returns>
|
||||||
|
public static string GetDTSString(string? profile)
|
||||||
|
{
|
||||||
|
if (string.Equals(profile, "DTS", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(profile, "DTS-ES", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(profile, "DTS 96/24", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return "dtsc";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(profile, "DTS-HD HRA", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(profile, "DTS-HD MA", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(profile, "DTS-HD MA + DTS:X", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(profile, "DTS-HD MA + DTS:X IMAX", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return "dtsh";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(profile, "DTS Express", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return "dtse";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to DTS core if profile is invalid
|
||||||
|
return "dtsc";
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a H.264 codec string.
|
/// Gets a H.264 codec string.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -45,15 +45,9 @@ public static class HlsHelpers
|
|||||||
using var reader = new StreamReader(fileStream);
|
using var reader = new StreamReader(fileStream);
|
||||||
var count = 0;
|
var count = 0;
|
||||||
|
|
||||||
while (!reader.EndOfStream)
|
string? line;
|
||||||
|
while ((line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) is not null)
|
||||||
{
|
{
|
||||||
var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
if (line is null)
|
|
||||||
{
|
|
||||||
// Nothing currently in buffer.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.Contains("#EXTINF:", StringComparison.OrdinalIgnoreCase))
|
if (line.Contains("#EXTINF:", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
count++;
|
count++;
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ public static class StreamingHelpers
|
|||||||
state.OutputVideoCodec = state.Request.VideoCodec;
|
state.OutputVideoCodec = state.Request.VideoCodec;
|
||||||
state.OutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
|
state.OutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
|
||||||
|
|
||||||
encodingHelper.TryStreamCopy(state);
|
encodingHelper.TryStreamCopy(state, encodingOptions);
|
||||||
|
|
||||||
if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.OutputVideoBitrate.HasValue)
|
if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.OutputVideoBitrate.HasValue)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Middleware;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes /emby and /mediabrowser from requested route.
|
|
||||||
/// </summary>
|
|
||||||
public class LegacyEmbyRouteRewriteMiddleware
|
|
||||||
{
|
|
||||||
private const string EmbyPath = "/emby";
|
|
||||||
private const string MediabrowserPath = "/mediabrowser";
|
|
||||||
|
|
||||||
private readonly RequestDelegate _next;
|
|
||||||
private readonly ILogger<LegacyEmbyRouteRewriteMiddleware> _logger;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="LegacyEmbyRouteRewriteMiddleware"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="next">The next delegate in the pipeline.</param>
|
|
||||||
/// <param name="logger">The logger.</param>
|
|
||||||
public LegacyEmbyRouteRewriteMiddleware(
|
|
||||||
RequestDelegate next,
|
|
||||||
ILogger<LegacyEmbyRouteRewriteMiddleware> logger)
|
|
||||||
{
|
|
||||||
_next = next;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Executes the middleware action.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="httpContext">The current HTTP context.</param>
|
|
||||||
/// <returns>The async task.</returns>
|
|
||||||
public async Task Invoke(HttpContext httpContext)
|
|
||||||
{
|
|
||||||
var localPath = httpContext.Request.Path.ToString();
|
|
||||||
if (localPath.StartsWith(EmbyPath, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
httpContext.Request.Path = localPath[EmbyPath.Length..];
|
|
||||||
_logger.LogDebug("Removing {EmbyPath} from route.", EmbyPath);
|
|
||||||
}
|
|
||||||
else if (localPath.StartsWith(MediabrowserPath, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
httpContext.Request.Path = localPath[MediabrowserPath.Length..];
|
|
||||||
_logger.LogDebug("Removing {MediabrowserPath} from route.", MediabrowserPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _next(httpContext).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
49
Jellyfin.Data/Enums/ActivityLogSortBy.cs
Normal file
49
Jellyfin.Data/Enums/ActivityLogSortBy.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
namespace Jellyfin.Data.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Activity log sorting options.
|
||||||
|
/// </summary>
|
||||||
|
public enum ActivityLogSortBy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sort by name.
|
||||||
|
/// </summary>
|
||||||
|
Name = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sort by overview.
|
||||||
|
/// </summary>
|
||||||
|
Overiew = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sort by short overview.
|
||||||
|
/// </summary>
|
||||||
|
ShortOverview = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sort by type.
|
||||||
|
/// </summary>
|
||||||
|
Type = 3,
|
||||||
|
|
||||||
|
/*
|
||||||
|
/// <summary>
|
||||||
|
/// Sort by item name.
|
||||||
|
/// </summary>
|
||||||
|
Item = 4,
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sort by date.
|
||||||
|
/// </summary>
|
||||||
|
DateCreated = 5,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sort by username.
|
||||||
|
/// </summary>
|
||||||
|
Username = 6,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sort by severity.
|
||||||
|
/// </summary>
|
||||||
|
LogSeverity = 7
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Data</PackageId>
|
<PackageId>Jellyfin.Data</PackageId>
|
||||||
<VersionPrefix>10.11.4</VersionPrefix>
|
<VersionPrefix>10.12.0</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1,20 +1,68 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Database.Implementations.Enums;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Jellyfin.Data.Queries
|
namespace Jellyfin.Data.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A class representing a query to the activity logs.
|
||||||
|
/// </summary>
|
||||||
|
public class ActivityLogQuery : PaginatedQuery
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A class representing a query to the activity logs.
|
/// Gets or sets a value indicating whether to take entries with a user id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ActivityLogQuery : PaginatedQuery
|
public bool? HasUserId { get; set; }
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether to take entries with a user id.
|
|
||||||
/// </summary>
|
|
||||||
public bool? HasUserId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the minimum date to query for.
|
/// Gets or sets the minimum date to query for.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? MinDate { get; set; }
|
public DateTime? MinDate { get; set; }
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the maximum date to query for.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? MaxDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name filter.
|
||||||
|
/// </summary>
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the overview filter.
|
||||||
|
/// </summary>
|
||||||
|
public string? Overview { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the short overview filter.
|
||||||
|
/// </summary>
|
||||||
|
public string? ShortOverview { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the type filter.
|
||||||
|
/// </summary>
|
||||||
|
public string? Type { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the item filter.
|
||||||
|
/// </summary>
|
||||||
|
public Guid? ItemId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the username filter.
|
||||||
|
/// </summary>
|
||||||
|
public string? Username { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the log level filter.
|
||||||
|
/// </summary>
|
||||||
|
public LogLevel? Severity { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the result ordering.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<(ActivityLogSortBy, SortOrder)>? OrderBy { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,103 +1,213 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Data.Events;
|
using Jellyfin.Data.Events;
|
||||||
using Jellyfin.Data.Queries;
|
using Jellyfin.Data.Queries;
|
||||||
using Jellyfin.Database.Implementations;
|
using Jellyfin.Database.Implementations;
|
||||||
using Jellyfin.Database.Implementations.Entities;
|
using Jellyfin.Database.Implementations.Entities;
|
||||||
|
using Jellyfin.Database.Implementations.Enums;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Model.Activity;
|
using MediaBrowser.Model.Activity;
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Jellyfin.Server.Implementations.Activity
|
namespace Jellyfin.Server.Implementations.Activity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manages the storage and retrieval of <see cref="ActivityLog"/> instances.
|
||||||
|
/// </summary>
|
||||||
|
public class ActivityManager : IActivityManager
|
||||||
{
|
{
|
||||||
|
private readonly IDbContextFactory<JellyfinDbContext> _provider;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manages the storage and retrieval of <see cref="ActivityLog"/> instances.
|
/// Initializes a new instance of the <see cref="ActivityManager"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ActivityManager : IActivityManager
|
/// <param name="provider">The Jellyfin database provider.</param>
|
||||||
|
public ActivityManager(IDbContextFactory<JellyfinDbContext> provider)
|
||||||
{
|
{
|
||||||
private readonly IDbContextFactory<JellyfinDbContext> _provider;
|
_provider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Initializes a new instance of the <see cref="ActivityManager"/> class.
|
public event EventHandler<GenericEventArgs<ActivityLogEntry>>? EntryCreated;
|
||||||
/// </summary>
|
|
||||||
/// <param name="provider">The Jellyfin database provider.</param>
|
/// <inheritdoc/>
|
||||||
public ActivityManager(IDbContextFactory<JellyfinDbContext> provider)
|
public async Task CreateAsync(ActivityLog entry)
|
||||||
|
{
|
||||||
|
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
_provider = provider;
|
dbContext.ActivityLogs.Add(entry);
|
||||||
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry)));
|
||||||
public event EventHandler<GenericEventArgs<ActivityLogEntry>>? EntryCreated;
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task CreateAsync(ActivityLog entry)
|
public async Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query)
|
||||||
|
{
|
||||||
|
// TODO allow sorting and filtering by item id. Currently not possible because ActivityLog stores the item id as a string.
|
||||||
|
|
||||||
|
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
|
// TODO switch to LeftJoin in .NET 10.
|
||||||
await using (dbContext.ConfigureAwait(false))
|
var entries = from a in dbContext.ActivityLogs
|
||||||
|
join u in dbContext.Users on a.UserId equals u.Id into ugj
|
||||||
|
from u in ugj.DefaultIfEmpty()
|
||||||
|
select new ExpandedActivityLog { ActivityLog = a, Username = u.Username };
|
||||||
|
|
||||||
|
if (query.HasUserId is not null)
|
||||||
{
|
{
|
||||||
dbContext.ActivityLogs.Add(entry);
|
entries = entries.Where(e => e.ActivityLog.UserId.Equals(default) != query.HasUserId.Value);
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry)));
|
if (query.MinDate is not null)
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public async Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query)
|
|
||||||
{
|
|
||||||
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
|
|
||||||
await using (dbContext.ConfigureAwait(false))
|
|
||||||
{
|
{
|
||||||
var entries = dbContext.ActivityLogs
|
entries = entries.Where(e => e.ActivityLog.DateCreated >= query.MinDate.Value);
|
||||||
.OrderByDescending(entry => entry.DateCreated)
|
|
||||||
.Where(entry => query.MinDate == null || entry.DateCreated >= query.MinDate)
|
|
||||||
.Where(entry => !query.HasUserId.HasValue || entry.UserId.Equals(default) != query.HasUserId.Value);
|
|
||||||
|
|
||||||
return new QueryResult<ActivityLogEntry>(
|
|
||||||
query.Skip,
|
|
||||||
await entries.CountAsync().ConfigureAwait(false),
|
|
||||||
await entries
|
|
||||||
.Skip(query.Skip ?? 0)
|
|
||||||
.Take(query.Limit ?? 100)
|
|
||||||
.Select(entity => new ActivityLogEntry(entity.Name, entity.Type, entity.UserId)
|
|
||||||
{
|
|
||||||
Id = entity.Id,
|
|
||||||
Overview = entity.Overview,
|
|
||||||
ShortOverview = entity.ShortOverview,
|
|
||||||
ItemId = entity.ItemId,
|
|
||||||
Date = entity.DateCreated,
|
|
||||||
Severity = entity.LogSeverity
|
|
||||||
})
|
|
||||||
.ToListAsync()
|
|
||||||
.ConfigureAwait(false));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
if (query.MaxDate is not null)
|
||||||
public async Task CleanAsync(DateTime startDate)
|
|
||||||
{
|
|
||||||
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
|
|
||||||
await using (dbContext.ConfigureAwait(false))
|
|
||||||
{
|
{
|
||||||
await dbContext.ActivityLogs
|
entries = entries.Where(e => e.ActivityLog.DateCreated <= query.MaxDate.Value);
|
||||||
.Where(entry => entry.DateCreated <= startDate)
|
|
||||||
.ExecuteDeleteAsync()
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private static ActivityLogEntry ConvertToOldModel(ActivityLog entry)
|
if (!string.IsNullOrEmpty(query.Name))
|
||||||
{
|
|
||||||
return new ActivityLogEntry(entry.Name, entry.Type, entry.UserId)
|
|
||||||
{
|
{
|
||||||
Id = entry.Id,
|
entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.Name, $"%{query.Name}%"));
|
||||||
Overview = entry.Overview,
|
}
|
||||||
ShortOverview = entry.ShortOverview,
|
|
||||||
ItemId = entry.ItemId,
|
if (!string.IsNullOrEmpty(query.Overview))
|
||||||
Date = entry.DateCreated,
|
{
|
||||||
Severity = entry.LogSeverity
|
entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.Overview, $"%{query.Overview}%"));
|
||||||
};
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(query.ShortOverview))
|
||||||
|
{
|
||||||
|
entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.ShortOverview, $"%{query.ShortOverview}%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(query.Type))
|
||||||
|
{
|
||||||
|
entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.Type, $"%{query.Type}%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!query.ItemId.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
var itemId = query.ItemId.Value.ToString("N");
|
||||||
|
entries = entries.Where(e => e.ActivityLog.ItemId == itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(query.Username))
|
||||||
|
{
|
||||||
|
entries = entries.Where(e => EF.Functions.Like(e.Username, $"%{query.Username}%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.Severity is not null)
|
||||||
|
{
|
||||||
|
entries = entries.Where(e => e.ActivityLog.LogSeverity == query.Severity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new QueryResult<ActivityLogEntry>(
|
||||||
|
query.Skip,
|
||||||
|
await entries.CountAsync().ConfigureAwait(false),
|
||||||
|
await ApplyOrdering(entries, query.OrderBy)
|
||||||
|
.Skip(query.Skip ?? 0)
|
||||||
|
.Take(query.Limit ?? 100)
|
||||||
|
.Select(entity => new ActivityLogEntry(entity.ActivityLog.Name, entity.ActivityLog.Type, entity.ActivityLog.UserId)
|
||||||
|
{
|
||||||
|
Id = entity.ActivityLog.Id,
|
||||||
|
Overview = entity.ActivityLog.Overview,
|
||||||
|
ShortOverview = entity.ActivityLog.ShortOverview,
|
||||||
|
ItemId = entity.ActivityLog.ItemId,
|
||||||
|
Date = entity.ActivityLog.DateCreated,
|
||||||
|
Severity = entity.ActivityLog.LogSeverity
|
||||||
|
})
|
||||||
|
.ToListAsync()
|
||||||
|
.ConfigureAwait(false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task CleanAsync(DateTime startDate)
|
||||||
|
{
|
||||||
|
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
|
await using (dbContext.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
await dbContext.ActivityLogs
|
||||||
|
.Where(entry => entry.DateCreated <= startDate)
|
||||||
|
.ExecuteDeleteAsync()
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ActivityLogEntry ConvertToOldModel(ActivityLog entry)
|
||||||
|
{
|
||||||
|
return new ActivityLogEntry(entry.Name, entry.Type, entry.UserId)
|
||||||
|
{
|
||||||
|
Id = entry.Id,
|
||||||
|
Overview = entry.Overview,
|
||||||
|
ShortOverview = entry.ShortOverview,
|
||||||
|
ItemId = entry.ItemId,
|
||||||
|
Date = entry.DateCreated,
|
||||||
|
Severity = entry.LogSeverity
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private IOrderedQueryable<ExpandedActivityLog> ApplyOrdering(IQueryable<ExpandedActivityLog> query, IReadOnlyCollection<(ActivityLogSortBy, SortOrder)>? sorting)
|
||||||
|
{
|
||||||
|
if (sorting is null || sorting.Count == 0)
|
||||||
|
{
|
||||||
|
return query.OrderByDescending(e => e.ActivityLog.DateCreated);
|
||||||
|
}
|
||||||
|
|
||||||
|
IOrderedQueryable<ExpandedActivityLog> ordered = null!;
|
||||||
|
|
||||||
|
foreach (var (sortBy, sortOrder) in sorting)
|
||||||
|
{
|
||||||
|
var orderBy = MapOrderBy(sortBy);
|
||||||
|
|
||||||
|
if (ordered == null)
|
||||||
|
{
|
||||||
|
ordered = sortOrder == SortOrder.Ascending
|
||||||
|
? query.OrderBy(orderBy)
|
||||||
|
: query.OrderByDescending(orderBy);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ordered = sortOrder == SortOrder.Ascending
|
||||||
|
? ordered.ThenBy(orderBy)
|
||||||
|
: ordered.ThenByDescending(orderBy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Expression<Func<ExpandedActivityLog, object?>> MapOrderBy(ActivityLogSortBy sortBy)
|
||||||
|
{
|
||||||
|
return sortBy switch
|
||||||
|
{
|
||||||
|
ActivityLogSortBy.Name => e => e.ActivityLog.Name,
|
||||||
|
ActivityLogSortBy.Overiew => e => e.ActivityLog.Overview,
|
||||||
|
ActivityLogSortBy.ShortOverview => e => e.ActivityLog.ShortOverview,
|
||||||
|
ActivityLogSortBy.Type => e => e.ActivityLog.Type,
|
||||||
|
ActivityLogSortBy.DateCreated => e => e.ActivityLog.DateCreated,
|
||||||
|
ActivityLogSortBy.Username => e => e.Username,
|
||||||
|
ActivityLogSortBy.LogSeverity => e => e.ActivityLog.LogSeverity,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(sortBy), sortBy, "Unhandled ActivityLogSortBy")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ExpandedActivityLog
|
||||||
|
{
|
||||||
|
public ActivityLog ActivityLog { get; set; } = null!;
|
||||||
|
|
||||||
|
public string? Username { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||||||
devices = devices.Skip(query.Skip.Value);
|
devices = devices.Skip(query.Skip.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.Limit.HasValue)
|
if (query.Limit.HasValue && query.Limit.Value > 0)
|
||||||
{
|
{
|
||||||
devices = devices.Take(query.Limit.Value);
|
devices = devices.Take(query.Limit.Value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ public class BackupService : IBackupService
|
|||||||
}
|
}
|
||||||
|
|
||||||
BackupManifest? manifest;
|
BackupManifest? manifest;
|
||||||
var manifestStream = zipArchiveEntry.Open();
|
var manifestStream = await zipArchiveEntry.OpenAsync().ConfigureAwait(false);
|
||||||
await using (manifestStream.ConfigureAwait(false))
|
await using (manifestStream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
manifest = await JsonSerializer.DeserializeAsync<BackupManifest>(manifestStream, _serializerSettings).ConfigureAwait(false);
|
manifest = await JsonSerializer.DeserializeAsync<BackupManifest>(manifestStream, _serializerSettings).ConfigureAwait(false);
|
||||||
@@ -160,7 +160,7 @@ public class BackupService : IBackupService
|
|||||||
}
|
}
|
||||||
|
|
||||||
HistoryRow[] historyEntries;
|
HistoryRow[] historyEntries;
|
||||||
var historyArchive = historyEntry.Open();
|
var historyArchive = await historyEntry.OpenAsync().ConfigureAwait(false);
|
||||||
await using (historyArchive.ConfigureAwait(false))
|
await using (historyArchive.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
historyEntries = await JsonSerializer.DeserializeAsync<HistoryRow[]>(historyArchive).ConfigureAwait(false) ??
|
historyEntries = await JsonSerializer.DeserializeAsync<HistoryRow[]>(historyArchive).ConfigureAwait(false) ??
|
||||||
@@ -204,7 +204,7 @@ public class BackupService : IBackupService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var zipEntryStream = zipEntry.Open();
|
var zipEntryStream = await zipEntry.OpenAsync().ConfigureAwait(false);
|
||||||
await using (zipEntryStream.ConfigureAwait(false))
|
await using (zipEntryStream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Restore backup of {Table}", entityType.Type.Name);
|
_logger.LogInformation("Restore backup of {Table}", entityType.Type.Name);
|
||||||
@@ -329,7 +329,7 @@ public class BackupService : IBackupService
|
|||||||
_logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName);
|
_logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName);
|
||||||
var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.SourceName}.json")));
|
var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.SourceName}.json")));
|
||||||
var entities = 0;
|
var entities = 0;
|
||||||
var zipEntryStream = zipEntry.Open();
|
var zipEntryStream = await zipEntry.OpenAsync().ConfigureAwait(false);
|
||||||
await using (zipEntryStream.ConfigureAwait(false))
|
await using (zipEntryStream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var jsonSerializer = new Utf8JsonWriter(zipEntryStream);
|
var jsonSerializer = new Utf8JsonWriter(zipEntryStream);
|
||||||
@@ -366,7 +366,7 @@ public class BackupService : IBackupService
|
|||||||
foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly)
|
foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly)
|
||||||
.Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", SearchOption.TopDirectoryOnly)))
|
.Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", SearchOption.TopDirectoryOnly)))
|
||||||
{
|
{
|
||||||
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item))));
|
await zipArchive.CreateEntryFromFileAsync(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item)))).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CopyDirectory(string source, string target, string filter = "*")
|
void CopyDirectory(string source, string target, string filter = "*")
|
||||||
@@ -380,6 +380,7 @@ public class BackupService : IBackupService
|
|||||||
|
|
||||||
foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
|
foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
|
||||||
{
|
{
|
||||||
|
// TODO: @bond make async
|
||||||
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item))));
|
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,7 +406,7 @@ public class BackupService : IBackupService
|
|||||||
CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
|
CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
|
||||||
}
|
}
|
||||||
|
|
||||||
var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open();
|
var manifestStream = await zipArchive.CreateEntry(ManifestEntryName).OpenAsync().ConfigureAwait(false);
|
||||||
await using (manifestStream.ConfigureAwait(false))
|
await using (manifestStream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false);
|
await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false);
|
||||||
@@ -505,7 +506,7 @@ public class BackupService : IBackupService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var manifestStream = manifestEntry.Open();
|
var manifestStream = await manifestEntry.OpenAsync().ConfigureAwait(false);
|
||||||
await using (manifestStream.ConfigureAwait(false))
|
await using (manifestStream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
return await JsonSerializer.DeserializeAsync<BackupManifest>(manifestStream, _serializerSettings).ConfigureAwait(false);
|
return await JsonSerializer.DeserializeAsync<BackupManifest>(manifestStream, _serializerSettings).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ public sealed class BaseItemRepository
|
|||||||
public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter)
|
public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(filter);
|
ArgumentNullException.ThrowIfNull(filter);
|
||||||
if (!filter.EnableTotalRecordCount || (!filter.Limit.HasValue && (filter.StartIndex ?? 0) == 0))
|
if (!filter.EnableTotalRecordCount || ((filter.Limit ?? 0) == 0 && (filter.StartIndex ?? 0) == 0))
|
||||||
{
|
{
|
||||||
var returnList = GetItemList(filter);
|
var returnList = GetItemList(filter);
|
||||||
return new QueryResult<BaseItemDto>(
|
return new QueryResult<BaseItemDto>(
|
||||||
@@ -275,8 +275,9 @@ public sealed class BaseItemRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||||
|
dbQuery = ApplyNavigations(dbQuery, filter);
|
||||||
|
|
||||||
result.Items = GetEntities(dbQuery, context, filter).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).Where(dto => dto is not null).ToArray()!;
|
||||||
result.StartIndex = filter.StartIndex ?? 0;
|
result.StartIndex = filter.StartIndex ?? 0;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -294,8 +295,9 @@ public sealed class BaseItemRepository
|
|||||||
|
|
||||||
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
|
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
|
||||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||||
|
dbQuery = ApplyNavigations(dbQuery, filter);
|
||||||
|
|
||||||
return GetEntities(dbQuery, context, filter).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).Where(dto => dto is not null).ToArray()!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -324,7 +326,7 @@ public sealed class BaseItemRepository
|
|||||||
.OrderByDescending(g => g.MaxDateCreated)
|
.OrderByDescending(g => g.MaxDateCreated)
|
||||||
.Select(g => g);
|
.Select(g => g);
|
||||||
|
|
||||||
if (filter.Limit.HasValue)
|
if (filter.Limit.HasValue && filter.Limit.Value > 0)
|
||||||
{
|
{
|
||||||
subqueryGrouped = subqueryGrouped.Take(filter.Limit.Value);
|
subqueryGrouped = subqueryGrouped.Take(filter.Limit.Value);
|
||||||
}
|
}
|
||||||
@@ -337,7 +339,9 @@ public sealed class BaseItemRepository
|
|||||||
mainquery = ApplyGroupingFilter(context, mainquery, filter);
|
mainquery = ApplyGroupingFilter(context, mainquery, filter);
|
||||||
mainquery = ApplyQueryPaging(mainquery, filter);
|
mainquery = ApplyQueryPaging(mainquery, filter);
|
||||||
|
|
||||||
return GetEntities(mainquery, context, filter).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
mainquery = ApplyNavigations(mainquery, filter);
|
||||||
|
|
||||||
|
return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).Where(dto => dto is not null).ToArray()!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -363,7 +367,7 @@ public sealed class BaseItemRepository
|
|||||||
.OrderByDescending(g => g.LastPlayedDate)
|
.OrderByDescending(g => g.LastPlayedDate)
|
||||||
.Select(g => g.Key!);
|
.Select(g => g.Key!);
|
||||||
|
|
||||||
if (filter.Limit.HasValue)
|
if (filter.Limit.HasValue && filter.Limit.Value > 0)
|
||||||
{
|
{
|
||||||
query = query.Take(filter.Limit.Value);
|
query = query.Take(filter.Limit.Value);
|
||||||
}
|
}
|
||||||
@@ -404,21 +408,46 @@ public sealed class BaseItemRepository
|
|||||||
return dbQuery;
|
return dbQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
|
||||||
|
{
|
||||||
|
if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
|
||||||
|
{
|
||||||
|
dbQuery = dbQuery.Include(e => e.TrailerTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.DtoOptions.ContainsField(ItemFields.ProviderIds))
|
||||||
|
{
|
||||||
|
dbQuery = dbQuery.Include(e => e.Provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.DtoOptions.ContainsField(ItemFields.Settings))
|
||||||
|
{
|
||||||
|
dbQuery = dbQuery.Include(e => e.LockedFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.DtoOptions.EnableUserData)
|
||||||
|
{
|
||||||
|
dbQuery = dbQuery.Include(e => e.UserData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.DtoOptions.EnableImages)
|
||||||
|
{
|
||||||
|
dbQuery = dbQuery.Include(e => e.Images);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbQuery;
|
||||||
|
}
|
||||||
|
|
||||||
private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
|
private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
|
||||||
{
|
{
|
||||||
if (filter.Limit.HasValue || filter.StartIndex.HasValue)
|
if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
|
||||||
{
|
{
|
||||||
var offset = filter.StartIndex ?? 0;
|
dbQuery = dbQuery.Skip(filter.StartIndex.Value);
|
||||||
|
}
|
||||||
|
|
||||||
if (offset > 0)
|
if (filter.Limit.HasValue && filter.Limit.Value > 0)
|
||||||
{
|
{
|
||||||
dbQuery = dbQuery.Skip(offset);
|
dbQuery = dbQuery.Take(filter.Limit.Value);
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.Limit.HasValue)
|
|
||||||
{
|
|
||||||
dbQuery = dbQuery.Take(filter.Limit.Value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dbQuery;
|
return dbQuery;
|
||||||
@@ -429,6 +458,7 @@ public sealed class BaseItemRepository
|
|||||||
dbQuery = TranslateQuery(dbQuery, context, filter);
|
dbQuery = TranslateQuery(dbQuery, context, filter);
|
||||||
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
|
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
|
||||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||||
|
dbQuery = ApplyNavigations(dbQuery, filter);
|
||||||
return dbQuery;
|
return dbQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,79 +470,6 @@ public sealed class BaseItemRepository
|
|||||||
return dbQuery;
|
return dbQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IReadOnlyList<BaseItemEntity> GetEntities(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, InternalItemsQuery filter)
|
|
||||||
{
|
|
||||||
var items = dbQuery.Where(e => e != null).ToDictionary(e => e.Id, e => e);
|
|
||||||
var itemIds = items.Keys.ToArray();
|
|
||||||
|
|
||||||
if (itemIds.Length == 0)
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
|
|
||||||
{
|
|
||||||
var values = context.BaseItemTrailerTypes.WhereOneOrMany(itemIds, e => e.ItemId).GroupBy(x => x.ItemId).ToArray();
|
|
||||||
foreach (var value in values)
|
|
||||||
{
|
|
||||||
if (items.TryGetValue(value.Key, out var item))
|
|
||||||
{
|
|
||||||
item.TrailerTypes = value.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.DtoOptions.ContainsField(ItemFields.ProviderIds))
|
|
||||||
{
|
|
||||||
var values = context.BaseItemProviders.WhereOneOrMany(itemIds, e => e.ItemId).GroupBy(x => x.ItemId).ToArray();
|
|
||||||
foreach (var value in values)
|
|
||||||
{
|
|
||||||
if (items.TryGetValue(value.Key, out var item))
|
|
||||||
{
|
|
||||||
item.Provider = value.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.DtoOptions.ContainsField(ItemFields.Settings))
|
|
||||||
{
|
|
||||||
var values = context.BaseItemMetadataFields.WhereOneOrMany(itemIds, e => e.ItemId).GroupBy(x => x.ItemId).ToArray();
|
|
||||||
foreach (var value in values)
|
|
||||||
{
|
|
||||||
if (items.TryGetValue(value.Key, out var item))
|
|
||||||
{
|
|
||||||
item.LockedFields = value.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.DtoOptions.EnableImages)
|
|
||||||
{
|
|
||||||
var values = context.BaseItemImageInfos.WhereOneOrMany(itemIds, e => e.ItemId).GroupBy(x => x.ItemId).ToArray();
|
|
||||||
foreach (var value in values)
|
|
||||||
{
|
|
||||||
if (items.TryGetValue(value.Key, out var item))
|
|
||||||
{
|
|
||||||
item.Images = value.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.DtoOptions.EnableUserData)
|
|
||||||
{
|
|
||||||
var values = context.UserData.WhereOneOrMany(itemIds, e => e.ItemId).GroupBy(x => x.ItemId).ToArray();
|
|
||||||
foreach (var value in values)
|
|
||||||
{
|
|
||||||
if (items.TryGetValue(value.Key, out var item))
|
|
||||||
{
|
|
||||||
item.UserData = value.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return items.Values.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public int GetCount(InternalItemsQuery filter)
|
public int GetCount(InternalItemsQuery filter)
|
||||||
{
|
{
|
||||||
@@ -605,22 +562,34 @@ public sealed class BaseItemRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void SaveImages(BaseItemDto item)
|
public async Task SaveImagesAsync(BaseItemDto item, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(item);
|
ArgumentNullException.ThrowIfNull(item);
|
||||||
|
|
||||||
var images = item.ImageInfos.Select(e => Map(item.Id, e));
|
var images = item.ImageInfos.Select(e => Map(item.Id, e)).ToArray();
|
||||||
using var context = _dbProvider.CreateDbContext();
|
|
||||||
|
|
||||||
if (!context.BaseItems.Any(bi => bi.Id == item.Id))
|
var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
await using (context.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Unable to save ImageInfo for non existing BaseItem");
|
if (!await context.BaseItems
|
||||||
return;
|
.AnyAsync(bi => bi.Id == item.Id, cancellationToken)
|
||||||
}
|
.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Unable to save ImageInfo for non existing BaseItem");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
context.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete();
|
await context.BaseItemImageInfos
|
||||||
context.BaseItemImageInfos.AddRange(images);
|
.Where(e => e.ItemId == item.Id)
|
||||||
context.SaveChanges();
|
.ExecuteDeleteAsync(cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
await context.BaseItemImageInfos
|
||||||
|
.AddRangeAsync(images, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -655,7 +624,6 @@ public sealed class BaseItemRepository
|
|||||||
|
|
||||||
var ids = tuples.Select(f => f.Item.Id).ToArray();
|
var ids = tuples.Select(f => f.Item.Id).ToArray();
|
||||||
var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
|
var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
|
||||||
var newItems = tuples.Where(e => !existingItems.Contains(e.Item.Id)).ToArray();
|
|
||||||
|
|
||||||
foreach (var item in tuples)
|
foreach (var item in tuples)
|
||||||
{
|
{
|
||||||
@@ -689,19 +657,6 @@ public sealed class BaseItemRepository
|
|||||||
|
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
|
|
||||||
foreach (var item in newItems)
|
|
||||||
{
|
|
||||||
// reattach old userData entries
|
|
||||||
var userKeys = item.UserDataKey.ToArray();
|
|
||||||
var retentionDate = (DateTime?)null;
|
|
||||||
context.UserData
|
|
||||||
.Where(e => e.ItemId == PlaceholderId)
|
|
||||||
.Where(e => userKeys.Contains(e.CustomDataKey))
|
|
||||||
.ExecuteUpdate(e => e
|
|
||||||
.SetProperty(f => f.ItemId, item.Item.Id)
|
|
||||||
.SetProperty(f => f.RetentionDate, retentionDate));
|
|
||||||
}
|
|
||||||
|
|
||||||
var itemValueMaps = tuples
|
var itemValueMaps = tuples
|
||||||
.Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
|
.Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
@@ -797,6 +752,29 @@ public sealed class BaseItemRepository
|
|||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task ReattachUserDataAsync(BaseItemDto item, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(item);
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
await using (dbContext.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
var userKeys = item.GetUserDataKeys().ToArray();
|
||||||
|
var retentionDate = (DateTime?)null;
|
||||||
|
await dbContext.UserData
|
||||||
|
.Where(e => e.ItemId == PlaceholderId)
|
||||||
|
.Where(e => userKeys.Contains(e.CustomDataKey))
|
||||||
|
.ExecuteUpdateAsync(
|
||||||
|
e => e
|
||||||
|
.SetProperty(f => f.ItemId, item.Id)
|
||||||
|
.SetProperty(f => f.RetentionDate, retentionDate),
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public BaseItemDto? RetrieveItem(Guid id)
|
public BaseItemDto? RetrieveItem(Guid id)
|
||||||
{
|
{
|
||||||
@@ -904,7 +882,7 @@ public sealed class BaseItemRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Parse(e)).ToArray();
|
dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Parse(e)).ToArray();
|
||||||
dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? [];
|
dto.ProductionLocations = entity.ProductionLocations?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
|
||||||
dto.Studios = entity.Studios?.Split('|') ?? [];
|
dto.Studios = entity.Studios?.Split('|') ?? [];
|
||||||
dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
|
dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
|
||||||
|
|
||||||
@@ -1066,7 +1044,7 @@ public sealed class BaseItemRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
|
entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
|
||||||
entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations) : null;
|
entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations.Where(p => !string.IsNullOrWhiteSpace(p))) : null;
|
||||||
entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
|
entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
|
||||||
entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
|
entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
|
||||||
entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
|
entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
|
||||||
@@ -1190,7 +1168,7 @@ public sealed class BaseItemRepository
|
|||||||
return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
|
return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
|
private BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
|
ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
|
||||||
if (_serverConfigurationManager?.Configuration is null)
|
if (_serverConfigurationManager?.Configuration is null)
|
||||||
@@ -1213,11 +1191,19 @@ public sealed class BaseItemRepository
|
|||||||
/// <param name="logger">Logger.</param>
|
/// <param name="logger">Logger.</param>
|
||||||
/// <param name="appHost">The application server Host.</param>
|
/// <param name="appHost">The application server Host.</param>
|
||||||
/// <param name="skipDeserialization">If only mapping should be processed.</param>
|
/// <param name="skipDeserialization">If only mapping should be processed.</param>
|
||||||
/// <returns>A mapped BaseItem.</returns>
|
/// <returns>A mapped BaseItem, or null if the item type is unknown.</returns>
|
||||||
/// <exception cref="InvalidOperationException">Will be thrown if an invalid serialisation is requested.</exception>
|
public static BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false)
|
||||||
public static BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false)
|
|
||||||
{
|
{
|
||||||
var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialize unknown type.");
|
var type = GetType(baseItemEntity.Type);
|
||||||
|
if (type is null)
|
||||||
|
{
|
||||||
|
logger.LogWarning(
|
||||||
|
"Skipping item {ItemId} with unknown type '{ItemType}'. This may indicate a removed plugin or database corruption.",
|
||||||
|
baseItemEntity.Id,
|
||||||
|
baseItemEntity.Type);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
BaseItemDto? dto = null;
|
BaseItemDto? dto = null;
|
||||||
if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
|
if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
|
||||||
{
|
{
|
||||||
@@ -1243,7 +1229,7 @@ public sealed class BaseItemRepository
|
|||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(filter);
|
ArgumentNullException.ThrowIfNull(filter);
|
||||||
|
|
||||||
if (!filter.Limit.HasValue)
|
if (!(filter.Limit.HasValue && filter.Limit.Value > 0))
|
||||||
{
|
{
|
||||||
filter.EnableTotalRecordCount = false;
|
filter.EnableTotalRecordCount = false;
|
||||||
}
|
}
|
||||||
@@ -1322,19 +1308,14 @@ public sealed class BaseItemRepository
|
|||||||
result.TotalRecordCount = query.Count();
|
result.TotalRecordCount = query.Count();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.Limit.HasValue || filter.StartIndex.HasValue)
|
if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
|
||||||
{
|
{
|
||||||
var offset = filter.StartIndex ?? 0;
|
query = query.Skip(filter.StartIndex.Value);
|
||||||
|
}
|
||||||
|
|
||||||
if (offset > 0)
|
if (filter.Limit.HasValue && filter.Limit.Value > 0)
|
||||||
{
|
{
|
||||||
query = query.Skip(offset);
|
query = query.Take(filter.Limit.Value);
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.Limit.HasValue)
|
|
||||||
{
|
|
||||||
query = query.Take(filter.Limit.Value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IQueryable<BaseItemEntity>? itemCountQuery = null;
|
IQueryable<BaseItemEntity>? itemCountQuery = null;
|
||||||
@@ -1389,10 +1370,9 @@ public sealed class BaseItemRepository
|
|||||||
.. resultQuery
|
.. resultQuery
|
||||||
.AsEnumerable()
|
.AsEnumerable()
|
||||||
.Where(e => e is not null)
|
.Where(e => e is not null)
|
||||||
.Select(e =>
|
.Select(e => (Item: DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount))
|
||||||
{
|
.Where(e => e.Item is not null)
|
||||||
return (DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
|
.Select(e => (e.Item!, e.itemCount))
|
||||||
})
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1403,10 +1383,9 @@ public sealed class BaseItemRepository
|
|||||||
.. query
|
.. query
|
||||||
.AsEnumerable()
|
.AsEnumerable()
|
||||||
.Where(e => e is not null)
|
.Where(e => e is not null)
|
||||||
.Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e =>
|
.Select(e => (Item: DeserializeBaseItem(e, filter.SkipDeserialization), ItemCounts: (ItemCounts?)null))
|
||||||
{
|
.Where(e => e.Item is not null)
|
||||||
return (DeserializeBaseItem(e, filter.SkipDeserialization), null);
|
.Select(e => (e.Item!, e.ItemCounts))
|
||||||
})
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1415,7 +1394,7 @@ public sealed class BaseItemRepository
|
|||||||
|
|
||||||
private static void PrepareFilterQuery(InternalItemsQuery query)
|
private static void PrepareFilterQuery(InternalItemsQuery query)
|
||||||
{
|
{
|
||||||
if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
|
if (query.Limit.HasValue && query.Limit.Value > 0 && query.EnableGroupByMetadataKey)
|
||||||
{
|
{
|
||||||
query.Limit = query.Limit.Value + 4;
|
query.Limit = query.Limit.Value + 4;
|
||||||
}
|
}
|
||||||
@@ -1426,14 +1405,54 @@ public sealed class BaseItemRepository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetCleanValue(string value)
|
/// <summary>
|
||||||
|
/// Gets the clean value for search and sorting purposes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value to clean.</param>
|
||||||
|
/// <returns>The cleaned value.</returns>
|
||||||
|
public static string GetCleanValue(string value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
{
|
{
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return value.RemoveDiacritics().ToLowerInvariant();
|
var noDiacritics = value.RemoveDiacritics();
|
||||||
|
|
||||||
|
// Build a string where any punctuation or symbol is treated as a separator (space).
|
||||||
|
var sb = new StringBuilder(noDiacritics.Length);
|
||||||
|
var previousWasSpace = false;
|
||||||
|
foreach (var ch in noDiacritics)
|
||||||
|
{
|
||||||
|
char outCh;
|
||||||
|
if (char.IsLetterOrDigit(ch) || char.IsWhiteSpace(ch))
|
||||||
|
{
|
||||||
|
outCh = ch;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
outCh = ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalize any whitespace character to a single ASCII space.
|
||||||
|
if (char.IsWhiteSpace(outCh))
|
||||||
|
{
|
||||||
|
if (!previousWasSpace)
|
||||||
|
{
|
||||||
|
sb.Append(' ');
|
||||||
|
previousWasSpace = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append(outCh);
|
||||||
|
previousWasSpace = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// trim leading/trailing spaces that may have been added.
|
||||||
|
var collapsed = sb.ToString().Trim();
|
||||||
|
return collapsed.ToLowerInvariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inheritedTags)
|
private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inheritedTags)
|
||||||
@@ -1596,29 +1615,36 @@ public sealed class BaseItemRepository
|
|||||||
|
|
||||||
IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
|
IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
|
||||||
|
|
||||||
|
// When searching, prioritize by match quality: exact match > prefix match > contains
|
||||||
|
if (hasSearch)
|
||||||
|
{
|
||||||
|
orderedQuery = query.OrderBy(OrderMapper.MapSearchRelevanceOrder(filter.SearchTerm!));
|
||||||
|
}
|
||||||
|
|
||||||
var firstOrdering = orderBy.FirstOrDefault();
|
var firstOrdering = orderBy.FirstOrDefault();
|
||||||
if (firstOrdering != default)
|
if (firstOrdering != default)
|
||||||
{
|
{
|
||||||
var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
|
var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
|
||||||
if (firstOrdering.SortOrder == SortOrder.Ascending)
|
if (orderedQuery is null)
|
||||||
{
|
{
|
||||||
orderedQuery = query.OrderBy(expression);
|
// No search relevance ordering, start fresh
|
||||||
|
orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
|
||||||
|
? query.OrderBy(expression)
|
||||||
|
: query.OrderByDescending(expression);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
orderedQuery = query.OrderByDescending(expression);
|
// Search relevance ordering already applied, chain with ThenBy
|
||||||
|
orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
|
||||||
|
? orderedQuery.ThenBy(expression)
|
||||||
|
: orderedQuery.ThenByDescending(expression);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
|
if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
|
||||||
{
|
{
|
||||||
if (firstOrdering.SortOrder is SortOrder.Ascending)
|
orderedQuery = firstOrdering.SortOrder is SortOrder.Ascending
|
||||||
{
|
? orderedQuery.ThenBy(e => e.Name)
|
||||||
orderedQuery = orderedQuery.ThenBy(e => e.Name);
|
: orderedQuery.ThenByDescending(e => e.Name);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
orderedQuery = orderedQuery.ThenByDescending(e => e.Name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2485,35 +2511,24 @@ public sealed class BaseItemRepository
|
|||||||
|
|
||||||
if (filter.ExcludeInheritedTags.Length > 0)
|
if (filter.ExcludeInheritedTags.Length > 0)
|
||||||
{
|
{
|
||||||
|
var excludedTags = filter.ExcludeInheritedTags;
|
||||||
baseQuery = baseQuery.Where(e =>
|
baseQuery = baseQuery.Where(e =>
|
||||||
!e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue))
|
!e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))
|
||||||
&& (e.Type != _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode] || !e.SeriesId.HasValue ||
|
&& (!e.SeriesId.HasValue || !context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))));
|
||||||
!context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue))));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.IncludeInheritedTags.Length > 0)
|
if (filter.IncludeInheritedTags.Length > 0)
|
||||||
{
|
{
|
||||||
// For seasons and episodes, we also need to check the parent series' tags.
|
var includeTags = filter.IncludeInheritedTags;
|
||||||
if (includeTypes.Any(t => t == BaseItemKind.Episode || t == BaseItemKind.Season))
|
var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist;
|
||||||
{
|
baseQuery = baseQuery.Where(e =>
|
||||||
baseQuery = baseQuery.Where(e =>
|
e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
|
||||||
e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
|
|
||||||
|| (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))));
|
|
||||||
}
|
|
||||||
|
|
||||||
// A playlist should be accessible to its owner regardless of allowed tags.
|
// For seasons and episodes, we also need to check the parent series' tags.
|
||||||
else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
|
|| (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue)))
|
||||||
{
|
|
||||||
baseQuery = baseQuery.Where(e =>
|
// A playlist should be accessible to its owner regardless of allowed tags
|
||||||
e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
|
|| (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
|
||||||
|| e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""));
|
|
||||||
// d ^^ this is stupid it hate this.
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
baseQuery = baseQuery.Where(e =>
|
|
||||||
e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.SeriesStatuses.Length > 0)
|
if (filter.SeriesStatuses.Length > 0)
|
||||||
@@ -2667,6 +2682,6 @@ public sealed class BaseItemRepository
|
|||||||
.Where(e => artistNames.Contains(e.Name))
|
.Where(e => artistNames.Contains(e.Name))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Cast<MusicArtist>().ToArray());
|
return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,6 +158,12 @@ public class MediaStreamRepository : IMediaStreamRepository
|
|||||||
dto.LocalizedDefault = _localization.GetLocalizedString("Default");
|
dto.LocalizedDefault = _localization.GetLocalizedString("Default");
|
||||||
dto.LocalizedExternal = _localization.GetLocalizedString("External");
|
dto.LocalizedExternal = _localization.GetLocalizedString("External");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(dto.Language))
|
||||||
|
{
|
||||||
|
var culture = _localization.FindLanguageInfo(dto.Language);
|
||||||
|
dto.LocalizedLanguage = culture?.DisplayName;
|
||||||
|
}
|
||||||
|
|
||||||
if (dto.Type is MediaStreamType.Subtitle)
|
if (dto.Type is MediaStreamType.Subtitle)
|
||||||
{
|
{
|
||||||
dto.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
|
dto.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Linq.Expressions;
|
|||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Database.Implementations;
|
using Jellyfin.Database.Implementations;
|
||||||
using Jellyfin.Database.Implementations.Entities;
|
using Jellyfin.Database.Implementations.Entities;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@@ -68,4 +69,30 @@ public static class OrderMapper
|
|||||||
_ => e => e.SortName
|
_ => e => e.SortName
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an expression to order search results by match quality.
|
||||||
|
/// Prioritizes: exact match (0) > prefix match with word boundary (1) > prefix match (2) > contains (3).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="searchTerm">The search term to match against.</param>
|
||||||
|
/// <returns>An expression that returns an integer representing match quality (lower is better).</returns>
|
||||||
|
public static Expression<Func<BaseItemEntity, int>> MapSearchRelevanceOrder(string searchTerm)
|
||||||
|
{
|
||||||
|
var cleanSearchTerm = GetCleanValue(searchTerm);
|
||||||
|
var searchPrefix = cleanSearchTerm + " ";
|
||||||
|
return e =>
|
||||||
|
e.CleanName == cleanSearchTerm ? 0 :
|
||||||
|
e.CleanName!.StartsWith(searchPrefix) ? 1 :
|
||||||
|
e.CleanName!.StartsWith(cleanSearchTerm) ? 2 : 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetCleanValue(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.RemoveDiacritics().ToLowerInvariant();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -27,7 +27,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AsyncKeyedLock" />
|
<PackageReference Include="AsyncKeyedLock" />
|
||||||
<PackageReference Include="System.Linq.Async" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
}
|
}
|
||||||
|
|
||||||
// As long as jellyfin supports password-less users, we need this little block here to accommodate
|
// As long as jellyfin supports password-less users, we need this little block here to accommodate
|
||||||
if (!HasPassword(resolvedUser) && string.IsNullOrEmpty(password))
|
if (string.IsNullOrEmpty(resolvedUser.Password) && string.IsNullOrEmpty(password))
|
||||||
{
|
{
|
||||||
return Task.FromResult(new ProviderAuthenticationResult
|
return Task.FromResult(new ProviderAuthenticationResult
|
||||||
{
|
{
|
||||||
@@ -93,10 +93,6 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool HasPassword(User user)
|
|
||||||
=> !string.IsNullOrEmpty(user?.Password);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task ChangePassword(User user, string newPassword)
|
public Task ChangePassword(User user, string newPassword)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,12 +21,6 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
throw new AuthenticationException("User Account cannot login with this provider. The Normal provider for this user cannot be found");
|
throw new AuthenticationException("User Account cannot login with this provider. The Normal provider for this user cannot be found");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool HasPassword(User user)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task ChangePassword(User user, string newPassword)
|
public Task ChangePassword(User user, string newPassword)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -306,15 +306,12 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public UserDto GetUserDto(User user, string? remoteEndPoint = null)
|
public UserDto GetUserDto(User user, string? remoteEndPoint = null)
|
||||||
{
|
{
|
||||||
var hasPassword = GetAuthenticationProvider(user).HasPassword(user);
|
|
||||||
var castReceiverApplications = _serverConfigurationManager.Configuration.CastReceiverApplications;
|
var castReceiverApplications = _serverConfigurationManager.Configuration.CastReceiverApplications;
|
||||||
return new UserDto
|
return new UserDto
|
||||||
{
|
{
|
||||||
Name = user.Username,
|
Name = user.Username,
|
||||||
Id = user.Id,
|
Id = user.Id,
|
||||||
ServerId = _appHost.SystemId,
|
ServerId = _appHost.SystemId,
|
||||||
HasPassword = hasPassword,
|
|
||||||
HasConfiguredPassword = hasPassword,
|
|
||||||
EnableAutoLogin = user.EnableAutoLogin,
|
EnableAutoLogin = user.EnableAutoLogin,
|
||||||
LastLoginDate = user.LastLoginDate,
|
LastLoginDate = user.LastLoginDate,
|
||||||
LastActivityDate = user.LastActivityDate,
|
LastActivityDate = user.LastActivityDate,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user