aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
l---------[-rw-r--r--].copr/Makefile60
-rw-r--r--.editorconfig17
-rw-r--r--.github/CODEOWNERS3
-rw-r--r--.gitignore19
-rw-r--r--DvdLib/BigEndianBinaryReader.cs2
-rw-r--r--DvdLib/DvdLib.csproj1
-rw-r--r--DvdLib/Ifo/Cell.cs2
-rw-r--r--DvdLib/Ifo/CellPlaybackInfo.cs2
-rw-r--r--DvdLib/Ifo/CellPositionInfo.cs2
-rw-r--r--DvdLib/Ifo/Chapter.cs2
-rw-r--r--DvdLib/Ifo/Dvd.cs2
-rw-r--r--DvdLib/Ifo/DvdTime.cs2
-rw-r--r--DvdLib/Ifo/Program.cs2
-rw-r--r--DvdLib/Ifo/ProgramChain.cs2
-rw-r--r--DvdLib/Ifo/Title.cs2
-rw-r--r--DvdLib/Ifo/UserOperation.cs2
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs5
-rw-r--r--Emby.Naming/Audio/AlbumParser.cs17
-rw-r--r--Emby.Naming/Audio/AudioFileParser.cs3
-rw-r--r--Emby.Naming/Common/EpisodeExpression.cs7
-rw-r--r--Emby.Naming/Subtitles/SubtitleParser.cs12
-rw-r--r--Emby.Naming/Video/VideoResolver.cs6
-rw-r--r--Emby.Notifications/NotificationEntryPoint.cs2
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs108
-rw-r--r--Emby.Server.Implementations/Browser/BrowserLauncher.cs8
-rw-r--r--Emby.Server.Implementations/ConfigurationOptions.cs1
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserDataRepository.cs10
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj1
-rw-r--r--Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs4
-rw-r--r--Emby.Server.Implementations/HttpServer/HttpListenerHost.cs222
-rw-r--r--Emby.Server.Implementations/HttpServer/HttpResultFactory.cs2
-rw-r--r--Emby.Server.Implementations/HttpServer/IHttpListener.cs39
-rw-r--r--Emby.Server.Implementations/HttpServer/ResponseFilter.cs23
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs257
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs6
-rw-r--r--Emby.Server.Implementations/Library/PathExtensions.cs22
-rw-r--r--Emby.Server.Implementations/Library/ResolverHelper.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs4
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs6
-rw-r--r--Emby.Server.Implementations/Library/UserManager.cs25
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs2
-rw-r--r--Emby.Server.Implementations/Localization/Core/af.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/bn.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-MX.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/fi.json26
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr-CA.json20
-rw-r--r--Emby.Server.Implementations/Localization/Core/gsw.json92
-rw-r--r--Emby.Server.Implementations/Localization/Core/he.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/hr.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/mk.json9
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/sl-SI.json23
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.json36
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-HK.json7
-rw-r--r--Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs39
-rw-r--r--Emby.Server.Implementations/Net/IWebSocket.cs48
-rw-r--r--Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs29
-rw-r--r--Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs16
-rw-r--r--Emby.Server.Implementations/Services/UrlExtensions.cs20
-rw-r--r--Emby.Server.Implementations/Session/HttpSessionController.cs191
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs15
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs33
-rw-r--r--Emby.Server.Implementations/Session/WebSocketController.cs86
-rw-r--r--Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs105
-rw-r--r--Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs135
-rw-r--r--Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs24
-rw-r--r--Emby.Server.Implementations/WebSockets/WebSocketHandler.cs10
-rw-r--r--Emby.Server.Implementations/WebSockets/WebSocketManager.cs102
-rw-r--r--Jellyfin.Data/Entities/Artwork.cs195
-rw-r--r--Jellyfin.Data/Entities/Book.cs69
-rw-r--r--Jellyfin.Data/Entities/BookMetadata.cs107
-rw-r--r--Jellyfin.Data/Entities/Chapter.cs263
-rw-r--r--Jellyfin.Data/Entities/Collection.cs120
-rw-r--r--Jellyfin.Data/Entities/CollectionItem.cs143
-rw-r--r--Jellyfin.Data/Entities/Company.cs137
-rw-r--r--Jellyfin.Data/Entities/CompanyMetadata.cs216
-rw-r--r--Jellyfin.Data/Entities/CustomItem.cs68
-rw-r--r--Jellyfin.Data/Entities/CustomItemMetadata.cs67
-rw-r--r--Jellyfin.Data/Entities/Episode.cs110
-rw-r--r--Jellyfin.Data/Entities/EpisodeMetadata.cs179
-rw-r--r--Jellyfin.Data/Entities/Genre.cs152
-rw-r--r--Jellyfin.Data/Entities/Group.cs109
-rw-r--r--Jellyfin.Data/Entities/Library.cs147
-rw-r--r--Jellyfin.Data/Entities/LibraryItem.cs170
-rw-r--r--Jellyfin.Data/Entities/LibraryRoot.cs192
-rw-r--r--Jellyfin.Data/Entities/MediaFile.cs200
-rw-r--r--Jellyfin.Data/Entities/MediaFileStream.cs149
-rw-r--r--Jellyfin.Data/Entities/Metadata.cs380
-rw-r--r--Jellyfin.Data/Entities/MetadataProvider.cs147
-rw-r--r--Jellyfin.Data/Entities/MetadataProviderId.cs179
-rw-r--r--Jellyfin.Data/Entities/Movie.cs69
-rw-r--r--Jellyfin.Data/Entities/MovieMetadata.cs223
-rw-r--r--Jellyfin.Data/Entities/MusicAlbum.cs68
-rw-r--r--Jellyfin.Data/Entities/MusicAlbumMetadata.cs187
-rw-r--r--Jellyfin.Data/Entities/Permission.cs144
-rw-r--r--Jellyfin.Data/Entities/Person.cs302
-rw-r--r--Jellyfin.Data/Entities/PersonRole.cs209
-rw-r--r--Jellyfin.Data/Entities/Photo.cs68
-rw-r--r--Jellyfin.Data/Entities/PhotoMetadata.cs68
-rw-r--r--Jellyfin.Data/Entities/Preference.cs107
-rw-r--r--Jellyfin.Data/Entities/ProviderMapping.cs123
-rw-r--r--Jellyfin.Data/Entities/Rating.cs187
-rw-r--r--Jellyfin.Data/Entities/RatingSource.cs231
-rw-r--r--Jellyfin.Data/Entities/Release.cs188
-rw-r--r--Jellyfin.Data/Entities/Season.cs111
-rw-r--r--Jellyfin.Data/Entities/SeasonMetadata.cs106
-rw-r--r--Jellyfin.Data/Entities/Series.cs167
-rw-r--r--Jellyfin.Data/Entities/SeriesMetadata.cs223
-rw-r--r--Jellyfin.Data/Entities/Track.cs112
-rw-r--r--Jellyfin.Data/Entities/TrackMetadata.cs68
-rw-r--r--Jellyfin.Data/Entities/User.cs235
-rw-r--r--Jellyfin.Data/Enums/ArtKind.cs11
-rw-r--r--Jellyfin.Data/Enums/MediaFileKind.cs11
-rw-r--r--Jellyfin.Data/Enums/PermissionKind.cs26
-rw-r--r--Jellyfin.Data/Enums/PersonRoleType.cs18
-rw-r--r--Jellyfin.Data/Enums/PreferenceKind.cs13
-rw-r--r--Jellyfin.Data/Enums/Weekday.cs13
-rw-r--r--Jellyfin.Data/ISavingChanges.cs9
-rw-r--r--Jellyfin.Data/Jellyfin.Data.csproj12
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj34
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDb.cs115
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj2
-rw-r--r--Jellyfin.Server/Migrations/IMigrationRoutine.cs4
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs19
-rw-r--r--Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs11
-rw-r--r--Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs17
-rw-r--r--Jellyfin.Server/Program.cs109
-rw-r--r--Jellyfin.Server/Startup.cs12
-rw-r--r--MediaBrowser.Api/BaseApiService.cs4
-rw-r--r--MediaBrowser.Api/Images/RemoteImageService.cs13
-rw-r--r--MediaBrowser.Api/ItemLookupService.cs14
-rw-r--r--MediaBrowser.Api/Library/LibraryService.cs6
-rw-r--r--MediaBrowser.Api/Movies/MoviesService.cs2
-rw-r--r--MediaBrowser.Api/Movies/TrailersService.cs12
-rw-r--r--MediaBrowser.Api/Playback/BaseStreamingService.cs5
-rw-r--r--MediaBrowser.Api/Playback/Hls/BaseHlsService.cs28
-rw-r--r--MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs199
-rw-r--r--MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs126
-rw-r--r--MediaBrowser.Api/Playback/MediaInfoService.cs2
-rw-r--r--MediaBrowser.Api/Playback/Progressive/AudioService.cs2
-rw-r--r--MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs2
-rw-r--r--MediaBrowser.Api/Playback/UniversalAudioService.cs9
-rw-r--r--MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs43
-rw-r--r--MediaBrowser.Api/System/ActivityLogWebSocketListener.cs16
-rw-r--r--MediaBrowser.Api/UserLibrary/ArtistsService.cs2
-rw-r--r--MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs2
-rw-r--r--MediaBrowser.Api/UserLibrary/ItemsService.cs2
-rw-r--r--MediaBrowser.Api/UserService.cs38
-rw-r--r--MediaBrowser.Common/Extensions/BaseExtensions.cs2
-rw-r--r--MediaBrowser.Common/Extensions/CopyToExtensions.cs2
-rw-r--r--MediaBrowser.Common/Extensions/MethodNotAllowedException.cs2
-rw-r--r--MediaBrowser.Common/Extensions/ProcessExtensions.cs2
-rw-r--r--MediaBrowser.Common/Extensions/RateLimitExceededException.cs1
-rw-r--r--MediaBrowser.Common/Extensions/ResourceNotFoundException.cs2
-rw-r--r--MediaBrowser.Common/Extensions/StringExtensions.cs37
-rw-r--r--MediaBrowser.Controller/IServerApplicationHost.cs57
-rw-r--r--MediaBrowser.Controller/Library/IUserManager.cs8
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs38
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs12
-rw-r--r--MediaBrowser.Controller/Net/IHttpServer.cs27
-rw-r--r--MediaBrowser.Controller/Net/IWebSocketConnection.cs41
-rw-r--r--MediaBrowser.Controller/Session/ISessionController.cs3
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs74
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs15
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs38
-rw-r--r--MediaBrowser.Model/Dlna/CodecProfile.cs4
-rw-r--r--MediaBrowser.Model/Dlna/ConditionProcessor.cs6
-rw-r--r--MediaBrowser.Model/Dlna/ContainerProfile.cs8
-rw-r--r--MediaBrowser.Model/Dlna/DeviceProfile.cs25
-rw-r--r--MediaBrowser.Model/Dlna/SubtitleProfile.cs5
-rw-r--r--MediaBrowser.Model/Dto/PublicUserDto.cs48
-rw-r--r--MediaBrowser.Model/Entities/ProviderIdsExtensions.cs8
-rw-r--r--MediaBrowser.Model/Extensions/ListHelper.cs27
-rw-r--r--MediaBrowser.Model/Net/MimeTypes.cs117
-rw-r--r--MediaBrowser.Model/Net/WebSocketMessage.cs8
-rw-r--r--MediaBrowser.Model/Notifications/NotificationOptions.cs14
-rw-r--r--MediaBrowser.Model/System/SystemInfo.cs18
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs24
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs15
-rw-r--r--MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs74
-rw-r--r--MediaBrowser.Providers/Tmdb/TmdbUtils.cs35
-rw-r--r--MediaBrowser.sln46
-rw-r--r--SharedVersion.cs4
l---------[-rwxr-xr-x]build198
-rwxr-xr-xbuild.sh114
-rw-r--r--build.yaml23
-rwxr-xr-xbump_version36
-rwxr-xr-xdebian/bin/restart.sh (renamed from deployment/debian-package-x64/pkg-src/bin/restart.sh)0
-rw-r--r--debian/changelog (renamed from deployment/debian-package-x64/pkg-src/changelog)6
-rw-r--r--debian/compat (renamed from deployment/debian-package-x64/pkg-src/compat)0
-rw-r--r--debian/conf/jellyfin (renamed from deployment/debian-package-x64/pkg-src/conf/jellyfin)5
-rw-r--r--debian/conf/jellyfin-sudoers (renamed from deployment/debian-package-x64/pkg-src/conf/jellyfin-sudoers)0
-rw-r--r--debian/conf/jellyfin.service.conf (renamed from deployment/debian-package-x64/pkg-src/conf/jellyfin.service.conf)0
-rw-r--r--debian/conf/logging.json (renamed from deployment/debian-package-x64/pkg-src/conf/logging.json)0
-rw-r--r--debian/control (renamed from deployment/debian-package-x64/pkg-src/control)17
-rw-r--r--debian/copyright (renamed from deployment/debian-package-x64/pkg-src/copyright)0
-rw-r--r--debian/gbp.conf (renamed from deployment/debian-package-x64/pkg-src/gbp.conf)0
-rw-r--r--debian/install (renamed from deployment/debian-package-x64/pkg-src/install)0
-rw-r--r--debian/jellyfin.init (renamed from deployment/debian-package-x64/pkg-src/jellyfin.init)0
-rw-r--r--debian/jellyfin.service (renamed from deployment/debian-package-x64/pkg-src/jellyfin.service)2
-rw-r--r--debian/jellyfin.upstart (renamed from deployment/debian-package-x64/pkg-src/jellyfin.upstart)0
-rw-r--r--debian/metapackage/jellyfin13
-rw-r--r--debian/po/POTFILES.in (renamed from deployment/debian-package-x64/pkg-src/po/POTFILES.in)0
-rw-r--r--debian/po/templates.pot (renamed from deployment/debian-package-x64/pkg-src/po/templates.pot)0
-rw-r--r--debian/postinst (renamed from deployment/debian-package-x64/pkg-src/postinst)0
-rw-r--r--debian/postrm (renamed from deployment/debian-package-x64/pkg-src/postrm)0
-rw-r--r--debian/preinst (renamed from deployment/debian-package-x64/pkg-src/preinst)0
-rw-r--r--debian/prerm (renamed from deployment/debian-package-x64/pkg-src/prerm)0
-rwxr-xr-xdebian/rules (renamed from deployment/debian-package-x64/pkg-src/rules)15
-rw-r--r--debian/source.lintian-overrides (renamed from deployment/debian-package-x64/pkg-src/source.lintian-overrides)0
-rw-r--r--debian/source/format (renamed from deployment/debian-package-x64/pkg-src/source/format)0
-rw-r--r--debian/source/options (renamed from deployment/debian-package-x64/pkg-src/source/options)0
-rw-r--r--deployment/Dockerfile.centos.amd6432
-rw-r--r--deployment/Dockerfile.debian.amd64 (renamed from deployment/win-x64/Dockerfile)18
-rw-r--r--deployment/Dockerfile.debian.arm64 (renamed from deployment/debian-package-arm64/Dockerfile.amd64)16
-rw-r--r--deployment/Dockerfile.debian.armhf (renamed from deployment/debian-package-armhf/Dockerfile.amd64)15
-rw-r--r--deployment/Dockerfile.fedora.amd64 (renamed from deployment/fedora-package-x64/Dockerfile)20
-rw-r--r--deployment/Dockerfile.linux.amd64 (renamed from deployment/macos/Dockerfile)16
-rw-r--r--deployment/Dockerfile.macos (renamed from deployment/portable/Dockerfile)16
-rw-r--r--deployment/Dockerfile.portable (renamed from deployment/linux-x64/Dockerfile)17
-rw-r--r--deployment/Dockerfile.ubuntu.amd6431
-rw-r--r--deployment/Dockerfile.ubuntu.arm64 (renamed from deployment/ubuntu-package-arm64/Dockerfile.amd64)19
-rw-r--r--deployment/Dockerfile.ubuntu.armhf (renamed from deployment/ubuntu-package-armhf/Dockerfile.amd64)19
-rw-r--r--deployment/Dockerfile.windows.amd64 (renamed from deployment/win-x86/Dockerfile)17
-rw-r--r--deployment/README.md62
-rwxr-xr-xdeployment/build.centos.amd6424
-rwxr-xr-xdeployment/build.debian.amd6428
-rwxr-xr-xdeployment/build.debian.arm6429
-rwxr-xr-xdeployment/build.debian.armhf29
-rwxr-xr-xdeployment/build.fedora.amd6424
-rwxr-xr-xdeployment/build.linux.amd6427
-rwxr-xr-xdeployment/build.macos27
-rwxr-xr-xdeployment/build.portable27
-rwxr-xr-xdeployment/build.ubuntu.amd6428
-rwxr-xr-xdeployment/build.ubuntu.arm6429
-rwxr-xr-xdeployment/build.ubuntu.armhf29
-rwxr-xr-xdeployment/build.windows.amd6454
-rw-r--r--deployment/centos-package-x64/Dockerfile39
-rwxr-xr-xdeployment/centos-package-x64/clean.sh32
-rw-r--r--deployment/centos-package-x64/dependencies.txt1
-rwxr-xr-xdeployment/centos-package-x64/docker-build.sh18
-rwxr-xr-xdeployment/centos-package-x64/package.sh34
l---------deployment/centos-package-x64/pkg-src1
-rw-r--r--deployment/debian-package-arm64/Dockerfile.arm6434
-rwxr-xr-xdeployment/debian-package-arm64/clean.sh27
-rw-r--r--deployment/debian-package-arm64/dependencies.txt1
-rwxr-xr-xdeployment/debian-package-arm64/docker-build.sh21
-rwxr-xr-xdeployment/debian-package-arm64/package.sh45
l---------deployment/debian-package-arm64/pkg-src1
-rw-r--r--deployment/debian-package-armhf/Dockerfile.armhf34
-rwxr-xr-xdeployment/debian-package-armhf/clean.sh27
-rw-r--r--deployment/debian-package-armhf/dependencies.txt1
-rwxr-xr-xdeployment/debian-package-armhf/docker-build.sh21
-rwxr-xr-xdeployment/debian-package-armhf/package.sh45
l---------deployment/debian-package-armhf/pkg-src1
-rw-r--r--deployment/debian-package-x64/Dockerfile34
-rwxr-xr-xdeployment/debian-package-x64/clean.sh27
-rw-r--r--deployment/debian-package-x64/dependencies.txt1
-rwxr-xr-xdeployment/debian-package-x64/docker-build.sh20
-rwxr-xr-xdeployment/debian-package-x64/package.sh34
-rwxr-xr-xdeployment/fedora-package-x64/clean.sh32
-rw-r--r--deployment/fedora-package-x64/dependencies.txt1
-rwxr-xr-xdeployment/fedora-package-x64/docker-build.sh18
-rwxr-xr-xdeployment/fedora-package-x64/package.sh34
-rwxr-xr-xdeployment/linux-x64/clean.sh27
-rw-r--r--deployment/linux-x64/dependencies.txt1
-rwxr-xr-xdeployment/linux-x64/docker-build.sh36
-rwxr-xr-xdeployment/linux-x64/package.sh34
-rwxr-xr-xdeployment/macos/clean.sh27
-rw-r--r--deployment/macos/dependencies.txt1
-rwxr-xr-xdeployment/macos/docker-build.sh36
-rwxr-xr-xdeployment/macos/package.sh34
-rwxr-xr-xdeployment/portable/clean.sh27
-rw-r--r--deployment/portable/dependencies.txt1
-rwxr-xr-xdeployment/portable/docker-build.sh36
-rwxr-xr-xdeployment/portable/package.sh34
-rw-r--r--deployment/ubuntu-package-arm64/Dockerfile.arm6440
-rwxr-xr-xdeployment/ubuntu-package-arm64/clean.sh27
-rw-r--r--deployment/ubuntu-package-arm64/dependencies.txt1
-rwxr-xr-xdeployment/ubuntu-package-arm64/docker-build.sh21
-rwxr-xr-xdeployment/ubuntu-package-arm64/package.sh45
l---------deployment/ubuntu-package-arm64/pkg-src1
-rw-r--r--deployment/ubuntu-package-armhf/Dockerfile.armhf40
-rwxr-xr-xdeployment/ubuntu-package-armhf/clean.sh27
-rw-r--r--deployment/ubuntu-package-armhf/dependencies.txt1
-rwxr-xr-xdeployment/ubuntu-package-armhf/docker-build.sh21
-rwxr-xr-xdeployment/ubuntu-package-armhf/package.sh45
l---------deployment/ubuntu-package-armhf/pkg-src1
-rw-r--r--deployment/ubuntu-package-x64/Dockerfile36
-rwxr-xr-xdeployment/ubuntu-package-x64/clean.sh27
-rw-r--r--deployment/ubuntu-package-x64/dependencies.txt1
-rwxr-xr-xdeployment/ubuntu-package-x64/docker-build.sh20
-rwxr-xr-xdeployment/ubuntu-package-x64/package.sh34
l---------deployment/ubuntu-package-x64/pkg-src1
-rwxr-xr-xdeployment/win-x64/clean.sh27
-rw-r--r--deployment/win-x64/dependencies.txt1
-rwxr-xr-xdeployment/win-x64/docker-build.sh61
-rwxr-xr-xdeployment/win-x64/package.sh34
-rwxr-xr-xdeployment/win-x86/clean.sh27
-rw-r--r--deployment/win-x86/dependencies.txt1
-rwxr-xr-xdeployment/win-x86/docker-build.sh61
-rwxr-xr-xdeployment/win-x86/package.sh34
-rw-r--r--fedora/.gitignore (renamed from deployment/fedora-package-x64/pkg-src/.gitignore)0
-rw-r--r--fedora/Makefile26
-rw-r--r--fedora/README.md (renamed from deployment/fedora-package-x64/pkg-src/README.md)0
-rw-r--r--fedora/jellyfin-firewalld.xml (renamed from deployment/fedora-package-x64/pkg-src/jellyfin-firewalld.xml)0
-rw-r--r--fedora/jellyfin.env (renamed from deployment/fedora-package-x64/pkg-src/jellyfin.env)3
-rw-r--r--fedora/jellyfin.override.conf (renamed from deployment/fedora-package-x64/pkg-src/jellyfin.override.conf)0
-rw-r--r--fedora/jellyfin.service (renamed from deployment/fedora-package-x64/pkg-src/jellyfin.service)2
-rw-r--r--fedora/jellyfin.spec (renamed from deployment/fedora-package-x64/pkg-src/jellyfin.spec)144
-rw-r--r--fedora/jellyfin.sudoers (renamed from deployment/fedora-package-x64/pkg-src/jellyfin.sudoers)0
-rwxr-xr-xfedora/restart.sh (renamed from deployment/fedora-package-x64/pkg-src/restart.sh)6
-rw-r--r--tests/Jellyfin.Common.Tests/Extensions/StringExtensionsTests.cs43
-rw-r--r--tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs19
-rw-r--r--tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj21
-rw-r--r--tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs90
-rw-r--r--tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs49
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/BaseVideoTest.cs13
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs1
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs2
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs59
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs5
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/StackTests.cs6
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/StubTests.cs10
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs4
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs459
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/HttpServer/ResponseFilterTests.cs18
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs27
-rw-r--r--tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs49
-rw-r--r--tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs120
-rw-r--r--tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj33
-rw-r--r--tests/jellyfin-tests.ruleset22
-rw-r--r--windows/build-jellyfin.ps1190
-rw-r--r--windows/dependencies.txt2
-rw-r--r--windows/dialogs/confirmation.nsddef24
-rw-r--r--windows/dialogs/confirmation.nsdinc61
-rw-r--r--windows/dialogs/service-config.nsddef13
-rw-r--r--windows/dialogs/service-config.nsdinc56
-rw-r--r--windows/dialogs/setuptype.nsddef12
-rw-r--r--windows/dialogs/setuptype.nsdinc50
-rw-r--r--windows/helpers/ShowError.nsh10
-rw-r--r--windows/helpers/StrSlash.nsh47
-rw-r--r--windows/jellyfin.nsi575
-rw-r--r--windows/legacy/install-jellyfin.ps1460
-rw-r--r--windows/legacy/install.bat1
352 files changed, 11638 insertions, 4239 deletions
diff --git a/.copr/Makefile b/.copr/Makefile
index ba330ada9..ec3c90dfd 100644..120000
--- a/.copr/Makefile
+++ b/.copr/Makefile
@@ -1,59 +1 @@
-VERSION := $(shell sed -ne '/^Version:/s/.* *//p' \
- deployment/fedora-package-x64/pkg-src/jellyfin.spec)
-
-deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz:
- curl -f -L -o deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz \
- https://github.com/jellyfin/jellyfin-web/archive/v$(VERSION).tar.gz \
- || curl -f -L -o deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz \
- https://github.com/jellyfin/jellyfin-web/archive/master.tar.gz \
-
-srpm: deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz
- cd deployment/fedora-package-x64; \
- SOURCE_DIR=../.. \
- WORKDIR="$${PWD}"; \
- package_temporary_dir="$${WORKDIR}/pkg-dist-tmp"; \
- pkg_src_dir="$${WORKDIR}/pkg-src"; \
- GNU_TAR=1; \
- tar \
- --transform "s,^\.,jellyfin-$(VERSION)," \
- --exclude='.git*' \
- --exclude='**/.git' \
- --exclude='**/.hg' \
- --exclude='**/.vs' \
- --exclude='**/.vscode' \
- --exclude='deployment' \
- --exclude='**/bin' \
- --exclude='**/obj' \
- --exclude='**/.nuget' \
- --exclude='*.deb' \
- --exclude='*.rpm' \
- -czf "pkg-src/jellyfin-$(VERSION).tar.gz" \
- -C $${SOURCE_DIR} ./ || GNU_TAR=0; \
- if [ $${GNU_TAR} -eq 0 ]; then \
- package_temporary_dir="$$(mktemp -d)"; \
- mkdir -p "$${package_temporary_dir}/jellyfin"; \
- tar \
- --exclude='.git*' \
- --exclude='**/.git' \
- --exclude='**/.hg' \
- --exclude='**/.vs' \
- --exclude='**/.vscode' \
- --exclude='deployment' \
- --exclude='**/bin' \
- --exclude='**/obj' \
- --exclude='**/.nuget' \
- --exclude='*.deb' \
- --exclude='*.rpm' \
- -czf "$${package_temporary_dir}/jellyfin/jellyfin-$(VERSION).tar.gz" \
- -C $${SOURCE_DIR} ./; \
- mkdir -p "$${package_temporary_dir}/jellyfin-$(VERSION)"; \
- tar -xzf "$${package_temporary_dir}/jellyfin/jellyfin-$(VERSION).tar.gz" \
- -C "$${package_temporary_dir}/jellyfin-$(VERSION); \
- rm -f "$${package_temporary_dir}/jellyfin/jellyfin-$(VERSION).tar.gz"; \
- tar -czf "$${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-$(VERSION).tar.gz" \
- -C "$${package_temporary_dir}" "jellyfin-$(VERSION); \
- rm -rf $${package_temporary_dir}; \
- fi; \
- rpmbuild -bs pkg-src/jellyfin.spec \
- --define "_sourcedir $$PWD/pkg-src/" \
- --define "_srcrpmdir $(outdir)"
+../fedora/Makefile \ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
index dc9aaa3ed..b84e563ef 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -13,7 +13,7 @@ charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf
-max_line_length = null
+max_line_length = off
# YAML indentation
[*.{yml,yaml}]
@@ -22,6 +22,7 @@ indent_size = 2
# XML indentation
[*.{csproj,xml}]
indent_size = 2
+
###############################
# .NET Coding Conventions #
###############################
@@ -51,11 +52,12 @@ dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
-dotnet_prefer_inferred_tuple_names = true:suggestion
-dotnet_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
+
###############################
# Naming Conventions #
###############################
@@ -67,7 +69,7 @@ dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
-dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected internal, private protected
+dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
@@ -159,6 +161,7 @@ csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_pattern_local_over_anonymous_function = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
+
###############################
# C# Formatting Rules #
###############################
@@ -189,9 +192,3 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false
# Wrapping preferences
csharp_preserve_single_line_statements = true
csharp_preserve_single_line_blocks = true
-###############################
-# VB Coding Conventions #
-###############################
-[*.vb]
-# Modifier preferences
-visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 000000000..e902dc712
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,3 @@
+# Joshua must review all changes to deployment and build.sh
+deployment/* @joshuaboniface
+build.sh @joshuaboniface
diff --git a/.gitignore b/.gitignore
index 523c45a7e..46f036ad9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -245,14 +245,14 @@ pip-log.txt
#########################
# Artifacts for debian-x64
-deployment/debian-package-x64/pkg-src/.debhelper/
-deployment/debian-package-x64/pkg-src/*.debhelper
-deployment/debian-package-x64/pkg-src/debhelper-build-stamp
-deployment/debian-package-x64/pkg-src/files
-deployment/debian-package-x64/pkg-src/jellyfin.substvars
-deployment/debian-package-x64/pkg-src/jellyfin/
+debian/.debhelper/
+debian/*.debhelper
+debian/debhelper-build-stamp
+debian/files
+debian/jellyfin.substvars
+debian/jellyfin/
# Don't ignore the debian/bin folder
-!deployment/debian-package-x64/pkg-src/bin/
+!debian/bin/
deployment/**/dist/
deployment/**/pkg-dist/
@@ -272,3 +272,8 @@ dist
# BenchmarkDotNet artifacts
BenchmarkDotNet.Artifacts
+
+# Ignore web artifacts from native builds
+web/
+web-src.*
+MediaBrowser.WebDashboard/jellyfin-web/
diff --git a/DvdLib/BigEndianBinaryReader.cs b/DvdLib/BigEndianBinaryReader.cs
index 473005b55..b3aad85ce 100644
--- a/DvdLib/BigEndianBinaryReader.cs
+++ b/DvdLib/BigEndianBinaryReader.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System.Buffers.Binary;
using System.IO;
diff --git a/DvdLib/DvdLib.csproj b/DvdLib/DvdLib.csproj
index fd0cb5e25..64d041cb0 100644
--- a/DvdLib/DvdLib.csproj
+++ b/DvdLib/DvdLib.csproj
@@ -13,6 +13,7 @@
<TargetFramework>netstandard2.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
diff --git a/DvdLib/Ifo/Cell.cs b/DvdLib/Ifo/Cell.cs
index 268ab897e..2eab400f7 100644
--- a/DvdLib/Ifo/Cell.cs
+++ b/DvdLib/Ifo/Cell.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System.IO;
namespace DvdLib.Ifo
diff --git a/DvdLib/Ifo/CellPlaybackInfo.cs b/DvdLib/Ifo/CellPlaybackInfo.cs
index e588e51ac..6e33a0ec5 100644
--- a/DvdLib/Ifo/CellPlaybackInfo.cs
+++ b/DvdLib/Ifo/CellPlaybackInfo.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System.IO;
namespace DvdLib.Ifo
diff --git a/DvdLib/Ifo/CellPositionInfo.cs b/DvdLib/Ifo/CellPositionInfo.cs
index 2b973e083..216aa0f77 100644
--- a/DvdLib/Ifo/CellPositionInfo.cs
+++ b/DvdLib/Ifo/CellPositionInfo.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System.IO;
namespace DvdLib.Ifo
diff --git a/DvdLib/Ifo/Chapter.cs b/DvdLib/Ifo/Chapter.cs
index bd3bd9704..1e69429f8 100644
--- a/DvdLib/Ifo/Chapter.cs
+++ b/DvdLib/Ifo/Chapter.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
namespace DvdLib.Ifo
{
public class Chapter
diff --git a/DvdLib/Ifo/Dvd.cs b/DvdLib/Ifo/Dvd.cs
index 5af58a2dc..ca20baa73 100644
--- a/DvdLib/Ifo/Dvd.cs
+++ b/DvdLib/Ifo/Dvd.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.IO;
diff --git a/DvdLib/Ifo/DvdTime.cs b/DvdLib/Ifo/DvdTime.cs
index 3688089ec..978af90c2 100644
--- a/DvdLib/Ifo/DvdTime.cs
+++ b/DvdLib/Ifo/DvdTime.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
namespace DvdLib.Ifo
diff --git a/DvdLib/Ifo/Program.cs b/DvdLib/Ifo/Program.cs
index af08afa35..9f6251270 100644
--- a/DvdLib/Ifo/Program.cs
+++ b/DvdLib/Ifo/Program.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System.Collections.Generic;
namespace DvdLib.Ifo
diff --git a/DvdLib/Ifo/ProgramChain.cs b/DvdLib/Ifo/ProgramChain.cs
index 7b003005b..4860360af 100644
--- a/DvdLib/Ifo/ProgramChain.cs
+++ b/DvdLib/Ifo/ProgramChain.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System.Collections.Generic;
using System.IO;
using System.Linq;
diff --git a/DvdLib/Ifo/Title.cs b/DvdLib/Ifo/Title.cs
index 335e92992..abf806d2c 100644
--- a/DvdLib/Ifo/Title.cs
+++ b/DvdLib/Ifo/Title.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System.Collections.Generic;
using System.IO;
diff --git a/DvdLib/Ifo/UserOperation.cs b/DvdLib/Ifo/UserOperation.cs
index 757a5a05d..5d111ebc0 100644
--- a/DvdLib/Ifo/UserOperation.cs
+++ b/DvdLib/Ifo/UserOperation.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
namespace DvdLib.Ifo
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 43e983054..9d7c0d365 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -908,7 +908,8 @@ namespace Emby.Dlna.PlayTo
return 0;
}
- public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken)
+ /// <inheritdoc />
+ public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
{
if (_disposed)
{
@@ -924,10 +925,12 @@ namespace Emby.Dlna.PlayTo
{
return SendPlayCommand(data as PlayRequest, cancellationToken);
}
+
if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
{
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
}
+
if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
{
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
diff --git a/Emby.Naming/Audio/AlbumParser.cs b/Emby.Naming/Audio/AlbumParser.cs
index 33f4468d9..b63be3a64 100644
--- a/Emby.Naming/Audio/AlbumParser.cs
+++ b/Emby.Naming/Audio/AlbumParser.cs
@@ -1,9 +1,9 @@
+#nullable enable
#pragma warning disable CS1591
using System;
using System.Globalization;
using System.IO;
-using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
@@ -21,8 +21,7 @@ namespace Emby.Naming.Audio
public bool IsMultiPart(string path)
{
var filename = Path.GetFileName(path);
-
- if (string.IsNullOrEmpty(filename))
+ if (filename.Length == 0)
{
return false;
}
@@ -39,18 +38,22 @@ namespace Emby.Naming.Audio
filename = filename.Replace(')', ' ');
filename = Regex.Replace(filename, @"\s+", " ");
- filename = filename.TrimStart();
+ ReadOnlySpan<char> trimmedFilename = filename.TrimStart();
foreach (var prefix in _options.AlbumStackingPrefixes)
{
- if (filename.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) != 0)
+ if (!trimmedFilename.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
continue;
}
- var tmp = filename.Substring(prefix.Length);
+ var tmp = trimmedFilename.Slice(prefix.Length).Trim();
- tmp = tmp.Trim().Split(' ').FirstOrDefault() ?? string.Empty;
+ int index = tmp.IndexOf(' ');
+ if (index != -1)
+ {
+ tmp = tmp.Slice(0, index);
+ }
if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
{
diff --git a/Emby.Naming/Audio/AudioFileParser.cs b/Emby.Naming/Audio/AudioFileParser.cs
index 25d5f8735..6b2f4be93 100644
--- a/Emby.Naming/Audio/AudioFileParser.cs
+++ b/Emby.Naming/Audio/AudioFileParser.cs
@@ -1,3 +1,4 @@
+#nullable enable
#pragma warning disable CS1591
using System;
@@ -11,7 +12,7 @@ namespace Emby.Naming.Audio
{
public static bool IsAudioFile(string path, NamingOptions options)
{
- var extension = Path.GetExtension(path) ?? string.Empty;
+ var extension = Path.GetExtension(path);
return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
}
}
diff --git a/Emby.Naming/Common/EpisodeExpression.cs b/Emby.Naming/Common/EpisodeExpression.cs
index 07de72851..ed6ba8881 100644
--- a/Emby.Naming/Common/EpisodeExpression.cs
+++ b/Emby.Naming/Common/EpisodeExpression.cs
@@ -23,11 +23,6 @@ namespace Emby.Naming.Common
{
}
- public EpisodeExpression()
- : this(null)
- {
- }
-
public string Expression
{
get => _expression;
@@ -48,6 +43,6 @@ namespace Emby.Naming.Common
public string[] DateTimeFormats { get; set; }
- public Regex Regex => _regex ?? (_regex = new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled));
+ public Regex Regex => _regex ??= new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled);
}
}
diff --git a/Emby.Naming/Subtitles/SubtitleParser.cs b/Emby.Naming/Subtitles/SubtitleParser.cs
index 88ec3e2d6..24e59f90a 100644
--- a/Emby.Naming/Subtitles/SubtitleParser.cs
+++ b/Emby.Naming/Subtitles/SubtitleParser.cs
@@ -1,3 +1,4 @@
+#nullable enable
#pragma warning disable CS1591
using System;
@@ -16,11 +17,11 @@ namespace Emby.Naming.Subtitles
_options = options;
}
- public SubtitleInfo ParseFile(string path)
+ public SubtitleInfo? ParseFile(string path)
{
- if (string.IsNullOrEmpty(path))
+ if (path.Length == 0)
{
- throw new ArgumentNullException(nameof(path));
+ throw new ArgumentException("File path can't be empty.", nameof(path));
}
var extension = Path.GetExtension(path);
@@ -52,11 +53,6 @@ namespace Emby.Naming.Subtitles
private string[] GetFlags(string path)
{
- if (string.IsNullOrEmpty(path))
- {
- throw new ArgumentNullException(nameof(path));
- }
-
// Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _.
var file = Path.GetFileName(path);
diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs
index 0b75a8cce..b4aee614b 100644
--- a/Emby.Naming/Video/VideoResolver.cs
+++ b/Emby.Naming/Video/VideoResolver.cs
@@ -89,14 +89,14 @@ namespace Emby.Naming.Video
if (parseName)
{
var cleanDateTimeResult = CleanDateTime(name);
+ name = cleanDateTimeResult.Name;
+ year = cleanDateTimeResult.Year;
if (extraResult.ExtraType == null
- && TryCleanString(cleanDateTimeResult.Name, out ReadOnlySpan<char> newName))
+ && TryCleanString(name, out ReadOnlySpan<char> newName))
{
name = newName.ToString();
}
-
- year = cleanDateTimeResult.Year;
}
return new VideoFileInfo
diff --git a/Emby.Notifications/NotificationEntryPoint.cs b/Emby.Notifications/NotificationEntryPoint.cs
index befecc570..869b7407e 100644
--- a/Emby.Notifications/NotificationEntryPoint.cs
+++ b/Emby.Notifications/NotificationEntryPoint.cs
@@ -143,7 +143,7 @@ namespace Emby.Notifications
var notification = new NotificationRequest
{
- Description = "Please see jellyfin.media for details.",
+ Description = "Please see jellyfin.org for details.",
NotificationType = type,
Name = _localization.GetLocalizedString("NewVersionIsAvailable")
};
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 730323c22..f10981ef0 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -44,7 +44,6 @@ using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Services;
using Emby.Server.Implementations.Session;
-using Emby.Server.Implementations.SocketSharp;
using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates;
using Emby.Server.Implementations.SyncPlay;
@@ -96,7 +95,6 @@ using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks;
-using MediaBrowser.Model.Updates;
using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.TheTvdb;
@@ -104,9 +102,9 @@ using MediaBrowser.Providers.Subtitles;
using MediaBrowser.WebDashboard.Api;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using Prometheus.DotNetRuntime;
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
namespace Emby.Server.Implementations
@@ -261,6 +259,12 @@ namespace Emby.Server.Implementations
_startupOptions = options;
+ // Initialize runtime stat collection
+ if (ServerConfigurationManager.Configuration.EnableMetrics)
+ {
+ DotNetRuntimeStatsBuilder.Default().StartCollecting();
+ }
+
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
_networkManager.NetworkChanged += OnNetworkChanged;
@@ -498,32 +502,8 @@ namespace Emby.Server.Implementations
RegisterServices(serviceCollection);
}
- public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next)
- {
- if (!context.WebSockets.IsWebSocketRequest)
- {
- await next().ConfigureAwait(false);
- return;
- }
-
- await _httpServer.ProcessWebSocketRequest(context).ConfigureAwait(false);
- }
-
- public async Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
- {
- if (context.WebSockets.IsWebSocketRequest)
- {
- await next().ConfigureAwait(false);
- return;
- }
-
- var request = context.Request;
- var response = context.Response;
- var localPath = context.Request.Path.ToString();
-
- var req = new WebSocketSharpRequest(request, response, request.Path, LoggerFactory.CreateLogger<WebSocketSharpRequest>());
- await _httpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted).ConfigureAwait(false);
- }
+ public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
+ => _httpServer.RequestHandler(context);
/// <summary>
/// Registers services/resources with the service collection that will be available via DI.
@@ -541,13 +521,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
- // TODO: Remove support for injecting ILogger completely
- serviceCollection.AddSingleton((provider) =>
- {
- Logger.LogWarning("Injecting ILogger directly is deprecated and should be replaced with ILogger<T>");
- return Logger;
- });
-
serviceCollection.AddSingleton(_fileSystemManager);
serviceCollection.AddSingleton<TvdbClientManager>();
@@ -616,7 +589,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
serviceCollection.AddSingleton<ServiceController>();
- serviceCollection.AddSingleton<IHttpListener, WebSocketSharpListener>();
serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
@@ -1147,9 +1119,6 @@ namespace Emby.Server.Implementations
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
CachePath = ApplicationPaths.CachePath,
- HttpServerPortNumber = HttpPort,
- SupportsHttps = SupportsHttps,
- HttpsPortNumber = HttpsPort,
OperatingSystem = OperatingSystem.Id.ToString(),
OperatingSystemDisplayName = OperatingSystem.Name,
CanSelfRestart = CanSelfRestart,
@@ -1185,23 +1154,22 @@ namespace Emby.Server.Implementations
};
}
- public bool EnableHttps => SupportsHttps && ServerConfigurationManager.Configuration.EnableHttps;
-
- public bool SupportsHttps => Certificate != null || ServerConfigurationManager.Configuration.IsBehindProxy;
+ /// <inheritdoc/>
+ public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps;
- public async Task<string> GetLocalApiUrl(CancellationToken cancellationToken, bool forceHttp = false)
+ /// <inheritdoc/>
+ public async Task<string> GetLocalApiUrl(CancellationToken cancellationToken)
{
try
{
// Return the first matched address, if found, or the first known local address
var addresses = await GetLocalIpAddressesInternal(false, 1, cancellationToken).ConfigureAwait(false);
-
- foreach (var address in addresses)
+ if (addresses.Count == 0)
{
- return GetLocalApiUrl(address, forceHttp);
+ return null;
}
- return null;
+ return GetLocalApiUrl(addresses.First());
}
catch (Exception ex)
{
@@ -1228,7 +1196,7 @@ namespace Emby.Server.Implementations
}
/// <inheritdoc />
- public string GetLocalApiUrl(IPAddress ipAddress, bool forceHttp = false)
+ public string GetLocalApiUrl(IPAddress ipAddress)
{
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
{
@@ -1238,29 +1206,30 @@ namespace Emby.Server.Implementations
str.CopyTo(span.Slice(1));
span[^1] = ']';
- return GetLocalApiUrl(span, forceHttp);
+ return GetLocalApiUrl(span);
}
- return GetLocalApiUrl(ipAddress.ToString(), forceHttp);
+ return GetLocalApiUrl(ipAddress.ToString());
}
- /// <inheritdoc />
- public string GetLocalApiUrl(ReadOnlySpan<char> host, bool forceHttp = false)
+ /// <inheritdoc/>
+ public string GetLoopbackHttpApiUrl()
{
- var url = new StringBuilder(64);
- bool useHttps = EnableHttps && !forceHttp;
- url.Append(useHttps ? "https://" : "http://")
- .Append(host)
- .Append(':')
- .Append(useHttps ? HttpsPort : HttpPort);
-
- string baseUrl = ServerConfigurationManager.Configuration.BaseUrl;
- if (baseUrl.Length != 0)
- {
- url.Append(baseUrl);
- }
+ return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort);
+ }
- return url.ToString();
+ /// <inheritdoc/>
+ public string GetLocalApiUrl(ReadOnlySpan<char> host, string scheme = null, int? port = null)
+ {
+ // NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
+ // not. For consistency, always trim the trailing slash.
+ return new UriBuilder
+ {
+ Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
+ Host = host.ToString(),
+ Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
+ Path = ServerConfigurationManager.Configuration.BaseUrl
+ }.ToString().TrimEnd('/');
}
public Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken)
@@ -1294,7 +1263,7 @@ namespace Emby.Server.Implementations
}
}
- var valid = await IsIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false);
+ var valid = await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false);
if (valid)
{
resultList.Add(address);
@@ -1328,7 +1297,7 @@ namespace Emby.Server.Implementations
private readonly ConcurrentDictionary<string, bool> _validAddressResults = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
- private async Task<bool> IsIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken)
+ private async Task<bool> IsLocalIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken)
{
if (address.Equals(IPAddress.Loopback)
|| address.Equals(IPAddress.IPv6Loopback))
@@ -1336,8 +1305,7 @@ namespace Emby.Server.Implementations
return true;
}
- var apiUrl = GetLocalApiUrl(address);
- apiUrl += "/system/ping";
+ var apiUrl = GetLocalApiUrl(address) + "/system/ping";
if (_validAddressResults.TryGetValue(apiUrl, out var cachedResult))
{
diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs
index 96096e142..7f7c6a0be 100644
--- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs
+++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs
@@ -31,18 +31,18 @@ namespace Emby.Server.Implementations.Browser
/// Opens the specified URL in an external browser window. Any exceptions will be logged, but ignored.
/// </summary>
/// <param name="appHost">The application host.</param>
- /// <param name="url">The URL.</param>
- private static void TryOpenUrl(IServerApplicationHost appHost, string url)
+ /// <param name="relativeUrl">The URL to open, relative to the server base URL.</param>
+ private static void TryOpenUrl(IServerApplicationHost appHost, string relativeUrl)
{
try
{
string baseUrl = appHost.GetLocalApiUrl("localhost");
- appHost.LaunchUrl(baseUrl + url);
+ appHost.LaunchUrl(baseUrl + relativeUrl);
}
catch (Exception ex)
{
var logger = appHost.Resolve<ILogger>();
- logger?.LogError(ex, "Failed to open browser window with URL {URL}", url);
+ logger?.LogError(ex, "Failed to open browser window with URL {URL}", relativeUrl);
}
}
}
diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs
index db7c35a7c..dea9b6682 100644
--- a/Emby.Server.Implementations/ConfigurationOptions.cs
+++ b/Emby.Server.Implementations/ConfigurationOptions.cs
@@ -1,7 +1,6 @@
using System.Collections.Generic;
using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.Updates;
-using MediaBrowser.Providers.Music;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Emby.Server.Implementations
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index 22955850a..6ee6230fc 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -375,5 +375,15 @@ namespace Emby.Server.Implementations.Data
return userData;
}
+
+ /// <inheritdoc/>
+ /// <remarks>
+ /// There is nothing to dispose here since <see cref="BaseSqliteRepository.WriteLock"/> and
+ /// <see cref="BaseSqliteRepository.WriteConnection"/> are managed by <see cref="SqliteItemRepository"/>.
+ /// See <see cref="Initialize(IUserManager, SemaphoreSlim, SQLiteDatabaseConnection)"/>.
+ /// </remarks>
+ protected override void Dispose(bool dispose)
+ {
+ }
}
}
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index bf4a0d939..44fc932e3 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -39,6 +39,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.3" />
<PackageReference Include="Mono.Nat" Version="2.0.1" />
+ <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.8.0" />
<PackageReference Include="sharpcompress" Version="0.25.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
index 37d7fd479..878cee23c 100644
--- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
+++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
@@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.EntryPoints
.Append(config.PublicHttpsPort).Append(Separator)
.Append(_appHost.HttpPort).Append(Separator)
.Append(_appHost.HttpsPort).Append(Separator)
- .Append(_appHost.EnableHttps).Append(Separator)
+ .Append(_appHost.ListenWithHttps).Append(Separator)
.Append(config.EnableRemoteAccess).Append(Separator)
.ToString();
}
@@ -158,7 +158,7 @@ namespace Emby.Server.Implementations.EntryPoints
{
yield return CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort);
- if (_appHost.EnableHttps)
+ if (_appHost.ListenWithHttps)
{
yield return CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort);
}
diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
index 211a0c1d9..794d55c04 100644
--- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
+++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
@@ -6,11 +6,12 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Sockets;
+using System.Net.WebSockets;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
-using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Services;
+using Emby.Server.Implementations.SocketSharp;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
@@ -22,15 +23,17 @@ using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Primitives;
using ServiceStack.Text.Jsv;
namespace Emby.Server.Implementations.HttpServer
{
- public class HttpListenerHost : IHttpServer, IDisposable
+ public class HttpListenerHost : IHttpServer
{
/// <summary>
/// The key for a setting that specifies the default redirect path
@@ -39,17 +42,17 @@ namespace Emby.Server.Implementations.HttpServer
public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath";
private readonly ILogger _logger;
+ private readonly ILoggerFactory _loggerFactory;
private readonly IServerConfigurationManager _config;
private readonly INetworkManager _networkManager;
private readonly IServerApplicationHost _appHost;
private readonly IJsonSerializer _jsonSerializer;
private readonly IXmlSerializer _xmlSerializer;
- private readonly IHttpListener _socketListener;
private readonly Func<Type, Func<string, object>> _funcParseFn;
private readonly string _defaultRedirectPath;
private readonly string _baseUrlPrefix;
+
private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>();
- private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>();
private readonly IHostEnvironment _hostEnvironment;
private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
@@ -63,10 +66,10 @@ namespace Emby.Server.Implementations.HttpServer
INetworkManager networkManager,
IJsonSerializer jsonSerializer,
IXmlSerializer xmlSerializer,
- IHttpListener socketListener,
ILocalizationManager localizationManager,
ServiceController serviceController,
- IHostEnvironment hostEnvironment)
+ IHostEnvironment hostEnvironment,
+ ILoggerFactory loggerFactory)
{
_appHost = applicationHost;
_logger = logger;
@@ -76,11 +79,9 @@ namespace Emby.Server.Implementations.HttpServer
_networkManager = networkManager;
_jsonSerializer = jsonSerializer;
_xmlSerializer = xmlSerializer;
- _socketListener = socketListener;
ServiceController = serviceController;
-
- _socketListener.WebSocketConnected = OnWebSocketConnected;
_hostEnvironment = hostEnvironment;
+ _loggerFactory = loggerFactory;
_funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
@@ -172,38 +173,6 @@ namespace Emby.Server.Implementations.HttpServer
return attributes;
}
- private void OnWebSocketConnected(WebSocketConnectEventArgs e)
- {
- if (_disposed)
- {
- return;
- }
-
- var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger)
- {
- OnReceive = ProcessWebSocketMessageReceived,
- Url = e.Url,
- QueryString = e.QueryString
- };
-
- connection.Closed += OnConnectionClosed;
-
- lock (_webSocketConnections)
- {
- _webSocketConnections.Add(connection);
- }
-
- WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
- }
-
- private void OnConnectionClosed(object sender, EventArgs e)
- {
- lock (_webSocketConnections)
- {
- _webSocketConnections.Remove((IWebSocketConnection)sender);
- }
- }
-
private static Exception GetActualException(Exception ex)
{
if (ex is AggregateException agg)
@@ -289,32 +258,6 @@ namespace Emby.Server.Implementations.HttpServer
.Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase);
}
- /// <summary>
- /// Shut down the Web Service
- /// </summary>
- public void Stop()
- {
- List<IWebSocketConnection> connections;
-
- lock (_webSocketConnections)
- {
- connections = _webSocketConnections.ToList();
- _webSocketConnections.Clear();
- }
-
- foreach (var connection in connections)
- {
- try
- {
- connection.Dispose();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error disposing connection");
- }
- }
- }
-
public static string RemoveQueryStringByKey(string url, string key)
{
var uri = new Uri(url);
@@ -424,33 +367,52 @@ namespace Emby.Server.Implementations.HttpServer
return true;
}
+ /// <summary>
+ /// Validate a connection from a remote IP address to a URL to see if a redirection to HTTPS is required.
+ /// </summary>
+ /// <returns>True if the request is valid, or false if the request is not valid and an HTTPS redirect is required.</returns>
private bool ValidateSsl(string remoteIp, string urlString)
{
- if (_config.Configuration.RequireHttps && _appHost.EnableHttps && !_config.Configuration.IsBehindProxy)
+ if (_config.Configuration.RequireHttps
+ && _appHost.ListenWithHttps
+ && !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase))
{
- if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1)
+ // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
+ if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
+ || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
{
- // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
- if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
- || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
- {
- return true;
- }
+ return true;
+ }
- if (!_networkManager.IsInLocalNetwork(remoteIp))
- {
- return false;
- }
+ if (!_networkManager.IsInLocalNetwork(remoteIp))
+ {
+ return false;
}
}
return true;
}
+ /// <inheritdoc />
+ public Task RequestHandler(HttpContext context)
+ {
+ if (context.WebSockets.IsWebSocketRequest)
+ {
+ return WebSocketRequestHandler(context);
+ }
+
+ var request = context.Request;
+ var response = context.Response;
+ var localPath = context.Request.Path.ToString();
+
+ var req = new WebSocketSharpRequest(request, response, request.Path, _logger);
+ return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted);
+ }
+
/// <summary>
/// Overridable method that can be used to implement a custom handler.
/// </summary>
- public async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
+ private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
{
var stopWatch = new Stopwatch();
stopWatch.Start();
@@ -493,9 +455,10 @@ namespace Emby.Server.Implementations.HttpServer
if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
{
httpRes.StatusCode = 200;
- httpRes.Headers.Add("Access-Control-Allow-Origin", "*");
- httpRes.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
- httpRes.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization");
+ foreach(var (key, value) in GetDefaultCorsHeaders(httpReq))
+ {
+ httpRes.Headers.Add(key, value);
+ }
httpRes.ContentType = "text/plain";
await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false);
return;
@@ -578,6 +541,68 @@ namespace Emby.Server.Implementations.HttpServer
}
}
+ private async Task WebSocketRequestHandler(HttpContext context)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ try
+ {
+ _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
+
+ WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
+
+ var connection = new WebSocketConnection(
+ _loggerFactory.CreateLogger<WebSocketConnection>(),
+ webSocket,
+ context.Connection.RemoteIpAddress,
+ context.Request.Query)
+ {
+ OnReceive = ProcessWebSocketMessageReceived
+ };
+
+ WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
+
+ await connection.ProcessAsync().ConfigureAwait(false);
+ _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
+ }
+ catch (Exception ex) // Otherwise ASP.Net will ignore the exception
+ {
+ _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress);
+ if (!context.Response.HasStarted)
+ {
+ context.Response.StatusCode = 500;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Get the default CORS headers
+ /// </summary>
+ /// <param name="req"></param>
+ /// <returns></returns>
+ public IDictionary<string, string> GetDefaultCorsHeaders(IRequest req)
+ {
+ var origin = req.Headers["Origin"];
+ if (origin == StringValues.Empty)
+ {
+ origin = req.Headers["Host"];
+ if (origin == StringValues.Empty)
+ {
+ origin = "*";
+ }
+ }
+
+ var headers = new Dictionary<string, string>();
+ headers.Add("Access-Control-Allow-Origin", origin);
+ headers.Add("Access-Control-Allow-Credentials", "true");
+ headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
+ headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization, Cookie");
+ return headers;
+ }
+
// Entry point for HttpListener
public ServiceHandler GetServiceHandler(IHttpRequest httpReq)
{
@@ -624,7 +649,7 @@ namespace Emby.Server.Implementations.HttpServer
ResponseFilters = new Action<IRequest, HttpResponse, object>[]
{
- new ResponseFilter(_logger).FilterResponse
+ new ResponseFilter(this, _logger).FilterResponse
};
}
@@ -685,11 +710,6 @@ namespace Emby.Server.Implementations.HttpServer
return _jsonSerializer.DeserializeFromStreamAsync(stream, type);
}
- public Task ProcessWebSocketRequest(HttpContext context)
- {
- return _socketListener.ProcessWebSocketRequest(context);
- }
-
private string NormalizeEmbyRoutePath(string path)
{
_logger.LogDebug("Normalizing /emby route");
@@ -708,28 +728,6 @@ namespace Emby.Server.Implementations.HttpServer
return _baseUrlPrefix + NormalizeUrlPath(path);
}
- /// <inheritdoc />
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- protected virtual void Dispose(bool disposing)
- {
- if (_disposed)
- {
- return;
- }
-
- if (disposing)
- {
- Stop();
- }
-
- _disposed = true;
- }
-
/// <summary>
/// Processes the web socket message received.
/// </summary>
@@ -741,8 +739,6 @@ namespace Emby.Server.Implementations.HttpServer
return Task.CompletedTask;
}
- _logger.LogDebug("Websocket message received: {0}", result.MessageType);
-
IEnumerable<Task> GetTasks()
{
foreach (var x in _webSocketListeners)
diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
index 464ca3a0b..2e9ecc4ae 100644
--- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
+++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
@@ -426,7 +426,7 @@ namespace Emby.Server.Implementations.HttpServer
if (!noCache)
{
- if (!DateTime.TryParseExact(requestContext.Headers[HeaderNames.IfModifiedSince], HttpDateFormat, _enUSculture, DateTimeStyles.AssumeUniversal, out var ifModifiedSinceHeader))
+ if (!DateTime.TryParseExact(requestContext.Headers[HeaderNames.IfModifiedSince], HttpDateFormat, _enUSculture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var ifModifiedSinceHeader))
{
_logger.LogDebug("Failed to parse If-Modified-Since header date: {0}", requestContext.Headers[HeaderNames.IfModifiedSince]);
return null;
diff --git a/Emby.Server.Implementations/HttpServer/IHttpListener.cs b/Emby.Server.Implementations/HttpServer/IHttpListener.cs
deleted file mode 100644
index 501593725..000000000
--- a/Emby.Server.Implementations/HttpServer/IHttpListener.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.Net;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-
-namespace Emby.Server.Implementations.HttpServer
-{
- public interface IHttpListener : IDisposable
- {
- /// <summary>
- /// Gets or sets the error handler.
- /// </summary>
- /// <value>The error handler.</value>
- Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; }
-
- /// <summary>
- /// Gets or sets the request handler.
- /// </summary>
- /// <value>The request handler.</value>
- Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; }
-
- /// <summary>
- /// Gets or sets the web socket handler.
- /// </summary>
- /// <value>The web socket handler.</value>
- Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
-
- /// <summary>
- /// Stops this instance.
- /// </summary>
- Task Stop();
-
- Task ProcessWebSocketRequest(HttpContext ctx);
- }
-}
diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs
index 5e0466629..85c3db9b2 100644
--- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs
+++ b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs
@@ -1,6 +1,8 @@
using System;
+using System.Collections.Generic;
using System.Globalization;
using System.Text;
+using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -13,14 +15,17 @@ namespace Emby.Server.Implementations.HttpServer
/// </summary>
public class ResponseFilter
{
+ private readonly IHttpServer _server;
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="ResponseFilter"/> class.
/// </summary>
+ /// <param name="server">The HTTP server.</param>
/// <param name="logger">The logger.</param>
- public ResponseFilter(ILogger logger)
+ public ResponseFilter(IHttpServer server, ILogger logger)
{
+ _server = server;
_logger = logger;
}
@@ -32,10 +37,16 @@ namespace Emby.Server.Implementations.HttpServer
/// <param name="dto">The dto.</param>
public void FilterResponse(IRequest req, HttpResponse res, object dto)
{
+ foreach(var (key, value) in _server.GetDefaultCorsHeaders(req))
+ {
+ res.Headers.Add(key, value);
+ }
// Try to prevent compatibility view
- res.Headers.Add("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization");
- res.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
- res.Headers.Add("Access-Control-Allow-Origin", "*");
+ res.Headers["Access-Control-Allow-Headers"] = ("Accept, Accept-Language, Authorization, Cache-Control, " +
+ "Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, " +
+ "Content-Type, Cookie, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, " +
+ "Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, " +
+ "X-Emby-Authorization");
if (dto is Exception exception)
{
@@ -82,6 +93,10 @@ namespace Emby.Server.Implementations.HttpServer
{
return null;
}
+ else if (inString.Length == 0)
+ {
+ return inString;
+ }
var newString = new StringBuilder(inString.Length);
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index 4c33ff71b..1f5a7d177 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -1,15 +1,18 @@
-using System;
+#nullable enable
+
+using System;
+using System.Buffers;
+using System.IO.Pipelines;
+using System.Net;
using System.Net.WebSockets;
-using System.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using Emby.Server.Implementations.Net;
+using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
-using UtfUnknown;
namespace Emby.Server.Implementations.HttpServer
{
@@ -24,69 +27,50 @@ namespace Emby.Server.Implementations.HttpServer
private readonly ILogger _logger;
/// <summary>
- /// The json serializer.
+ /// The json serializer options.
/// </summary>
- private readonly IJsonSerializer _jsonSerializer;
+ private readonly JsonSerializerOptions _jsonOptions;
/// <summary>
/// The socket.
/// </summary>
- private readonly IWebSocket _socket;
+ private readonly WebSocket _socket;
/// <summary>
/// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
/// </summary>
+ /// <param name="logger">The logger.</param>
/// <param name="socket">The socket.</param>
/// <param name="remoteEndPoint">The remote end point.</param>
- /// <param name="jsonSerializer">The json serializer.</param>
- /// <param name="logger">The logger.</param>
- /// <exception cref="ArgumentNullException">socket</exception>
- public WebSocketConnection(IWebSocket socket, string remoteEndPoint, IJsonSerializer jsonSerializer, ILogger logger)
+ /// <param name="query">The query.</param>
+ public WebSocketConnection(
+ ILogger<WebSocketConnection> logger,
+ WebSocket socket,
+ IPAddress? remoteEndPoint,
+ IQueryCollection query)
{
- if (socket == null)
- {
- throw new ArgumentNullException(nameof(socket));
- }
-
- if (string.IsNullOrEmpty(remoteEndPoint))
- {
- throw new ArgumentNullException(nameof(remoteEndPoint));
- }
-
- if (jsonSerializer == null)
- {
- throw new ArgumentNullException(nameof(jsonSerializer));
- }
-
- if (logger == null)
- {
- throw new ArgumentNullException(nameof(logger));
- }
-
- Id = Guid.NewGuid();
- _jsonSerializer = jsonSerializer;
+ _logger = logger;
_socket = socket;
- _socket.OnReceiveBytes = OnReceiveInternal;
-
RemoteEndPoint = remoteEndPoint;
- _logger = logger;
+ QueryString = query;
- socket.Closed += OnSocketClosed;
+ _jsonOptions = JsonDefaults.GetOptions();
+ LastActivityDate = DateTime.Now;
}
/// <inheritdoc />
- public event EventHandler<EventArgs> Closed;
+ public event EventHandler<EventArgs>? Closed;
/// <summary>
/// Gets or sets the remote end point.
/// </summary>
- public string RemoteEndPoint { get; private set; }
+ public IPAddress? RemoteEndPoint { get; }
/// <summary>
/// Gets or sets the receive action.
/// </summary>
/// <value>The receive action.</value>
- public Func<WebSocketMessageInfo, Task> OnReceive { get; set; }
+ public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
/// <summary>
/// Gets the last activity date.
@@ -98,22 +82,10 @@ namespace Emby.Server.Implementations.HttpServer
public DateTime LastKeepAliveDate { get; set; }
/// <summary>
- /// Gets the id.
- /// </summary>
- /// <value>The id.</value>
- public Guid Id { get; private set; }
-
- /// <summary>
- /// Gets or sets the URL.
- /// </summary>
- /// <value>The URL.</value>
- public string Url { get; set; }
-
- /// <summary>
/// Gets or sets the query string.
/// </summary>
/// <value>The query string.</value>
- public IQueryCollection QueryString { get; set; }
+ public IQueryCollection QueryString { get; }
/// <summary>
/// Gets the state.
@@ -121,121 +93,142 @@ namespace Emby.Server.Implementations.HttpServer
/// <value>The state.</value>
public WebSocketState State => _socket.State;
- void OnSocketClosed(object sender, EventArgs e)
+ /// <summary>
+ /// Sends a message asynchronously.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="message">The message.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
{
- Closed?.Invoke(this, EventArgs.Empty);
+ var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
+ return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
}
- /// <summary>
- /// Called when [receive].
- /// </summary>
- /// <param name="bytes">The bytes.</param>
- private void OnReceiveInternal(byte[] bytes)
+ /// <inheritdoc />
+ public async Task ProcessAsync(CancellationToken cancellationToken = default)
{
- LastActivityDate = DateTime.UtcNow;
+ var pipe = new Pipe();
+ var writer = pipe.Writer;
- if (OnReceive == null)
+ ValueWebSocketReceiveResult receiveresult;
+ do
{
- return;
- }
- var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName;
+ // Allocate at least 512 bytes from the PipeWriter
+ Memory<byte> memory = writer.GetMemory(512);
+ try
+ {
+ receiveresult = await _socket.ReceiveAsync(memory, cancellationToken);
+ }
+ catch (WebSocketException ex)
+ {
+ _logger.LogWarning("WS {IP} error receiving data: {Message}", RemoteEndPoint, ex.Message);
+ break;
+ }
- if (string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase))
- {
- OnReceiveInternal(Encoding.UTF8.GetString(bytes, 0, bytes.Length));
- }
- else
+ int bytesRead = receiveresult.Count;
+ if (bytesRead == 0)
+ {
+ break;
+ }
+
+ // Tell the PipeWriter how much was read from the Socket
+ writer.Advance(bytesRead);
+
+ // Make the data available to the PipeReader
+ FlushResult flushResult = await writer.FlushAsync();
+ if (flushResult.IsCompleted)
+ {
+ // The PipeReader stopped reading
+ break;
+ }
+
+ LastActivityDate = DateTime.UtcNow;
+
+ if (receiveresult.EndOfMessage)
+ {
+ await ProcessInternal(pipe.Reader).ConfigureAwait(false);
+ }
+ } while (
+ (_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
+ && receiveresult.MessageType != WebSocketMessageType.Close);
+
+ Closed?.Invoke(this, EventArgs.Empty);
+
+ if (_socket.State == WebSocketState.Open
+ || _socket.State == WebSocketState.CloseReceived
+ || _socket.State == WebSocketState.CloseSent)
{
- OnReceiveInternal(Encoding.ASCII.GetString(bytes, 0, bytes.Length));
+ await _socket.CloseAsync(
+ WebSocketCloseStatus.NormalClosure,
+ string.Empty,
+ cancellationToken).ConfigureAwait(false);
}
}
- private void OnReceiveInternal(string message)
+ private async Task ProcessInternal(PipeReader reader)
{
- LastActivityDate = DateTime.UtcNow;
+ ReadResult result = await reader.ReadAsync().ConfigureAwait(false);
+ ReadOnlySequence<byte> buffer = result.Buffer;
- if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase))
+ if (OnReceive == null)
{
- // This info is useful sometimes but also clogs up the log
- _logger.LogDebug("Received web socket message that is not a json structure: {message}", message);
+ // Tell the PipeReader how much of the buffer we have consumed
+ reader.AdvanceTo(buffer.End);
return;
}
+ WebSocketMessage<object> stub;
try
{
- var stub = (WebSocketMessage<object>)_jsonSerializer.DeserializeFromString(message, typeof(WebSocketMessage<object>));
-
- var info = new WebSocketMessageInfo
- {
- MessageType = stub.MessageType,
- Data = stub.Data?.ToString(),
- Connection = this
- };
- if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
+ if (buffer.IsSingleSegment)
{
- SendKeepAliveResponse();
+ stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buffer.FirstSpan, _jsonOptions);
}
else
{
- OnReceive?.Invoke(info);
+ var buf = ArrayPool<byte>.Shared.Rent(Convert.ToInt32(buffer.Length));
+ try
+ {
+ buffer.CopyTo(buf);
+ stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buf, _jsonOptions);
+ }
+ finally
+ {
+ ArrayPool<byte>.Shared.Return(buf);
+ }
}
}
- catch (Exception ex)
+ catch (JsonException ex)
{
+ // Tell the PipeReader how much of the buffer we have consumed
+ reader.AdvanceTo(buffer.End);
_logger.LogError(ex, "Error processing web socket message");
+ return;
}
- }
- /// <summary>
- /// Sends a message asynchronously.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="message">The message.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">message</exception>
- public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
- {
- if (message == null)
- {
- throw new ArgumentNullException(nameof(message));
- }
+ // Tell the PipeReader how much of the buffer we have consumed
+ reader.AdvanceTo(buffer.End);
- var json = _jsonSerializer.SerializeToString(message);
+ _logger.LogDebug("WS {IP} received message: {@Message}", RemoteEndPoint, stub);
- return SendAsync(json, cancellationToken);
- }
+ var info = new WebSocketMessageInfo
+ {
+ MessageType = stub.MessageType,
+ Data = stub.Data?.ToString(), // Data can be null
+ Connection = this
+ };
- /// <summary>
- /// Sends a message asynchronously.
- /// </summary>
- /// <param name="buffer">The buffer.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public Task SendAsync(byte[] buffer, CancellationToken cancellationToken)
- {
- if (buffer == null)
+ if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
{
- throw new ArgumentNullException(nameof(buffer));
+ SendKeepAliveResponse();
}
-
- cancellationToken.ThrowIfCancellationRequested();
-
- return _socket.SendAsync(buffer, true, cancellationToken);
- }
-
- /// <inheritdoc />
- public Task SendAsync(string text, CancellationToken cancellationToken)
- {
- if (string.IsNullOrEmpty(text))
+ else
{
- throw new ArgumentNullException(nameof(text));
+ await OnReceive(info).ConfigureAwait(false);
}
-
- cancellationToken.ThrowIfCancellationRequested();
-
- return _socket.SendAsync(text, true, cancellationToken);
}
private void SendKeepAliveResponse()
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index 6b9f4d052..e27145a1d 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -35,7 +35,8 @@ namespace Emby.Server.Implementations.Library
return null;
}
- public static int? GetDefaultSubtitleStreamIndex(List<MediaStream> streams,
+ public static int? GetDefaultSubtitleStreamIndex(
+ List<MediaStream> streams,
string[] preferredLanguages,
SubtitlePlaybackMode mode,
string audioTrackLanguage)
@@ -115,7 +116,8 @@ namespace Emby.Server.Implementations.Library
.ThenBy(i => i.Index);
}
- public static void SetSubtitleStreamScores(List<MediaStream> streams,
+ public static void SetSubtitleStreamScores(
+ List<MediaStream> streams,
string[] preferredLanguages,
SubtitlePlaybackMode mode,
string audioTrackLanguage)
diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs
index 1d61ed57e..06ff3e611 100644
--- a/Emby.Server.Implementations/Library/PathExtensions.cs
+++ b/Emby.Server.Implementations/Library/PathExtensions.cs
@@ -1,3 +1,5 @@
+#nullable enable
+
using System;
using System.Text.RegularExpressions;
@@ -12,24 +14,24 @@ namespace Emby.Server.Implementations.Library
/// Gets the attribute value.
/// </summary>
/// <param name="str">The STR.</param>
- /// <param name="attrib">The attrib.</param>
+ /// <param name="attribute">The attrib.</param>
/// <returns>System.String.</returns>
- /// <exception cref="ArgumentNullException">attrib</exception>
- public static string GetAttributeValue(this string str, string attrib)
+ /// <exception cref="ArgumentException"><paramref name="str" /> or <paramref name="attribute" /> is empty.</exception>
+ public static string? GetAttributeValue(this string str, string attribute)
{
- if (string.IsNullOrEmpty(str))
+ if (str.Length == 0)
{
- throw new ArgumentNullException(nameof(str));
+ throw new ArgumentException("String can't be empty.", nameof(str));
}
- if (string.IsNullOrEmpty(attrib))
+ if (attribute.Length == 0)
{
- throw new ArgumentNullException(nameof(attrib));
+ throw new ArgumentException("String can't be empty.", nameof(attribute));
}
- string srch = "[" + attrib + "=";
+ string srch = "[" + attribute + "=";
int start = str.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
- if (start > -1)
+ if (start != -1)
{
start += srch.Length;
int end = str.IndexOf(']', start);
@@ -37,7 +39,7 @@ namespace Emby.Server.Implementations.Library
}
// for imdbid we also accept pattern matching
- if (string.Equals(attrib, "imdbid", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(attribute, "imdbid", StringComparison.OrdinalIgnoreCase))
{
var m = Regex.Match(str, "tt([0-9]{7,8})", RegexOptions.IgnoreCase);
return m.Success ? m.Value : null;
diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs
index 34dcbbe28..7ca15b4e5 100644
--- a/Emby.Server.Implementations/Library/ResolverHelper.cs
+++ b/Emby.Server.Implementations/Library/ResolverHelper.cs
@@ -118,10 +118,12 @@ namespace Emby.Server.Implementations.Library
{
throw new ArgumentNullException(nameof(fileSystem));
}
+
if (item == null)
{
throw new ArgumentNullException(nameof(item));
}
+
if (args == null)
{
throw new ArgumentNullException(nameof(args));
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
index 85b1b6e32..6c9ba7c27 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// </summary>
public class MusicAlbumResolver : ItemResolver<MusicAlbum>
{
- private readonly ILogger _logger;
+ private readonly ILogger<MusicAlbumResolver> _logger;
private readonly IFileSystem _fileSystem;
private readonly ILibraryManager _libraryManager;
@@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// <param name="logger">The logger.</param>
/// <param name="fileSystem">The file system.</param>
/// <param name="libraryManager">The library manager.</param>
- public MusicAlbumResolver(ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager)
+ public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, IFileSystem fileSystem, ILibraryManager libraryManager)
{
_logger = logger;
_fileSystem = fileSystem;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
index 681db4896..5f5cd0e92 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
@@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// </summary>
public class MusicArtistResolver : ItemResolver<MusicArtist>
{
- private readonly ILogger _logger;
+ private readonly ILogger<MusicAlbumResolver> _logger;
private readonly IFileSystem _fileSystem;
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _config;
@@ -23,12 +23,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// <summary>
/// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
/// </summary>
- /// <param name="logger">The logger.</param>
+ /// <param name="logger">The logger for the created <see cref="MusicAlbumResolver"/> instances.</param>
/// <param name="fileSystem">The file system.</param>
/// <param name="libraryManager">The library manager.</param>
/// <param name="config">The configuration manager.</param>
public MusicArtistResolver(
- ILogger<MusicArtistResolver> logger,
+ ILogger<MusicAlbumResolver> logger,
IFileSystem fileSystem,
ILibraryManager libraryManager,
IServerConfigurationManager config)
diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs
index d63bc6bda..b8feb5535 100644
--- a/Emby.Server.Implementations/Library/UserManager.cs
+++ b/Emby.Server.Implementations/Library/UserManager.cs
@@ -608,6 +608,31 @@ namespace Emby.Server.Implementations.Library
return dto;
}
+ public PublicUserDto GetPublicUserDto(User user, string remoteEndPoint = null)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ IAuthenticationProvider authenticationProvider = GetAuthenticationProvider(user);
+ bool hasConfiguredPassword = authenticationProvider.HasPassword(user);
+ bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(authenticationProvider.GetEasyPasswordHash(user));
+
+ bool hasPassword = user.Configuration.EnableLocalPassword &&
+ !string.IsNullOrEmpty(remoteEndPoint) &&
+ _networkManager.IsInLocalNetwork(remoteEndPoint) ? hasConfiguredEasyPassword : hasConfiguredPassword;
+
+ PublicUserDto dto = new PublicUserDto
+ {
+ Name = user.Name,
+ HasPassword = hasPassword,
+ HasConfiguredPassword = hasConfiguredPassword,
+ };
+
+ return dto;
+ }
+
public UserDto GetOfflineUserDto(User user)
{
var dto = GetUserDto(user);
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 33f4ca146..3efe1ee25 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -1059,7 +1059,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var stream = new MediaSourceInfo
{
- EncoderPath = _appHost.GetLocalApiUrl("127.0.0.1", true) + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
+ EncoderPath = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
EncoderProtocol = MediaProtocol.Http,
Path = info.Path,
Protocol = MediaProtocol.File,
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
index d89a816b3..82b1f3cf1 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
@@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
//OpenedMediaSource.Path = tempFile;
//OpenedMediaSource.ReadAtNativeFramerate = true;
- MediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1", true) + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
+ MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
MediaSource.Protocol = MediaProtocol.Http;
//OpenedMediaSource.SupportsDirectPlay = false;
//OpenedMediaSource.SupportsDirectStream = true;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index f5dda79db..f7c9c736e 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public M3UTunerHost(
IServerConfigurationManager config,
IMediaSourceManager mediaSourceManager,
- ILogger logger,
+ ILogger<M3UTunerHost> logger,
IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
IHttpClient httpClient,
@@ -83,7 +83,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return Task.FromResult(list);
}
- private static readonly string[] _disallowedSharedStreamExtensions = new string[]
+ private static readonly string[] _disallowedSharedStreamExtensions =
{
".mkv",
".mp4",
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
index 0e600202a..083fcd029 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -106,7 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
//OpenedMediaSource.Path = tempFile;
//OpenedMediaSource.ReadAtNativeFramerate = true;
- MediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1", true) + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
+ MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
MediaSource.Protocol = MediaProtocol.Http;
//OpenedMediaSource.Path = TempFilePath;
diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json
index 1363eaf85..20447347b 100644
--- a/Emby.Server.Implementations/Localization/Core/af.json
+++ b/Emby.Server.Implementations/Localization/Core/af.json
@@ -4,7 +4,7 @@
"Folders": "Fouers",
"Favorites": "Gunstelinge",
"HeaderFavoriteShows": "Gunsteling Vertonings",
- "ValueSpecialEpisodeName": "Spesiaal - {0}",
+ "ValueSpecialEpisodeName": "Spesiale - {0}",
"HeaderAlbumArtists": "Album Kunstenaars",
"Books": "Boeke",
"HeaderNextUp": "Volgende",
diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json
index ef7792356..4949b10e6 100644
--- a/Emby.Server.Implementations/Localization/Core/bn.json
+++ b/Emby.Server.Implementations/Localization/Core/bn.json
@@ -91,5 +91,7 @@
"HeaderNextUp": "এরপরে আসছে",
"HeaderLiveTV": "লাইভ টিভি",
"HeaderFavoriteSongs": "প্রিয় গানগুলো",
- "HeaderFavoriteShows": "প্রিয় শোগুলো"
+ "HeaderFavoriteShows": "প্রিয় শোগুলো",
+ "TasksLibraryCategory": "গ্রন্থাগার",
+ "TasksMaintenanceCategory": "রক্ষণাবেক্ষণ"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json
index e0bbe90b3..d93920f43 100644
--- a/Emby.Server.Implementations/Localization/Core/es-MX.json
+++ b/Emby.Server.Implementations/Localization/Core/es-MX.json
@@ -11,7 +11,7 @@
"Collections": "Colecciones",
"DeviceOfflineWithName": "{0} se ha desconectado",
"DeviceOnlineWithName": "{0} está conectado",
- "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión de {0}",
+ "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}",
"Favorites": "Favoritos",
"Folders": "Carpetas",
"Genres": "Géneros",
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index de1baada8..e7bd3959b 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -71,7 +71,7 @@
"ScheduledTaskFailedWithName": "{0} falló",
"ScheduledTaskStartedWithName": "{0} iniciada",
"ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
- "Shows": "Series",
+ "Shows": "Mostrar",
"Songs": "Canciones",
"StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.",
"SubtitleDownloadFailureForItem": "Error al descargar subtítulos para {0}",
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index b39adefe7..f8d6e0e09 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -1,5 +1,5 @@
{
- "HeaderLiveTV": "Suorat lähetykset",
+ "HeaderLiveTV": "Live-TV",
"NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.",
"NameSeasonUnknown": "Tuntematon Kausi",
"NameSeasonNumber": "Kausi {0}",
@@ -12,7 +12,7 @@
"MessageNamedServerConfigurationUpdatedWithValue": "Palvelimen asetusryhmä {0} on päivitetty",
"MessageApplicationUpdatedTo": "Jellyfin palvelin on päivitetty versioon {0}",
"MessageApplicationUpdated": "Jellyfin palvelin on päivitetty",
- "Latest": "Viimeisin",
+ "Latest": "Uusimmat",
"LabelRunningTimeValue": "Toiston kesto: {0}",
"LabelIpAddressValue": "IP-osoite: {0}",
"ItemRemovedWithName": "{0} poistettiin kirjastosta",
@@ -41,7 +41,7 @@
"CameraImageUploadedFrom": "Uusi kamerakuva on ladattu {0}",
"Books": "Kirjat",
"AuthenticationSucceededWithUserName": "{0} todennus onnistui",
- "Artists": "Esiintyjät",
+ "Artists": "Artistit",
"Application": "Sovellus",
"AppDeviceValues": "Sovellus: {0}, Laite: {1}",
"Albums": "Albumit",
@@ -67,21 +67,21 @@
"UserDownloadingItemWithValues": "{0} lataa {1}",
"UserDeletedWithName": "Käyttäjä {0} poistettu",
"UserCreatedWithName": "Käyttäjä {0} luotu",
- "TvShows": "TV-Ohjelmat",
+ "TvShows": "TV-sarjat",
"Sync": "Synkronoi",
- "SubtitleDownloadFailureFromForItem": "Tekstityksen lataaminen epäonnistui {0} - {1}",
+ "SubtitleDownloadFailureFromForItem": "Tekstitysten lataus ({0} -> {1}) epäonnistui //this string would have to be generated for each provider and movie because of finnish cases, sorry",
"StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Kokeile hetken kuluttua uudelleen.",
"Songs": "Kappaleet",
- "Shows": "Ohjelmat",
- "ServerNameNeedsToBeRestarted": "{0} vaatii uudelleenkäynnistyksen",
+ "Shows": "Sarjat",
+ "ServerNameNeedsToBeRestarted": "{0} täytyy käynnistää uudelleen",
"ProviderValue": "Tarjoaja: {0}",
"Plugin": "Liitännäinen",
"NotificationOptionVideoPlaybackStopped": "Videon toisto pysäytetty",
- "NotificationOptionVideoPlayback": "Videon toisto aloitettu",
- "NotificationOptionUserLockedOut": "Käyttäjä lukittu",
+ "NotificationOptionVideoPlayback": "Videota toistetaan",
+ "NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos",
"NotificationOptionTaskFailed": "Ajastettu tehtävä epäonnistui",
- "NotificationOptionServerRestartRequired": "Palvelimen uudelleenkäynnistys vaaditaan",
- "NotificationOptionPluginUpdateInstalled": "Lisäosan päivitys asennettu",
+ "NotificationOptionServerRestartRequired": "Palvelin pitää käynnistää uudelleen",
+ "NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty",
"NotificationOptionPluginUninstalled": "Liitännäinen poistettu",
"NotificationOptionPluginInstalled": "Liitännäinen asennettu",
"NotificationOptionPluginError": "Ongelma liitännäisessä",
@@ -90,8 +90,8 @@
"NotificationOptionCameraImageUploaded": "Kameran kuva ladattu",
"NotificationOptionAudioPlaybackStopped": "Äänen toisto lopetettu",
"NotificationOptionAudioPlayback": "Toistetaan ääntä",
- "NotificationOptionApplicationUpdateInstalled": "Uusi sovellusversio asennettu",
- "NotificationOptionApplicationUpdateAvailable": "Sovelluksesta on uusi versio saatavilla",
+ "NotificationOptionApplicationUpdateInstalled": "Sovelluspäivitys asennettu",
+ "NotificationOptionApplicationUpdateAvailable": "Ohjelmistopäivitys saatavilla",
"TasksMaintenanceCategory": "Ylläpito",
"TaskDownloadMissingSubtitlesDescription": "Etsii puuttuvia tekstityksiä videon metadatatietojen pohjalta.",
"TaskDownloadMissingSubtitles": "Lataa puuttuvat tekstitykset",
diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json
index 2c9dae6a1..c2349ba5b 100644
--- a/Emby.Server.Implementations/Localization/Core/fr-CA.json
+++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json
@@ -94,5 +94,23 @@
"ValueSpecialEpisodeName": "Spécial - {0}",
"VersionNumber": "Version {0}",
"TasksLibraryCategory": "Bibliothèque",
- "TasksMaintenanceCategory": "Entretien"
+ "TasksMaintenanceCategory": "Entretien",
+ "TaskDownloadMissingSubtitlesDescription": "Recherche l'internet pour des sous-titres manquants à base de métadonnées configurées.",
+ "TaskDownloadMissingSubtitles": "Télécharger des sous-titres manquants",
+ "TaskRefreshChannelsDescription": "Rafraîchit des informations des chaines d'internet.",
+ "TaskRefreshChannels": "Rafraîchir des chaines",
+ "TaskCleanTranscodeDescription": "Retirer des fichiers de transcodage de plus qu'un jour.",
+ "TaskCleanTranscode": "Nettoyer le directoire de transcodage",
+ "TaskUpdatePluginsDescription": "Télécharger et installer des mises à jours des plugins qui sont configurés m.à.j. automisés.",
+ "TaskUpdatePlugins": "Mise à jour des plugins",
+ "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque.",
+ "TaskRefreshPeople": "Rafraîchir les acteurs",
+ "TaskCleanLogsDescription": "Retire les données qui ont plus que {0} jours.",
+ "TaskCleanLogs": "Nettoyer les données de directoire",
+ "TaskRefreshLibraryDescription": "Analyse votre bibliothèque média pour des nouveaux fichiers et rafraîchit les métadonnées.",
+ "TaskRefreshChapterImages": "Extraire des images du chapitre",
+ "TaskRefreshChapterImagesDescription": "Créer des vignettes pour des vidéos qui ont des chapitres",
+ "TaskRefreshLibrary": "Analyser la bibliothèque de média",
+ "TaskCleanCache": "Nettoyer le cache de directoire",
+ "TasksApplicationCategory": "Application"
}
diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json
index 9611e33f5..8780a884b 100644
--- a/Emby.Server.Implementations/Localization/Core/gsw.json
+++ b/Emby.Server.Implementations/Localization/Core/gsw.json
@@ -1,41 +1,41 @@
{
- "Albums": "Albom",
- "AppDeviceValues": "App: {0}, Grät: {1}",
- "Application": "Aawändig",
- "Artists": "Könstler",
- "AuthenticationSucceededWithUserName": "{0} het sech aagmäudet",
- "Books": "Büecher",
- "CameraImageUploadedFrom": "Es nöis Foti esch ufeglade worde vo {0}",
- "Channels": "Kanäu",
- "ChapterNameValue": "Kapitu {0}",
- "Collections": "Sammlige",
- "DeviceOfflineWithName": "{0} esch offline gange",
- "DeviceOnlineWithName": "{0} esch online cho",
- "FailedLoginAttemptWithUserName": "Fäugschlagne Aamäudeversuech vo {0}",
- "Favorites": "Favorite",
+ "Albums": "Alben",
+ "AppDeviceValues": "App: {0}, Gerät: {1}",
+ "Application": "Anwendung",
+ "Artists": "Künstler",
+ "AuthenticationSucceededWithUserName": "{0} hat sich angemeldet",
+ "Books": "Bücher",
+ "CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
+ "Channels": "Kanäle",
+ "ChapterNameValue": "Kapitel {0}",
+ "Collections": "Sammlungen",
+ "DeviceOfflineWithName": "{0} wurde getrennt",
+ "DeviceOnlineWithName": "{0} ist verbunden",
+ "FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
+ "Favorites": "Favoriten",
"Folders": "Ordner",
"Genres": "Genres",
- "HeaderAlbumArtists": "Albom-Könstler",
+ "HeaderAlbumArtists": "Album-Künstler",
"HeaderCameraUploads": "Kamera-Uploads",
- "HeaderContinueWatching": "Wiiterluege",
- "HeaderFavoriteAlbums": "Lieblingsalbe",
- "HeaderFavoriteArtists": "Lieblings-Interprete",
- "HeaderFavoriteEpisodes": "Lieblingsepisode",
- "HeaderFavoriteShows": "Lieblingsserie",
+ "HeaderContinueWatching": "weiter schauen",
+ "HeaderFavoriteAlbums": "Lieblingsalben",
+ "HeaderFavoriteArtists": "Lieblings-Künstler",
+ "HeaderFavoriteEpisodes": "Lieblingsepisoden",
+ "HeaderFavoriteShows": "Lieblingsserien",
"HeaderFavoriteSongs": "Lieblingslieder",
- "HeaderLiveTV": "Live-Färnseh",
- "HeaderNextUp": "Als nächts",
- "HeaderRecordingGroups": "Ufnahmegruppe",
- "HomeVideos": "Heimfilmli",
- "Inherit": "Hinzuefüege",
- "ItemAddedWithName": "{0} esch de Bibliothek dezuegfüegt worde",
- "ItemRemovedWithName": "{0} esch vo de Bibliothek entfärnt worde",
- "LabelIpAddressValue": "IP-Adrässe: {0}",
- "LabelRunningTimeValue": "Loufziit: {0}",
- "Latest": "Nöischti",
- "MessageApplicationUpdated": "Jellyfin Server esch aktualisiert worde",
- "MessageApplicationUpdatedTo": "Jellyfin Server esch of Version {0} aktualisiert worde",
- "MessageNamedServerConfigurationUpdatedWithValue": "De Serveriistöuigsberiich {0} esch aktualisiert worde",
+ "HeaderLiveTV": "Live-Fernseh",
+ "HeaderNextUp": "Als Nächstes",
+ "HeaderRecordingGroups": "Aufnahme-Gruppen",
+ "HomeVideos": "Heimvideos",
+ "Inherit": "Vererben",
+ "ItemAddedWithName": "{0} wurde der Bibliothek hinzugefügt",
+ "ItemRemovedWithName": "{0} wurde aus der Bibliothek entfernt",
+ "LabelIpAddressValue": "IP-Adresse: {0}",
+ "LabelRunningTimeValue": "Laufzeit: {0}",
+ "Latest": "Neueste",
+ "MessageApplicationUpdated": "Jellyfin-Server wurde aktualisiert",
+ "MessageApplicationUpdatedTo": "Jellyfin-Server wurde auf Version {0} aktualisiert",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Der Server-Einstellungsbereich {0} wurde aktualisiert",
"MessageServerConfigurationUpdated": "Serveriistöuige send aktualisiert worde",
"MixedContent": "Gmeschti Inhäut",
"Movies": "Film",
@@ -50,7 +50,7 @@
"NotificationOptionAudioPlayback": "Audiowedergab gstartet",
"NotificationOptionAudioPlaybackStopped": "Audiwedergab gstoppt",
"NotificationOptionCameraImageUploaded": "Foti ueglade",
- "NotificationOptionInstallationFailed": "Installationsfäuer",
+ "NotificationOptionInstallationFailed": "Installationsfehler",
"NotificationOptionNewLibraryContent": "Nöie Inhaut hinzuegfüegt",
"NotificationOptionPluginError": "Plugin-Fäuer",
"NotificationOptionPluginInstalled": "Plugin installiert",
@@ -92,5 +92,27 @@
"UserStoppedPlayingItemWithValues": "{0} het d'Wedergab vo {1} of {2} gstoppt",
"ValueHasBeenAddedToLibrary": "{0} esch dinnere Biblithek hinzuegfüegt worde",
"ValueSpecialEpisodeName": "Extra - {0}",
- "VersionNumber": "Version {0}"
+ "VersionNumber": "Version {0}",
+ "TaskCleanLogs": "Lösche Log Pfad",
+ "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
+ "TaskRefreshLibrary": "Scanne alle Bibliotheken",
+ "TaskRefreshChapterImagesDescription": "Kreiert Vorschaubilder für Videos welche Kapitel haben.",
+ "TaskRefreshChapterImages": "Extrahiere Kapitel-Bilder",
+ "TaskCleanCacheDescription": "Löscht Zwischenspeicherdatein die nicht länger von System gebraucht werden.",
+ "TaskCleanCache": "Leere Cache Pfad",
+ "TasksChannelsCategory": "Internet Kanäle",
+ "TasksApplicationCategory": "Applikation",
+ "TasksLibraryCategory": "Bibliothek",
+ "TasksMaintenanceCategory": "Verwaltung",
+ "TaskDownloadMissingSubtitlesDescription": "Durchsucht das Internet nach fehlenden Untertiteln, basierend auf den Metadaten Einstellungen.",
+ "TaskDownloadMissingSubtitles": "Lade fehlende Untertitel herunter",
+ "TaskRefreshChannelsDescription": "Aktualisiert Internet Kanal Informationen.",
+ "TaskRefreshChannels": "Aktualisiere Kanäle",
+ "TaskCleanTranscodeDescription": "Löscht Transkodierdateien welche älter als ein Tag sind.",
+ "TaskCleanTranscode": "Räume Transcodier Verzeichnis auf",
+ "TaskUpdatePluginsDescription": "Lädt Aktualisierungen für Erweiterungen herunter und installiert diese, für welche automatische Aktualisierungen konfiguriert sind.",
+ "TaskUpdatePlugins": "Aktualisiere Erweiterungen",
+ "TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schausteller und Regisseure in deiner Bibliothek.",
+ "TaskRefreshPeople": "Aktualisiere Schauspieler",
+ "TaskCleanLogsDescription": "Löscht Log Dateien die älter als {0} Tage sind."
}
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index 266291362..4e54b9f7a 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -62,7 +62,7 @@
"NotificationOptionVideoPlayback": "Video playback started",
"NotificationOptionVideoPlaybackStopped": "Video playback stopped",
"Photos": "תמונות",
- "Playlists": "רשימות ניגון",
+ "Playlists": "רשימות הפעלה",
"Plugin": "Plugin",
"PluginInstalledWithName": "{0} was installed",
"PluginUninstalledWithName": "{0} was uninstalled",
@@ -99,5 +99,13 @@
"TaskCleanCache": "נקה תיקיית מטמון",
"TasksApplicationCategory": "יישום",
"TasksLibraryCategory": "ספרייה",
- "TasksMaintenanceCategory": "תחזוקה"
+ "TasksMaintenanceCategory": "תחזוקה",
+ "TaskUpdatePlugins": "עדכן תוספים",
+ "TaskRefreshPeopleDescription": "מעדכן מטא נתונים עבור שחקנים ובמאים בספריית המדיה שלך.",
+ "TaskRefreshPeople": "רענן אנשים",
+ "TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.",
+ "TaskCleanLogs": "נקה תיקיית יומן",
+ "TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.",
+ "TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
+ "TasksChannelsCategory": "ערוצי אינטרנט"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index 6947178d7..c169a35e7 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -30,7 +30,7 @@
"Inherit": "Naslijedi",
"ItemAddedWithName": "{0} je dodano u biblioteku",
"ItemRemovedWithName": "{0} je uklonjen iz biblioteke",
- "LabelIpAddressValue": "Ip adresa: {0}",
+ "LabelIpAddressValue": "IP adresa: {0}",
"LabelRunningTimeValue": "Vrijeme rada: {0}",
"Latest": "Najnovije",
"MessageApplicationUpdated": "Jellyfin Server je ažuriran",
@@ -92,5 +92,13 @@
"UserStoppedPlayingItemWithValues": "{0} je zaustavio {1}",
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
"ValueSpecialEpisodeName": "Specijal - {0}",
- "VersionNumber": "Verzija {0}"
+ "VersionNumber": "Verzija {0}",
+ "TaskRefreshLibraryDescription": "Skenira vašu medijsku knjižnicu sa novim datotekama i osvježuje metapodatke.",
+ "TaskRefreshLibrary": "Skeniraj medijsku knjižnicu",
+ "TaskRefreshChapterImagesDescription": "Stvara sličice za videozapise koji imaju poglavlja.",
+ "TaskRefreshChapterImages": "Raspakiraj slike poglavlja",
+ "TaskCleanCacheDescription": "Briše priručne datoteke nepotrebne za sistem.",
+ "TaskCleanCache": "Očisti priručnu memoriju",
+ "TasksApplicationCategory": "Aplikacija",
+ "TasksMaintenanceCategory": "Održavanje"
}
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index 0758bbe9c..7f5a56e86 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -5,7 +5,7 @@
"Artists": "Artisti",
"AuthenticationSucceededWithUserName": "{0} autenticato con successo",
"Books": "Libri",
- "CameraImageUploadedFrom": "È stata caricata una nuova immagine della fotocamera dal device {0}",
+ "CameraImageUploadedFrom": "È stata caricata una nuova fotografia da {0}",
"Channels": "Canali",
"ChapterNameValue": "Capitolo {0}",
"Collections": "Collezioni",
diff --git a/Emby.Server.Implementations/Localization/Core/mk.json b/Emby.Server.Implementations/Localization/Core/mk.json
index 8df137302..bbdf99aba 100644
--- a/Emby.Server.Implementations/Localization/Core/mk.json
+++ b/Emby.Server.Implementations/Localization/Core/mk.json
@@ -91,5 +91,12 @@
"Songs": "Песни",
"Shows": "Серии",
"ServerNameNeedsToBeRestarted": "{0} треба да се рестартира",
- "ScheduledTaskStartedWithName": "{0} започна"
+ "ScheduledTaskStartedWithName": "{0} започна",
+ "TaskRefreshChapterImages": "Извези Слики од Поглавје",
+ "TaskCleanCacheDescription": "Ги брише кешираните фајлови што не се повеќе потребни од системот.",
+ "TaskCleanCache": "Исчисти Го Кешот",
+ "TasksChannelsCategory": "Интернет Канали",
+ "TasksApplicationCategory": "Апликација",
+ "TasksLibraryCategory": "Библиотека",
+ "TasksMaintenanceCategory": "Одржување"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index 50d0d083c..5637ce346 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -97,5 +97,9 @@
"TasksApplicationCategory": "Applikasjon",
"TasksLibraryCategory": "Bibliotek",
"TasksMaintenanceCategory": "Vedlikehold",
- "TaskCleanCache": "Tøm buffer katalog"
+ "TaskCleanCache": "Tøm buffer katalog",
+ "TaskRefreshLibrary": "Skann mediebibliotek",
+ "TaskRefreshChapterImagesDescription": "Lager forhåndsvisningsbilder for videoer som har kapitler.",
+ "TaskRefreshChapterImages": "Trekk ut Kapittelbilder",
+ "TaskCleanCacheDescription": "Sletter mellomlagrede filer som ikke lengre trengs av systemet."
}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index baa12e98e..41c74d54d 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -5,7 +5,7 @@
"Artists": "Artiesten",
"AuthenticationSucceededWithUserName": "{0} is succesvol geverifieerd",
"Books": "Boeken",
- "CameraImageUploadedFrom": "Er is een nieuwe afbeelding toegevoegd via {0}",
+ "CameraImageUploadedFrom": "Er is een nieuwe camera afbeelding toegevoegd via {0}",
"Channels": "Kanalen",
"ChapterNameValue": "Hoofdstuk {0}",
"Collections": "Verzamelingen",
@@ -26,7 +26,7 @@
"HeaderLiveTV": "Live TV",
"HeaderNextUp": "Volgende",
"HeaderRecordingGroups": "Opnamegroepen",
- "HomeVideos": "Start video's",
+ "HomeVideos": "Home video's",
"Inherit": "Overerven",
"ItemAddedWithName": "{0} is toegevoegd aan de bibliotheek",
"ItemRemovedWithName": "{0} is verwijderd uit de bibliotheek",
@@ -50,7 +50,7 @@
"NotificationOptionAudioPlayback": "Muziek gestart",
"NotificationOptionAudioPlaybackStopped": "Muziek gestopt",
"NotificationOptionCameraImageUploaded": "Camera-afbeelding geüpload",
- "NotificationOptionInstallationFailed": "Installatie mislukking",
+ "NotificationOptionInstallationFailed": "Installatie mislukt",
"NotificationOptionNewLibraryContent": "Nieuwe content toegevoegd",
"NotificationOptionPluginError": "Plug-in fout",
"NotificationOptionPluginInstalled": "Plug-in geïnstalleerd",
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index b60dd33bd..60c58d472 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -92,5 +92,26 @@
"UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}",
"ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici",
"ValueSpecialEpisodeName": "Poseben - {0}",
- "VersionNumber": "Različica {0}"
+ "VersionNumber": "Različica {0}",
+ "TaskDownloadMissingSubtitles": "Prenesi manjkajoče podnapise",
+ "TaskRefreshChannelsDescription": "Osveži podatke spletnih kanalov.",
+ "TaskRefreshChannels": "Osveži kanale",
+ "TaskCleanTranscodeDescription": "Izbriše več kot dan stare datoteke prekodiranja.",
+ "TaskCleanTranscode": "Počisti mapo prekodiranja",
+ "TaskUpdatePluginsDescription": "Prenese in namesti posodobitve za dodatke, ki imajo omogočene samodejne posodobitve.",
+ "TaskUpdatePlugins": "Posodobi dodatke",
+ "TaskRefreshPeopleDescription": "Osveži metapodatke za igralce in režiserje v vaši knjižnici.",
+ "TaskRefreshPeople": "Osveži osebe",
+ "TaskCleanLogsDescription": "Izbriše dnevniške datoteke starejše od {0} dni.",
+ "TaskCleanLogs": "Počisti mapo dnevnika",
+ "TaskRefreshLibraryDescription": "Preišče vašo knjižnico za nove datoteke in osveži metapodatke.",
+ "TaskRefreshLibrary": "Preišči knjižnico predstavnosti",
+ "TaskRefreshChapterImagesDescription": "Ustvari sličice za poglavja videoposnetkov.",
+ "TaskRefreshChapterImages": "Izvleči slike poglavij",
+ "TaskCleanCacheDescription": "Izbriše predpomnjene datoteke, ki niso več potrebne.",
+ "TaskCleanCache": "Počisti mapo predpomnilnika",
+ "TasksChannelsCategory": "Spletni kanali",
+ "TasksApplicationCategory": "Aplikacija",
+ "TasksLibraryCategory": "Knjižnica",
+ "TasksMaintenanceCategory": "Vzdrževanje"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index b7c50394a..c8662b2ca 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -9,7 +9,7 @@
"Channels": "Kanaler",
"ChapterNameValue": "Kapitel {0}",
"Collections": "Samlingar",
- "DeviceOfflineWithName": "{0} har tappat anslutningen",
+ "DeviceOfflineWithName": "{0} har kopplat från",
"DeviceOnlineWithName": "{0} är ansluten",
"FailedLoginAttemptWithUserName": "Misslyckat inloggningsförsök från {0}",
"Favorites": "Favoriter",
@@ -50,7 +50,7 @@
"NotificationOptionAudioPlayback": "Ljuduppspelning har påbörjats",
"NotificationOptionAudioPlaybackStopped": "Ljuduppspelning stoppades",
"NotificationOptionCameraImageUploaded": "Kamerabild har laddats upp",
- "NotificationOptionInstallationFailed": "Fel vid installation",
+ "NotificationOptionInstallationFailed": "Installationen misslyckades",
"NotificationOptionNewLibraryContent": "Nytt innehåll har lagts till",
"NotificationOptionPluginError": "Fel uppstod med tillägget",
"NotificationOptionPluginInstalled": "Tillägg har installerats",
@@ -113,5 +113,6 @@
"TasksChannelsCategory": "Internetkanaler",
"TasksApplicationCategory": "Applikation",
"TasksLibraryCategory": "Bibliotek",
- "TasksMaintenanceCategory": "Underhåll"
+ "TasksMaintenanceCategory": "Underhåll",
+ "TaskRefreshPeople": "Uppdatera Personer"
}
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
new file mode 100644
index 000000000..b2e0b66fe
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -0,0 +1,36 @@
+{
+ "MusicVideos": "Музичні відео",
+ "Music": "Музика",
+ "Movies": "Фільми",
+ "MessageApplicationUpdatedTo": "Jellyfin Server був оновлений до версії {0}",
+ "MessageApplicationUpdated": "Jellyfin Server був оновлений",
+ "Latest": "Останні",
+ "LabelIpAddressValue": "IP-адреси: {0}",
+ "ItemRemovedWithName": "{0} видалено з бібліотеки",
+ "ItemAddedWithName": "{0} додано до бібліотеки",
+ "HeaderNextUp": "Наступний",
+ "HeaderLiveTV": "Ефірне ТБ",
+ "HeaderFavoriteSongs": "Улюблені пісні",
+ "HeaderFavoriteShows": "Улюблені шоу",
+ "HeaderFavoriteEpisodes": "Улюблені серії",
+ "HeaderFavoriteArtists": "Улюблені виконавці",
+ "HeaderFavoriteAlbums": "Улюблені альбоми",
+ "HeaderContinueWatching": "Продовжити перегляд",
+ "HeaderCameraUploads": "Завантажено з камери",
+ "HeaderAlbumArtists": "Виконавці альбомів",
+ "Genres": "Жанри",
+ "Folders": "Директорії",
+ "Favorites": "Улюблені",
+ "DeviceOnlineWithName": "{0} під'єднано",
+ "DeviceOfflineWithName": "{0} від'єднано",
+ "Collections": "Колекції",
+ "ChapterNameValue": "Глава {0}",
+ "Channels": "Канали",
+ "CameraImageUploadedFrom": "Нова фотографія завантажена з {0}",
+ "Books": "Книги",
+ "AuthenticationSucceededWithUserName": "{0} успішно авторизовані",
+ "Artists": "Виконавці",
+ "Application": "Додаток",
+ "AppDeviceValues": "Додаток: {0}, Пристрій: {1}",
+ "Albums": "Альбоми"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index 224748e61..a67a67582 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -1,6 +1,6 @@
{
"Albums": "專輯",
- "AppDeviceValues": "軟體: {0}, 設備: {1}",
+ "AppDeviceValues": "軟件: {0}, 設備: {1}",
"Application": "應用程式",
"Artists": "藝人",
"AuthenticationSucceededWithUserName": "{0} 授權成功",
@@ -92,5 +92,8 @@
"UserStoppedPlayingItemWithValues": "{0} 已在 {2} 上停止播放 {1}",
"ValueHasBeenAddedToLibrary": "{0} 已添加到你的媒體庫",
"ValueSpecialEpisodeName": "特典 - {0}",
- "VersionNumber": "版本{0}"
+ "VersionNumber": "版本{0}",
+ "TaskDownloadMissingSubtitles": "下載遺失的字幕",
+ "TaskUpdatePlugins": "更新插件",
+ "TasksApplicationCategory": "應用程式"
}
diff --git a/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs b/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs
deleted file mode 100644
index fda32da5e..000000000
--- a/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-using WebSocketManager = Emby.Server.Implementations.WebSockets.WebSocketManager;
-
-namespace Emby.Server.Implementations.Middleware
-{
- public class WebSocketMiddleware
- {
- private readonly RequestDelegate _next;
- private readonly ILogger<WebSocketMiddleware> _logger;
- private readonly WebSocketManager _webSocketManager;
-
- public WebSocketMiddleware(RequestDelegate next, ILogger<WebSocketMiddleware> logger, WebSocketManager webSocketManager)
- {
- _next = next;
- _logger = logger;
- _webSocketManager = webSocketManager;
- }
-
- public async Task Invoke(HttpContext httpContext)
- {
- _logger.LogInformation("Handling request: " + httpContext.Request.Path);
-
- if (httpContext.WebSockets.IsWebSocketRequest)
- {
- var webSocketContext = await httpContext.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false);
- if (webSocketContext != null)
- {
- await _webSocketManager.OnWebSocketConnected(webSocketContext).ConfigureAwait(false);
- }
- }
- else
- {
- await _next.Invoke(httpContext).ConfigureAwait(false);
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Net/IWebSocket.cs b/Emby.Server.Implementations/Net/IWebSocket.cs
deleted file mode 100644
index 4d160aa66..000000000
--- a/Emby.Server.Implementations/Net/IWebSocket.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-using System;
-using System.Net.WebSockets;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Emby.Server.Implementations.Net
-{
- /// <summary>
- /// Interface IWebSocket
- /// </summary>
- public interface IWebSocket : IDisposable
- {
- /// <summary>
- /// Occurs when [closed].
- /// </summary>
- event EventHandler<EventArgs> Closed;
-
- /// <summary>
- /// Gets or sets the state.
- /// </summary>
- /// <value>The state.</value>
- WebSocketState State { get; }
-
- /// <summary>
- /// Gets or sets the receive action.
- /// </summary>
- /// <value>The receive action.</value>
- Action<byte[]> OnReceiveBytes { get; set; }
-
- /// <summary>
- /// Sends the async.
- /// </summary>
- /// <param name="bytes">The bytes.</param>
- /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken);
-
- /// <summary>
- /// Sends the asynchronous.
- /// </summary>
- /// <param name="text">The text.</param>
- /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken);
- }
-}
diff --git a/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs b/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs
deleted file mode 100644
index 6880766f9..000000000
--- a/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using System;
-using Microsoft.AspNetCore.Http;
-
-namespace Emby.Server.Implementations.Net
-{
- public class WebSocketConnectEventArgs : EventArgs
- {
- /// <summary>
- /// Gets or sets the URL.
- /// </summary>
- /// <value>The URL.</value>
- public string Url { get; set; }
- /// <summary>
- /// Gets or sets the query string.
- /// </summary>
- /// <value>The query string.</value>
- public IQueryCollection QueryString { get; set; }
- /// <summary>
- /// Gets or sets the web socket.
- /// </summary>
- /// <value>The web socket.</value>
- public IWebSocket WebSocket { get; set; }
- /// <summary>
- /// Gets or sets the endpoint.
- /// </summary>
- /// <value>The endpoint.</value>
- public string Endpoint { get; set; }
- }
-}
diff --git a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
index 23e22afd5..56e23d549 100644
--- a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
+++ b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Reflection;
+using MediaBrowser.Common.Extensions;
namespace Emby.Server.Implementations.Services
{
@@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.Services
if (propertySerializerEntry.PropertyType == typeof(bool))
{
//InputExtensions.cs#530 MVC Checkbox helper emits extra hidden input field, generating 2 values, first is the real value
- propertyTextValue = LeftPart(propertyTextValue, ',');
+ propertyTextValue = StringExtensions.LeftPart(propertyTextValue, ',').ToString();
}
var value = propertySerializerEntry.PropertyParseStringFn(propertyTextValue);
@@ -95,19 +96,6 @@ namespace Emby.Server.Implementations.Services
return instance;
}
-
- public static string LeftPart(string strVal, char needle)
- {
- if (strVal == null)
- {
- return null;
- }
-
- var pos = strVal.IndexOf(needle);
- return pos == -1
- ? strVal
- : strVal.Substring(0, pos);
- }
}
internal static class TypeAccessor
diff --git a/Emby.Server.Implementations/Services/UrlExtensions.cs b/Emby.Server.Implementations/Services/UrlExtensions.cs
index 5d4407f3b..483c63ade 100644
--- a/Emby.Server.Implementations/Services/UrlExtensions.cs
+++ b/Emby.Server.Implementations/Services/UrlExtensions.cs
@@ -1,4 +1,5 @@
using System;
+using MediaBrowser.Common.Extensions;
namespace Emby.Server.Implementations.Services
{
@@ -13,25 +14,12 @@ namespace Emby.Server.Implementations.Services
public static string GetMethodName(this Type type)
{
var typeName = type.FullName != null // can be null, e.g. generic types
- ? LeftPart(type.FullName, "[[") // Generic Fullname
- .Replace(type.Namespace + ".", string.Empty) // Trim Namespaces
- .Replace("+", ".") // Convert nested into normal type
+ ? StringExtensions.LeftPart(type.FullName, "[[", StringComparison.Ordinal).ToString() // Generic Fullname
+ .Replace(type.Namespace + ".", string.Empty, StringComparison.Ordinal) // Trim Namespaces
+ .Replace("+", ".", StringComparison.Ordinal) // Convert nested into normal type
: type.Name;
return type.IsGenericParameter ? "'" + typeName : typeName;
}
-
- private static string LeftPart(string strVal, string needle)
- {
- if (strVal == null)
- {
- return null;
- }
-
- var pos = strVal.IndexOf(needle, StringComparison.OrdinalIgnoreCase);
- return pos == -1
- ? strVal
- : strVal.Substring(0, pos);
- }
}
}
diff --git a/Emby.Server.Implementations/Session/HttpSessionController.cs b/Emby.Server.Implementations/Session/HttpSessionController.cs
deleted file mode 100644
index dfb81816c..000000000
--- a/Emby.Server.Implementations/Session/HttpSessionController.cs
+++ /dev/null
@@ -1,191 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Session;
-
-namespace Emby.Server.Implementations.Session
-{
- public class HttpSessionController : ISessionController
- {
- private readonly IHttpClient _httpClient;
- private readonly IJsonSerializer _json;
- private readonly ISessionManager _sessionManager;
-
- public SessionInfo Session { get; private set; }
-
- private readonly string _postUrl;
-
- public HttpSessionController(IHttpClient httpClient,
- IJsonSerializer json,
- SessionInfo session,
- string postUrl, ISessionManager sessionManager)
- {
- _httpClient = httpClient;
- _json = json;
- Session = session;
- _postUrl = postUrl;
- _sessionManager = sessionManager;
- }
-
- private string PostUrl => string.Format("http://{0}{1}", Session.RemoteEndPoint, _postUrl);
-
- public bool IsSessionActive => (DateTime.UtcNow - Session.LastActivityDate).TotalMinutes <= 5;
-
- public bool SupportsMediaControl => true;
-
- private Task SendMessage(string name, string messageId, CancellationToken cancellationToken)
- {
- return SendMessage(name, messageId, new Dictionary<string, string>(), cancellationToken);
- }
-
- private Task SendMessage(string name, string messageId, Dictionary<string, string> args, CancellationToken cancellationToken)
- {
- args["messageId"] = messageId;
- var url = PostUrl + "/" + name + ToQueryString(args);
-
- return SendRequest(new HttpRequestOptions
- {
- Url = url,
- CancellationToken = cancellationToken,
- BufferContent = false
- });
- }
-
- private Task SendPlayCommand(PlayRequest command, string messageId, CancellationToken cancellationToken)
- {
- var dict = new Dictionary<string, string>();
-
- dict["ItemIds"] = string.Join(",", command.ItemIds.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
-
- if (command.StartPositionTicks.HasValue)
- {
- dict["StartPositionTicks"] = command.StartPositionTicks.Value.ToString(CultureInfo.InvariantCulture);
- }
- if (command.AudioStreamIndex.HasValue)
- {
- dict["AudioStreamIndex"] = command.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture);
- }
- if (command.SubtitleStreamIndex.HasValue)
- {
- dict["SubtitleStreamIndex"] = command.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture);
- }
- if (command.StartIndex.HasValue)
- {
- dict["StartIndex"] = command.StartIndex.Value.ToString(CultureInfo.InvariantCulture);
- }
- if (!string.IsNullOrEmpty(command.MediaSourceId))
- {
- dict["MediaSourceId"] = command.MediaSourceId;
- }
-
- return SendMessage(command.PlayCommand.ToString(), messageId, dict, cancellationToken);
- }
-
- private Task SendPlaystateCommand(PlaystateRequest command, string messageId, CancellationToken cancellationToken)
- {
- var args = new Dictionary<string, string>();
-
- if (command.Command == PlaystateCommand.Seek)
- {
- if (!command.SeekPositionTicks.HasValue)
- {
- throw new ArgumentException("SeekPositionTicks cannot be null");
- }
-
- args["SeekPositionTicks"] = command.SeekPositionTicks.Value.ToString(CultureInfo.InvariantCulture);
- }
-
- return SendMessage(command.Command.ToString(), messageId, args, cancellationToken);
- }
-
- private string[] _supportedMessages = Array.Empty<string>();
- public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken)
- {
- if (!IsSessionActive)
- {
- return Task.CompletedTask;
- }
-
- if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
- {
- return SendPlayCommand(data as PlayRequest, messageId, cancellationToken);
- }
- if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
- {
- return SendPlaystateCommand(data as PlaystateRequest, messageId, cancellationToken);
- }
- if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
- {
- var command = data as GeneralCommand;
- return SendMessage(command.Name, messageId, command.Arguments, cancellationToken);
- }
-
- if (!_supportedMessages.Contains(name, StringComparer.OrdinalIgnoreCase))
- {
- return Task.CompletedTask;
- }
-
- var url = PostUrl + "/" + name;
-
- url += "?messageId=" + messageId;
-
- var options = new HttpRequestOptions
- {
- Url = url,
- CancellationToken = cancellationToken,
- BufferContent = false
- };
-
- if (data != null)
- {
- if (typeof(T) == typeof(string))
- {
- var str = data as string;
- if (!string.IsNullOrEmpty(str))
- {
- options.RequestContent = str;
- options.RequestContentType = "application/json";
- }
- }
- else
- {
- options.RequestContent = _json.SerializeToString(data);
- options.RequestContentType = "application/json";
- }
- }
-
- return SendRequest(options);
- }
-
- private async Task SendRequest(HttpRequestOptions options)
- {
- using (var response = await _httpClient.Post(options).ConfigureAwait(false))
- {
-
- }
- }
-
- private static string ToQueryString(Dictionary<string, string> nvc)
- {
- var array = (from item in nvc
- select string.Format("{0}={1}", WebUtility.UrlEncode(item.Key), WebUtility.UrlEncode(item.Value)))
- .ToArray();
-
- var args = string.Join("&", array);
-
- if (string.IsNullOrEmpty(args))
- {
- return args;
- }
-
- return "?" + args;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index aab745de4..2b09a93ef 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -478,8 +478,7 @@ namespace Emby.Server.Implementations.Session
Client = appName,
DeviceId = deviceId,
ApplicationVersion = appVersion,
- Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
- ServerId = _appHost.SystemId
+ Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture)
};
var username = user?.Name;
@@ -1043,12 +1042,12 @@ namespace Emby.Server.Implementations.Session
private static async Task SendMessageToSession<T>(SessionInfo session, string name, T data, CancellationToken cancellationToken)
{
- var controllers = session.SessionControllers.ToArray();
- var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+ var controllers = session.SessionControllers;
+ var messageId = Guid.NewGuid();
foreach (var controller in controllers)
{
- await controller.SendMessage(name, messageId, data, controllers, cancellationToken).ConfigureAwait(false);
+ await controller.SendMessage(name, messageId, data, cancellationToken).ConfigureAwait(false);
}
}
@@ -1056,13 +1055,13 @@ namespace Emby.Server.Implementations.Session
{
IEnumerable<Task> GetTasks()
{
- var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+ var messageId = Guid.NewGuid();
foreach (var session in sessions)
{
var controllers = session.SessionControllers;
foreach (var controller in controllers)
{
- yield return controller.SendMessage(name, messageId, data, controllers, cancellationToken);
+ yield return controller.SendMessage(name, messageId, data, cancellationToken);
}
}
}
@@ -1779,7 +1778,7 @@ namespace Emby.Server.Implementations.Session
throw new ArgumentNullException(nameof(info));
}
- var user = info.UserId.Equals(Guid.Empty)
+ var user = info.UserId == Guid.Empty
? null
: _userManager.GetUserById(info.UserId);
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index 3704445ab..a5293b41c 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -8,7 +8,6 @@ using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -17,7 +16,7 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// Class SessionWebSocketListener
/// </summary>
- public class SessionWebSocketListener : IWebSocketListener, IDisposable
+ public sealed class SessionWebSocketListener : IWebSocketListener, IDisposable
{
/// <summary>
/// The timeout in seconds after which a WebSocket is considered to be lost.
@@ -43,11 +42,7 @@ namespace Emby.Server.Implementations.Session
/// The _logger
/// </summary>
private readonly ILogger _logger;
-
- /// <summary>
- /// The _dto service
- /// </summary>
- private readonly IJsonSerializer _json;
+ private readonly ILoggerFactory _loggerFactory;
private readonly IHttpServer _httpServer;
@@ -74,23 +69,27 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class.
/// </summary>
+ /// <param name="logger">The logger.</param>
/// <param name="sessionManager">The session manager.</param>
/// <param name="loggerFactory">The logger factory.</param>
- /// <param name="json">The json.</param>
/// <param name="httpServer">The HTTP server.</param>
- public SessionWebSocketListener(ISessionManager sessionManager, ILoggerFactory loggerFactory, IJsonSerializer json, IHttpServer httpServer)
+ public SessionWebSocketListener(
+ ILogger<SessionWebSocketListener> logger,
+ ISessionManager sessionManager,
+ ILoggerFactory loggerFactory,
+ IHttpServer httpServer)
{
+ _logger = logger;
_sessionManager = sessionManager;
- _logger = loggerFactory.CreateLogger(GetType().Name);
- _json = json;
+ _loggerFactory = loggerFactory;
_httpServer = httpServer;
+
httpServer.WebSocketConnected += OnServerManagerWebSocketConnected;
}
- void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
+ private void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
{
- var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint);
-
+ var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint.ToString());
if (session != null)
{
EnsureController(session, e.Argument);
@@ -98,7 +97,7 @@ namespace Emby.Server.Implementations.Session
}
else
{
- _logger.LogWarning("Unable to determine session based on url: {0}", e.Argument.Url);
+ _logger.LogWarning("Unable to determine session based on query string: {0}", e.Argument.QueryString);
}
}
@@ -119,6 +118,7 @@ namespace Emby.Server.Implementations.Session
return _sessionManager.GetSessionByAuthenticationToken(token, deviceId, remoteEndpoint);
}
+ /// <inheritdoc />
public void Dispose()
{
_httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected;
@@ -135,7 +135,8 @@ namespace Emby.Server.Implementations.Session
private void EnsureController(SessionInfo session, IWebSocketConnection connection)
{
- var controllerInfo = session.EnsureController<WebSocketController>(s => new WebSocketController(s, _logger, _sessionManager));
+ var controllerInfo = session.EnsureController<WebSocketController>(
+ s => new WebSocketController(_loggerFactory.CreateLogger<WebSocketController>(), s, _sessionManager));
var controller = (WebSocketController)controllerInfo.Item1;
controller.AddWebSocket(connection);
diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs
index 0d483c55f..a0274acd2 100644
--- a/Emby.Server.Implementations/Session/WebSocketController.cs
+++ b/Emby.Server.Implementations/Session/WebSocketController.cs
@@ -1,3 +1,7 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+#nullable enable
+
using System;
using System.Collections.Generic;
using System.Linq;
@@ -11,60 +15,63 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Session
{
- public class WebSocketController : ISessionController, IDisposable
+ public sealed class WebSocketController : ISessionController, IDisposable
{
- public SessionInfo Session { get; private set; }
- public IReadOnlyList<IWebSocketConnection> Sockets { get; private set; }
-
private readonly ILogger _logger;
-
private readonly ISessionManager _sessionManager;
+ private readonly SessionInfo _session;
- public WebSocketController(SessionInfo session, ILogger logger, ISessionManager sessionManager)
+ private readonly List<IWebSocketConnection> _sockets;
+ private bool _disposed = false;
+
+ public WebSocketController(
+ ILogger<WebSocketController> logger,
+ SessionInfo session,
+ ISessionManager sessionManager)
{
- Session = session;
_logger = logger;
+ _session = session;
_sessionManager = sessionManager;
- Sockets = new List<IWebSocketConnection>();
+ _sockets = new List<IWebSocketConnection>();
}
private bool HasOpenSockets => GetActiveSockets().Any();
+ /// <inheritdoc />
public bool SupportsMediaControl => HasOpenSockets;
+ /// <inheritdoc />
public bool IsSessionActive => HasOpenSockets;
private IEnumerable<IWebSocketConnection> GetActiveSockets()
- {
- return Sockets
- .OrderByDescending(i => i.LastActivityDate)
- .Where(i => i.State == WebSocketState.Open);
- }
+ => _sockets.Where(i => i.State == WebSocketState.Open);
public void AddWebSocket(IWebSocketConnection connection)
{
- var sockets = Sockets.ToList();
- sockets.Add(connection);
+ _logger.LogDebug("Adding websocket to session {Session}", _session.Id);
+ _sockets.Add(connection);
- Sockets = sockets;
-
- connection.Closed += connection_Closed;
+ connection.Closed += OnConnectionClosed;
}
- void connection_Closed(object sender, EventArgs e)
+ private void OnConnectionClosed(object sender, EventArgs e)
{
var connection = (IWebSocketConnection)sender;
- var sockets = Sockets.ToList();
- sockets.Remove(connection);
-
- Sockets = sockets;
-
- _sessionManager.CloseIfNeeded(Session);
+ _logger.LogDebug("Removing websocket from session {Session}", _session.Id);
+ _sockets.Remove(connection);
+ connection.Closed -= OnConnectionClosed;
+ _sessionManager.CloseIfNeeded(_session);
}
- public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken)
+ /// <inheritdoc />
+ public Task SendMessage<T>(
+ string name,
+ Guid messageId,
+ T data,
+ CancellationToken cancellationToken)
{
var socket = GetActiveSockets()
+ .OrderByDescending(i => i.LastActivityDate)
.FirstOrDefault();
if (socket == null)
@@ -72,21 +79,30 @@ namespace Emby.Server.Implementations.Session
return Task.CompletedTask;
}
- return socket.SendAsync(new WebSocketMessage<T>
- {
- Data = data,
- MessageType = name,
- MessageId = messageId
-
- }, cancellationToken);
+ return socket.SendAsync(
+ new WebSocketMessage<T>
+ {
+ Data = data,
+ MessageType = name,
+ MessageId = messageId
+ },
+ cancellationToken);
}
+ /// <inheritdoc />
public void Dispose()
{
- foreach (var socket in Sockets.ToList())
+ if (_disposed)
{
- socket.Closed -= connection_Closed;
+ return;
}
+
+ foreach (var socket in _sockets)
+ {
+ socket.Closed -= OnConnectionClosed;
+ }
+
+ _disposed = true;
}
}
}
diff --git a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs
deleted file mode 100644
index 67521d6c6..000000000
--- a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-using System;
-using System.Net.WebSockets;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.Net;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.SocketSharp
-{
- public class SharpWebSocket : IWebSocket
- {
- /// <summary>
- /// The logger
- /// </summary>
- private readonly ILogger _logger;
-
- public event EventHandler<EventArgs> Closed;
-
- /// <summary>
- /// Gets or sets the web socket.
- /// </summary>
- /// <value>The web socket.</value>
- private readonly WebSocket _webSocket;
-
- private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
- private bool _disposed;
-
- public SharpWebSocket(WebSocket socket, ILogger logger)
- {
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- _webSocket = socket ?? throw new ArgumentNullException(nameof(socket));
- }
-
- /// <summary>
- /// Gets the state.
- /// </summary>
- /// <value>The state.</value>
- public WebSocketState State => _webSocket.State;
-
- /// <summary>
- /// Sends the async.
- /// </summary>
- /// <param name="bytes">The bytes.</param>
- /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken)
- {
- return _webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Binary, endOfMessage, cancellationToken);
- }
-
- /// <summary>
- /// Sends the asynchronous.
- /// </summary>
- /// <param name="text">The text.</param>
- /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken)
- {
- return _webSocket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(text)), WebSocketMessageType.Text, endOfMessage, cancellationToken);
- }
-
- /// <summary>
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
- /// </summary>
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Releases unmanaged and - optionally - managed resources.
- /// </summary>
- /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool dispose)
- {
- if (_disposed)
- {
- return;
- }
-
- if (dispose)
- {
- _cancellationTokenSource.Cancel();
- if (_webSocket.State == WebSocketState.Open)
- {
- _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client",
- CancellationToken.None);
- }
- Closed?.Invoke(this, EventArgs.Empty);
- }
-
- _disposed = true;
- }
-
- /// <summary>
- /// Gets or sets the receive action.
- /// </summary>
- /// <value>The receive action.</value>
- public Action<byte[]> OnReceiveBytes { get; set; }
- }
-}
diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs
deleted file mode 100644
index b85750c9b..000000000
--- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs
+++ /dev/null
@@ -1,135 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.WebSockets;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.HttpServer;
-using Emby.Server.Implementations.Net;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.SocketSharp
-{
- public class WebSocketSharpListener : IHttpListener
- {
- private readonly ILogger _logger;
-
- private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
- private CancellationToken _disposeCancellationToken;
-
- public WebSocketSharpListener(ILogger<WebSocketSharpListener> logger)
- {
- _logger = logger;
- _disposeCancellationToken = _disposeCancellationTokenSource.Token;
- }
-
- public Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; }
-
- public Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; }
-
- public Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
-
- private static void LogRequest(ILogger logger, HttpRequest request)
- {
- var url = request.GetDisplayUrl();
-
- logger.LogInformation("WS {Url}. UserAgent: {UserAgent}", url, request.Headers[HeaderNames.UserAgent].ToString());
- }
-
- public async Task ProcessWebSocketRequest(HttpContext ctx)
- {
- try
- {
- LogRequest(_logger, ctx.Request);
- var endpoint = ctx.Connection.RemoteIpAddress.ToString();
- var url = ctx.Request.GetDisplayUrl();
-
- var webSocketContext = await ctx.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false);
- var socket = new SharpWebSocket(webSocketContext, _logger);
-
- WebSocketConnected(new WebSocketConnectEventArgs
- {
- Url = url,
- QueryString = ctx.Request.Query,
- WebSocket = socket,
- Endpoint = endpoint
- });
-
- WebSocketReceiveResult result;
- var message = new List<byte>();
-
- do
- {
- var buffer = WebSocket.CreateServerBuffer(4096);
- result = await webSocketContext.ReceiveAsync(buffer, _disposeCancellationToken);
- message.AddRange(buffer.Array.Take(result.Count));
-
- if (result.EndOfMessage)
- {
- socket.OnReceiveBytes(message.ToArray());
- message.Clear();
- }
- } while (socket.State == WebSocketState.Open && result.MessageType != WebSocketMessageType.Close);
-
-
- if (webSocketContext.State == WebSocketState.Open)
- {
- await webSocketContext.CloseAsync(
- result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
- result.CloseStatusDescription,
- _disposeCancellationToken).ConfigureAwait(false);
- }
-
- socket.Dispose();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "AcceptWebSocketAsync error");
- if (!ctx.Response.HasStarted)
- {
- ctx.Response.StatusCode = 500;
- }
- }
- }
-
- public Task Stop()
- {
- _disposeCancellationTokenSource.Cancel();
- return Task.CompletedTask;
- }
-
- /// <summary>
- /// Releases the unmanaged resources and disposes of the managed resources used.
- /// </summary>
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- private bool _disposed;
-
- /// <summary>
- /// Releases the unmanaged resources and disposes of the managed resources used.
- /// </summary>
- /// <param name="disposing">Whether or not the managed resources should be disposed.</param>
- protected virtual void Dispose(bool disposing)
- {
- if (_disposed)
- {
- return;
- }
-
- if (disposing)
- {
- Stop().GetAwaiter().GetResult();
- }
-
- _disposed = true;
- }
- }
-}
diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
index 1781df8b5..ee5131c1f 100644
--- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
+++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Mime;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
@@ -62,6 +63,9 @@ namespace Emby.Server.Implementations.SocketSharp
if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip))
{
ip = Request.HttpContext.Connection.RemoteIpAddress;
+
+ // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests)
+ ip ??= IPAddress.Loopback;
}
}
@@ -89,7 +93,10 @@ namespace Emby.Server.Implementations.SocketSharp
public IQueryCollection QueryString => Request.Query;
- public bool IsLocal => Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress);
+ public bool IsLocal =>
+ (Request.HttpContext.Connection.LocalIpAddress == null
+ && Request.HttpContext.Connection.RemoteIpAddress == null)
+ || Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress);
public string HttpMethod => Request.Method;
@@ -216,14 +223,14 @@ namespace Emby.Server.Implementations.SocketSharp
pi = pi.Slice(1);
}
- format = LeftPart(pi, '/');
+ format = pi.LeftPart('/');
if (format.Length > FormatMaxLength)
{
return null;
}
}
- format = LeftPart(format, '.');
+ format = format.LeftPart('.');
if (format.Contains("json", StringComparison.OrdinalIgnoreCase))
{
return "application/json";
@@ -235,16 +242,5 @@ namespace Emby.Server.Implementations.SocketSharp
return null;
}
-
- public static ReadOnlySpan<char> LeftPart(ReadOnlySpan<char> strVal, char needle)
- {
- if (strVal == null)
- {
- return null;
- }
-
- var pos = strVal.IndexOf(needle);
- return pos == -1 ? strVal : strVal.Slice(0, pos);
- }
}
}
diff --git a/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs b/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs
deleted file mode 100644
index eb1877440..000000000
--- a/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Model.Net;
-
-namespace Emby.Server.Implementations.WebSockets
-{
- public interface IWebSocketHandler
- {
- Task ProcessMessage(WebSocketMessage<object> message, TaskCompletionSource<bool> taskCompletionSource);
- }
-}
diff --git a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs b/Emby.Server.Implementations/WebSockets/WebSocketManager.cs
deleted file mode 100644
index 31a7468fb..000000000
--- a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs
+++ /dev/null
@@ -1,102 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.WebSockets;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.Logging;
-using UtfUnknown;
-
-namespace Emby.Server.Implementations.WebSockets
-{
- public class WebSocketManager
- {
- private readonly IWebSocketHandler[] _webSocketHandlers;
- private readonly IJsonSerializer _jsonSerializer;
- private readonly ILogger<WebSocketManager> _logger;
- private const int BufferSize = 4096;
-
- public WebSocketManager(IWebSocketHandler[] webSocketHandlers, IJsonSerializer jsonSerializer, ILogger<WebSocketManager> logger)
- {
- _webSocketHandlers = webSocketHandlers;
- _jsonSerializer = jsonSerializer;
- _logger = logger;
- }
-
- public async Task OnWebSocketConnected(WebSocket webSocket)
- {
- var taskCompletionSource = new TaskCompletionSource<bool>();
- var cancellationToken = new CancellationTokenSource().Token;
- WebSocketReceiveResult result;
- var message = new List<byte>();
-
- // Keep listening for incoming messages, otherwise the socket closes automatically
- do
- {
- var buffer = WebSocket.CreateServerBuffer(BufferSize);
- result = await webSocket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false);
- message.AddRange(buffer.Array.Take(result.Count));
-
- if (result.EndOfMessage)
- {
- await ProcessMessage(message.ToArray(), taskCompletionSource).ConfigureAwait(false);
- message.Clear();
- }
- } while (!taskCompletionSource.Task.IsCompleted &&
- webSocket.State == WebSocketState.Open &&
- result.MessageType != WebSocketMessageType.Close);
-
- if (webSocket.State == WebSocketState.Open)
- {
- await webSocket.CloseAsync(
- result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
- result.CloseStatusDescription,
- cancellationToken).ConfigureAwait(false);
- }
- }
-
- private async Task ProcessMessage(byte[] messageBytes, TaskCompletionSource<bool> taskCompletionSource)
- {
- var charset = CharsetDetector.DetectFromBytes(messageBytes).Detected?.EncodingName;
- var message = string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase)
- ? Encoding.UTF8.GetString(messageBytes, 0, messageBytes.Length)
- : Encoding.ASCII.GetString(messageBytes, 0, messageBytes.Length);
-
- // All messages are expected to be valid JSON objects
- if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase))
- {
- _logger.LogDebug("Received web socket message that is not a json structure: {Message}", message);
- return;
- }
-
- try
- {
- var info = _jsonSerializer.DeserializeFromString<WebSocketMessage<object>>(message);
-
- _logger.LogDebug("Websocket message received: {0}", info.MessageType);
-
- var tasks = _webSocketHandlers.Select(handler => Task.Run(() =>
- {
- try
- {
- handler.ProcessMessage(info, taskCompletionSource).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "{HandlerType} failed processing WebSocket message {MessageType}",
- handler.GetType().Name, info.MessageType ?? string.Empty);
- }
- }));
-
- await Task.WhenAll(tasks);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error processing web socket message");
- }
- }
- }
-}
diff --git a/Jellyfin.Data/Entities/Artwork.cs b/Jellyfin.Data/Entities/Artwork.cs
new file mode 100644
index 000000000..bf3029368
--- /dev/null
+++ b/Jellyfin.Data/Entities/Artwork.cs
@@ -0,0 +1,195 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class Artwork
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected Artwork()
+ {
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static Artwork CreateArtworkUnsafe()
+ {
+ return new Artwork();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="path"></param>
+ /// <param name="kind"></param>
+ /// <param name="_metadata0"></param>
+ /// <param name="_personrole1"></param>
+ public Artwork(string path, Enums.ArtKind kind, Metadata _metadata0, PersonRole _personrole1)
+ {
+ if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path));
+ this.Path = path;
+
+ this.Kind = kind;
+
+ if (_metadata0 == null) throw new ArgumentNullException(nameof(_metadata0));
+ _metadata0.Artwork.Add(this);
+
+ if (_personrole1 == null) throw new ArgumentNullException(nameof(_personrole1));
+ _personrole1.Artwork = this;
+
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="path"></param>
+ /// <param name="kind"></param>
+ /// <param name="_metadata0"></param>
+ /// <param name="_personrole1"></param>
+ public static Artwork Create(string path, Enums.ArtKind kind, Metadata _metadata0, PersonRole _personrole1)
+ {
+ return new Artwork(path, kind, _metadata0, _personrole1);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Id
+ /// </summary>
+ internal int _Id;
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before setting.
+ /// </summary>
+ partial void SetId(int oldValue, ref int newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before returning.
+ /// </summary>
+ partial void GetId(ref int result);
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ public int Id
+ {
+ get
+ {
+ int value = _Id;
+ GetId(ref value);
+ return (_Id = value);
+ }
+ protected set
+ {
+ int oldValue = _Id;
+ SetId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Id = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Path
+ /// </summary>
+ protected string _Path;
+ /// <summary>
+ /// When provided in a partial class, allows value of Path to be changed before setting.
+ /// </summary>
+ partial void SetPath(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Path to be changed before returning.
+ /// </summary>
+ partial void GetPath(ref string result);
+
+ /// <summary>
+ /// Required, Max length = 65535
+ /// </summary>
+ [Required]
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string Path
+ {
+ get
+ {
+ string value = _Path;
+ GetPath(ref value);
+ return (_Path = value);
+ }
+ set
+ {
+ string oldValue = _Path;
+ SetPath(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Path = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Kind
+ /// </summary>
+ internal Enums.ArtKind _Kind;
+ /// <summary>
+ /// When provided in a partial class, allows value of Kind to be changed before setting.
+ /// </summary>
+ partial void SetKind(Enums.ArtKind oldValue, ref Enums.ArtKind newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Kind to be changed before returning.
+ /// </summary>
+ partial void GetKind(ref Enums.ArtKind result);
+
+ /// <summary>
+ /// Indexed, Required
+ /// </summary>
+ [Required]
+ public Enums.ArtKind Kind
+ {
+ get
+ {
+ Enums.ArtKind value = _Kind;
+ GetKind(ref value);
+ return (_Kind = value);
+ }
+ set
+ {
+ Enums.ArtKind oldValue = _Kind;
+ SetKind(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Kind = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/Book.cs b/Jellyfin.Data/Entities/Book.cs
new file mode 100644
index 000000000..42d24e31d
--- /dev/null
+++ b/Jellyfin.Data/Entities/Book.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class Book : LibraryItem
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected Book()
+ {
+ BookMetadata = new HashSet<BookMetadata>();
+ Releases = new HashSet<Release>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static Book CreateBookUnsafe()
+ {
+ return new Book();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+ public Book(Guid urlid, DateTime dateadded)
+ {
+ this.UrlId = urlid;
+
+ this.BookMetadata = new HashSet<BookMetadata>();
+ this.Releases = new HashSet<Release>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+ public static Book Create(Guid urlid, DateTime dateadded)
+ {
+ return new Book(urlid, dateadded);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ [ForeignKey("BookMetadata_BookMetadata_Id")]
+ public virtual ICollection<BookMetadata> BookMetadata { get; protected set; }
+
+ [ForeignKey("Release_Releases_Id")]
+ public virtual ICollection<Release> Releases { get; protected set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/BookMetadata.cs b/Jellyfin.Data/Entities/BookMetadata.cs
new file mode 100644
index 000000000..d52fe7605
--- /dev/null
+++ b/Jellyfin.Data/Entities/BookMetadata.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class BookMetadata : Metadata
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected BookMetadata()
+ {
+ Publishers = new HashSet<Company>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static BookMetadata CreateBookMetadataUnsafe()
+ {
+ return new BookMetadata();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="title">The title or name of the object</param>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="_book0"></param>
+ public BookMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Book _book0)
+ {
+ if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title));
+ this.Title = title;
+
+ if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language));
+ this.Language = language;
+
+ if (_book0 == null) throw new ArgumentNullException(nameof(_book0));
+ _book0.BookMetadata.Add(this);
+
+ this.Publishers = new HashSet<Company>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="title">The title or name of the object</param>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="_book0"></param>
+ public static BookMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Book _book0)
+ {
+ return new BookMetadata(title, language, dateadded, datemodified, _book0);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for ISBN
+ /// </summary>
+ protected long? _ISBN;
+ /// <summary>
+ /// When provided in a partial class, allows value of ISBN to be changed before setting.
+ /// </summary>
+ partial void SetISBN(long? oldValue, ref long? newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of ISBN to be changed before returning.
+ /// </summary>
+ partial void GetISBN(ref long? result);
+
+ public long? ISBN
+ {
+ get
+ {
+ long? value = _ISBN;
+ GetISBN(ref value);
+ return (_ISBN = value);
+ }
+ set
+ {
+ long? oldValue = _ISBN;
+ SetISBN(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _ISBN = value;
+ }
+ }
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ [ForeignKey("Company_Publishers_Id")]
+ public virtual ICollection<Company> Publishers { get; protected set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/Chapter.cs b/Jellyfin.Data/Entities/Chapter.cs
new file mode 100644
index 000000000..d48cb9b62
--- /dev/null
+++ b/Jellyfin.Data/Entities/Chapter.cs
@@ -0,0 +1,263 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class Chapter
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected Chapter()
+ {
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static Chapter CreateChapterUnsafe()
+ {
+ return new Chapter();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="timestart"></param>
+ /// <param name="_release0"></param>
+ public Chapter(string language, long timestart, Release _release0)
+ {
+ if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language));
+ this.Language = language;
+
+ this.TimeStart = timestart;
+
+ if (_release0 == null) throw new ArgumentNullException(nameof(_release0));
+ _release0.Chapters.Add(this);
+
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="timestart"></param>
+ /// <param name="_release0"></param>
+ public static Chapter Create(string language, long timestart, Release _release0)
+ {
+ return new Chapter(language, timestart, _release0);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Id
+ /// </summary>
+ internal int _Id;
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before setting.
+ /// </summary>
+ partial void SetId(int oldValue, ref int newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before returning.
+ /// </summary>
+ partial void GetId(ref int result);
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id
+ {
+ get
+ {
+ int value = _Id;
+ GetId(ref value);
+ return (_Id = value);
+ }
+ protected set
+ {
+ int oldValue = _Id;
+ SetId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Id = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Name
+ /// </summary>
+ protected string _Name;
+ /// <summary>
+ /// When provided in a partial class, allows value of Name to be changed before setting.
+ /// </summary>
+ partial void SetName(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Name to be changed before returning.
+ /// </summary>
+ partial void GetName(ref string result);
+
+ /// <summary>
+ /// Max length = 1024
+ /// </summary>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string Name
+ {
+ get
+ {
+ string value = _Name;
+ GetName(ref value);
+ return (_Name = value);
+ }
+ set
+ {
+ string oldValue = _Name;
+ SetName(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Name = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Language
+ /// </summary>
+ protected string _Language;
+ /// <summary>
+ /// When provided in a partial class, allows value of Language to be changed before setting.
+ /// </summary>
+ partial void SetLanguage(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Language to be changed before returning.
+ /// </summary>
+ partial void GetLanguage(ref string result);
+
+ /// <summary>
+ /// Required, Min length = 3, Max length = 3
+ /// ISO-639-3 3-character language codes
+ /// </summary>
+ [Required]
+ [MinLength(3)]
+ [MaxLength(3)]
+ [StringLength(3)]
+ public string Language
+ {
+ get
+ {
+ string value = _Language;
+ GetLanguage(ref value);
+ return (_Language = value);
+ }
+ set
+ {
+ string oldValue = _Language;
+ SetLanguage(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Language = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for TimeStart
+ /// </summary>
+ protected long _TimeStart;
+ /// <summary>
+ /// When provided in a partial class, allows value of TimeStart to be changed before setting.
+ /// </summary>
+ partial void SetTimeStart(long oldValue, ref long newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of TimeStart to be changed before returning.
+ /// </summary>
+ partial void GetTimeStart(ref long result);
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [Required]
+ public long TimeStart
+ {
+ get
+ {
+ long value = _TimeStart;
+ GetTimeStart(ref value);
+ return (_TimeStart = value);
+ }
+ set
+ {
+ long oldValue = _TimeStart;
+ SetTimeStart(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _TimeStart = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for TimeEnd
+ /// </summary>
+ protected long? _TimeEnd;
+ /// <summary>
+ /// When provided in a partial class, allows value of TimeEnd to be changed before setting.
+ /// </summary>
+ partial void SetTimeEnd(long? oldValue, ref long? newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of TimeEnd to be changed before returning.
+ /// </summary>
+ partial void GetTimeEnd(ref long? result);
+
+ public long? TimeEnd
+ {
+ get
+ {
+ long? value = _TimeEnd;
+ GetTimeEnd(ref value);
+ return (_TimeEnd = value);
+ }
+ set
+ {
+ long? oldValue = _TimeEnd;
+ SetTimeEnd(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _TimeEnd = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/Collection.cs b/Jellyfin.Data/Entities/Collection.cs
new file mode 100644
index 000000000..e2fa3a5bd
--- /dev/null
+++ b/Jellyfin.Data/Entities/Collection.cs
@@ -0,0 +1,120 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class Collection
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor
+ /// </summary>
+ public Collection()
+ {
+ CollectionItem = new LinkedList<CollectionItem>();
+
+ Init();
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Id
+ /// </summary>
+ internal int _Id;
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before setting.
+ /// </summary>
+ partial void SetId(int oldValue, ref int newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before returning.
+ /// </summary>
+ partial void GetId(ref int result);
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id
+ {
+ get
+ {
+ int value = _Id;
+ GetId(ref value);
+ return (_Id = value);
+ }
+ protected set
+ {
+ int oldValue = _Id;
+ SetId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Id = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Name
+ /// </summary>
+ protected string _Name;
+ /// <summary>
+ /// When provided in a partial class, allows value of Name to be changed before setting.
+ /// </summary>
+ partial void SetName(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Name to be changed before returning.
+ /// </summary>
+ partial void GetName(ref string result);
+
+ /// <summary>
+ /// Max length = 1024
+ /// </summary>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string Name
+ {
+ get
+ {
+ string value = _Name;
+ GetName(ref value);
+ return (_Name = value);
+ }
+ set
+ {
+ string oldValue = _Name;
+ SetName(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Name = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+ [ForeignKey("CollectionItem_CollectionItem_Id")]
+ public virtual ICollection<CollectionItem> CollectionItem { get; protected set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/CollectionItem.cs b/Jellyfin.Data/Entities/CollectionItem.cs
new file mode 100644
index 000000000..4a3d06639
--- /dev/null
+++ b/Jellyfin.Data/Entities/CollectionItem.cs
@@ -0,0 +1,143 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class CollectionItem
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected CollectionItem()
+ {
+ // NOTE: This class has one-to-one associations with CollectionItem.
+ // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static CollectionItem CreateCollectionItemUnsafe()
+ {
+ return new CollectionItem();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="_collection0"></param>
+ /// <param name="_collectionitem1"></param>
+ /// <param name="_collectionitem2"></param>
+ public CollectionItem(Collection _collection0, CollectionItem _collectionitem1, CollectionItem _collectionitem2)
+ {
+ // NOTE: This class has one-to-one associations with CollectionItem.
+ // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
+
+ if (_collection0 == null) throw new ArgumentNullException(nameof(_collection0));
+ _collection0.CollectionItem.Add(this);
+
+ if (_collectionitem1 == null) throw new ArgumentNullException(nameof(_collectionitem1));
+ _collectionitem1.Next = this;
+
+ if (_collectionitem2 == null) throw new ArgumentNullException(nameof(_collectionitem2));
+ _collectionitem2.Previous = this;
+
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="_collection0"></param>
+ /// <param name="_collectionitem1"></param>
+ /// <param name="_collectionitem2"></param>
+ public static CollectionItem Create(Collection _collection0, CollectionItem _collectionitem1, CollectionItem _collectionitem2)
+ {
+ return new CollectionItem(_collection0, _collectionitem1, _collectionitem2);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Id
+ /// </summary>
+ internal int _Id;
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before setting.
+ /// </summary>
+ partial void SetId(int oldValue, ref int newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before returning.
+ /// </summary>
+ partial void GetId(ref int result);
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id
+ {
+ get
+ {
+ int value = _Id;
+ GetId(ref value);
+ return (_Id = value);
+ }
+ protected set
+ {
+ int oldValue = _Id;
+ SetId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Id = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [ForeignKey("LibraryItem_Id")]
+ public virtual LibraryItem LibraryItem { get; set; }
+
+ /// <remarks>
+ /// TODO check if this properly updated dependant and has the proper principal relationship
+ /// </remarks>
+ [ForeignKey("CollectionItem_Next_Id")]
+ public virtual CollectionItem Next { get; set; }
+
+ /// <remarks>
+ /// TODO check if this properly updated dependant and has the proper principal relationship
+ /// </remarks>
+ [ForeignKey("CollectionItem_Previous_Id")]
+ public virtual CollectionItem Previous { get; set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/Company.cs b/Jellyfin.Data/Entities/Company.cs
new file mode 100644
index 000000000..0650271c6
--- /dev/null
+++ b/Jellyfin.Data/Entities/Company.cs
@@ -0,0 +1,137 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class Company
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected Company()
+ {
+ CompanyMetadata = new HashSet<CompanyMetadata>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static Company CreateCompanyUnsafe()
+ {
+ return new Company();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="_moviemetadata0"></param>
+ /// <param name="_seriesmetadata1"></param>
+ /// <param name="_musicalbummetadata2"></param>
+ /// <param name="_bookmetadata3"></param>
+ /// <param name="_company4"></param>
+ public Company(MovieMetadata _moviemetadata0, SeriesMetadata _seriesmetadata1, MusicAlbumMetadata _musicalbummetadata2, BookMetadata _bookmetadata3, Company _company4)
+ {
+ if (_moviemetadata0 == null) throw new ArgumentNullException(nameof(_moviemetadata0));
+ _moviemetadata0.Studios.Add(this);
+
+ if (_seriesmetadata1 == null) throw new ArgumentNullException(nameof(_seriesmetadata1));
+ _seriesmetadata1.Networks.Add(this);
+
+ if (_musicalbummetadata2 == null) throw new ArgumentNullException(nameof(_musicalbummetadata2));
+ _musicalbummetadata2.Labels.Add(this);
+
+ if (_bookmetadata3 == null) throw new ArgumentNullException(nameof(_bookmetadata3));
+ _bookmetadata3.Publishers.Add(this);
+
+ if (_company4 == null) throw new ArgumentNullException(nameof(_company4));
+ _company4.Parent = this;
+
+ this.CompanyMetadata = new HashSet<CompanyMetadata>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="_moviemetadata0"></param>
+ /// <param name="_seriesmetadata1"></param>
+ /// <param name="_musicalbummetadata2"></param>
+ /// <param name="_bookmetadata3"></param>
+ /// <param name="_company4"></param>
+ public static Company Create(MovieMetadata _moviemetadata0, SeriesMetadata _seriesmetadata1, MusicAlbumMetadata _musicalbummetadata2, BookMetadata _bookmetadata3, Company _company4)
+ {
+ return new Company(_moviemetadata0, _seriesmetadata1, _musicalbummetadata2, _bookmetadata3, _company4);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Id
+ /// </summary>
+ internal int _Id;
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before setting.
+ /// </summary>
+ partial void SetId(int oldValue, ref int newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before returning.
+ /// </summary>
+ partial void GetId(ref int result);
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id
+ {
+ get
+ {
+ int value = _Id;
+ GetId(ref value);
+ return (_Id = value);
+ }
+ protected set
+ {
+ int oldValue = _Id;
+ SetId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Id = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+ [ForeignKey("CompanyMetadata_CompanyMetadata_Id")]
+ public virtual ICollection<CompanyMetadata> CompanyMetadata { get; protected set; }
+ [ForeignKey("Company_Parent_Id")]
+ public virtual Company Parent { get; set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/CompanyMetadata.cs b/Jellyfin.Data/Entities/CompanyMetadata.cs
new file mode 100644
index 000000000..b3ec9c1a7
--- /dev/null
+++ b/Jellyfin.Data/Entities/CompanyMetadata.cs
@@ -0,0 +1,216 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class CompanyMetadata : Metadata
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected CompanyMetadata()
+ {
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static CompanyMetadata CreateCompanyMetadataUnsafe()
+ {
+ return new CompanyMetadata();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="title">The title or name of the object</param>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="_company0"></param>
+ public CompanyMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Company _company0)
+ {
+ if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title));
+ this.Title = title;
+
+ if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language));
+ this.Language = language;
+
+ if (_company0 == null) throw new ArgumentNullException(nameof(_company0));
+ _company0.CompanyMetadata.Add(this);
+
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="title">The title or name of the object</param>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="_company0"></param>
+ public static CompanyMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Company _company0)
+ {
+ return new CompanyMetadata(title, language, dateadded, datemodified, _company0);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Description
+ /// </summary>
+ protected string _Description;
+ /// <summary>
+ /// When provided in a partial class, allows value of Description to be changed before setting.
+ /// </summary>
+ partial void SetDescription(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Description to be changed before returning.
+ /// </summary>
+ partial void GetDescription(ref string result);
+
+ /// <summary>
+ /// Max length = 65535
+ /// </summary>
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string Description
+ {
+ get
+ {
+ string value = _Description;
+ GetDescription(ref value);
+ return (_Description = value);
+ }
+ set
+ {
+ string oldValue = _Description;
+ SetDescription(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Description = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Headquarters
+ /// </summary>
+ protected string _Headquarters;
+ /// <summary>
+ /// When provided in a partial class, allows value of Headquarters to be changed before setting.
+ /// </summary>
+ partial void SetHeadquarters(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Headquarters to be changed before returning.
+ /// </summary>
+ partial void GetHeadquarters(ref string result);
+
+ /// <summary>
+ /// Max length = 255
+ /// </summary>
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string Headquarters
+ {
+ get
+ {
+ string value = _Headquarters;
+ GetHeadquarters(ref value);
+ return (_Headquarters = value);
+ }
+ set
+ {
+ string oldValue = _Headquarters;
+ SetHeadquarters(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Headquarters = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Country
+ /// </summary>
+ protected string _Country;
+ /// <summary>
+ /// When provided in a partial class, allows value of Country to be changed before setting.
+ /// </summary>
+ partial void SetCountry(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Country to be changed before returning.
+ /// </summary>
+ partial void GetCountry(ref string result);
+
+ /// <summary>
+ /// Max length = 2
+ /// </summary>
+ [MaxLength(2)]
+ [StringLength(2)]
+ public string Country
+ {
+ get
+ {
+ string value = _Country;
+ GetCountry(ref value);
+ return (_Country = value);
+ }
+ set
+ {
+ string oldValue = _Country;
+ SetCountry(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Country = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Homepage
+ /// </summary>
+ protected string _Homepage;
+ /// <summary>
+ /// When provided in a partial class, allows value of Homepage to be changed before setting.
+ /// </summary>
+ partial void SetHomepage(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Homepage to be changed before returning.
+ /// </summary>
+ partial void GetHomepage(ref string result);
+
+ /// <summary>
+ /// Max length = 1024
+ /// </summary>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string Homepage
+ {
+ get
+ {
+ string value = _Homepage;
+ GetHomepage(ref value);
+ return (_Homepage = value);
+ }
+ set
+ {
+ string oldValue = _Homepage;
+ SetHomepage(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Homepage = value;
+ }
+ }
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/CustomItem.cs b/Jellyfin.Data/Entities/CustomItem.cs
new file mode 100644
index 000000000..2006717bf
--- /dev/null
+++ b/Jellyfin.Data/Entities/CustomItem.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class CustomItem : LibraryItem
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected CustomItem()
+ {
+ CustomItemMetadata = new HashSet<CustomItemMetadata>();
+ Releases = new HashSet<Release>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static CustomItem CreateCustomItemUnsafe()
+ {
+ return new CustomItem();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+ public CustomItem(Guid urlid, DateTime dateadded)
+ {
+ this.UrlId = urlid;
+
+ this.CustomItemMetadata = new HashSet<CustomItemMetadata>();
+ this.Releases = new HashSet<Release>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+ public static CustomItem Create(Guid urlid, DateTime dateadded)
+ {
+ return new CustomItem(urlid, dateadded);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+ [ForeignKey("CustomItemMetadata_CustomItemMetadata_Id")]
+ public virtual ICollection<CustomItemMetadata> CustomItemMetadata { get; protected set; }
+
+ [ForeignKey("Release_Releases_Id")]
+ public virtual ICollection<Release> Releases { get; protected set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/CustomItemMetadata.cs b/Jellyfin.Data/Entities/CustomItemMetadata.cs
new file mode 100644
index 000000000..e09e4467a
--- /dev/null
+++ b/Jellyfin.Data/Entities/CustomItemMetadata.cs
@@ -0,0 +1,67 @@
+using System;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class CustomItemMetadata : Metadata
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected CustomItemMetadata()
+ {
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static CustomItemMetadata CreateCustomItemMetadataUnsafe()
+ {
+ return new CustomItemMetadata();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="title">The title or name of the object</param>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="_customitem0"></param>
+ public CustomItemMetadata(string title, string language, DateTime dateadded, DateTime datemodified, CustomItem _customitem0)
+ {
+ if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title));
+ this.Title = title;
+
+ if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language));
+ this.Language = language;
+
+ if (_customitem0 == null) throw new ArgumentNullException(nameof(_customitem0));
+ _customitem0.CustomItemMetadata.Add(this);
+
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="title">The title or name of the object</param>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="_customitem0"></param>
+ public static CustomItemMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, CustomItem _customitem0)
+ {
+ return new CustomItemMetadata(title, language, dateadded, datemodified, _customitem0);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/Episode.cs b/Jellyfin.Data/Entities/Episode.cs
new file mode 100644
index 000000000..6f6baa14d
--- /dev/null
+++ b/Jellyfin.Data/Entities/Episode.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class Episode : LibraryItem
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected Episode()
+ {
+ // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem.
+ // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
+
+ Releases = new HashSet<Release>();
+ EpisodeMetadata = new HashSet<EpisodeMetadata>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static Episode CreateEpisodeUnsafe()
+ {
+ return new Episode();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+ /// <param name="_season0"></param>
+ public Episode(Guid urlid, DateTime dateadded, Season _season0)
+ {
+ // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem.
+ // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
+
+ this.UrlId = urlid;
+
+ if (_season0 == null) throw new ArgumentNullException(nameof(_season0));
+ _season0.Episodes.Add(this);
+
+ this.Releases = new HashSet<Release>();
+ this.EpisodeMetadata = new HashSet<EpisodeMetadata>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+ /// <param name="_season0"></param>
+ public static Episode Create(Guid urlid, DateTime dateadded, Season _season0)
+ {
+ return new Episode(urlid, dateadded, _season0);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for EpisodeNumber
+ /// </summary>
+ protected int? _EpisodeNumber;
+ /// <summary>
+ /// When provided in a partial class, allows value of EpisodeNumber to be changed before setting.
+ /// </summary>
+ partial void SetEpisodeNumber(int? oldValue, ref int? newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of EpisodeNumber to be changed before returning.
+ /// </summary>
+ partial void GetEpisodeNumber(ref int? result);
+
+ public int? EpisodeNumber
+ {
+ get
+ {
+ int? value = _EpisodeNumber;
+ GetEpisodeNumber(ref value);
+ return (_EpisodeNumber = value);
+ }
+ set
+ {
+ int? oldValue = _EpisodeNumber;
+ SetEpisodeNumber(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _EpisodeNumber = value;
+ }
+ }
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+ [ForeignKey("Release_Releases_Id")]
+ public virtual ICollection<Release> Releases { get; protected set; }
+ [ForeignKey("EpisodeMetadata_EpisodeMetadata_Id")]
+ public virtual ICollection<EpisodeMetadata> EpisodeMetadata { get; protected set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/EpisodeMetadata.cs b/Jellyfin.Data/Entities/EpisodeMetadata.cs
new file mode 100644
index 000000000..e5431bf22
--- /dev/null
+++ b/Jellyfin.Data/Entities/EpisodeMetadata.cs
@@ -0,0 +1,179 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class EpisodeMetadata : Metadata
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected EpisodeMetadata()
+ {
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static EpisodeMetadata CreateEpisodeMetadataUnsafe()
+ {
+ return new EpisodeMetadata();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="title">The title or name of the object</param>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="_episode0"></param>
+ public EpisodeMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Episode _episode0)
+ {
+ if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title));
+ this.Title = title;
+
+ if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language));
+ this.Language = language;
+
+ if (_episode0 == null) throw new ArgumentNullException(nameof(_episode0));
+ _episode0.EpisodeMetadata.Add(this);
+
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="title">The title or name of the object</param>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="_episode0"></param>
+ public static EpisodeMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Episode _episode0)
+ {
+ return new EpisodeMetadata(title, language, dateadded, datemodified, _episode0);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Outline
+ /// </summary>
+ protected string _Outline;
+ /// <summary>
+ /// When provided in a partial class, allows value of Outline to be changed before setting.
+ /// </summary>
+ partial void SetOutline(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Outline to be changed before returning.
+ /// </summary>
+ partial void GetOutline(ref string result);
+
+ /// <summary>
+ /// Max length = 1024
+ /// </summary>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string Outline
+ {
+ get
+ {
+ string value = _Outline;
+ GetOutline(ref value);
+ return (_Outline = value);
+ }
+ set
+ {
+ string oldValue = _Outline;
+ SetOutline(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Outline = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Plot
+ /// </summary>
+ protected string _Plot;
+ /// <summary>
+ /// When provided in a partial class, allows value of Plot to be changed before setting.
+ /// </summary>
+ partial void SetPlot(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Plot to be changed before returning.
+ /// </summary>
+ partial void GetPlot(ref string result);
+
+ /// <summary>
+ /// Max length = 65535
+ /// </summary>
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string Plot
+ {
+ get
+ {
+ string value = _Plot;
+ GetPlot(ref value);
+ return (_Plot = value);
+ }
+ set
+ {
+ string oldValue = _Plot;
+ SetPlot(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Plot = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Tagline
+ /// </summary>
+ protected string _Tagline;
+ /// <summary>
+ /// When provided in a partial class, allows value of Tagline to be changed before setting.
+ /// </summary>
+ partial void SetTagline(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Tagline to be changed before returning.
+ /// </summary>
+ partial void GetTagline(ref string result);
+
+ /// <summary>
+ /// Max length = 1024
+ /// </summary>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string Tagline
+ {
+ get
+ {
+ string value = _Tagline;
+ GetTagline(ref value);
+ return (_Tagline = value);
+ }
+ set
+ {
+ string oldValue = _Tagline;
+ SetTagline(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Tagline = value;
+ }
+ }
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/Genre.cs b/Jellyfin.Data/Entities/Genre.cs
new file mode 100644
index 000000000..38f289a8e
--- /dev/null
+++ b/Jellyfin.Data/Entities/Genre.cs
@@ -0,0 +1,152 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class Genre
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected Genre()
+ {
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static Genre CreateGenreUnsafe()
+ {
+ return new Genre();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="name"></param>
+ /// <param name="_metadata0"></param>
+ public Genre(string name, Metadata _metadata0)
+ {
+ if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));
+ this.Name = name;
+
+ if (_metadata0 == null) throw new ArgumentNullException(nameof(_metadata0));
+ _metadata0.Genres.Add(this);
+
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="name"></param>
+ /// <param name="_metadata0"></param>
+ public static Genre Create(string name, Metadata _metadata0)
+ {
+ return new Genre(name, _metadata0);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Id
+ /// </summary>
+ internal int _Id;
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before setting.
+ /// </summary>
+ partial void SetId(int oldValue, ref int newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before returning.
+ /// </summary>
+ partial void GetId(ref int result);
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id
+ {
+ get
+ {
+ int value = _Id;
+ GetId(ref value);
+ return (_Id = value);
+ }
+ protected set
+ {
+ int oldValue = _Id;
+ SetId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Id = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Name
+ /// </summary>
+ internal string _Name;
+ /// <summary>
+ /// When provided in a partial class, allows value of Name to be changed before setting.
+ /// </summary>
+ partial void SetName(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Name to be changed before returning.
+ /// </summary>
+ partial void GetName(ref string result);
+
+ /// <summary>
+ /// Indexed, Required, Max length = 255
+ /// </summary>
+ [Required]
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string Name
+ {
+ get
+ {
+ string value = _Name;
+ GetName(ref value);
+ return (_Name = value);
+ }
+ set
+ {
+ string oldValue = _Name;
+ SetName(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Name = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/Group.cs b/Jellyfin.Data/Entities/Group.cs
new file mode 100644
index 000000000..54f9f4905
--- /dev/null
+++ b/Jellyfin.Data/Entities/Group.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class Group
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected Group()
+ {
+ GroupPermissions = new HashSet<Permission>();
+ ProviderMappings = new HashSet<ProviderMapping>();
+ Preferences = new HashSet<Preference>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static Group CreateGroupUnsafe()
+ {
+ return new Group();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="name"></param>
+ /// <param name="_user0"></param>
+ public Group(string name, User _user0)
+ {
+ if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));
+ this.Name = name;
+
+ if (_user0 == null) throw new ArgumentNullException(nameof(_user0));
+ _user0.Groups.Add(this);
+
+ this.GroupPermissions = new HashSet<Permission>();
+ this.ProviderMappings = new HashSet<ProviderMapping>();
+ this.Preferences = new HashSet<Preference>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="name"></param>
+ /// <param name="_user0"></param>
+ public static Group Create(string name, User _user0)
+ {
+ return new Group(name, _user0);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; protected set; }
+
+ /// <summary>
+ /// Required, Max length = 255
+ /// </summary>
+ [Required]
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ [ForeignKey("Permission_GroupPermissions_Id")]
+ public virtual ICollection<Permission> GroupPermissions { get; protected set; }
+
+ [ForeignKey("ProviderMapping_ProviderMappings_Id")]
+ public virtual ICollection<ProviderMapping> ProviderMappings { get; protected set; }
+
+ [ForeignKey("Preference_Preferences_Id")]
+ public virtual ICollection<Preference> Preferences { get; protected set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/Library.cs b/Jellyfin.Data/Entities/Library.cs
new file mode 100644
index 000000000..c11c09e91
--- /dev/null
+++ b/Jellyfin.Data/Entities/Library.cs
@@ -0,0 +1,147 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class Library
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected Library()
+ {
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static Library CreateLibraryUnsafe()
+ {
+ return new Library();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="name"></param>
+ public Library(string name)
+ {
+ if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));
+ this.Name = name;
+
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="name"></param>
+ public static Library Create(string name)
+ {
+ return new Library(name);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Id
+ /// </summary>
+ internal int _Id;
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before setting.
+ /// </summary>
+ partial void SetId(int oldValue, ref int newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before returning.
+ /// </summary>
+ partial void GetId(ref int result);
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id
+ {
+ get
+ {
+ int value = _Id;
+ GetId(ref value);
+ return (_Id = value);
+ }
+ protected set
+ {
+ int oldValue = _Id;
+ SetId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Id = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Name
+ /// </summary>
+ protected string _Name;
+ /// <summary>
+ /// When provided in a partial class, allows value of Name to be changed before setting.
+ /// </summary>
+ partial void SetName(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Name to be changed before returning.
+ /// </summary>
+ partial void GetName(ref string result);
+
+ /// <summary>
+ /// Required, Max length = 1024
+ /// </summary>
+ [Required]
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string Name
+ {
+ get
+ {
+ string value = _Name;
+ GetName(ref value);
+ return (_Name = value);
+ }
+ set
+ {
+ string oldValue = _Name;
+ SetName(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Name = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/LibraryItem.cs b/Jellyfin.Data/Entities/LibraryItem.cs
new file mode 100644
index 000000000..af6c640b9
--- /dev/null
+++ b/Jellyfin.Data/Entities/LibraryItem.cs
@@ -0,0 +1,170 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public abstract partial class LibraryItem
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to being abstract.
+ /// </summary>
+ protected LibraryItem()
+ {
+ Init();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+ protected LibraryItem(Guid urlid, DateTime dateadded)
+ {
+ this.UrlId = urlid;
+
+
+ Init();
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Id
+ /// </summary>
+ internal int _Id;
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before setting.
+ /// </summary>
+ partial void SetId(int oldValue, ref int newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before returning.
+ /// </summary>
+ partial void GetId(ref int result);
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id
+ {
+ get
+ {
+ int value = _Id;
+ GetId(ref value);
+ return (_Id = value);
+ }
+ protected set
+ {
+ int oldValue = _Id;
+ SetId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Id = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for UrlId
+ /// </summary>
+ internal Guid _UrlId;
+ /// <summary>
+ /// When provided in a partial class, allows value of UrlId to be changed before setting.
+ /// </summary>
+ partial void SetUrlId(Guid oldValue, ref Guid newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of UrlId to be changed before returning.
+ /// </summary>
+ partial void GetUrlId(ref Guid result);
+
+ /// <summary>
+ /// Indexed, Required
+ /// This is whats gets displayed in the Urls and API requests. This could also be a string.
+ /// </summary>
+ [Required]
+ public Guid UrlId
+ {
+ get
+ {
+ Guid value = _UrlId;
+ GetUrlId(ref value);
+ return (_UrlId = value);
+ }
+ set
+ {
+ Guid oldValue = _UrlId;
+ SetUrlId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _UrlId = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for DateAdded
+ /// </summary>
+ protected DateTime _DateAdded;
+ /// <summary>
+ /// When provided in a partial class, allows value of DateAdded to be changed before setting.
+ /// </summary>
+ partial void SetDateAdded(DateTime oldValue, ref DateTime newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of DateAdded to be changed before returning.
+ /// </summary>
+ partial void GetDateAdded(ref DateTime result);
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [Required]
+ public DateTime DateAdded
+ {
+ get
+ {
+ DateTime value = _DateAdded;
+ GetDateAdded(ref value);
+ return (_DateAdded = value);
+ }
+ internal set
+ {
+ DateTime oldValue = _DateAdded;
+ SetDateAdded(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _DateAdded = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [ForeignKey("LibraryRoot_Id")]
+ public virtual LibraryRoot LibraryRoot { get; set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/LibraryRoot.cs b/Jellyfin.Data/Entities/LibraryRoot.cs
new file mode 100644
index 000000000..bbc23e1c9
--- /dev/null
+++ b/Jellyfin.Data/Entities/LibraryRoot.cs
@@ -0,0 +1,192 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class LibraryRoot
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected LibraryRoot()
+ {
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static LibraryRoot CreateLibraryRootUnsafe()
+ {
+ return new LibraryRoot();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="path">Absolute Path</param>
+ public LibraryRoot(string path)
+ {
+ if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path));
+ this.Path = path;
+
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="path">Absolute Path</param>
+ public static LibraryRoot Create(string path)
+ {
+ return new LibraryRoot(path);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Id
+ /// </summary>
+ internal int _Id;
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before setting.
+ /// </summary>
+ partial void SetId(int oldValue, ref int newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before returning.
+ /// </summary>
+ partial void GetId(ref int result);
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id
+ {
+ get
+ {
+ int value = _Id;
+ GetId(ref value);
+ return (_Id = value);
+ }
+ protected set
+ {
+ int oldValue = _Id;
+ SetId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Id = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Path
+ /// </summary>
+ protected string _Path;
+ /// <summary>
+ /// When provided in a partial class, allows value of Path to be changed before setting.
+ /// </summary>
+ partial void SetPath(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Path to be changed before returning.
+ /// </summary>
+ partial void GetPath(ref string result);
+
+ /// <summary>
+ /// Required, Max length = 65535
+ /// Absolute Path
+ /// </summary>
+ [Required]
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string Path
+ {
+ get
+ {
+ string value = _Path;
+ GetPath(ref value);
+ return (_Path = value);
+ }
+ set
+ {
+ string oldValue = _Path;
+ SetPath(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Path = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for NetworkPath
+ /// </summary>
+ protected string _NetworkPath;
+ /// <summary>
+ /// When provided in a partial class, allows value of NetworkPath to be changed before setting.
+ /// </summary>
+ partial void SetNetworkPath(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of NetworkPath to be changed before returning.
+ /// </summary>
+ partial void GetNetworkPath(ref string result);
+
+ /// <summary>
+ /// Max length = 65535
+ /// Absolute network path, for example for transcoding sattelites.
+ /// </summary>
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string NetworkPath
+ {
+ get
+ {
+ string value = _NetworkPath;
+ GetNetworkPath(ref value);
+ return (_NetworkPath = value);
+ }
+ set
+ {
+ string oldValue = _NetworkPath;
+ SetNetworkPath(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _NetworkPath = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [ForeignKey("Library_Id")]
+ public virtual Library Library { get; set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/MediaFile.cs b/Jellyfin.Data/Entities/MediaFile.cs
new file mode 100644
index 000000000..719539e5c
--- /dev/null
+++ b/Jellyfin.Data/Entities/MediaFile.cs
@@ -0,0 +1,200 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class MediaFile
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected MediaFile()
+ {
+ MediaFileStreams = new HashSet<MediaFileStream>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static MediaFile CreateMediaFileUnsafe()
+ {
+ return new MediaFile();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="path">Relative to the LibraryRoot</param>
+ /// <param name="kind"></param>
+ /// <param name="_release0"></param>
+ public MediaFile(string path, Enums.MediaFileKind kind, Release _release0)
+ {
+ if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path));
+ this.Path = path;
+
+ this.Kind = kind;
+
+ if (_release0 == null) throw new ArgumentNullException(nameof(_release0));
+ _release0.MediaFiles.Add(this);
+
+ this.MediaFileStreams = new HashSet<MediaFileStream>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="path">Relative to the LibraryRoot</param>
+ /// <param name="kind"></param>
+ /// <param name="_release0"></param>
+ public static MediaFile Create(string path, Enums.MediaFileKind kind, Release _release0)
+ {
+ return new MediaFile(path, kind, _release0);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Id
+ /// </summary>
+ internal int _Id;
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before setting.
+ /// </summary>
+ partial void SetId(int oldValue, ref int newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before returning.
+ /// </summary>
+ partial void GetId(ref int result);
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id
+ {
+ get
+ {
+ int value = _Id;
+ GetId(ref value);
+ return (_Id = value);
+ }
+ protected set
+ {
+ int oldValue = _Id;
+ SetId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Id = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Path
+ /// </summary>
+ protected string _Path;
+ /// <summary>
+ /// When provided in a partial class, allows value of Path to be changed before setting.
+ /// </summary>
+ partial void SetPath(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Path to be changed before returning.
+ /// </summary>
+ partial void GetPath(ref string result);
+
+ /// <summary>
+ /// Required, Max length = 65535
+ /// Relative to the LibraryRoot
+ /// </summary>
+ [Required]
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string Path
+ {
+ get
+ {
+ string value = _Path;
+ GetPath(ref value);
+ return (_Path = value);
+ }
+ set
+ {
+ string oldValue = _Path;
+ SetPath(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Path = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Kind
+ /// </summary>
+ protected Enums.MediaFileKind _Kind;
+ /// <summary>
+ /// When provided in a partial class, allows value of Kind to be changed before setting.
+ /// </summary>
+ partial void SetKind(Enums.MediaFileKind oldValue, ref Enums.MediaFileKind newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Kind to be changed before returning.
+ /// </summary>
+ partial void GetKind(ref Enums.MediaFileKind result);
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [Required]
+ public Enums.MediaFileKind Kind
+ {
+ get
+ {
+ Enums.MediaFileKind value = _Kind;
+ GetKind(ref value);
+ return (_Kind = value);
+ }
+ set
+ {
+ Enums.MediaFileKind oldValue = _Kind;
+ SetKind(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Kind = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ [ForeignKey("MediaFileStream_MediaFileStreams_Id")]
+ public virtual ICollection<MediaFileStream> MediaFileStreams { get; protected set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/MediaFileStream.cs b/Jellyfin.Data/Entities/MediaFileStream.cs
new file mode 100644
index 000000000..7b3399731
--- /dev/null
+++ b/Jellyfin.Data/Entities/MediaFileStream.cs
@@ -0,0 +1,149 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class MediaFileStream
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected MediaFileStream()
+ {
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static MediaFileStream CreateMediaFileStreamUnsafe()
+ {
+ return new MediaFileStream();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="streamnumber"></param>
+ /// <param name="_mediafile0"></param>
+ public MediaFileStream(int streamnumber, MediaFile _mediafile0)
+ {
+ this.StreamNumber = streamnumber;
+
+ if (_mediafile0 == null) throw new ArgumentNullException(nameof(_mediafile0));
+ _mediafile0.MediaFileStreams.Add(this);
+
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="streamnumber"></param>
+ /// <param name="_mediafile0"></param>
+ public static MediaFileStream Create(int streamnumber, MediaFile _mediafile0)
+ {
+ return new MediaFileStream(streamnumber, _mediafile0);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Id
+ /// </summary>
+ internal int _Id;
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before setting.
+ /// </summary>
+ partial void SetId(int oldValue, ref int newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before returning.
+ /// </summary>
+ partial void GetId(ref int result);
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id
+ {
+ get
+ {
+ int value = _Id;
+ GetId(ref value);
+ return (_Id = value);
+ }
+ protected set
+ {
+ int oldValue = _Id;
+ SetId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Id = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for StreamNumber
+ /// </summary>
+ protected int _StreamNumber;
+ /// <summary>
+ /// When provided in a partial class, allows value of StreamNumber to be changed before setting.
+ /// </summary>
+ partial void SetStreamNumber(int oldValue, ref int newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of StreamNumber to be changed before returning.
+ /// </summary>
+ partial void GetStreamNumber(ref int result);
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [Required]
+ public int StreamNumber
+ {
+ get
+ {
+ int value = _StreamNumber;
+ GetStreamNumber(ref value);
+ return (_StreamNumber = value);
+ }
+ set
+ {
+ int oldValue = _StreamNumber;
+ SetStreamNumber(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _StreamNumber = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/Metadata.cs b/Jellyfin.Data/Entities/Metadata.cs
new file mode 100644
index 000000000..467ee6822
--- /dev/null
+++ b/Jellyfin.Data/Entities/Metadata.cs
@@ -0,0 +1,380 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public abstract partial class Metadata
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to being abstract.
+ /// </summary>
+ protected Metadata()
+ {
+ PersonRoles = new HashSet<PersonRole>();
+ Genres = new HashSet<Genre>();
+ Artwork = new HashSet<Artwork>();
+ Ratings = new HashSet<Rating>();
+ Sources = new HashSet<MetadataProviderId>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="title">The title or name of the object</param>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ protected Metadata(string title, string language, DateTime dateadded, DateTime datemodified)
+ {
+ if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title));
+ this.Title = title;
+
+ if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language));
+ this.Language = language;
+
+ this.PersonRoles = new HashSet<PersonRole>();
+ this.Genres = new HashSet<Genre>();
+ this.Artwork = new HashSet<Artwork>();
+ this.Ratings = new HashSet<Rating>();
+ this.Sources = new HashSet<MetadataProviderId>();
+
+ Init();
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Id
+ /// </summary>
+ internal int _Id;
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before setting.
+ /// </summary>
+ partial void SetId(int oldValue, ref int newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before returning.
+ /// </summary>
+ partial void GetId(ref int result);
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id
+ {
+ get
+ {
+ int value = _Id;
+ GetId(ref value);
+ return (_Id = value);
+ }
+ protected set
+ {
+ int oldValue = _Id;
+ SetId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Id = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Title
+ /// </summary>
+ protected string _Title;
+ /// <summary>
+ /// When provided in a partial class, allows value of Title to be changed before setting.
+ /// </summary>
+ partial void SetTitle(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Title to be changed before returning.
+ /// </summary>
+ partial void GetTitle(ref string result);
+
+ /// <summary>
+ /// Required, Max length = 1024
+ /// The title or name of the object
+ /// </summary>
+ [Required]
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string Title
+ {
+ get
+ {
+ string value = _Title;
+ GetTitle(ref value);
+ return (_Title = value);
+ }
+ set
+ {
+ string oldValue = _Title;
+ SetTitle(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Title = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for OriginalTitle
+ /// </summary>
+ protected string _OriginalTitle;
+ /// <summary>
+ /// When provided in a partial class, allows value of OriginalTitle to be changed before setting.
+ /// </summary>
+ partial void SetOriginalTitle(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of OriginalTitle to be changed before returning.
+ /// </summary>
+ partial void GetOriginalTitle(ref string result);
+
+ /// <summary>
+ /// Max length = 1024
+ /// </summary>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string OriginalTitle
+ {
+ get
+ {
+ string value = _OriginalTitle;
+ GetOriginalTitle(ref value);
+ return (_OriginalTitle = value);
+ }
+ set
+ {
+ string oldValue = _OriginalTitle;
+ SetOriginalTitle(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _OriginalTitle = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for SortTitle
+ /// </summary>
+ protected string _SortTitle;
+ /// <summary>
+ /// When provided in a partial class, allows value of SortTitle to be changed before setting.
+ /// </summary>
+ partial void SetSortTitle(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of SortTitle to be changed before returning.
+ /// </summary>
+ partial void GetSortTitle(ref string result);
+
+ /// <summary>
+ /// Max length = 1024
+ /// </summary>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string SortTitle
+ {
+ get
+ {
+ string value = _SortTitle;
+ GetSortTitle(ref value);
+ return (_SortTitle = value);
+ }
+ set
+ {
+ string oldValue = _SortTitle;
+ SetSortTitle(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _SortTitle = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Language
+ /// </summary>
+ protected string _Language;
+ /// <summary>
+ /// When provided in a partial class, allows value of Language to be changed before setting.
+ /// </summary>
+ partial void SetLanguage(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Language to be changed before returning.
+ /// </summary>
+ partial void GetLanguage(ref string result);
+
+ /// <summary>
+ /// Required, Min length = 3, Max length = 3
+ /// ISO-639-3 3-character language codes
+ /// </summary>
+ [Required]
+ [MinLength(3)]
+ [MaxLength(3)]
+ [StringLength(3)]
+ public string Language
+ {
+ get
+ {
+ string value = _Language;
+ GetLanguage(ref value);
+ return (_Language = value);
+ }
+ set
+ {
+ string oldValue = _Language;
+ SetLanguage(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Language = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for ReleaseDate
+ /// </summary>
+ protected DateTimeOffset? _ReleaseDate;
+ /// <summary>
+ /// When provided in a partial class, allows value of ReleaseDate to be changed before setting.
+ /// </summary>
+ partial void SetReleaseDate(DateTimeOffset? oldValue, ref DateTimeOffset? newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of ReleaseDate to be changed before returning.
+ /// </summary>
+ partial void GetReleaseDate(ref DateTimeOffset? result);
+
+ public DateTimeOffset? ReleaseDate
+ {
+ get
+ {
+ DateTimeOffset? value = _ReleaseDate;
+ GetReleaseDate(ref value);
+ return (_ReleaseDate = value);
+ }
+ set
+ {
+ DateTimeOffset? oldValue = _ReleaseDate;
+ SetReleaseDate(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _ReleaseDate = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for DateAdded
+ /// </summary>
+ protected DateTime _DateAdded;
+ /// <summary>
+ /// When provided in a partial class, allows value of DateAdded to be changed before setting.
+ /// </summary>
+ partial void SetDateAdded(DateTime oldValue, ref DateTime newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of DateAdded to be changed before returning.
+ /// </summary>
+ partial void GetDateAdded(ref DateTime result);
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [Required]
+ public DateTime DateAdded
+ {
+ get
+ {
+ DateTime value = _DateAdded;
+ GetDateAdded(ref value);
+ return (_DateAdded = value);
+ }
+ internal set
+ {
+ DateTime oldValue = _DateAdded;
+ SetDateAdded(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _DateAdded = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for DateModified
+ /// </summary>
+ protected DateTime _DateModified;
+ /// <summary>
+ /// When provided in a partial class, allows value of DateModified to be changed before setting.
+ /// </summary>
+ partial void SetDateModified(DateTime oldValue, ref DateTime newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of DateModified to be changed before returning.
+ /// </summary>
+ partial void GetDateModified(ref DateTime result);
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [Required]
+ public DateTime DateModified
+ {
+ get
+ {
+ DateTime value = _DateModified;
+ GetDateModified(ref value);
+ return (_DateModified = value);
+ }
+ internal set
+ {
+ DateTime oldValue = _DateModified;
+ SetDateModified(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _DateModified = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ [ForeignKey("PersonRole_PersonRoles_Id")]
+ public virtual ICollection<PersonRole> PersonRoles { get; protected set; }
+
+ [ForeignKey("PersonRole_PersonRoles_Id")]
+ public virtual ICollection<Genre> Genres { get; protected set; }
+
+ [ForeignKey("PersonRole_PersonRoles_Id")]
+ public virtual ICollection<Artwork> Artwork { get; protected set; }
+
+ [ForeignKey("PersonRole_PersonRoles_Id")]
+ public virtual ICollection<Rating> Ratings { get; protected set; }
+
+ [ForeignKey("PersonRole_PersonRoles_Id")]
+ public virtual ICollection<MetadataProviderId> Sources { get; protected set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/MetadataProvider.cs b/Jellyfin.Data/Entities/MetadataProvider.cs
new file mode 100644
index 000000000..4e4f107fb
--- /dev/null
+++ b/Jellyfin.Data/Entities/MetadataProvider.cs
@@ -0,0 +1,147 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class MetadataProvider
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected MetadataProvider()
+ {
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static MetadataProvider CreateMetadataProviderUnsafe()
+ {
+ return new MetadataProvider();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="name"></param>
+ public MetadataProvider(string name)
+ {
+ if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));
+ this.Name = name;
+
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="name"></param>
+ public static MetadataProvider Create(string name)
+ {
+ return new MetadataProvider(name);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Id
+ /// </summary>
+ internal int _Id;
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before setting.
+ /// </summary>
+ partial void SetId(int oldValue, ref int newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before returning.
+ /// </summary>
+ partial void GetId(ref int result);
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id
+ {
+ get
+ {
+ int value = _Id;
+ GetId(ref value);
+ return (_Id = value);
+ }
+ protected set
+ {
+ int oldValue = _Id;
+ SetId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Id = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Name
+ /// </summary>
+ protected string _Name;
+ /// <summary>
+ /// When provided in a partial class, allows value of Name to be changed before setting.
+ /// </summary>
+ partial void SetName(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Name to be changed before returning.
+ /// </summary>
+ partial void GetName(ref string result);
+
+ /// <summary>
+ /// Required, Max length = 1024
+ /// </summary>
+ [Required]
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string Name
+ {
+ get
+ {
+ string value = _Name;
+ GetName(ref value);
+ return (_Name = value);
+ }
+ set
+ {
+ string oldValue = _Name;
+ SetName(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Name = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/MetadataProviderId.cs b/Jellyfin.Data/Entities/MetadataProviderId.cs
new file mode 100644
index 000000000..926f223de
--- /dev/null
+++ b/Jellyfin.Data/Entities/MetadataProviderId.cs
@@ -0,0 +1,179 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class MetadataProviderId
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected MetadataProviderId()
+ {
+ // NOTE: This class has one-to-one associations with MetadataProviderId.
+ // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static MetadataProviderId CreateMetadataProviderIdUnsafe()
+ {
+ return new MetadataProviderId();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="providerid"></param>
+ /// <param name="_metadata0"></param>
+ /// <param name="_person1"></param>
+ /// <param name="_personrole2"></param>
+ /// <param name="_ratingsource3"></param>
+ public MetadataProviderId(string providerid, Metadata _metadata0, Person _person1, PersonRole _personrole2, RatingSource _ratingsource3)
+ {
+ // NOTE: This class has one-to-one associations with MetadataProviderId.
+ // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
+
+ if (string.IsNullOrEmpty(providerid)) throw new ArgumentNullException(nameof(providerid));
+ this.ProviderId = providerid;
+
+ if (_metadata0 == null) throw new ArgumentNullException(nameof(_metadata0));
+ _metadata0.Sources.Add(this);
+
+ if (_person1 == null) throw new ArgumentNullException(nameof(_person1));
+ _person1.Sources.Add(this);
+
+ if (_personrole2 == null) throw new ArgumentNullException(nameof(_personrole2));
+ _personrole2.Sources.Add(this);
+
+ if (_ratingsource3 == null) throw new ArgumentNullException(nameof(_ratingsource3));
+ _ratingsource3.Source = this;
+
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="providerid"></param>
+ /// <param name="_metadata0"></param>
+ /// <param name="_person1"></param>
+ /// <param name="_personrole2"></param>
+ /// <param name="_ratingsource3"></param>
+ public static MetadataProviderId Create(string providerid, Metadata _metadata0, Person _person1, PersonRole _personrole2, RatingSource _ratingsource3)
+ {
+ return new MetadataProviderId(providerid, _metadata0, _person1, _personrole2, _ratingsource3);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Id
+ /// </summary>
+ internal int _Id;
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before setting.
+ /// </summary>
+ partial void SetId(int oldValue, ref int newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before returning.
+ /// </summary>
+ partial void GetId(ref int result);
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id
+ {
+ get
+ {
+ int value = _Id;
+ GetId(ref value);
+ return (_Id = value);
+ }
+ protected set
+ {
+ int oldValue = _Id;
+ SetId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Id = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for ProviderId
+ /// </summary>
+ protected string _ProviderId;
+ /// <summary>
+ /// When provided in a partial class, allows value of ProviderId to be changed before setting.
+ /// </summary>
+ partial void SetProviderId(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of ProviderId to be changed before returning.
+ /// </summary>
+ partial void GetProviderId(ref string result);
+
+ /// <summary>
+ /// Required, Max length = 255
+ /// </summary>
+ [Required]
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string ProviderId
+ {
+ get
+ {
+ string value = _ProviderId;
+ GetProviderId(ref value);
+ return (_ProviderId = value);
+ }
+ set
+ {
+ string oldValue = _ProviderId;
+ SetProviderId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _ProviderId = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [ForeignKey("MetadataProvider_Id")]
+ public virtual MetadataProvider MetadataProvider { get; set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/Movie.cs b/Jellyfin.Data/Entities/Movie.cs
new file mode 100644
index 000000000..b359b42fc
--- /dev/null
+++ b/Jellyfin.Data/Entities/Movie.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class Movie : LibraryItem
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected Movie()
+ {
+ Releases = new HashSet<Release>();
+ MovieMetadata = new HashSet<MovieMetadata>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static Movie CreateMovieUnsafe()
+ {
+ return new Movie();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+ public Movie(Guid urlid, DateTime dateadded)
+ {
+ this.UrlId = urlid;
+
+ this.Releases = new HashSet<Release>();
+ this.MovieMetadata = new HashSet<MovieMetadata>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+ public static Movie Create(Guid urlid, DateTime dateadded)
+ {
+ return new Movie(urlid, dateadded);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ [ForeignKey("Release_Releases_Id")]
+ public virtual ICollection<Release> Releases { get; protected set; }
+
+ [ForeignKey("MovieMetadata_MovieMetadata_Id")]
+ public virtual ICollection<MovieMetadata> MovieMetadata { get; protected set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/MovieMetadata.cs b/Jellyfin.Data/Entities/MovieMetadata.cs
new file mode 100644
index 000000000..319ae94e5
--- /dev/null
+++ b/Jellyfin.Data/Entities/MovieMetadata.cs
@@ -0,0 +1,223 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class MovieMetadata : Metadata
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected MovieMetadata()
+ {
+ Studios = new HashSet<Company>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static MovieMetadata CreateMovieMetadataUnsafe()
+ {
+ return new MovieMetadata();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="title">The title or name of the object</param>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="_movie0"></param>
+ public MovieMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Movie _movie0)
+ {
+ if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title));
+ this.Title = title;
+
+ if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language));
+ this.Language = language;
+
+ if (_movie0 == null) throw new ArgumentNullException(nameof(_movie0));
+ _movie0.MovieMetadata.Add(this);
+
+ this.Studios = new HashSet<Company>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="title">The title or name of the object</param>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="_movie0"></param>
+ public static MovieMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Movie _movie0)
+ {
+ return new MovieMetadata(title, language, dateadded, datemodified, _movie0);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Outline
+ /// </summary>
+ protected string _Outline;
+ /// <summary>
+ /// When provided in a partial class, allows value of Outline to be changed before setting.
+ /// </summary>
+ partial void SetOutline(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Outline to be changed before returning.
+ /// </summary>
+ partial void GetOutline(ref string result);
+
+ /// <summary>
+ /// Max length = 1024
+ /// </summary>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string Outline
+ {
+ get
+ {
+ string value = _Outline;
+ GetOutline(ref value);
+ return (_Outline = value);
+ }
+ set
+ {
+ string oldValue = _Outline;
+ SetOutline(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Outline = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Plot
+ /// </summary>
+ protected string _Plot;
+ /// <summary>
+ /// When provided in a partial class, allows value of Plot to be changed before setting.
+ /// </summary>
+ partial void SetPlot(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Plot to be changed before returning.
+ /// </summary>
+ partial void GetPlot(ref string result);
+
+ /// <summary>
+ /// Max length = 65535
+ /// </summary>
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string Plot
+ {
+ get
+ {
+ string value = _Plot;
+ GetPlot(ref value);
+ return (_Plot = value);
+ }
+ set
+ {
+ string oldValue = _Plot;
+ SetPlot(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Plot = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Tagline
+ /// </summary>
+ protected string _Tagline;
+ /// <summary>
+ /// When provided in a partial class, allows value of Tagline to be changed before setting.
+ /// </summary>
+ partial void SetTagline(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Tagline to be changed before returning.
+ /// </summary>
+ partial void GetTagline(ref string result);
+
+ /// <summary>
+ /// Max length = 1024
+ /// </summary>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string Tagline
+ {
+ get
+ {
+ string value = _Tagline;
+ GetTagline(ref value);
+ return (_Tagline = value);
+ }
+ set
+ {
+ string oldValue = _Tagline;
+ SetTagline(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Tagline = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Country
+ /// </summary>
+ protected string _Country;
+ /// <summary>
+ /// When provided in a partial class, allows value of Country to be changed before setting.
+ /// </summary>
+ partial void SetCountry(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Country to be changed before returning.
+ /// </summary>
+ partial void GetCountry(ref string result);
+
+ /// <summary>
+ /// Max length = 2
+ /// </summary>
+ [MaxLength(2)]
+ [StringLength(2)]
+ public string Country
+ {
+ get
+ {
+ string value = _Country;
+ GetCountry(ref value);
+ return (_Country = value);
+ }
+ set
+ {
+ string oldValue = _Country;
+ SetCountry(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Country = value;
+ }
+ }
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+ [ForeignKey("Company_Studios_Id")]
+ public virtual ICollection<Company> Studios { get; protected set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/MusicAlbum.cs b/Jellyfin.Data/Entities/MusicAlbum.cs
new file mode 100644
index 000000000..00cb8fe00
--- /dev/null
+++ b/Jellyfin.Data/Entities/MusicAlbum.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class MusicAlbum : LibraryItem
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected MusicAlbum()
+ {
+ MusicAlbumMetadata = new HashSet<MusicAlbumMetadata>();
+ Tracks = new HashSet<Track>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static MusicAlbum CreateMusicAlbumUnsafe()
+ {
+ return new MusicAlbum();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+ public MusicAlbum(Guid urlid, DateTime dateadded)
+ {
+ this.UrlId = urlid;
+
+ this.MusicAlbumMetadata = new HashSet<MusicAlbumMetadata>();
+ this.Tracks = new HashSet<Track>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+ public static MusicAlbum Create(Guid urlid, DateTime dateadded)
+ {
+ return new MusicAlbum(urlid, dateadded);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+ [ForeignKey("MusicAlbumMetadata_MusicAlbumMetadata_Id")]
+ public virtual ICollection<MusicAlbumMetadata> MusicAlbumMetadata { get; protected set; }
+
+ [ForeignKey("Track_Tracks_Id")]
+ public virtual ICollection<Track> Tracks { get; protected set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/MusicAlbumMetadata.cs b/Jellyfin.Data/Entities/MusicAlbumMetadata.cs
new file mode 100644
index 000000000..b52ca6564
--- /dev/null
+++ b/Jellyfin.Data/Entities/MusicAlbumMetadata.cs
@@ -0,0 +1,187 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class MusicAlbumMetadata : Metadata
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected MusicAlbumMetadata()
+ {
+ Labels = new HashSet<Company>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static MusicAlbumMetadata CreateMusicAlbumMetadataUnsafe()
+ {
+ return new MusicAlbumMetadata();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="title">The title or name of the object</param>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="_musicalbum0"></param>
+ public MusicAlbumMetadata(string title, string language, DateTime dateadded, DateTime datemodified, MusicAlbum _musicalbum0)
+ {
+ if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title));
+ this.Title = title;
+
+ if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language));
+ this.Language = language;
+
+ if (_musicalbum0 == null) throw new ArgumentNullException(nameof(_musicalbum0));
+ _musicalbum0.MusicAlbumMetadata.Add(this);
+
+ this.Labels = new HashSet<Company>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="title">The title or name of the object</param>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="_musicalbum0"></param>
+ public static MusicAlbumMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, MusicAlbum _musicalbum0)
+ {
+ return new MusicAlbumMetadata(title, language, dateadded, datemodified, _musicalbum0);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Barcode
+ /// </summary>
+ protected string _Barcode;
+ /// <summary>
+ /// When provided in a partial class, allows value of Barcode to be changed before setting.
+ /// </summary>
+ partial void SetBarcode(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Barcode to be changed before returning.
+ /// </summary>
+ partial void GetBarcode(ref string result);
+
+ /// <summary>
+ /// Max length = 255
+ /// </summary>
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string Barcode
+ {
+ get
+ {
+ string value = _Barcode;
+ GetBarcode(ref value);
+ return (_Barcode = value);
+ }
+ set
+ {
+ string oldValue = _Barcode;
+ SetBarcode(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Barcode = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for LabelNumber
+ /// </summary>
+ protected string _LabelNumber;
+ /// <summary>
+ /// When provided in a partial class, allows value of LabelNumber to be changed before setting.
+ /// </summary>
+ partial void SetLabelNumber(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of LabelNumber to be changed before returning.
+ /// </summary>
+ partial void GetLabelNumber(ref string result);
+
+ /// <summary>
+ /// Max length = 255
+ /// </summary>
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string LabelNumber
+ {
+ get
+ {
+ string value = _LabelNumber;
+ GetLabelNumber(ref value);
+ return (_LabelNumber = value);
+ }
+ set
+ {
+ string oldValue = _LabelNumber;
+ SetLabelNumber(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _LabelNumber = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Country
+ /// </summary>
+ protected string _Country;
+ /// <summary>
+ /// When provided in a partial class, allows value of Country to be changed before setting.
+ /// </summary>
+ partial void SetCountry(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Country to be changed before returning.
+ /// </summary>
+ partial void GetCountry(ref string result);
+
+ /// <summary>
+ /// Max length = 2
+ /// </summary>
+ [MaxLength(2)]
+ [StringLength(2)]
+ public string Country
+ {
+ get
+ {
+ string value = _Country;
+ GetCountry(ref value);
+ return (_Country = value);
+ }
+ set
+ {
+ string oldValue = _Country;
+ SetCountry(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Country = value;
+ }
+ }
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ [ForeignKey("Company_Labels_Id")]
+ public virtual ICollection<Company> Labels { get; protected set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/Permission.cs b/Jellyfin.Data/Entities/Permission.cs
new file mode 100644
index 000000000..0b5b52cbd
--- /dev/null
+++ b/Jellyfin.Data/Entities/Permission.cs
@@ -0,0 +1,144 @@
+using System;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Runtime.CompilerServices;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class Permission
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected Permission()
+ {
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static Permission CreatePermissionUnsafe()
+ {
+ return new Permission();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="kind"></param>
+ /// <param name="value"></param>
+ /// <param name="_user0"></param>
+ /// <param name="_group1"></param>
+ public Permission(Enums.PermissionKind kind, bool value, User _user0, Group _group1)
+ {
+ this.Kind = kind;
+
+ this.Value = value;
+
+ if (_user0 == null) throw new ArgumentNullException(nameof(_user0));
+ _user0.Permissions.Add(this);
+
+ if (_group1 == null) throw new ArgumentNullException(nameof(_group1));
+ _group1.GroupPermissions.Add(this);
+
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="kind"></param>
+ /// <param name="value"></param>
+ /// <param name="_user0"></param>
+ /// <param name="_group1"></param>
+ public static Permission Create(Enums.PermissionKind kind, bool value, User _user0, Group _group1)
+ {
+ return new Permission(kind, value, _user0, _group1);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; protected set; }
+
+ /// <summary>
+ /// Backing field for Kind
+ /// </summary>
+ protected Enums.PermissionKind _Kind;
+ /// <summary>
+ /// When provided in a partial class, allows value of Kind to be changed before setting.
+ /// </summary>
+ partial void SetKind(Enums.PermissionKind oldValue, ref Enums.PermissionKind newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Kind to be changed before returning.
+ /// </summary>
+ partial void GetKind(ref Enums.PermissionKind result);
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [Required]
+ public Enums.PermissionKind Kind
+ {
+ get
+ {
+ Enums.PermissionKind value = _Kind;
+ GetKind(ref value);
+ return (_Kind = value);
+ }
+ set
+ {
+ Enums.PermissionKind oldValue = _Kind;
+ SetKind(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Kind = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [Required]
+ public bool Value { get; set; }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ public virtual event PropertyChangedEventHandler PropertyChanged;
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/Person.cs b/Jellyfin.Data/Entities/Person.cs
new file mode 100644
index 000000000..d893b7e39
--- /dev/null
+++ b/Jellyfin.Data/Entities/Person.cs
@@ -0,0 +1,302 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class Person
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected Person()
+ {
+ Sources = new HashSet<MetadataProviderId>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static Person CreatePersonUnsafe()
+ {
+ return new Person();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="urlid"></param>
+ /// <param name="name"></param>
+ public Person(Guid urlid, string name, DateTime dateadded, DateTime datemodified)
+ {
+ this.UrlId = urlid;
+
+ if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));
+ this.Name = name;
+
+ this.Sources = new HashSet<MetadataProviderId>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="urlid"></param>
+ /// <param name="name"></param>
+ public static Person Create(Guid urlid, string name, DateTime dateadded, DateTime datemodified)
+ {
+ return new Person(urlid, name, dateadded, datemodified);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Id
+ /// </summary>
+ internal int _Id;
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before setting.
+ /// </summary>
+ partial void SetId(int oldValue, ref int newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before returning.
+ /// </summary>
+ partial void GetId(ref int result);
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id
+ {
+ get
+ {
+ int value = _Id;
+ GetId(ref value);
+ return (_Id = value);
+ }
+ protected set
+ {
+ int oldValue = _Id;
+ SetId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Id = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for UrlId
+ /// </summary>
+ protected Guid _UrlId;
+ /// <summary>
+ /// When provided in a partial class, allows value of UrlId to be changed before setting.
+ /// </summary>
+ partial void SetUrlId(Guid oldValue, ref Guid newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of UrlId to be changed before returning.
+ /// </summary>
+ partial void GetUrlId(ref Guid result);
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [Required]
+ public Guid UrlId
+ {
+ get
+ {
+ Guid value = _UrlId;
+ GetUrlId(ref value);
+ return (_UrlId = value);
+ }
+ set
+ {
+ Guid oldValue = _UrlId;
+ SetUrlId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _UrlId = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Name
+ /// </summary>
+ protected string _Name;
+ /// <summary>
+ /// When provided in a partial class, allows value of Name to be changed before setting.
+ /// </summary>
+ partial void SetName(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Name to be changed before returning.
+ /// </summary>
+ partial void GetName(ref string result);
+
+ /// <summary>
+ /// Required, Max length = 1024
+ /// </summary>
+ [Required]
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string Name
+ {
+ get
+ {
+ string value = _Name;
+ GetName(ref value);
+ return (_Name = value);
+ }
+ set
+ {
+ string oldValue = _Name;
+ SetName(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Name = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for SourceId
+ /// </summary>
+ protected string _SourceId;
+ /// <summary>
+ /// When provided in a partial class, allows value of SourceId to be changed before setting.
+ /// </summary>
+ partial void SetSourceId(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of SourceId to be changed before returning.
+ /// </summary>
+ partial void GetSourceId(ref string result);
+
+ /// <summary>
+ /// Max length = 255
+ /// </summary>
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string SourceId
+ {
+ get
+ {
+ string value = _SourceId;
+ GetSourceId(ref value);
+ return (_SourceId = value);
+ }
+ set
+ {
+ string oldValue = _SourceId;
+ SetSourceId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _SourceId = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for DateAdded
+ /// </summary>
+ protected DateTime _DateAdded;
+ /// <summary>
+ /// When provided in a partial class, allows value of DateAdded to be changed before setting.
+ /// </summary>
+ partial void SetDateAdded(DateTime oldValue, ref DateTime newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of DateAdded to be changed before returning.
+ /// </summary>
+ partial void GetDateAdded(ref DateTime result);
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [Required]
+ public DateTime DateAdded
+ {
+ get
+ {
+ DateTime value = _DateAdded;
+ GetDateAdded(ref value);
+ return (_DateAdded = value);
+ }
+ internal set
+ {
+ DateTime oldValue = _DateAdded;
+ SetDateAdded(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _DateAdded = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for DateModified
+ /// </summary>
+ protected DateTime _DateModified;
+ /// <summary>
+ /// When provided in a partial class, allows value of DateModified to be changed before setting.
+ /// </summary>
+ partial void SetDateModified(DateTime oldValue, ref DateTime newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of DateModified to be changed before returning.
+ /// </summary>
+ partial void GetDateModified(ref DateTime result);
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [Required]
+ public DateTime DateModified
+ {
+ get
+ {
+ DateTime value = _DateModified;
+ GetDateModified(ref value);
+ return (_DateModified = value);
+ }
+ internal set
+ {
+ DateTime oldValue = _DateModified;
+ SetDateModified(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _DateModified = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+ [ForeignKey("MetadataProviderId_Sources_Id")]
+ public virtual ICollection<MetadataProviderId> Sources { get; protected set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/PersonRole.cs b/Jellyfin.Data/Entities/PersonRole.cs
new file mode 100644
index 000000000..9bd12c7fb
--- /dev/null
+++ b/Jellyfin.Data/Entities/PersonRole.cs
@@ -0,0 +1,209 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class PersonRole
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected PersonRole()
+ {
+ // NOTE: This class has one-to-one associations with PersonRole.
+ // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
+
+ Sources = new HashSet<MetadataProviderId>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static PersonRole CreatePersonRoleUnsafe()
+ {
+ return new PersonRole();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="type"></param>
+ /// <param name="_metadata0"></param>
+ public PersonRole(Enums.PersonRoleType type, Metadata _metadata0)
+ {
+ // NOTE: This class has one-to-one associations with PersonRole.
+ // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
+
+ this.Type = type;
+
+ if (_metadata0 == null) throw new ArgumentNullException(nameof(_metadata0));
+ _metadata0.PersonRoles.Add(this);
+
+ this.Sources = new HashSet<MetadataProviderId>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="type"></param>
+ /// <param name="_metadata0"></param>
+ public static PersonRole Create(Enums.PersonRoleType type, Metadata _metadata0)
+ {
+ return new PersonRole(type, _metadata0);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Id
+ /// </summary>
+ internal int _Id;
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before setting.
+ /// </summary>
+ partial void SetId(int oldValue, ref int newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before returning.
+ /// </summary>
+ partial void GetId(ref int result);
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id
+ {
+ get
+ {
+ int value = _Id;
+ GetId(ref value);
+ return (_Id = value);
+ }
+ protected set
+ {
+ int oldValue = _Id;
+ SetId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Id = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Role
+ /// </summary>
+ protected string _Role;
+ /// <summary>
+ /// When provided in a partial class, allows value of Role to be changed before setting.
+ /// </summary>
+ partial void SetRole(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Role to be changed before returning.
+ /// </summary>
+ partial void GetRole(ref string result);
+
+ /// <summary>
+ /// Max length = 1024
+ /// </summary>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string Role
+ {
+ get
+ {
+ string value = _Role;
+ GetRole(ref value);
+ return (_Role = value);
+ }
+ set
+ {
+ string oldValue = _Role;
+ SetRole(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Role = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Type
+ /// </summary>
+ protected Enums.PersonRoleType _Type;
+ /// <summary>
+ /// When provided in a partial class, allows value of Type to be changed before setting.
+ /// </summary>
+ partial void SetType(Enums.PersonRoleType oldValue, ref Enums.PersonRoleType newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Type to be changed before returning.
+ /// </summary>
+ partial void GetType(ref Enums.PersonRoleType result);
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [Required]
+ public Enums.PersonRoleType Type
+ {
+ get
+ {
+ Enums.PersonRoleType value = _Type;
+ GetType(ref value);
+ return (_Type = value);
+ }
+ set
+ {
+ Enums.PersonRoleType oldValue = _Type;
+ SetType(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Type = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [ForeignKey("Person_Id")]
+
+ public virtual Person Person { get; set; }
+
+ [ForeignKey("Artwork_Artwork_Id")]
+ public virtual Artwork Artwork { get; set; }
+
+ [ForeignKey("MetadataProviderId_Sources_Id")]
+ public virtual ICollection<MetadataProviderId> Sources { get; protected set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/Photo.cs b/Jellyfin.Data/Entities/Photo.cs
new file mode 100644
index 000000000..7abe62891
--- /dev/null
+++ b/Jellyfin.Data/Entities/Photo.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class Photo : LibraryItem
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected Photo()
+ {
+ PhotoMetadata = new HashSet<PhotoMetadata>();
+ Releases = new HashSet<Release>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static Photo CreatePhotoUnsafe()
+ {
+ return new Photo();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+ public Photo(Guid urlid, DateTime dateadded)
+ {
+ this.UrlId = urlid;
+
+ this.PhotoMetadata = new HashSet<PhotoMetadata>();
+ this.Releases = new HashSet<Release>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+ public static Photo Create(Guid urlid, DateTime dateadded)
+ {
+ return new Photo(urlid, dateadded);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+ [ForeignKey("PhotoMetadata_PhotoMetadata_Id")]
+ public virtual ICollection<PhotoMetadata> PhotoMetadata { get; protected set; }
+
+ [ForeignKey("Release_Releases_Id")]
+ public virtual ICollection<Release> Releases { get; protected set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/PhotoMetadata.cs b/Jellyfin.Data/Entities/PhotoMetadata.cs
new file mode 100644
index 000000000..c5502f707
--- /dev/null
+++ b/Jellyfin.Data/Entities/PhotoMetadata.cs
@@ -0,0 +1,68 @@
+using System;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class PhotoMetadata : Metadata
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected PhotoMetadata()
+ {
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static PhotoMetadata CreatePhotoMetadataUnsafe()
+ {
+ return new PhotoMetadata();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="title">The title or name of the object</param>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="_photo0"></param>
+ public PhotoMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Photo _photo0)
+ {
+ if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title));
+ this.Title = title;
+
+ if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language));
+ this.Language = language;
+
+ if (_photo0 == null) throw new ArgumentNullException(nameof(_photo0));
+ _photo0.PhotoMetadata.Add(this);
+
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="title">The title or name of the object</param>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="_photo0"></param>
+ public static PhotoMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Photo _photo0)
+ {
+ return new PhotoMetadata(title, language, dateadded, datemodified, _photo0);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/Preference.cs b/Jellyfin.Data/Entities/Preference.cs
new file mode 100644
index 000000000..505f52e6b
--- /dev/null
+++ b/Jellyfin.Data/Entities/Preference.cs
@@ -0,0 +1,107 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class Preference
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected Preference()
+ {
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static Preference CreatePreferenceUnsafe()
+ {
+ return new Preference();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="kind"></param>
+ /// <param name="value"></param>
+ /// <param name="_user0"></param>
+ /// <param name="_group1"></param>
+ public Preference(Enums.PreferenceKind kind, string value, User _user0, Group _group1)
+ {
+ this.Kind = kind;
+
+ if (string.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(value));
+ this.Value = value;
+
+ if (_user0 == null) throw new ArgumentNullException(nameof(_user0));
+ _user0.Preferences.Add(this);
+
+ if (_group1 == null) throw new ArgumentNullException(nameof(_group1));
+ _group1.Preferences.Add(this);
+
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="kind"></param>
+ /// <param name="value"></param>
+ /// <param name="_user0"></param>
+ /// <param name="_group1"></param>
+ public static Preference Create(Enums.PreferenceKind kind, string value, User _user0, Group _group1)
+ {
+ return new Preference(kind, value, _user0, _group1);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; protected set; }
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [Required]
+ public Enums.PreferenceKind Kind { get; set; }
+
+ /// <summary>
+ /// Required, Max length = 65535
+ /// </summary>
+ [Required]
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string Value { get; set; }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/ProviderMapping.cs b/Jellyfin.Data/Entities/ProviderMapping.cs
new file mode 100644
index 000000000..6197bd97b
--- /dev/null
+++ b/Jellyfin.Data/Entities/ProviderMapping.cs
@@ -0,0 +1,123 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class ProviderMapping
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected ProviderMapping()
+ {
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static ProviderMapping CreateProviderMappingUnsafe()
+ {
+ return new ProviderMapping();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="providername"></param>
+ /// <param name="providersecrets"></param>
+ /// <param name="providerdata"></param>
+ /// <param name="_user0"></param>
+ /// <param name="_group1"></param>
+ public ProviderMapping(string providername, string providersecrets, string providerdata, User _user0, Group _group1)
+ {
+ if (string.IsNullOrEmpty(providername)) throw new ArgumentNullException(nameof(providername));
+ this.ProviderName = providername;
+
+ if (string.IsNullOrEmpty(providersecrets)) throw new ArgumentNullException(nameof(providersecrets));
+ this.ProviderSecrets = providersecrets;
+
+ if (string.IsNullOrEmpty(providerdata)) throw new ArgumentNullException(nameof(providerdata));
+ this.ProviderData = providerdata;
+
+ if (_user0 == null) throw new ArgumentNullException(nameof(_user0));
+ _user0.ProviderMappings.Add(this);
+
+ if (_group1 == null) throw new ArgumentNullException(nameof(_group1));
+ _group1.ProviderMappings.Add(this);
+
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="providername"></param>
+ /// <param name="providersecrets"></param>
+ /// <param name="providerdata"></param>
+ /// <param name="_user0"></param>
+ /// <param name="_group1"></param>
+ public static ProviderMapping Create(string providername, string providersecrets, string providerdata, User _user0, Group _group1)
+ {
+ return new ProviderMapping(providername, providersecrets, providerdata, _user0, _group1);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; protected set; }
+
+ /// <summary>
+ /// Required, Max length = 255
+ /// </summary>
+ [Required]
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string ProviderName { get; set; }
+
+ /// <summary>
+ /// Required, Max length = 65535
+ /// </summary>
+ [Required]
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string ProviderSecrets { get; set; }
+
+ /// <summary>
+ /// Required, Max length = 65535
+ /// </summary>
+ [Required]
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string ProviderData { get; set; }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/Rating.cs b/Jellyfin.Data/Entities/Rating.cs
new file mode 100644
index 000000000..f70ea8b33
--- /dev/null
+++ b/Jellyfin.Data/Entities/Rating.cs
@@ -0,0 +1,187 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class Rating
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected Rating()
+ {
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static Rating CreateRatingUnsafe()
+ {
+ return new Rating();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="value"></param>
+ /// <param name="_metadata0"></param>
+ public Rating(double value, Metadata _metadata0)
+ {
+ this.Value = value;
+
+ if (_metadata0 == null) throw new ArgumentNullException(nameof(_metadata0));
+ _metadata0.Ratings.Add(this);
+
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="value"></param>
+ /// <param name="_metadata0"></param>
+ public static Rating Create(double value, Metadata _metadata0)
+ {
+ return new Rating(value, _metadata0);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Id
+ /// </summary>
+ internal int _Id;
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before setting.
+ /// </summary>
+ partial void SetId(int oldValue, ref int newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before returning.
+ /// </summary>
+ partial void GetId(ref int result);
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id
+ {
+ get
+ {
+ int value = _Id;
+ GetId(ref value);
+ return (_Id = value);
+ }
+ protected set
+ {
+ int oldValue = _Id;
+ SetId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Id = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Value
+ /// </summary>
+ protected double _Value;
+ /// <summary>
+ /// When provided in a partial class, allows value of Value to be changed before setting.
+ /// </summary>
+ partial void SetValue(double oldValue, ref double newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Value to be changed before returning.
+ /// </summary>
+ partial void GetValue(ref double result);
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [Required]
+ public double Value
+ {
+ get
+ {
+ double value = _Value;
+ GetValue(ref value);
+ return (_Value = value);
+ }
+ set
+ {
+ double oldValue = _Value;
+ SetValue(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Value = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Votes
+ /// </summary>
+ protected int? _Votes;
+ /// <summary>
+ /// When provided in a partial class, allows value of Votes to be changed before setting.
+ /// </summary>
+ partial void SetVotes(int? oldValue, ref int? newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Votes to be changed before returning.
+ /// </summary>
+ partial void GetVotes(ref int? result);
+
+ public int? Votes
+ {
+ get
+ {
+ int? value = _Votes;
+ GetVotes(ref value);
+ return (_Votes = value);
+ }
+ set
+ {
+ int? oldValue = _Votes;
+ SetVotes(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Votes = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ /// <summary>
+ /// If this is NULL it&apos;s the internal user rating.
+ /// </summary>
+ [ForeignKey("RatingSource_RatingType_Id")]
+ public virtual RatingSource RatingType { get; set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/RatingSource.cs b/Jellyfin.Data/Entities/RatingSource.cs
new file mode 100644
index 000000000..070f1ae27
--- /dev/null
+++ b/Jellyfin.Data/Entities/RatingSource.cs
@@ -0,0 +1,231 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ /// <summary>
+ /// This is the entity to store review ratings, not age ratings
+ /// </summary>
+ public partial class RatingSource
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected RatingSource()
+ {
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static RatingSource CreateRatingSourceUnsafe()
+ {
+ return new RatingSource();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="maximumvalue"></param>
+ /// <param name="minimumvalue"></param>
+ /// <param name="_rating0"></param>
+ public RatingSource(double maximumvalue, double minimumvalue, Rating _rating0)
+ {
+ this.MaximumValue = maximumvalue;
+
+ this.MinimumValue = minimumvalue;
+
+ if (_rating0 == null) throw new ArgumentNullException(nameof(_rating0));
+ _rating0.RatingType = this;
+
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="maximumvalue"></param>
+ /// <param name="minimumvalue"></param>
+ /// <param name="_rating0"></param>
+ public static RatingSource Create(double maximumvalue, double minimumvalue, Rating _rating0)
+ {
+ return new RatingSource(maximumvalue, minimumvalue, _rating0);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Id
+ /// </summary>
+ internal int _Id;
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before setting.
+ /// </summary>
+ partial void SetId(int oldValue, ref int newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before returning.
+ /// </summary>
+ partial void GetId(ref int result);
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id
+ {
+ get
+ {
+ int value = _Id;
+ GetId(ref value);
+ return (_Id = value);
+ }
+ protected set
+ {
+ int oldValue = _Id;
+ SetId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Id = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Name
+ /// </summary>
+ protected string _Name;
+ /// <summary>
+ /// When provided in a partial class, allows value of Name to be changed before setting.
+ /// </summary>
+ partial void SetName(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Name to be changed before returning.
+ /// </summary>
+ partial void GetName(ref string result);
+
+ /// <summary>
+ /// Max length = 1024
+ /// </summary>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string Name
+ {
+ get
+ {
+ string value = _Name;
+ GetName(ref value);
+ return (_Name = value);
+ }
+ set
+ {
+ string oldValue = _Name;
+ SetName(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Name = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for MaximumValue
+ /// </summary>
+ protected double _MaximumValue;
+ /// <summary>
+ /// When provided in a partial class, allows value of MaximumValue to be changed before setting.
+ /// </summary>
+ partial void SetMaximumValue(double oldValue, ref double newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of MaximumValue to be changed before returning.
+ /// </summary>
+ partial void GetMaximumValue(ref double result);
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [Required]
+ public double MaximumValue
+ {
+ get
+ {
+ double value = _MaximumValue;
+ GetMaximumValue(ref value);
+ return (_MaximumValue = value);
+ }
+ set
+ {
+ double oldValue = _MaximumValue;
+ SetMaximumValue(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _MaximumValue = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for MinimumValue
+ /// </summary>
+ protected double _MinimumValue;
+ /// <summary>
+ /// When provided in a partial class, allows value of MinimumValue to be changed before setting.
+ /// </summary>
+ partial void SetMinimumValue(double oldValue, ref double newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of MinimumValue to be changed before returning.
+ /// </summary>
+ partial void GetMinimumValue(ref double result);
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [Required]
+ public double MinimumValue
+ {
+ get
+ {
+ double value = _MinimumValue;
+ GetMinimumValue(ref value);
+ return (_MinimumValue = value);
+ }
+ set
+ {
+ double oldValue = _MinimumValue;
+ SetMinimumValue(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _MinimumValue = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+ [ForeignKey("MetadataProviderId_Source_Id")]
+ public virtual MetadataProviderId Source { get; set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/Release.cs b/Jellyfin.Data/Entities/Release.cs
new file mode 100644
index 000000000..d1928fcf7
--- /dev/null
+++ b/Jellyfin.Data/Entities/Release.cs
@@ -0,0 +1,188 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class Release
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected Release()
+ {
+ MediaFiles = new HashSet<MediaFile>();
+ Chapters = new HashSet<Chapter>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static Release CreateReleaseUnsafe()
+ {
+ return new Release();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="name"></param>
+ /// <param name="_movie0"></param>
+ /// <param name="_episode1"></param>
+ /// <param name="_track2"></param>
+ /// <param name="_customitem3"></param>
+ /// <param name="_book4"></param>
+ /// <param name="_photo5"></param>
+ public Release(string name, Movie _movie0, Episode _episode1, Track _track2, CustomItem _customitem3, Book _book4, Photo _photo5)
+ {
+ if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));
+ this.Name = name;
+
+ if (_movie0 == null) throw new ArgumentNullException(nameof(_movie0));
+ _movie0.Releases.Add(this);
+
+ if (_episode1 == null) throw new ArgumentNullException(nameof(_episode1));
+ _episode1.Releases.Add(this);
+
+ if (_track2 == null) throw new ArgumentNullException(nameof(_track2));
+ _track2.Releases.Add(this);
+
+ if (_customitem3 == null) throw new ArgumentNullException(nameof(_customitem3));
+ _customitem3.Releases.Add(this);
+
+ if (_book4 == null) throw new ArgumentNullException(nameof(_book4));
+ _book4.Releases.Add(this);
+
+ if (_photo5 == null) throw new ArgumentNullException(nameof(_photo5));
+ _photo5.Releases.Add(this);
+
+ this.MediaFiles = new HashSet<MediaFile>();
+ this.Chapters = new HashSet<Chapter>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="name"></param>
+ /// <param name="_movie0"></param>
+ /// <param name="_episode1"></param>
+ /// <param name="_track2"></param>
+ /// <param name="_customitem3"></param>
+ /// <param name="_book4"></param>
+ /// <param name="_photo5"></param>
+ public static Release Create(string name, Movie _movie0, Episode _episode1, Track _track2, CustomItem _customitem3, Book _book4, Photo _photo5)
+ {
+ return new Release(name, _movie0, _episode1, _track2, _customitem3, _book4, _photo5);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Id
+ /// </summary>
+ internal int _Id;
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before setting.
+ /// </summary>
+ partial void SetId(int oldValue, ref int newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Id to be changed before returning.
+ /// </summary>
+ partial void GetId(ref int result);
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id
+ {
+ get
+ {
+ int value = _Id;
+ GetId(ref value);
+ return (_Id = value);
+ }
+ protected set
+ {
+ int oldValue = _Id;
+ SetId(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Id = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Name
+ /// </summary>
+ protected string _Name;
+ /// <summary>
+ /// When provided in a partial class, allows value of Name to be changed before setting.
+ /// </summary>
+ partial void SetName(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Name to be changed before returning.
+ /// </summary>
+ partial void GetName(ref string result);
+
+ /// <summary>
+ /// Required, Max length = 1024
+ /// </summary>
+ [Required]
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string Name
+ {
+ get
+ {
+ string value = _Name;
+ GetName(ref value);
+ return (_Name = value);
+ }
+ set
+ {
+ string oldValue = _Name;
+ SetName(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Name = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+ [ForeignKey("MediaFile_MediaFiles_Id")]
+ public virtual ICollection<MediaFile> MediaFiles { get; protected set; }
+
+ [ForeignKey("Chapter_Chapters_Id")]
+ public virtual ICollection<Chapter> Chapters { get; protected set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/Season.cs b/Jellyfin.Data/Entities/Season.cs
new file mode 100644
index 000000000..96e89cde0
--- /dev/null
+++ b/Jellyfin.Data/Entities/Season.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class Season : LibraryItem
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected Season()
+ {
+ // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem.
+ // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
+
+ SeasonMetadata = new HashSet<SeasonMetadata>();
+ Episodes = new HashSet<Episode>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static Season CreateSeasonUnsafe()
+ {
+ return new Season();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+ /// <param name="_series0"></param>
+ public Season(Guid urlid, DateTime dateadded, Series _series0)
+ {
+ // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem.
+ // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
+
+ this.UrlId = urlid;
+
+ if (_series0 == null) throw new ArgumentNullException(nameof(_series0));
+ _series0.Seasons.Add(this);
+
+ this.SeasonMetadata = new HashSet<SeasonMetadata>();
+ this.Episodes = new HashSet<Episode>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+ /// <param name="_series0"></param>
+ public static Season Create(Guid urlid, DateTime dateadded, Series _series0)
+ {
+ return new Season(urlid, dateadded, _series0);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for SeasonNumber
+ /// </summary>
+ protected int? _SeasonNumber;
+ /// <summary>
+ /// When provided in a partial class, allows value of SeasonNumber to be changed before setting.
+ /// </summary>
+ partial void SetSeasonNumber(int? oldValue, ref int? newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of SeasonNumber to be changed before returning.
+ /// </summary>
+ partial void GetSeasonNumber(ref int? result);
+
+ public int? SeasonNumber
+ {
+ get
+ {
+ int? value = _SeasonNumber;
+ GetSeasonNumber(ref value);
+ return (_SeasonNumber = value);
+ }
+ set
+ {
+ int? oldValue = _SeasonNumber;
+ SetSeasonNumber(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _SeasonNumber = value;
+ }
+ }
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+ [ForeignKey("SeasonMetadata_SeasonMetadata_Id")]
+ public virtual ICollection<SeasonMetadata> SeasonMetadata { get; protected set; }
+
+ [ForeignKey("Episode_Episodes_Id")]
+ public virtual ICollection<Episode> Episodes { get; protected set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/SeasonMetadata.cs b/Jellyfin.Data/Entities/SeasonMetadata.cs
new file mode 100644
index 000000000..64ecbfbfa
--- /dev/null
+++ b/Jellyfin.Data/Entities/SeasonMetadata.cs
@@ -0,0 +1,106 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class SeasonMetadata : Metadata
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected SeasonMetadata()
+ {
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static SeasonMetadata CreateSeasonMetadataUnsafe()
+ {
+ return new SeasonMetadata();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="title">The title or name of the object</param>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="_season0"></param>
+ public SeasonMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Season _season0)
+ {
+ if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title));
+ this.Title = title;
+
+ if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language));
+ this.Language = language;
+
+ if (_season0 == null) throw new ArgumentNullException(nameof(_season0));
+ _season0.SeasonMetadata.Add(this);
+
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="title">The title or name of the object</param>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="_season0"></param>
+ public static SeasonMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Season _season0)
+ {
+ return new SeasonMetadata(title, language, dateadded, datemodified, _season0);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Outline
+ /// </summary>
+ protected string _Outline;
+ /// <summary>
+ /// When provided in a partial class, allows value of Outline to be changed before setting.
+ /// </summary>
+ partial void SetOutline(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Outline to be changed before returning.
+ /// </summary>
+ partial void GetOutline(ref string result);
+
+ /// <summary>
+ /// Max length = 1024
+ /// </summary>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string Outline
+ {
+ get
+ {
+ string value = _Outline;
+ GetOutline(ref value);
+ return (_Outline = value);
+ }
+ set
+ {
+ string oldValue = _Outline;
+ SetOutline(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Outline = value;
+ }
+ }
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/Series.cs b/Jellyfin.Data/Entities/Series.cs
new file mode 100644
index 000000000..097b9958e
--- /dev/null
+++ b/Jellyfin.Data/Entities/Series.cs
@@ -0,0 +1,167 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class Series : LibraryItem
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected Series()
+ {
+ SeriesMetadata = new HashSet<SeriesMetadata>();
+ Seasons = new HashSet<Season>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static Series CreateSeriesUnsafe()
+ {
+ return new Series();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+ public Series(Guid urlid, DateTime dateadded)
+ {
+ this.UrlId = urlid;
+
+ this.SeriesMetadata = new HashSet<SeriesMetadata>();
+ this.Seasons = new HashSet<Season>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+ public static Series Create(Guid urlid, DateTime dateadded)
+ {
+ return new Series(urlid, dateadded);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for AirsDayOfWeek
+ /// </summary>
+ protected Enums.Weekday? _AirsDayOfWeek;
+ /// <summary>
+ /// When provided in a partial class, allows value of AirsDayOfWeek to be changed before setting.
+ /// </summary>
+ partial void SetAirsDayOfWeek(Enums.Weekday? oldValue, ref Enums.Weekday? newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of AirsDayOfWeek to be changed before returning.
+ /// </summary>
+ partial void GetAirsDayOfWeek(ref Enums.Weekday? result);
+
+ public Enums.Weekday? AirsDayOfWeek
+ {
+ get
+ {
+ Enums.Weekday? value = _AirsDayOfWeek;
+ GetAirsDayOfWeek(ref value);
+ return (_AirsDayOfWeek = value);
+ }
+ set
+ {
+ Enums.Weekday? oldValue = _AirsDayOfWeek;
+ SetAirsDayOfWeek(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _AirsDayOfWeek = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for AirsTime
+ /// </summary>
+ protected DateTimeOffset? _AirsTime;
+ /// <summary>
+ /// When provided in a partial class, allows value of AirsTime to be changed before setting.
+ /// </summary>
+ partial void SetAirsTime(DateTimeOffset? oldValue, ref DateTimeOffset? newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of AirsTime to be changed before returning.
+ /// </summary>
+ partial void GetAirsTime(ref DateTimeOffset? result);
+
+ /// <summary>
+ /// The time the show airs, ignore the date portion
+ /// </summary>
+ public DateTimeOffset? AirsTime
+ {
+ get
+ {
+ DateTimeOffset? value = _AirsTime;
+ GetAirsTime(ref value);
+ return (_AirsTime = value);
+ }
+ set
+ {
+ DateTimeOffset? oldValue = _AirsTime;
+ SetAirsTime(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _AirsTime = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for FirstAired
+ /// </summary>
+ protected DateTimeOffset? _FirstAired;
+ /// <summary>
+ /// When provided in a partial class, allows value of FirstAired to be changed before setting.
+ /// </summary>
+ partial void SetFirstAired(DateTimeOffset? oldValue, ref DateTimeOffset? newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of FirstAired to be changed before returning.
+ /// </summary>
+ partial void GetFirstAired(ref DateTimeOffset? result);
+
+ public DateTimeOffset? FirstAired
+ {
+ get
+ {
+ DateTimeOffset? value = _FirstAired;
+ GetFirstAired(ref value);
+ return (_FirstAired = value);
+ }
+ set
+ {
+ DateTimeOffset? oldValue = _FirstAired;
+ SetFirstAired(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _FirstAired = value;
+ }
+ }
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+ [ForeignKey("SeriesMetadata_SeriesMetadata_Id")]
+ public virtual ICollection<SeriesMetadata> SeriesMetadata { get; protected set; }
+
+ [ForeignKey("Season_Seasons_Id")]
+ public virtual ICollection<Season> Seasons { get; protected set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/SeriesMetadata.cs b/Jellyfin.Data/Entities/SeriesMetadata.cs
new file mode 100644
index 000000000..52691783f
--- /dev/null
+++ b/Jellyfin.Data/Entities/SeriesMetadata.cs
@@ -0,0 +1,223 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class SeriesMetadata : Metadata
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected SeriesMetadata()
+ {
+ Networks = new HashSet<Company>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static SeriesMetadata CreateSeriesMetadataUnsafe()
+ {
+ return new SeriesMetadata();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="title">The title or name of the object</param>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="_series0"></param>
+ public SeriesMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Series _series0)
+ {
+ if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title));
+ this.Title = title;
+
+ if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language));
+ this.Language = language;
+
+ if (_series0 == null) throw new ArgumentNullException(nameof(_series0));
+ _series0.SeriesMetadata.Add(this);
+
+ this.Networks = new HashSet<Company>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="title">The title or name of the object</param>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="_series0"></param>
+ public static SeriesMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Series _series0)
+ {
+ return new SeriesMetadata(title, language, dateadded, datemodified, _series0);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for Outline
+ /// </summary>
+ protected string _Outline;
+ /// <summary>
+ /// When provided in a partial class, allows value of Outline to be changed before setting.
+ /// </summary>
+ partial void SetOutline(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Outline to be changed before returning.
+ /// </summary>
+ partial void GetOutline(ref string result);
+
+ /// <summary>
+ /// Max length = 1024
+ /// </summary>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string Outline
+ {
+ get
+ {
+ string value = _Outline;
+ GetOutline(ref value);
+ return (_Outline = value);
+ }
+ set
+ {
+ string oldValue = _Outline;
+ SetOutline(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Outline = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Plot
+ /// </summary>
+ protected string _Plot;
+ /// <summary>
+ /// When provided in a partial class, allows value of Plot to be changed before setting.
+ /// </summary>
+ partial void SetPlot(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Plot to be changed before returning.
+ /// </summary>
+ partial void GetPlot(ref string result);
+
+ /// <summary>
+ /// Max length = 65535
+ /// </summary>
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string Plot
+ {
+ get
+ {
+ string value = _Plot;
+ GetPlot(ref value);
+ return (_Plot = value);
+ }
+ set
+ {
+ string oldValue = _Plot;
+ SetPlot(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Plot = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Tagline
+ /// </summary>
+ protected string _Tagline;
+ /// <summary>
+ /// When provided in a partial class, allows value of Tagline to be changed before setting.
+ /// </summary>
+ partial void SetTagline(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Tagline to be changed before returning.
+ /// </summary>
+ partial void GetTagline(ref string result);
+
+ /// <summary>
+ /// Max length = 1024
+ /// </summary>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string Tagline
+ {
+ get
+ {
+ string value = _Tagline;
+ GetTagline(ref value);
+ return (_Tagline = value);
+ }
+ set
+ {
+ string oldValue = _Tagline;
+ SetTagline(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Tagline = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Backing field for Country
+ /// </summary>
+ protected string _Country;
+ /// <summary>
+ /// When provided in a partial class, allows value of Country to be changed before setting.
+ /// </summary>
+ partial void SetCountry(string oldValue, ref string newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of Country to be changed before returning.
+ /// </summary>
+ partial void GetCountry(ref string result);
+
+ /// <summary>
+ /// Max length = 2
+ /// </summary>
+ [MaxLength(2)]
+ [StringLength(2)]
+ public string Country
+ {
+ get
+ {
+ string value = _Country;
+ GetCountry(ref value);
+ return (_Country = value);
+ }
+ set
+ {
+ string oldValue = _Country;
+ SetCountry(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _Country = value;
+ }
+ }
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+ [ForeignKey("Company_Networks_Id")]
+ public virtual ICollection<Company> Networks { get; protected set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/Track.cs b/Jellyfin.Data/Entities/Track.cs
new file mode 100644
index 000000000..079d73d2b
--- /dev/null
+++ b/Jellyfin.Data/Entities/Track.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class Track : LibraryItem
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected Track()
+ {
+ // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem.
+ // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
+
+ Releases = new HashSet<Release>();
+ TrackMetadata = new HashSet<TrackMetadata>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static Track CreateTrackUnsafe()
+ {
+ return new Track();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+ /// <param name="_musicalbum0"></param>
+ public Track(Guid urlid, DateTime dateadded, MusicAlbum _musicalbum0)
+ {
+ // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem.
+ // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
+
+ this.UrlId = urlid;
+
+ if (_musicalbum0 == null) throw new ArgumentNullException(nameof(_musicalbum0));
+ _musicalbum0.Tracks.Add(this);
+
+ this.Releases = new HashSet<Release>();
+ this.TrackMetadata = new HashSet<TrackMetadata>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+ /// <param name="_musicalbum0"></param>
+ public static Track Create(Guid urlid, DateTime dateadded, MusicAlbum _musicalbum0)
+ {
+ return new Track(urlid, dateadded, _musicalbum0);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Backing field for TrackNumber
+ /// </summary>
+ protected int? _TrackNumber;
+ /// <summary>
+ /// When provided in a partial class, allows value of TrackNumber to be changed before setting.
+ /// </summary>
+ partial void SetTrackNumber(int? oldValue, ref int? newValue);
+ /// <summary>
+ /// When provided in a partial class, allows value of TrackNumber to be changed before returning.
+ /// </summary>
+ partial void GetTrackNumber(ref int? result);
+
+ public int? TrackNumber
+ {
+ get
+ {
+ int? value = _TrackNumber;
+ GetTrackNumber(ref value);
+ return (_TrackNumber = value);
+ }
+ set
+ {
+ int? oldValue = _TrackNumber;
+ SetTrackNumber(oldValue, ref value);
+ if (oldValue != value)
+ {
+ _TrackNumber = value;
+ }
+ }
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ [ForeignKey("Release_Releases_Id")]
+ public virtual ICollection<Release> Releases { get; protected set; }
+
+ [ForeignKey("TrackMetadata_TrackMetadata_Id")]
+ public virtual ICollection<TrackMetadata> TrackMetadata { get; protected set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/TrackMetadata.cs b/Jellyfin.Data/Entities/TrackMetadata.cs
new file mode 100644
index 000000000..86c9161f6
--- /dev/null
+++ b/Jellyfin.Data/Entities/TrackMetadata.cs
@@ -0,0 +1,68 @@
+using System;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class TrackMetadata : Metadata
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected TrackMetadata()
+ {
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static TrackMetadata CreateTrackMetadataUnsafe()
+ {
+ return new TrackMetadata();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="title">The title or name of the object</param>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="_track0"></param>
+ public TrackMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Track _track0)
+ {
+ if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title));
+ this.Title = title;
+
+ if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language));
+ this.Language = language;
+
+ if (_track0 == null) throw new ArgumentNullException(nameof(_track0));
+ _track0.TrackMetadata.Add(this);
+
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="title">The title or name of the object</param>
+ /// <param name="language">ISO-639-3 3-character language codes</param>
+ /// <param name="_track0"></param>
+ public static TrackMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Track _track0)
+ {
+ return new TrackMetadata(title, language, dateadded, datemodified, _track0);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+
+ }
+}
+
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
new file mode 100644
index 000000000..a81d5215b
--- /dev/null
+++ b/Jellyfin.Data/Entities/User.cs
@@ -0,0 +1,235 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+ public partial class User
+ {
+ partial void Init();
+
+ /// <summary>
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected User()
+ {
+ Groups = new HashSet<Group>();
+ Permissions = new HashSet<Permission>();
+ ProviderMappings = new HashSet<ProviderMapping>();
+ Preferences = new HashSet<Preference>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+ /// </summary>
+ public static User CreateUserUnsafe()
+ {
+ return new User();
+ }
+
+ /// <summary>
+ /// Public constructor with required data
+ /// </summary>
+ /// <param name="username"></param>
+ /// <param name="mustupdatepassword"></param>
+ /// <param name="audiolanguagepreference"></param>
+ /// <param name="authenticationproviderid"></param>
+ /// <param name="invalidloginattemptcount"></param>
+ /// <param name="subtitlemode"></param>
+ /// <param name="playdefaultaudiotrack"></param>
+ public User(string username, bool mustupdatepassword, string audiolanguagepreference, string authenticationproviderid, int invalidloginattemptcount, string subtitlemode, bool playdefaultaudiotrack)
+ {
+ if (string.IsNullOrEmpty(username)) throw new ArgumentNullException(nameof(username));
+ this.Username = username;
+
+ this.MustUpdatePassword = mustupdatepassword;
+
+ if (string.IsNullOrEmpty(audiolanguagepreference)) throw new ArgumentNullException(nameof(audiolanguagepreference));
+ this.AudioLanguagePreference = audiolanguagepreference;
+
+ if (string.IsNullOrEmpty(authenticationproviderid)) throw new ArgumentNullException(nameof(authenticationproviderid));
+ this.AuthenticationProviderId = authenticationproviderid;
+
+ this.InvalidLoginAttemptCount = invalidloginattemptcount;
+
+ if (string.IsNullOrEmpty(subtitlemode)) throw new ArgumentNullException(nameof(subtitlemode));
+ this.SubtitleMode = subtitlemode;
+
+ this.PlayDefaultAudioTrack = playdefaultaudiotrack;
+
+ this.Groups = new HashSet<Group>();
+ this.Permissions = new HashSet<Permission>();
+ this.ProviderMappings = new HashSet<ProviderMapping>();
+ this.Preferences = new HashSet<Preference>();
+
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="username"></param>
+ /// <param name="mustupdatepassword"></param>
+ /// <param name="audiolanguagepreference"></param>
+ /// <param name="authenticationproviderid"></param>
+ /// <param name="invalidloginattemptcount"></param>
+ /// <param name="subtitlemode"></param>
+ /// <param name="playdefaultaudiotrack"></param>
+ public static User Create(string username, bool mustupdatepassword, string audiolanguagepreference, string authenticationproviderid, int invalidloginattemptcount, string subtitlemode, bool playdefaultaudiotrack)
+ {
+ return new User(username, mustupdatepassword, audiolanguagepreference, authenticationproviderid, invalidloginattemptcount, subtitlemode, playdefaultaudiotrack);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Identity, Indexed, Required
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; protected set; }
+
+ /// <summary>
+ /// Required, Max length = 255
+ /// </summary>
+ [Required]
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string Username { get; set; }
+
+ /// <summary>
+ /// Max length = 65535
+ /// </summary>
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string Password { get; set; }
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [Required]
+ public bool MustUpdatePassword { get; set; }
+
+ /// <summary>
+ /// Required, Max length = 255
+ /// </summary>
+ [Required]
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string AudioLanguagePreference { get; set; }
+
+ /// <summary>
+ /// Required, Max length = 255
+ /// </summary>
+ [Required]
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string AuthenticationProviderId { get; set; }
+
+ /// <summary>
+ /// Max length = 65535
+ /// </summary>
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string GroupedFolders { get; set; }
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [Required]
+ public int InvalidLoginAttemptCount { get; set; }
+
+ /// <summary>
+ /// Max length = 65535
+ /// </summary>
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string LatestItemExcludes { get; set; }
+
+ public int? LoginAttemptsBeforeLockout { get; set; }
+
+ /// <summary>
+ /// Max length = 65535
+ /// </summary>
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string MyMediaExcludes { get; set; }
+
+ /// <summary>
+ /// Max length = 65535
+ /// </summary>
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string OrderedViews { get; set; }
+
+ /// <summary>
+ /// Required, Max length = 255
+ /// </summary>
+ [Required]
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string SubtitleMode { get; set; }
+
+ /// <summary>
+ /// Required
+ /// </summary>
+ [Required]
+ public bool PlayDefaultAudioTrack { get; set; }
+
+ /// <summary>
+ /// Max length = 255
+ /// </summary>
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string SubtitleLanguagePrefernce { get; set; }
+
+ public bool? DisplayMissingEpisodes { get; set; }
+
+ public bool? DisplayCollectionsView { get; set; }
+
+ public bool? HidePlayedInLatest { get; set; }
+
+ public bool? RememberAudioSelections { get; set; }
+
+ public bool? RememberSubtitleSelections { get; set; }
+
+ public bool? EnableNextEpisodeAutoPlay { get; set; }
+
+ public bool? EnableUserPreferenceAccess { get; set; }
+
+ /// <summary>
+ /// Required, ConcurrenyToken
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+
+ /*************************************************************************
+ * Navigation properties
+ *************************************************************************/
+ [ForeignKey("Group_Groups_Id")]
+ public virtual ICollection<Group> Groups { get; protected set; }
+
+ [ForeignKey("Permission_Permissions_Id")]
+ public virtual ICollection<Permission> Permissions { get; protected set; }
+
+ [ForeignKey("ProviderMapping_ProviderMappings_Id")]
+ public virtual ICollection<ProviderMapping> ProviderMappings { get; protected set; }
+
+ [ForeignKey("Preference_Preferences_Id")]
+ public virtual ICollection<Preference> Preferences { get; protected set; }
+
+ }
+}
+
diff --git a/Jellyfin.Data/Enums/ArtKind.cs b/Jellyfin.Data/Enums/ArtKind.cs
new file mode 100644
index 000000000..6b69d68b2
--- /dev/null
+++ b/Jellyfin.Data/Enums/ArtKind.cs
@@ -0,0 +1,11 @@
+namespace Jellyfin.Data.Enums
+{
+ public enum ArtKind
+ {
+ Other,
+ Poster,
+ Banner,
+ Thumbnail,
+ Logo
+ }
+}
diff --git a/Jellyfin.Data/Enums/MediaFileKind.cs b/Jellyfin.Data/Enums/MediaFileKind.cs
new file mode 100644
index 000000000..12f48c558
--- /dev/null
+++ b/Jellyfin.Data/Enums/MediaFileKind.cs
@@ -0,0 +1,11 @@
+namespace Jellyfin.Data.Enums
+{
+ public enum MediaFileKind
+ {
+ Main,
+ Sidecar,
+ AdditionalPart,
+ AlternativeFormat,
+ AdditionalStream
+ }
+}
diff --git a/Jellyfin.Data/Enums/PermissionKind.cs b/Jellyfin.Data/Enums/PermissionKind.cs
new file mode 100644
index 000000000..1506471e8
--- /dev/null
+++ b/Jellyfin.Data/Enums/PermissionKind.cs
@@ -0,0 +1,26 @@
+namespace Jellyfin.Data.Enums
+{
+ public enum PermissionKind
+ {
+ IsAdministrator,
+ IsHidden,
+ IsDisabled,
+ BlockUnrateditems,
+ EnbleSharedDeviceControl,
+ EnableRemoteAccess,
+ EnableLiveTvManagement,
+ EnableLiveTvAccess,
+ EnableMediaPlayback,
+ EnableAudioPlaybackTranscoding,
+ EnableVideoPlaybackTranscoding,
+ EnableContentDeletion,
+ EnableContentDownloading,
+ EnableSyncTranscoding,
+ EnableMediaConversion,
+ EnableAllDevices,
+ EnableAllChannels,
+ EnableAllFolders,
+ EnablePublicSharing,
+ AccessSchedules
+ }
+}
diff --git a/Jellyfin.Data/Enums/PersonRoleType.cs b/Jellyfin.Data/Enums/PersonRoleType.cs
new file mode 100644
index 000000000..6e52f2c85
--- /dev/null
+++ b/Jellyfin.Data/Enums/PersonRoleType.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Data.Enums
+{
+ public enum PersonRoleType
+ {
+ Other,
+ Director,
+ Artist,
+ OriginalArtist,
+ Actor,
+ VoiceActor,
+ Producer,
+ Remixer,
+ Conductor,
+ Composer,
+ Author,
+ Editor
+ }
+}
diff --git a/Jellyfin.Data/Enums/PreferenceKind.cs b/Jellyfin.Data/Enums/PreferenceKind.cs
new file mode 100644
index 000000000..cd2cb791a
--- /dev/null
+++ b/Jellyfin.Data/Enums/PreferenceKind.cs
@@ -0,0 +1,13 @@
+namespace Jellyfin.Data.Enums
+{
+ public enum PreferenceKind
+ {
+ MaxParentalRating,
+ BlockedTags,
+ RemoteClientBitrateLimit,
+ EnabledDevices,
+ EnabledChannels,
+ EnabledFolders,
+ EnableContentDeletionFromFolders
+ }
+}
diff --git a/Jellyfin.Data/Enums/Weekday.cs b/Jellyfin.Data/Enums/Weekday.cs
new file mode 100644
index 000000000..b80a03a33
--- /dev/null
+++ b/Jellyfin.Data/Enums/Weekday.cs
@@ -0,0 +1,13 @@
+namespace Jellyfin.Data.Enums
+{
+ public enum Weekday
+ {
+ Sunday,
+ Monday,
+ Tuesday,
+ Wednesday,
+ Thursday,
+ Friday,
+ Saturday
+ }
+}
diff --git a/Jellyfin.Data/ISavingChanges.cs b/Jellyfin.Data/ISavingChanges.cs
new file mode 100644
index 000000000..f392dae6a
--- /dev/null
+++ b/Jellyfin.Data/ISavingChanges.cs
@@ -0,0 +1,9 @@
+#pragma warning disable CS1591
+
+namespace Jellyfin.Data
+{
+ public interface ISavingChanges
+ {
+ void OnSavingChanges();
+ }
+}
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
new file mode 100644
index 000000000..73ea593b0
--- /dev/null
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="2.2.4" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.4" />
+ </ItemGroup>
+
+</Project>
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
new file mode 100644
index 000000000..a31f28f64
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -0,0 +1,34 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp3.1</TargetFramework>
+ <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+
+ <!-- Code analysers-->
+ <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Compile Include="..\SharedVersion.cs" />
+ <Compile Remove="Migrations\20200430214405_InitialSchema.cs" />
+ <Compile Remove="Migrations\20200430214405_InitialSchema.Designer.cs" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Jellyfin.Data\Jellyfin.Data.csproj" />
+ <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
+ <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs
new file mode 100644
index 000000000..76343edf9
--- /dev/null
+++ b/Jellyfin.Server.Implementations/JellyfinDb.cs
@@ -0,0 +1,115 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1201 // Constuctors should not follow properties
+#pragma warning disable SA1516 // Elements should be followed by a blank line
+#pragma warning disable SA1623 // Property's documentation should begin with gets or sets
+#pragma warning disable SA1629 // Documentation should end with a period
+#pragma warning disable SA1648 // Inheritdoc should be used with inheriting class
+
+using System.Linq;
+using Jellyfin.Data;
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Server.Implementations
+{
+ /// <inheritdoc/>
+ public partial class JellyfinDb : DbContext
+ {
+ /*public virtual DbSet<Artwork> Artwork { get; set; }
+ public virtual DbSet<Book> Books { get; set; }
+ public virtual DbSet<BookMetadata> BookMetadata { get; set; }
+ public virtual DbSet<Chapter> Chapters { get; set; }
+ public virtual DbSet<Collection> Collections { get; set; }
+ public virtual DbSet<CollectionItem> CollectionItems { get; set; }
+ public virtual DbSet<Company> Companies { get; set; }
+ public virtual DbSet<CompanyMetadata> CompanyMetadata { get; set; }
+ public virtual DbSet<CustomItem> CustomItems { get; set; }
+ public virtual DbSet<CustomItemMetadata> CustomItemMetadata { get; set; }
+ public virtual DbSet<Episode> Episodes { get; set; }
+ public virtual DbSet<EpisodeMetadata> EpisodeMetadata { get; set; }
+ public virtual DbSet<Genre> Genres { get; set; }
+ public virtual DbSet<Group> Groups { get; set; }
+ public virtual DbSet<Library> Libraries { get; set; }
+ public virtual DbSet<LibraryItem> LibraryItems { get; set; }
+ public virtual DbSet<LibraryRoot> LibraryRoot { get; set; }
+ public virtual DbSet<MediaFile> MediaFiles { get; set; }
+ public virtual DbSet<MediaFileStream> MediaFileStream { get; set; }
+ public virtual DbSet<Metadata> Metadata { get; set; }
+ public virtual DbSet<MetadataProvider> MetadataProviders { get; set; }
+ public virtual DbSet<MetadataProviderId> MetadataProviderIds { get; set; }
+ public virtual DbSet<Movie> Movies { get; set; }
+ public virtual DbSet<MovieMetadata> MovieMetadata { get; set; }
+ public virtual DbSet<MusicAlbum> MusicAlbums { get; set; }
+ public virtual DbSet<MusicAlbumMetadata> MusicAlbumMetadata { get; set; }
+ public virtual DbSet<Permission> Permissions { get; set; }
+ public virtual DbSet<Person> People { get; set; }
+ public virtual DbSet<PersonRole> PersonRoles { get; set; }
+ public virtual DbSet<Photo> Photo { get; set; }
+ public virtual DbSet<PhotoMetadata> PhotoMetadata { get; set; }
+ public virtual DbSet<Preference> Preferences { get; set; }
+ public virtual DbSet<ProviderMapping> ProviderMappings { get; set; }
+ public virtual DbSet<Rating> Ratings { get; set; }
+ /// <summary>
+ /// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to
+ /// store review ratings, not age ratings
+ /// </summary>
+ public virtual DbSet<RatingSource> RatingSources { get; set; }
+ public virtual DbSet<Release> Releases { get; set; }
+ public virtual DbSet<Season> Seasons { get; set; }
+ public virtual DbSet<SeasonMetadata> SeasonMetadata { get; set; }
+ public virtual DbSet<Series> Series { get; set; }
+ public virtual DbSet<SeriesMetadata> SeriesMetadata { get; set; }
+ public virtual DbSet<Track> Tracks { get; set; }
+ public virtual DbSet<TrackMetadata> TrackMetadata { get; set; }
+ public virtual DbSet<User> Users { get; set; } */
+
+ /// <summary>
+ /// Gets or sets the default connection string.
+ /// </summary>
+ public static string ConnectionString { get; set; } = @"Data Source=jellyfin.db";
+
+ /// <inheritdoc />
+ public JellyfinDb(DbContextOptions<JellyfinDb> options) : base(options)
+ {
+ }
+
+ partial void CustomInit(DbContextOptionsBuilder optionsBuilder);
+
+ /// <inheritdoc />
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ CustomInit(optionsBuilder);
+ }
+
+ partial void OnModelCreatingImpl(ModelBuilder modelBuilder);
+ partial void OnModelCreatedImpl(ModelBuilder modelBuilder);
+
+ /// <inheritdoc />
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ base.OnModelCreating(modelBuilder);
+ OnModelCreatingImpl(modelBuilder);
+
+ modelBuilder.HasDefaultSchema("jellyfin");
+
+ /*modelBuilder.Entity<Artwork>().HasIndex(t => t.Kind);
+ modelBuilder.Entity<Genre>().HasIndex(t => t.Name)
+ .IsUnique();
+ modelBuilder.Entity<LibraryItem>().HasIndex(t => t.UrlId)
+ .IsUnique();*/
+
+ OnModelCreatedImpl(modelBuilder);
+ }
+
+ public override int SaveChanges()
+ {
+ foreach (var entity in ChangeTracker.Entries().Where(e => e.State == EntityState.Modified))
+ {
+ var saveEntity = entity.Entity as ISavingChanges;
+ saveEntity.OnSavingChanges();
+ }
+
+ return base.SaveChanges();
+ }
+ }
+}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 270cdeaaf..88114d999 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -43,6 +43,8 @@
<PackageReference Include="CommandLineParser" Version="2.7.82" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.3" />
+ <PackageReference Include="prometheus-net" Version="3.5.0" />
+ <PackageReference Include="prometheus-net.AspNetCore" Version="3.5.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
diff --git a/Jellyfin.Server/Migrations/IMigrationRoutine.cs b/Jellyfin.Server/Migrations/IMigrationRoutine.cs
index eab995d67..b79fdeac0 100644
--- a/Jellyfin.Server/Migrations/IMigrationRoutine.cs
+++ b/Jellyfin.Server/Migrations/IMigrationRoutine.cs
@@ -21,8 +21,6 @@ namespace Jellyfin.Server.Migrations
/// <summary>
/// Execute the migration routine.
/// </summary>
- /// <param name="host">Host that hosts current version.</param>
- /// <param name="logger">Host logger.</param>
- public void Perform(CoreAppHost host, ILogger logger);
+ public void Perform();
}
}
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index b5ea04dca..ca1748282 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using MediaBrowser.Common.Configuration;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations
@@ -13,10 +14,10 @@ namespace Jellyfin.Server.Migrations
/// <summary>
/// The list of known migrations, in order of applicability.
/// </summary>
- internal static readonly IMigrationRoutine[] Migrations =
+ private static readonly Type[] _migrationTypes =
{
- new Routines.DisableTranscodingThrottling(),
- new Routines.CreateUserLoggingConfigFile()
+ typeof(Routines.DisableTranscodingThrottling),
+ typeof(Routines.CreateUserLoggingConfigFile)
};
/// <summary>
@@ -27,6 +28,10 @@ namespace Jellyfin.Server.Migrations
public static void Run(CoreAppHost host, ILoggerFactory loggerFactory)
{
var logger = loggerFactory.CreateLogger<MigrationRunner>();
+ var migrations = _migrationTypes
+ .Select(m => ActivatorUtilities.CreateInstance(host.ServiceProvider, m))
+ .OfType<IMigrationRoutine>()
+ .ToArray();
var migrationOptions = ((IConfigurationManager)host.ServerConfigurationManager).GetConfiguration<MigrationOptions>(MigrationsListStore.StoreKey);
if (!host.ServerConfigurationManager.Configuration.IsStartupWizardCompleted && migrationOptions.Applied.Count == 0)
@@ -34,16 +39,16 @@ namespace Jellyfin.Server.Migrations
// If startup wizard is not finished, this is a fresh install.
// Don't run any migrations, just mark all of them as applied.
logger.LogInformation("Marking all known migrations as applied because this is a fresh install");
- migrationOptions.Applied.AddRange(Migrations.Select(m => (m.Id, m.Name)));
+ migrationOptions.Applied.AddRange(migrations.Select(m => (m.Id, m.Name)));
host.ServerConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions);
return;
}
var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet();
- for (var i = 0; i < Migrations.Length; i++)
+ for (var i = 0; i < migrations.Length; i++)
{
- var migrationRoutine = Migrations[i];
+ var migrationRoutine = migrations[i];
if (appliedMigrationIds.Contains(migrationRoutine.Id))
{
logger.LogDebug("Skipping migration '{Name}' since it is already applied", migrationRoutine.Name);
@@ -54,7 +59,7 @@ namespace Jellyfin.Server.Migrations
try
{
- migrationRoutine.Perform(host, logger);
+ migrationRoutine.Perform();
}
catch (Exception ex)
{
diff --git a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
index 3bc32c047..89514c89b 100644
--- a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
+++ b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
@@ -36,6 +36,13 @@ namespace Jellyfin.Server.Migrations.Routines
@"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}:{Message}{NewLine}{Exception}""}}]}}],""Enrich"":[""FromLogContext"",""WithThreadId""]}}",
};
+ private readonly IApplicationPaths _appPaths;
+
+ public CreateUserLoggingConfigFile(IApplicationPaths appPaths)
+ {
+ _appPaths = appPaths;
+ }
+
/// <inheritdoc/>
public Guid Id => Guid.Parse("{EF103419-8451-40D8-9F34-D1A8E93A1679}");
@@ -43,9 +50,9 @@ namespace Jellyfin.Server.Migrations.Routines
public string Name => "CreateLoggingConfigHeirarchy";
/// <inheritdoc/>
- public void Perform(CoreAppHost host, ILogger logger)
+ public void Perform()
{
- var logDirectory = host.Resolve<IApplicationPaths>().ConfigurationDirectoryPath;
+ var logDirectory = _appPaths.ConfigurationDirectoryPath;
var existingConfigPath = Path.Combine(logDirectory, "logging.json");
// If the existing logging.json config file is unmodified, then 'reset' it by moving it to 'logging.old.json'
diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
index 6f8e4a8ff..b2e957d5b 100644
--- a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
+++ b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
@@ -10,6 +10,15 @@ namespace Jellyfin.Server.Migrations.Routines
/// </summary>
internal class DisableTranscodingThrottling : IMigrationRoutine
{
+ private readonly ILogger _logger;
+ private readonly IConfigurationManager _configManager;
+
+ public DisableTranscodingThrottling(ILogger<DisableTranscodingThrottling> logger, IConfigurationManager configManager)
+ {
+ _logger = logger;
+ _configManager = configManager;
+ }
+
/// <inheritdoc/>
public Guid Id => Guid.Parse("{4124C2CD-E939-4FFB-9BE9-9B311C413638}");
@@ -17,16 +26,16 @@ namespace Jellyfin.Server.Migrations.Routines
public string Name => "DisableTranscodingThrottling";
/// <inheritdoc/>
- public void Perform(CoreAppHost host, ILogger logger)
+ public void Perform()
{
// Set EnableThrottling to false since it wasn't used before and may introduce issues
- var encoding = ((IConfigurationManager)host.ServerConfigurationManager).GetConfiguration<EncodingOptions>("encoding");
+ var encoding = _configManager.GetConfiguration<EncodingOptions>("encoding");
if (encoding.EnableThrottling)
{
- logger.LogInformation("Disabling transcoding throttling during migration");
+ _logger.LogInformation("Disabling transcoding throttling during migration");
encoding.EnableThrottling = false;
- host.ServerConfigurationManager.SaveConfiguration("encoding", encoding);
+ _configManager.SaveConfiguration("encoding", encoding);
}
}
}
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 9635cc6ec..b9895386f 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -10,14 +10,11 @@ using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using CommandLine;
-using Emby.Drawing;
using Emby.Server.Implementations;
using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Networking;
-using Jellyfin.Drawing.Skia;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.WebDashboard.Api;
using Microsoft.AspNetCore.Hosting;
@@ -161,23 +158,7 @@ namespace Jellyfin.Server
ApplicationHost.LogEnvironmentInfo(_logger, appPaths);
- // Make sure we have all the code pages we can get
- // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks
- Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
-
- // Increase the max http request limit
- // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others.
- ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit);
-
- // Disable the "Expect: 100-Continue" header by default
- // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
- ServicePointManager.Expect100Continue = false;
-
- Batteries_V2.Init();
- if (raw.sqlite3_enable_shared_cache(1) != raw.SQLITE_OK)
- {
- _logger.LogWarning("Failed to enable shared cache for SQLite");
- }
+ PerformStaticInitialization();
var appHost = new CoreAppHost(
appPaths,
@@ -205,7 +186,7 @@ namespace Jellyfin.Server
ServiceCollection serviceCollection = new ServiceCollection();
appHost.Init(serviceCollection);
- var webHost = CreateWebHostBuilder(appHost, serviceCollection, options, startupConfig, appPaths).Build();
+ var webHost = new WebHostBuilder().ConfigureWebHostBuilder(appHost, serviceCollection, options, startupConfig, appPaths).Build();
// Re-use the web host service provider in the app host since ASP.NET doesn't allow a custom service collection.
appHost.ServiceProvider = webHost.Services;
@@ -250,14 +231,49 @@ namespace Jellyfin.Server
}
}
- private static IWebHostBuilder CreateWebHostBuilder(
+ /// <summary>
+ /// Call static initialization methods for the application.
+ /// </summary>
+ public static void PerformStaticInitialization()
+ {
+ // Make sure we have all the code pages we can get
+ // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks
+ Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+
+ // Increase the max http request limit
+ // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others.
+ ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit);
+
+ // Disable the "Expect: 100-Continue" header by default
+ // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
+ ServicePointManager.Expect100Continue = false;
+
+ Batteries_V2.Init();
+ if (raw.sqlite3_enable_shared_cache(1) != raw.SQLITE_OK)
+ {
+ _logger.LogWarning("Failed to enable shared cache for SQLite");
+ }
+ }
+
+ /// <summary>
+ /// Configure the web host builder.
+ /// </summary>
+ /// <param name="builder">The builder to configure.</param>
+ /// <param name="appHost">The application host.</param>
+ /// <param name="serviceCollection">The application service collection.</param>
+ /// <param name="commandLineOpts">The command line options passed to the application.</param>
+ /// <param name="startupConfig">The application configuration.</param>
+ /// <param name="appPaths">The application paths.</param>
+ /// <returns>The configured web host builder.</returns>
+ public static IWebHostBuilder ConfigureWebHostBuilder(
+ this IWebHostBuilder builder,
ApplicationHost appHost,
IServiceCollection serviceCollection,
StartupOptions commandLineOpts,
IConfiguration startupConfig,
IApplicationPaths appPaths)
{
- return new WebHostBuilder()
+ return builder
.UseKestrel((builderContext, options) =>
{
var addresses = appHost.ServerConfigurationManager
@@ -278,8 +294,7 @@ namespace Jellyfin.Server
{
_logger.LogInformation("Kestrel listening on {IpAddress}", address);
options.Listen(address, appHost.HttpPort);
-
- if (appHost.EnableHttps && appHost.Certificate != null)
+ if (appHost.ListenWithHttps)
{
options.Listen(address, appHost.HttpsPort, listenOptions =>
{
@@ -289,11 +304,18 @@ namespace Jellyfin.Server
}
else if (builderContext.HostingEnvironment.IsDevelopment())
{
- options.Listen(address, appHost.HttpsPort, listenOptions =>
+ try
{
- listenOptions.UseHttps();
- listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
- });
+ options.Listen(address, appHost.HttpsPort, listenOptions =>
+ {
+ listenOptions.UseHttps();
+ listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
+ });
+ }
+ catch (InvalidOperationException ex)
+ {
+ _logger.LogError(ex, "Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted.");
+ }
}
}
}
@@ -302,7 +324,7 @@ namespace Jellyfin.Server
_logger.LogInformation("Kestrel listening on all interfaces");
options.ListenAnyIP(appHost.HttpPort);
- if (appHost.EnableHttps && appHost.Certificate != null)
+ if (appHost.ListenWithHttps)
{
options.ListenAnyIP(appHost.HttpsPort, listenOptions =>
{
@@ -312,11 +334,18 @@ namespace Jellyfin.Server
}
else if (builderContext.HostingEnvironment.IsDevelopment())
{
- options.ListenAnyIP(appHost.HttpsPort, listenOptions =>
+ try
{
- listenOptions.UseHttps();
- listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
- });
+ options.ListenAnyIP(appHost.HttpsPort, listenOptions =>
+ {
+ listenOptions.UseHttps();
+ listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
+ });
+ }
+ catch (InvalidOperationException ex)
+ {
+ _logger.LogError(ex, "Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted.");
+ }
}
}
})
@@ -496,7 +525,9 @@ namespace Jellyfin.Server
/// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist
/// already.
/// </summary>
- private static async Task InitLoggingConfigFile(IApplicationPaths appPaths)
+ /// <param name="appPaths">The application paths.</param>
+ /// <returns>A task representing the creation of the configuration file, or a completed task if the file already exists.</returns>
+ public static async Task InitLoggingConfigFile(IApplicationPaths appPaths)
{
// Do nothing if the config file already exists
string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, LoggingConfigFileDefault);
@@ -516,7 +547,13 @@ namespace Jellyfin.Server
await resource.CopyToAsync(dst).ConfigureAwait(false);
}
- private static IConfiguration CreateAppConfiguration(StartupOptions commandLineOpts, IApplicationPaths appPaths)
+ /// <summary>
+ /// Create the application configuration.
+ /// </summary>
+ /// <param name="commandLineOpts">The command line options passed to the program.</param>
+ /// <param name="appPaths">The application paths.</param>
+ /// <returns>The application configuration.</returns>
+ public static IConfiguration CreateAppConfiguration(StartupOptions commandLineOpts, IApplicationPaths appPaths)
{
return new ConfigurationBuilder()
.ConfigureAppConfiguration(commandLineOpts, appPaths)
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 4d7d56e9d..5f9a5c161 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
+using Prometheus;
namespace Jellyfin.Server
{
@@ -63,15 +64,24 @@ namespace Jellyfin.Server
app.UseResponseCompression();
// TODO app.UseMiddleware<WebSocketMiddleware>();
- app.Use(serverApplicationHost.ExecuteWebsocketHandlerAsync);
// TODO use when old API is removed: app.UseAuthentication();
app.UseJellyfinApiSwagger();
app.UseRouting();
app.UseAuthorization();
+ if (_serverConfigurationManager.Configuration.EnableMetrics)
+ {
+ // Must be registered after any middleware that could chagne HTTP response codes or the data will be bad
+ app.UseHttpMetrics();
+ }
+
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
+ if (_serverConfigurationManager.Configuration.EnableMetrics)
+ {
+ endpoints.MapMetrics(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/') + "/metrics");
+ }
});
app.Use(serverApplicationHost.ExecuteHttpHandlerAsync);
diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs
index 1a1d86362..2cd68ac1b 100644
--- a/MediaBrowser.Api/BaseApiService.cs
+++ b/MediaBrowser.Api/BaseApiService.cs
@@ -21,7 +21,7 @@ namespace MediaBrowser.Api
public abstract class BaseApiService : IService, IRequiresRequest
{
public BaseApiService(
- ILogger logger,
+ ILogger<BaseApiService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory)
{
@@ -34,7 +34,7 @@ namespace MediaBrowser.Api
/// Gets the logger.
/// </summary>
/// <value>The logger.</value>
- protected ILogger Logger { get; }
+ protected ILogger<BaseApiService> Logger { get; }
/// <summary>
/// Gets or sets the server configuration manager.
diff --git a/MediaBrowser.Api/Images/RemoteImageService.cs b/MediaBrowser.Api/Images/RemoteImageService.cs
index 222bb34d3..23bf54712 100644
--- a/MediaBrowser.Api/Images/RemoteImageService.cs
+++ b/MediaBrowser.Api/Images/RemoteImageService.cs
@@ -265,17 +265,20 @@ namespace MediaBrowser.Api.Images
{
Url = url,
BufferContent = false
-
}).ConfigureAwait(false);
- var ext = result.ContentType.Split('/').Last();
+ var ext = result.ContentType.Split('/')[^1];
var fullCachePath = GetFullCachePath(urlHash + "." + ext);
Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
- using (var stream = result.Content)
+ var stream = result.Content;
+ await using (stream.ConfigureAwait(false))
{
- using var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
- await stream.CopyToAsync(filestream).ConfigureAwait(false);
+ var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+ await using (filestream.ConfigureAwait(false))
+ {
+ await stream.CopyToAsync(filestream).ConfigureAwait(false);
+ }
}
Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
diff --git a/MediaBrowser.Api/ItemLookupService.cs b/MediaBrowser.Api/ItemLookupService.cs
index 0bbe7e1cf..68e3dfa59 100644
--- a/MediaBrowser.Api/ItemLookupService.cs
+++ b/MediaBrowser.Api/ItemLookupService.cs
@@ -299,22 +299,26 @@ namespace MediaBrowser.Api
{
var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
- var ext = result.ContentType.Split('/').Last();
+ var ext = result.ContentType.Split('/')[^1];
var fullCachePath = GetFullCachePath(urlHash + "." + ext);
Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
- using (var stream = result.Content)
+ var stream = result.Content;
+
+ await using (stream.ConfigureAwait(false))
{
- using var fileStream = new FileStream(
+ var fileStream = new FileStream(
fullCachePath,
FileMode.Create,
FileAccess.Write,
FileShare.Read,
IODefaults.FileStreamBufferSize,
true);
-
- await stream.CopyToAsync(fileStream).ConfigureAwait(false);
+ await using (fileStream.ConfigureAwait(false))
+ {
+ await stream.CopyToAsync(fileStream).ConfigureAwait(false);
+ }
}
Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs
index a54640b2f..2d1977d2e 100644
--- a/MediaBrowser.Api/Library/LibraryService.cs
+++ b/MediaBrowser.Api/Library/LibraryService.cs
@@ -319,11 +319,14 @@ namespace MediaBrowser.Api.Library
private readonly ILocalizationManager _localization;
private readonly ILibraryMonitor _libraryMonitor;
+ private readonly ILogger<MoviesService> _moviesServiceLogger;
+
/// <summary>
/// Initializes a new instance of the <see cref="LibraryService" /> class.
/// </summary>
public LibraryService(
ILogger<LibraryService> logger,
+ ILogger<MoviesService> moviesServiceLogger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IProviderManager providerManager,
@@ -344,6 +347,7 @@ namespace MediaBrowser.Api.Library
_activityManager = activityManager;
_localization = localization;
_libraryMonitor = libraryMonitor;
+ _moviesServiceLogger = moviesServiceLogger;
}
private string[] GetRepresentativeItemTypes(string contentType)
@@ -543,7 +547,7 @@ namespace MediaBrowser.Api.Library
if (item is Movie || (program != null && program.IsMovie) || item is Trailer)
{
return new MoviesService(
- Logger,
+ _moviesServiceLogger,
ServerConfigurationManager,
ResultFactory,
_userManager,
diff --git a/MediaBrowser.Api/Movies/MoviesService.cs b/MediaBrowser.Api/Movies/MoviesService.cs
index 46da8b909..cdd027634 100644
--- a/MediaBrowser.Api/Movies/MoviesService.cs
+++ b/MediaBrowser.Api/Movies/MoviesService.cs
@@ -82,7 +82,7 @@ namespace MediaBrowser.Api.Movies
/// Initializes a new instance of the <see cref="MoviesService" /> class.
/// </summary>
public MoviesService(
- ILogger logger,
+ ILogger<MoviesService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IUserManager userManager,
diff --git a/MediaBrowser.Api/Movies/TrailersService.cs b/MediaBrowser.Api/Movies/TrailersService.cs
index 8adf9c621..0b5334235 100644
--- a/MediaBrowser.Api/Movies/TrailersService.cs
+++ b/MediaBrowser.Api/Movies/TrailersService.cs
@@ -33,13 +33,18 @@ namespace MediaBrowser.Api.Movies
/// </summary>
private readonly ILibraryManager _libraryManager;
+ /// <summary>
+ /// The logger for the created <see cref="ItemsService"/> instances.
+ /// </summary>
+ private readonly ILogger<ItemsService> _logger;
+
private readonly IDtoService _dtoService;
private readonly ILocalizationManager _localizationManager;
private readonly IJsonSerializer _json;
private readonly IAuthorizationContext _authContext;
public TrailersService(
- ILogger<TrailersService> logger,
+ ILoggerFactory loggerFactory,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IUserManager userManager,
@@ -48,7 +53,7 @@ namespace MediaBrowser.Api.Movies
ILocalizationManager localizationManager,
IJsonSerializer json,
IAuthorizationContext authContext)
- : base(logger, serverConfigurationManager, httpResultFactory)
+ : base(loggerFactory.CreateLogger<TrailersService>(), serverConfigurationManager, httpResultFactory)
{
_userManager = userManager;
_libraryManager = libraryManager;
@@ -56,6 +61,7 @@ namespace MediaBrowser.Api.Movies
_localizationManager = localizationManager;
_json = json;
_authContext = authContext;
+ _logger = loggerFactory.CreateLogger<ItemsService>();
}
public object Get(Getrailers request)
@@ -66,7 +72,7 @@ namespace MediaBrowser.Api.Movies
getItems.IncludeItemTypes = "Trailer";
return new ItemsService(
- Logger,
+ _logger,
ServerConfigurationManager,
ResultFactory,
_userManager,
diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs
index 770539357..f796aa486 100644
--- a/MediaBrowser.Api/Playback/BaseStreamingService.cs
+++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs
@@ -81,7 +81,7 @@ namespace MediaBrowser.Api.Playback
/// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
/// </summary>
protected BaseStreamingService(
- ILogger logger,
+ ILogger<BaseStreamingService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IUserManager userManager,
@@ -321,7 +321,7 @@ namespace MediaBrowser.Api.Playback
var encodingOptions = ServerConfigurationManager.GetEncodingOptions();
// enable throttling when NOT using hardware acceleration
- if (encodingOptions.HardwareAccelerationType == string.Empty)
+ if (string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
{
return state.InputProtocol == MediaProtocol.File &&
state.RunTimeTicks.HasValue &&
@@ -330,6 +330,7 @@ namespace MediaBrowser.Api.Playback
state.VideoType == VideoType.VideoFile &&
!string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase);
}
+
return false;
}
diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
index 52962366c..627421aac 100644
--- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
+++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
@@ -25,7 +25,7 @@ namespace MediaBrowser.Api.Playback.Hls
public abstract class BaseHlsService : BaseStreamingService
{
public BaseHlsService(
- ILogger logger,
+ ILogger<BaseHlsService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IUserManager userManager,
@@ -209,24 +209,28 @@ namespace MediaBrowser.Api.Playback.Hls
try
{
// Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
- using var fileStream = GetPlaylistFileStream(playlist);
- using var reader = new StreamReader(fileStream);
- var count = 0;
-
- while (!reader.EndOfStream)
+ var fileStream = GetPlaylistFileStream(playlist);
+ await using (fileStream.ConfigureAwait(false))
{
- var line = reader.ReadLine();
+ using var reader = new StreamReader(fileStream);
+ var count = 0;
- if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
+ while (!reader.EndOfStream)
{
- count++;
- if (count >= segmentCount)
+ var line = await reader.ReadLineAsync().ConfigureAwait(false);
+
+ if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
{
- Logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
- return;
+ count++;
+ if (count >= segmentCount)
+ {
+ Logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
+ return;
+ }
}
}
}
+
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
catch (IOException)
diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
index 20e18cc26..061316cb8 100644
--- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
+++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
@@ -94,7 +94,7 @@ namespace MediaBrowser.Api.Playback.Hls
public class DynamicHlsService : BaseHlsService
{
public DynamicHlsService(
- ILogger logger,
+ ILogger<DynamicHlsService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IUserManager userManager,
@@ -720,22 +720,203 @@ namespace MediaBrowser.Api.Playback.Hls
//return state.VideoRequest.VideoBitRate.HasValue;
}
+ /// <summary>
+ /// Get the H.26X level of the output video stream.
+ /// </summary>
+ /// <param name="state">StreamState of the current stream.</param>
+ /// <returns>H.26X level of the output video stream.</returns>
+ private int? GetOutputVideoCodecLevel(StreamState state)
+ {
+ string levelString;
+ if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)
+ && state.VideoStream.Level.HasValue)
+ {
+ levelString = state.VideoStream?.Level.ToString();
+ }
+ else
+ {
+ levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec);
+ }
+
+ if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
+ {
+ return parsedLevel;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Gets a formatted string of the output audio codec, for use in the CODECS field.
+ /// </summary>
+ /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
+ /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
+ /// <param name="state">StreamState of the current stream.</param>
+ /// <returns>Formatted audio codec string.</returns>
+ private string GetPlaylistAudioCodecs(StreamState state)
+ {
+
+ if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
+ {
+ string profile = state.GetRequestedProfiles("aac").FirstOrDefault();
+
+ return HlsCodecStringFactory.GetAACString(profile);
+ }
+ else if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
+ {
+ return HlsCodecStringFactory.GetMP3String();
+ }
+ else if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
+ {
+ return HlsCodecStringFactory.GetAC3String();
+ }
+ else if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
+ {
+ return HlsCodecStringFactory.GetEAC3String();
+ }
+
+ return string.Empty;
+ }
+
+ /// <summary>
+ /// Gets a formatted string of the output video codec, for use in the CODECS field.
+ /// </summary>
+ /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
+ /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
+ /// <param name="state">StreamState of the current stream.</param>
+ /// <returns>Formatted video codec string.</returns>
+ private string GetPlaylistVideoCodecs(StreamState state, string codec, int level)
+ {
+ if (level == 0)
+ {
+ // This is 0 when there's no requested H.26X level in the device profile
+ // and the source is not encoded in H.26X
+ Logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist");
+ return string.Empty;
+ }
+
+ if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
+ {
+ string profile = state.GetRequestedProfiles("h264").FirstOrDefault();
+
+ return HlsCodecStringFactory.GetH264String(profile, level);
+ }
+ else if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
+ {
+ string profile = state.GetRequestedProfiles("h265").FirstOrDefault();
+
+ return HlsCodecStringFactory.GetH265String(profile, level);
+ }
+
+ return string.Empty;
+ }
+
+ /// <summary>
+ /// Appends a CODECS field containing formatted strings of
+ /// the active streams output video and audio codecs.
+ /// </summary>
+ /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+ /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
+ /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
+ /// <param name="builder">StringBuilder to append the field to.</param>
+ /// <param name="state">StreamState of the current stream.</param>
+ private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
+ {
+ // Video
+ string videoCodecs = string.Empty;
+ int? videoCodecLevel = GetOutputVideoCodecLevel(state);
+ if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue)
+ {
+ videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
+ }
+
+ // Audio
+ string audioCodecs = string.Empty;
+ if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
+ {
+ audioCodecs = GetPlaylistAudioCodecs(state);
+ }
+
+ StringBuilder codecs = new StringBuilder();
+
+ codecs.Append(videoCodecs)
+ .Append(',')
+ .Append(audioCodecs);
+
+ if (codecs.Length > 1)
+ {
+ builder.Append(",CODECS=\"")
+ .Append(codecs)
+ .Append('"');
+ }
+ }
+
+ /// <summary>
+ /// Appends a FRAME-RATE field containing the framerate of the output stream.
+ /// </summary>
+ /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+ /// <param name="builder">StringBuilder to append the field to.</param>
+ /// <param name="state">StreamState of the current stream.</param>
+ private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state)
+ {
+ double? framerate = null;
+ if (state.TargetFramerate.HasValue)
+ {
+ framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
+ }
+ else if (state.VideoStream.RealFrameRate.HasValue)
+ {
+ framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
+ }
+
+ if (framerate.HasValue)
+ {
+ builder.Append(",FRAME-RATE=\"")
+ .Append(framerate.Value)
+ .Append('"');
+ }
+ }
+
+ /// <summary>
+ /// Appends a RESOLUTION field containing the resolution of the output stream.
+ /// </summary>
+ /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+ /// <param name="builder">StringBuilder to append the field to.</param>
+ /// <param name="state">StreamState of the current stream.</param>
+ private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state)
+ {
+ if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
+ {
+ builder.Append(",RESOLUTION=\"")
+ .Append(state.OutputWidth.GetValueOrDefault())
+ .Append('x')
+ .Append(state.OutputHeight.GetValueOrDefault())
+ .Append('"');
+ }
+ }
+
private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string subtitleGroup)
{
- var header = "#EXT-X-STREAM-INF:BANDWIDTH=" + bitrate.ToString(CultureInfo.InvariantCulture) + ",AVERAGE-BANDWIDTH=" + bitrate.ToString(CultureInfo.InvariantCulture);
+ builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
+ .Append(bitrate.ToString(CultureInfo.InvariantCulture))
+ .Append(",AVERAGE-BANDWIDTH=")
+ .Append(bitrate.ToString(CultureInfo.InvariantCulture));
- // tvos wants resolution, codecs, framerate
- //if (state.TargetFramerate.HasValue)
- //{
- // header += string.Format(",FRAME-RATE=\"{0}\"", state.TargetFramerate.Value.ToString(CultureInfo.InvariantCulture));
- //}
+ AppendPlaylistCodecsField(builder, state);
+
+ AppendPlaylistResolutionField(builder, state);
+
+ AppendPlaylistFramerateField(builder, state);
if (!string.IsNullOrWhiteSpace(subtitleGroup))
{
- header += string.Format(",SUBTITLES=\"{0}\"", subtitleGroup);
+ builder.Append(",SUBTITLES=\"")
+ .Append(subtitleGroup)
+ .Append('"');
}
- builder.AppendLine(header);
+ builder.Append(Environment.NewLine);
builder.AppendLine(url);
}
diff --git a/MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs b/MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs
new file mode 100644
index 000000000..3bbb77a65
--- /dev/null
+++ b/MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Text;
+
+
+namespace MediaBrowser.Api.Playback
+{
+ /// <summary>
+ /// Get various codec strings for use in HLS playlists.
+ /// </summary>
+ static class HlsCodecStringFactory
+ {
+
+ /// <summary>
+ /// Gets a MP3 codec string.
+ /// </summary>
+ /// <returns>MP3 codec string.</returns>
+ public static string GetMP3String()
+ {
+ return "mp4a.40.34";
+ }
+
+ /// <summary>
+ /// Gets an AAC codec string.
+ /// </summary>
+ /// <param name="profile">AAC profile.</param>
+ /// <returns>AAC codec string.</returns>
+ public static string GetAACString(string profile)
+ {
+ StringBuilder result = new StringBuilder("mp4a", 9);
+
+ if (string.Equals(profile, "HE", StringComparison.OrdinalIgnoreCase))
+ {
+ result.Append(".40.5");
+ }
+ else
+ {
+ // Default to LC if profile is invalid
+ result.Append(".40.2");
+ }
+
+ return result.ToString();
+ }
+
+ /// <summary>
+ /// Gets a H.264 codec string.
+ /// </summary>
+ /// <param name="profile">H.264 profile.</param>
+ /// <param name="level">H.264 level.</param>
+ /// <returns>H.264 string.</returns>
+ public static string GetH264String(string profile, int level)
+ {
+ StringBuilder result = new StringBuilder("avc1", 11);
+
+ if (string.Equals(profile, "high", StringComparison.OrdinalIgnoreCase))
+ {
+ result.Append(".6400");
+ }
+ else if (string.Equals(profile, "main", StringComparison.OrdinalIgnoreCase))
+ {
+ result.Append(".4D40");
+ }
+ else if (string.Equals(profile, "baseline", StringComparison.OrdinalIgnoreCase))
+ {
+ result.Append(".42E0");
+ }
+ else
+ {
+ // Default to constrained baseline if profile is invalid
+ result.Append(".4240");
+ }
+
+ string levelHex = level.ToString("X2");
+ result.Append(levelHex);
+
+ return result.ToString();
+ }
+
+ /// <summary>
+ /// Gets a H.265 codec string.
+ /// </summary>
+ /// <param name="profile">H.265 profile.</param>
+ /// <param name="level">H.265 level.</param>
+ /// <returns>H.265 string.</returns>
+ public static string GetH265String(string profile, int level)
+ {
+ // The h265 syntax is a bit of a mystery at the time this comment was written.
+ // This is what I've found through various sources:
+ // FORMAT: [codecTag].[profile].[constraint?].L[level * 30].[UNKNOWN]
+ StringBuilder result = new StringBuilder("hev1", 16);
+
+ if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase))
+ {
+ result.Append(".2.6");
+ }
+ else
+ {
+ // Default to main if profile is invalid
+ result.Append(".1.6");
+ }
+
+ result.Append(".L")
+ .Append(level * 3)
+ .Append(".B0");
+
+ return result.ToString();
+ }
+
+ /// <summary>
+ /// Gets an AC-3 codec string.
+ /// </summary>
+ /// <returns>AC-3 codec string.</returns>
+ public static string GetAC3String()
+ {
+ return "mp4a.a5";
+ }
+
+ /// <summary>
+ /// Gets an E-AC-3 codec string.
+ /// </summary>
+ /// <returns>E-AC-3 codec string.</returns>
+ public static string GetEAC3String()
+ {
+ return "mp4a.a6";
+ }
+ }
+}
diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs
index db24eaca6..e2d771ec6 100644
--- a/MediaBrowser.Api/Playback/MediaInfoService.cs
+++ b/MediaBrowser.Api/Playback/MediaInfoService.cs
@@ -79,7 +79,7 @@ namespace MediaBrowser.Api.Playback
private readonly IAuthorizationContext _authContext;
public MediaInfoService(
- ILogger logger,
+ ILogger<MediaInfoService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IMediaSourceManager mediaSourceManager,
diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs
index 8d1e3a3f2..34c7986ca 100644
--- a/MediaBrowser.Api/Playback/Progressive/AudioService.cs
+++ b/MediaBrowser.Api/Playback/Progressive/AudioService.cs
@@ -33,7 +33,7 @@ namespace MediaBrowser.Api.Playback.Progressive
public class AudioService : BaseProgressiveStreamingService
{
public AudioService(
- ILogger logger,
+ ILogger<AudioService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IHttpClient httpClient,
diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
index ed68219c9..c7bf055fb 100644
--- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
+++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
@@ -28,7 +28,7 @@ namespace MediaBrowser.Api.Playback.Progressive
protected IHttpClient HttpClient { get; private set; }
public BaseProgressiveStreamingService(
- ILogger logger,
+ ILogger<BaseProgressiveStreamingService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IHttpClient httpClient,
diff --git a/MediaBrowser.Api/Playback/UniversalAudioService.cs b/MediaBrowser.Api/Playback/UniversalAudioService.cs
index cebd4b49a..a3b319d44 100644
--- a/MediaBrowser.Api/Playback/UniversalAudioService.cs
+++ b/MediaBrowser.Api/Playback/UniversalAudioService.cs
@@ -75,9 +75,11 @@ namespace MediaBrowser.Api.Playback
public class UniversalAudioService : BaseApiService
{
private readonly EncodingHelper _encodingHelper;
+ private readonly ILoggerFactory _loggerFactory;
public UniversalAudioService(
ILogger<UniversalAudioService> logger,
+ ILoggerFactory loggerFactory,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IHttpClient httpClient,
@@ -108,6 +110,7 @@ namespace MediaBrowser.Api.Playback
AuthorizationContext = authorizationContext;
NetworkManager = networkManager;
_encodingHelper = encodingHelper;
+ _loggerFactory = loggerFactory;
}
protected IHttpClient HttpClient { get; private set; }
@@ -233,7 +236,7 @@ namespace MediaBrowser.Api.Playback
AuthorizationContext.GetAuthorizationInfo(Request).DeviceId = request.DeviceId;
var mediaInfoService = new MediaInfoService(
- Logger,
+ _loggerFactory.CreateLogger<MediaInfoService>(),
ServerConfigurationManager,
ResultFactory,
MediaSourceManager,
@@ -277,7 +280,7 @@ namespace MediaBrowser.Api.Playback
if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
{
var service = new DynamicHlsService(
- Logger,
+ _loggerFactory.CreateLogger<DynamicHlsService>(),
ServerConfigurationManager,
ResultFactory,
UserManager,
@@ -331,7 +334,7 @@ namespace MediaBrowser.Api.Playback
else
{
var service = new AudioService(
- Logger,
+ _loggerFactory.CreateLogger<AudioService>(),
ServerConfigurationManager,
ResultFactory,
HttpClient,
diff --git a/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs b/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs
index d882aac88..0e74c9267 100644
--- a/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs
+++ b/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs
@@ -31,46 +31,46 @@ namespace MediaBrowser.Api.Sessions
{
_sessionManager = sessionManager;
- _sessionManager.SessionStarted += _sessionManager_SessionStarted;
- _sessionManager.SessionEnded += _sessionManager_SessionEnded;
- _sessionManager.PlaybackStart += _sessionManager_PlaybackStart;
- _sessionManager.PlaybackStopped += _sessionManager_PlaybackStopped;
- _sessionManager.PlaybackProgress += _sessionManager_PlaybackProgress;
- _sessionManager.CapabilitiesChanged += _sessionManager_CapabilitiesChanged;
- _sessionManager.SessionActivity += _sessionManager_SessionActivity;
+ _sessionManager.SessionStarted += OnSessionManagerSessionStarted;
+ _sessionManager.SessionEnded += OnSessionManagerSessionEnded;
+ _sessionManager.PlaybackStart += OnSessionManagerPlaybackStart;
+ _sessionManager.PlaybackStopped += OnSessionManagerPlaybackStopped;
+ _sessionManager.PlaybackProgress += OnSessionManagerPlaybackProgress;
+ _sessionManager.CapabilitiesChanged += OnSessionManagerCapabilitiesChanged;
+ _sessionManager.SessionActivity += OnSessionManagerSessionActivity;
}
- void _sessionManager_SessionActivity(object sender, SessionEventArgs e)
+ private void OnSessionManagerSessionActivity(object sender, SessionEventArgs e)
{
SendData(false);
}
- void _sessionManager_CapabilitiesChanged(object sender, SessionEventArgs e)
+ private void OnSessionManagerCapabilitiesChanged(object sender, SessionEventArgs e)
{
SendData(true);
}
- void _sessionManager_PlaybackProgress(object sender, PlaybackProgressEventArgs e)
+ private void OnSessionManagerPlaybackProgress(object sender, PlaybackProgressEventArgs e)
{
SendData(!e.IsAutomated);
}
- void _sessionManager_PlaybackStopped(object sender, PlaybackStopEventArgs e)
+ private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e)
{
SendData(true);
}
- void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e)
+ private void OnSessionManagerPlaybackStart(object sender, PlaybackProgressEventArgs e)
{
SendData(true);
}
- void _sessionManager_SessionEnded(object sender, SessionEventArgs e)
+ private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e)
{
SendData(true);
}
- void _sessionManager_SessionStarted(object sender, SessionEventArgs e)
+ private void OnSessionManagerSessionStarted(object sender, SessionEventArgs e)
{
SendData(true);
}
@@ -84,15 +84,16 @@ namespace MediaBrowser.Api.Sessions
return Task.FromResult(_sessionManager.Sessions);
}
+ /// <inheritdoc />
protected override void Dispose(bool dispose)
{
- _sessionManager.SessionStarted -= _sessionManager_SessionStarted;
- _sessionManager.SessionEnded -= _sessionManager_SessionEnded;
- _sessionManager.PlaybackStart -= _sessionManager_PlaybackStart;
- _sessionManager.PlaybackStopped -= _sessionManager_PlaybackStopped;
- _sessionManager.PlaybackProgress -= _sessionManager_PlaybackProgress;
- _sessionManager.CapabilitiesChanged -= _sessionManager_CapabilitiesChanged;
- _sessionManager.SessionActivity -= _sessionManager_SessionActivity;
+ _sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
+ _sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
+ _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
+ _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
+ _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
+ _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
+ _sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
base.Dispose(dispose);
}
diff --git a/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs b/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs
index f8b6ee65d..8e4860be4 100644
--- a/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs
+++ b/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System;
using System.Threading.Tasks;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Activity;
@@ -10,7 +10,7 @@ namespace MediaBrowser.Api.System
/// <summary>
/// Class SessionInfoWebSocketListener
/// </summary>
- public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<List<ActivityLogEntry>, WebSocketListenerState>
+ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<ActivityLogEntry[], WebSocketListenerState>
{
/// <summary>
/// Gets the name.
@@ -26,10 +26,10 @@ namespace MediaBrowser.Api.System
public ActivityLogWebSocketListener(ILogger<ActivityLogWebSocketListener> logger, IActivityManager activityManager) : base(logger)
{
_activityManager = activityManager;
- _activityManager.EntryCreated += _activityManager_EntryCreated;
+ _activityManager.EntryCreated += OnEntryCreated;
}
- void _activityManager_EntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
+ private void OnEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
{
SendData(true);
}
@@ -38,15 +38,15 @@ namespace MediaBrowser.Api.System
/// Gets the data to send.
/// </summary>
/// <returns>Task{SystemInfo}.</returns>
- protected override Task<List<ActivityLogEntry>> GetDataToSend()
+ protected override Task<ActivityLogEntry[]> GetDataToSend()
{
- return Task.FromResult(new List<ActivityLogEntry>());
+ return Task.FromResult(Array.Empty<ActivityLogEntry>());
}
-
+ /// <inheritdoc />
protected override void Dispose(bool dispose)
{
- _activityManager.EntryCreated -= _activityManager_EntryCreated;
+ _activityManager.EntryCreated -= OnEntryCreated;
base.Dispose(dispose);
}
diff --git a/MediaBrowser.Api/UserLibrary/ArtistsService.cs b/MediaBrowser.Api/UserLibrary/ArtistsService.cs
index 3d08d5437..bef91d54d 100644
--- a/MediaBrowser.Api/UserLibrary/ArtistsService.cs
+++ b/MediaBrowser.Api/UserLibrary/ArtistsService.cs
@@ -51,7 +51,7 @@ namespace MediaBrowser.Api.UserLibrary
public class ArtistsService : BaseItemsByNameService<MusicArtist>
{
public ArtistsService(
- ILogger<GenresService> logger,
+ ILogger<ArtistsService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IUserManager userManager,
diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
index c4a52d5f5..559082ff4 100644
--- a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
+++ b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
@@ -28,7 +28,7 @@ namespace MediaBrowser.Api.UserLibrary
/// <param name="userDataRepository">The user data repository.</param>
/// <param name="dtoService">The dto service.</param>
protected BaseItemsByNameService(
- ILogger logger,
+ ILogger<BaseItemsByNameService<TItemType>> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IUserManager userManager,
diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs
index c4d44042b..f3c0441e1 100644
--- a/MediaBrowser.Api/UserLibrary/ItemsService.cs
+++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs
@@ -59,7 +59,7 @@ namespace MediaBrowser.Api.UserLibrary
/// <param name="localization">The localization.</param>
/// <param name="dtoService">The dto service.</param>
public ItemsService(
- ILogger logger,
+ ILogger<ItemsService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IUserManager userManager,
diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs
index 78fc6c694..7d4d5fcf9 100644
--- a/MediaBrowser.Api/UserService.cs
+++ b/MediaBrowser.Api/UserService.cs
@@ -35,7 +35,7 @@ namespace MediaBrowser.Api
}
[Route("/Users/Public", "GET", Summary = "Gets a list of publicly visible users for display on a login screen.")]
- public class GetPublicUsers : IReturn<UserDto[]>
+ public class GetPublicUsers : IReturn<PublicUserDto[]>
{
}
@@ -266,22 +266,38 @@ namespace MediaBrowser.Api
_authContext = authContext;
}
+ /// <summary>
+ /// Gets the public available Users information
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <returns>System.Object.</returns>
public object Get(GetPublicUsers request)
{
- // If the startup wizard hasn't been completed then just return all users
- if (!ServerConfigurationManager.Configuration.IsStartupWizardCompleted)
+ var result = _userManager
+ .Users
+ .Where(item => !item.Policy.IsDisabled);
+
+ if (ServerConfigurationManager.Configuration.IsStartupWizardCompleted)
{
- return Get(new GetUsers
+ var deviceId = _authContext.GetAuthorizationInfo(Request).DeviceId;
+ result = result.Where(item => !item.Policy.IsHidden);
+
+ if (!string.IsNullOrWhiteSpace(deviceId))
{
- IsDisabled = false
- });
+ result = result.Where(i => _deviceManager.CanAccessDevice(i, deviceId));
+ }
+
+ if (!_networkManager.IsInLocalNetwork(Request.RemoteIp))
+ {
+ result = result.Where(i => i.Policy.EnableRemoteAccess);
+ }
}
- return Get(new GetUsers
- {
- IsHidden = false,
- IsDisabled = false
- }, true, true);
+ return ToOptimizedResult(result
+ .OrderBy(u => u.Name)
+ .Select(i => _userManager.GetPublicUserDto(i, Request.RemoteIp))
+ .ToArray()
+ );
}
/// <summary>
diff --git a/MediaBrowser.Common/Extensions/BaseExtensions.cs b/MediaBrowser.Common/Extensions/BaseExtensions.cs
index 08964420e..40020093b 100644
--- a/MediaBrowser.Common/Extensions/BaseExtensions.cs
+++ b/MediaBrowser.Common/Extensions/BaseExtensions.cs
@@ -1,3 +1,5 @@
+#nullable enable
+
using System;
using System.Security.Cryptography;
using System.Text;
diff --git a/MediaBrowser.Common/Extensions/CopyToExtensions.cs b/MediaBrowser.Common/Extensions/CopyToExtensions.cs
index 2ecbc6539..94bf7c740 100644
--- a/MediaBrowser.Common/Extensions/CopyToExtensions.cs
+++ b/MediaBrowser.Common/Extensions/CopyToExtensions.cs
@@ -1,3 +1,5 @@
+#nullable enable
+
using System.Collections.Generic;
namespace MediaBrowser.Common.Extensions
diff --git a/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs b/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs
index 48e758ee4..258bd6662 100644
--- a/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs
+++ b/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs
@@ -1,3 +1,5 @@
+#nullable enable
+
using System;
namespace MediaBrowser.Common.Extensions
diff --git a/MediaBrowser.Common/Extensions/ProcessExtensions.cs b/MediaBrowser.Common/Extensions/ProcessExtensions.cs
index c74787122..2f52ba196 100644
--- a/MediaBrowser.Common/Extensions/ProcessExtensions.cs
+++ b/MediaBrowser.Common/Extensions/ProcessExtensions.cs
@@ -1,3 +1,5 @@
+#nullable enable
+
using System;
using System.Diagnostics;
using System.Threading;
diff --git a/MediaBrowser.Common/Extensions/RateLimitExceededException.cs b/MediaBrowser.Common/Extensions/RateLimitExceededException.cs
index 95802a462..7c7bdaa92 100644
--- a/MediaBrowser.Common/Extensions/RateLimitExceededException.cs
+++ b/MediaBrowser.Common/Extensions/RateLimitExceededException.cs
@@ -1,3 +1,4 @@
+#nullable enable
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs b/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs
index 22130c5a1..ebac9d8e6 100644
--- a/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs
+++ b/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs
@@ -1,3 +1,5 @@
+#nullable enable
+
using System;
namespace MediaBrowser.Common.Extensions
diff --git a/MediaBrowser.Common/Extensions/StringExtensions.cs b/MediaBrowser.Common/Extensions/StringExtensions.cs
new file mode 100644
index 000000000..764301741
--- /dev/null
+++ b/MediaBrowser.Common/Extensions/StringExtensions.cs
@@ -0,0 +1,37 @@
+#nullable enable
+
+using System;
+
+namespace MediaBrowser.Common.Extensions
+{
+ /// <summary>
+ /// Extensions methods to simplify string operations.
+ /// </summary>
+ public static class StringExtensions
+ {
+ /// <summary>
+ /// Returns the part on the left of the <c>needle</c>.
+ /// </summary>
+ /// <param name="haystack">The string to seek.</param>
+ /// <param name="needle">The needle to find.</param>
+ /// <returns>The part left of the <paramref name="needle" />.</returns>
+ public static ReadOnlySpan<char> LeftPart(this ReadOnlySpan<char> haystack, char needle)
+ {
+ var pos = haystack.IndexOf(needle);
+ return pos == -1 ? haystack : haystack[..pos];
+ }
+
+ /// <summary>
+ /// Returns the part on the left of the <c>needle</c>.
+ /// </summary>
+ /// <param name="haystack">The string to seek.</param>
+ /// <param name="needle">The needle to find.</param>
+ /// <param name="stringComparison">One of the enumeration values that specifies the rules for the search.</param>
+ /// <returns>The part left of the <c>needle</c>.</returns>
+ public static ReadOnlySpan<char> LeftPart(this ReadOnlySpan<char> haystack, ReadOnlySpan<char> needle, StringComparison stringComparison = default)
+ {
+ var pos = haystack.IndexOf(needle, stringComparison);
+ return pos == -1 ? haystack : haystack[..pos];
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
index 04ba0fabc..d1d6c74b8 100644
--- a/MediaBrowser.Controller/IServerApplicationHost.cs
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -39,10 +39,9 @@ namespace MediaBrowser.Controller
int HttpsPort { get; }
/// <summary>
- /// Gets a value indicating whether [supports HTTPS].
+ /// Gets a value indicating whether the server should listen on an HTTPS port.
/// </summary>
- /// <value><c>true</c> if [supports HTTPS]; otherwise, <c>false</c>.</value>
- bool EnableHttps { get; }
+ bool ListenWithHttps { get; }
/// <summary>
/// Gets a value indicating whether this instance has update available.
@@ -57,34 +56,50 @@ namespace MediaBrowser.Controller
string FriendlyName { get; }
/// <summary>
- /// Gets the local ip address.
+ /// Gets all the local IP addresses of this API instance. Each address is validated by sending a 'ping' request
+ /// to the API that should exist at the address.
/// </summary>
- /// <value>The local ip address.</value>
+ /// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
+ /// <returns>A list containing all the local IP addresses of the server.</returns>
Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken);
/// <summary>
- /// Gets the local API URL.
+ /// Gets a local (LAN) URL that can be used to access the API. The hostname used is the first valid configured
+ /// IP address that can be found via <see cref="GetLocalIpAddresses"/>. HTTPS will be preferred when available.
/// </summary>
- /// <param name="cancellationToken">Token to cancel the request if needed.</param>
- /// <param name="forceHttp">Whether to force usage of plain HTTP protocol.</param>
- /// <value>The local API URL.</value>
- Task<string> GetLocalApiUrl(CancellationToken cancellationToken, bool forceHttp = false);
+ /// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
+ /// <returns>The server URL.</returns>
+ Task<string> GetLocalApiUrl(CancellationToken cancellationToken);
/// <summary>
- /// Gets the local API URL.
+ /// Gets a localhost URL that can be used to access the API using the loop-back IP address (127.0.0.1)
+ /// over HTTP (not HTTPS).
/// </summary>
- /// <param name="hostname">The hostname.</param>
- /// <param name="forceHttp">Whether to force usage of plain HTTP protocol.</param>
- /// <returns>The local API URL.</returns>
- string GetLocalApiUrl(ReadOnlySpan<char> hostname, bool forceHttp = false);
+ /// <returns>The API URL.</returns>
+ string GetLoopbackHttpApiUrl();
/// <summary>
- /// Gets the local API URL.
+ /// Gets a local (LAN) URL that can be used to access the API. HTTPS will be preferred when available.
/// </summary>
- /// <param name="address">The IP address.</param>
- /// <param name="forceHttp">Whether to force usage of plain HTTP protocol.</param>
- /// <returns>The local API URL.</returns>
- string GetLocalApiUrl(IPAddress address, bool forceHttp = false);
+ /// <param name="address">The IP address to use as the hostname in the URL.</param>
+ /// <returns>The API URL.</returns>
+ string GetLocalApiUrl(IPAddress address);
+
+ /// <summary>
+ /// Gets a local (LAN) URL that can be used to access the API.
+ /// Note: if passing non-null scheme or port it is up to the caller to ensure they form the correct pair.
+ /// </summary>
+ /// <param name="hostname">The hostname to use in the URL.</param>
+ /// <param name="scheme">
+ /// The scheme to use for the URL. If null, the scheme will be selected automatically,
+ /// preferring HTTPS, if available.
+ /// </param>
+ /// <param name="port">
+ /// The port to use for the URL. If null, the port will be selected automatically,
+ /// preferring the HTTPS port, if available.
+ /// </param>
+ /// <returns>The API URL.</returns>
+ string GetLocalApiUrl(ReadOnlySpan<char> hostname, string scheme = null, int? port = null);
/// <summary>
/// Open a URL in an external browser window.
@@ -101,7 +116,5 @@ namespace MediaBrowser.Controller
string ReverseVirtualPath(string path);
Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next);
-
- Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next);
}
}
diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs
index be7b4ce59..ec6cb35eb 100644
--- a/MediaBrowser.Controller/Library/IUserManager.cs
+++ b/MediaBrowser.Controller/Library/IUserManager.cs
@@ -144,6 +144,14 @@ namespace MediaBrowser.Controller.Library
UserDto GetUserDto(User user, string remoteEndPoint = null);
/// <summary>
+ /// Gets the user public dto.
+ /// </summary>
+ /// <param name="user">Ther user.</param>\
+ /// <param name="remoteEndPoint">The remote end point.</param>
+ /// <returns>A public UserDto, aka a UserDto stripped of personal data.</returns>
+ PublicUserDto GetPublicUserDto(User user, string remoteEndPoint = null);
+
+ /// <summary>
/// Authenticates the user.
/// </summary>
Task<User> AuthenticateUser(string username, string password, string passwordSha1, string remoteEndPoint, bool isUserSession);
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 5efb10d3a..61a330675 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -2547,7 +2547,7 @@ namespace MediaBrowser.Controller.MediaEncoding
encodingOptions.HardwareDecodingCodecs = Array.Empty<string>();
return null;
}
- return "-c:v h264_qsv ";
+ return "-c:v h264_qsv";
}
break;
case "hevc":
@@ -2555,19 +2555,19 @@ namespace MediaBrowser.Controller.MediaEncoding
if (_mediaEncoder.SupportsDecoder("hevc_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase))
{
//return "-c:v hevc_qsv -load_plugin hevc_hw ";
- return "-c:v hevc_qsv ";
+ return "-c:v hevc_qsv";
}
break;
case "mpeg2video":
if (_mediaEncoder.SupportsDecoder("mpeg2_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase))
{
- return "-c:v mpeg2_qsv ";
+ return "-c:v mpeg2_qsv";
}
break;
case "vc1":
if (_mediaEncoder.SupportsDecoder("vc1_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase))
{
- return "-c:v vc1_qsv ";
+ return "-c:v vc1_qsv";
}
break;
}
@@ -2587,32 +2587,32 @@ namespace MediaBrowser.Controller.MediaEncoding
encodingOptions.HardwareDecodingCodecs = Array.Empty<string>();
return null;
}
- return "-c:v h264_cuvid ";
+ return "-c:v h264_cuvid";
}
break;
case "hevc":
case "h265":
if (_mediaEncoder.SupportsDecoder("hevc_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase))
{
- return "-c:v hevc_cuvid ";
+ return "-c:v hevc_cuvid";
}
break;
case "mpeg2video":
if (_mediaEncoder.SupportsDecoder("mpeg2_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase))
{
- return "-c:v mpeg2_cuvid ";
+ return "-c:v mpeg2_cuvid";
}
break;
case "vc1":
if (_mediaEncoder.SupportsDecoder("vc1_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase))
{
- return "-c:v vc1_cuvid ";
+ return "-c:v vc1_cuvid";
}
break;
case "mpeg4":
if (_mediaEncoder.SupportsDecoder("mpeg4_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase))
{
- return "-c:v mpeg4_cuvid ";
+ return "-c:v mpeg4_cuvid";
}
break;
}
@@ -2626,38 +2626,38 @@ namespace MediaBrowser.Controller.MediaEncoding
case "h264":
if (_mediaEncoder.SupportsDecoder("h264_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase))
{
- return "-c:v h264_mediacodec ";
+ return "-c:v h264_mediacodec";
}
break;
case "hevc":
case "h265":
if (_mediaEncoder.SupportsDecoder("hevc_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase))
{
- return "-c:v hevc_mediacodec ";
+ return "-c:v hevc_mediacodec";
}
break;
case "mpeg2video":
if (_mediaEncoder.SupportsDecoder("mpeg2_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase))
{
- return "-c:v mpeg2_mediacodec ";
+ return "-c:v mpeg2_mediacodec";
}
break;
case "mpeg4":
if (_mediaEncoder.SupportsDecoder("mpeg4_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase))
{
- return "-c:v mpeg4_mediacodec ";
+ return "-c:v mpeg4_mediacodec";
}
break;
case "vp8":
if (_mediaEncoder.SupportsDecoder("vp8_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("vp8", StringComparer.OrdinalIgnoreCase))
{
- return "-c:v vp8_mediacodec ";
+ return "-c:v vp8_mediacodec";
}
break;
case "vp9":
if (_mediaEncoder.SupportsDecoder("vp9_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("vp9", StringComparer.OrdinalIgnoreCase))
{
- return "-c:v vp9_mediacodec ";
+ return "-c:v vp9_mediacodec";
}
break;
}
@@ -2671,25 +2671,25 @@ namespace MediaBrowser.Controller.MediaEncoding
case "h264":
if (_mediaEncoder.SupportsDecoder("h264_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase))
{
- return "-c:v h264_mmal ";
+ return "-c:v h264_mmal";
}
break;
case "mpeg2video":
if (_mediaEncoder.SupportsDecoder("mpeg2_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase))
{
- return "-c:v mpeg2_mmal ";
+ return "-c:v mpeg2_mmal";
}
break;
case "mpeg4":
if (_mediaEncoder.SupportsDecoder("mpeg4_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase))
{
- return "-c:v mpeg4_mmal ";
+ return "-c:v mpeg4_mmal";
}
break;
case "vc1":
if (_mediaEncoder.SupportsDecoder("vc1_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase))
{
- return "-c:v vc1_mmal ";
+ return "-c:v vc1_mmal";
}
break;
}
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index b710318ee..1162bff13 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -77,8 +77,6 @@ namespace MediaBrowser.Controller.Net
return Task.CompletedTask;
}
- protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
/// <summary>
/// Starts sending messages over a web socket
/// </summary>
@@ -87,12 +85,12 @@ namespace MediaBrowser.Controller.Net
{
var vals = message.Data.Split(',');
- var dueTimeMs = long.Parse(vals[0], UsCulture);
- var periodMs = long.Parse(vals[1], UsCulture);
+ var dueTimeMs = long.Parse(vals[0], CultureInfo.InvariantCulture);
+ var periodMs = long.Parse(vals[1], CultureInfo.InvariantCulture);
var cancellationTokenSource = new CancellationTokenSource();
- Logger.LogDebug("{1} Begin transmitting over websocket to {0}", message.Connection.RemoteEndPoint, GetType().Name);
+ Logger.LogDebug("WS {1} begin transmitting to {0}", message.Connection.RemoteEndPoint, GetType().Name);
var state = new TStateType
{
@@ -154,7 +152,6 @@ namespace MediaBrowser.Controller.Net
{
MessageType = Name,
Data = data
-
}, cancellationToken).ConfigureAwait(false);
state.DateLastSendUtc = DateTime.UtcNow;
@@ -197,7 +194,7 @@ namespace MediaBrowser.Controller.Net
/// <param name="connection">The connection.</param>
private void DisposeConnection(Tuple<IWebSocketConnection, CancellationTokenSource, TStateType> connection)
{
- Logger.LogDebug("{1} stop transmitting over websocket to {0}", connection.Item1.RemoteEndPoint, GetType().Name);
+ Logger.LogDebug("WS {1} stop transmitting to {0}", connection.Item1.RemoteEndPoint, GetType().Name);
// TODO disposing the connection seems to break websockets in subtle ways, so what is the purpose of this function really...
// connection.Item1.Dispose();
@@ -242,6 +239,7 @@ namespace MediaBrowser.Controller.Net
public void Dispose()
{
Dispose(true);
+ GC.SuppressFinalize(this);
}
}
diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs
index 806478864..efb5f4ac3 100644
--- a/MediaBrowser.Controller/Net/IHttpServer.cs
+++ b/MediaBrowser.Controller/Net/IHttpServer.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Services;
@@ -9,9 +8,9 @@ using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
{
/// <summary>
- /// Interface IHttpServer
+ /// Interface IHttpServer.
/// </summary>
- public interface IHttpServer : IDisposable
+ public interface IHttpServer
{
/// <summary>
/// Gets the URL prefix.
@@ -20,11 +19,6 @@ namespace MediaBrowser.Controller.Net
string[] UrlPrefixes { get; }
/// <summary>
- /// Stops this instance.
- /// </summary>
- void Stop();
-
- /// <summary>
/// Occurs when [web socket connected].
/// </summary>
event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
@@ -40,22 +34,17 @@ namespace MediaBrowser.Controller.Net
string GlobalResponse { get; set; }
/// <summary>
- /// Sends the http context to the socket listener
+ /// The HTTP request handler
/// </summary>
- /// <param name="ctx"></param>
+ /// <param name="context"></param>
/// <returns></returns>
- Task ProcessWebSocketRequest(HttpContext ctx);
+ Task RequestHandler(HttpContext context);
/// <summary>
- /// The HTTP request handler
+ /// Get the default CORS headers
/// </summary>
- /// <param name="httpReq"></param>
- /// <param name="urlString"></param>
- /// <param name="host"></param>
- /// <param name="localPath"></param>
- /// <param name="cancellationToken"></param>
+ /// <param name="req"></param>
/// <returns></returns>
- Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath,
- CancellationToken cancellationToken);
+ IDictionary<string, string> GetDefaultCorsHeaders(IRequest req);
}
}
diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
index b371a59e9..3ef8e5f6d 100644
--- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs
+++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
@@ -1,4 +1,7 @@
+#nullable enable
+
using System;
+using System.Net;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
@@ -7,18 +10,12 @@ using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
{
- public interface IWebSocketConnection : IDisposable
+ public interface IWebSocketConnection
{
/// <summary>
/// Occurs when [closed].
/// </summary>
- event EventHandler<EventArgs> Closed;
-
- /// <summary>
- /// Gets the id.
- /// </summary>
- /// <value>The id.</value>
- Guid Id { get; }
+ event EventHandler<EventArgs>? Closed;
/// <summary>
/// Gets the last activity date.
@@ -33,21 +30,16 @@ namespace MediaBrowser.Controller.Net
DateTime LastKeepAliveDate { get; set; }
/// <summary>
- /// Gets or sets the URL.
- /// </summary>
- /// <value>The URL.</value>
- string Url { get; set; }
- /// <summary>
/// Gets or sets the query string.
/// </summary>
/// <value>The query string.</value>
- IQueryCollection QueryString { get; set; }
+ IQueryCollection QueryString { get; }
/// <summary>
/// Gets or sets the receive action.
/// </summary>
/// <value>The receive action.</value>
- Func<WebSocketMessageInfo, Task> OnReceive { get; set; }
+ Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
/// <summary>
/// Gets the state.
@@ -59,7 +51,7 @@ namespace MediaBrowser.Controller.Net
/// Gets the remote end point.
/// </summary>
/// <value>The remote end point.</value>
- string RemoteEndPoint { get; }
+ IPAddress? RemoteEndPoint { get; }
/// <summary>
/// Sends a message asynchronously.
@@ -71,21 +63,6 @@ namespace MediaBrowser.Controller.Net
/// <exception cref="ArgumentNullException">message</exception>
Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken);
- /// <summary>
- /// Sends a message asynchronously.
- /// </summary>
- /// <param name="buffer">The buffer.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- Task SendAsync(byte[] buffer, CancellationToken cancellationToken);
-
- /// <summary>
- /// Sends a message asynchronously.
- /// </summary>
- /// <param name="text">The text.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">buffer</exception>
- Task SendAsync(string text, CancellationToken cancellationToken);
+ Task ProcessAsync(CancellationToken cancellationToken = default);
}
}
diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs
index a59c96ac7..04450085b 100644
--- a/MediaBrowser.Controller/Session/ISessionController.cs
+++ b/MediaBrowser.Controller/Session/ISessionController.cs
@@ -1,3 +1,4 @@
+using System;
using System.Threading;
using System.Threading.Tasks;
@@ -20,6 +21,6 @@ namespace MediaBrowser.Controller.Session
/// <summary>
/// Sends the message.
/// </summary>
- Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken);
+ Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index f1f10a3a3..2ba7c9fec 100644
--- a/MediaBrowser.Controller/Session/SessionInfo.cs
+++ b/MediaBrowser.Controller/Session/SessionInfo.cs
@@ -10,13 +10,23 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.Session
{
/// <summary>
- /// Class SessionInfo
+ /// Class SessionInfo.
/// </summary>
- public class SessionInfo : IDisposable
+ public sealed class SessionInfo : IDisposable
{
- private ISessionManager _sessionManager;
+ // 1 second
+ private const long ProgressIncrement = 10000000;
+
+ private readonly ISessionManager _sessionManager;
private readonly ILogger _logger;
+
+ private readonly object _progressLock = new object();
+ private Timer _progressTimer;
+ private PlaybackProgressInfo _lastProgressInfo;
+
+ private bool _disposed = false;
+
public SessionInfo(ISessionManager sessionManager, ILogger logger)
{
_sessionManager = sessionManager;
@@ -97,8 +107,6 @@ namespace MediaBrowser.Controller.Session
/// <value>The name of the device.</value>
public string DeviceName { get; set; }
- public string DeviceType { get; set; }
-
/// <summary>
/// Gets or sets the now playing item.
/// </summary>
@@ -128,22 +136,6 @@ namespace MediaBrowser.Controller.Session
[JsonIgnore]
public ISessionController[] SessionControllers { get; set; }
- /// <summary>
- /// Gets or sets the supported commands.
- /// </summary>
- /// <value>The supported commands.</value>
- public string[] SupportedCommands
- {
- get
- {
- if (Capabilities == null)
- {
- return new string[] { };
- }
- return Capabilities.SupportedCommands;
- }
- }
-
public TranscodingInfo TranscodingInfo { get; set; }
/// <summary>
@@ -215,6 +207,14 @@ namespace MediaBrowser.Controller.Session
}
}
+ public QueueItem[] NowPlayingQueue { get; set; }
+
+ public bool HasCustomDeviceName { get; set; }
+
+ public string PlaylistItemId { get; set; }
+
+ public string UserPrimaryImageTag { get; set; }
+
public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory)
{
var controllers = SessionControllers.ToList();
@@ -258,10 +258,6 @@ namespace MediaBrowser.Controller.Session
return false;
}
- private readonly object _progressLock = new object();
- private Timer _progressTimer;
- private PlaybackProgressInfo _lastProgressInfo;
-
public void StartAutomaticProgress(PlaybackProgressInfo progressInfo)
{
if (_disposed)
@@ -284,9 +280,6 @@ namespace MediaBrowser.Controller.Session
}
}
- // 1 second
- private const long ProgressIncrement = 10000000;
-
private async void OnProgressTimerCallback(object state)
{
if (_disposed)
@@ -345,8 +338,7 @@ namespace MediaBrowser.Controller.Session
}
}
- private bool _disposed = false;
-
+ /// <inheritdoc />
public void Dispose()
{
_disposed = true;
@@ -358,30 +350,12 @@ namespace MediaBrowser.Controller.Session
foreach (var controller in controllers)
{
- var disposable = controller as IDisposable;
-
- if (disposable != null)
+ if (controller is IDisposable disposable)
{
_logger.LogDebug("Disposing session controller {0}", disposable.GetType().Name);
-
- try
- {
- disposable.Dispose();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error disposing session controller");
- }
+ disposable.Dispose();
}
}
-
- _sessionManager = null;
}
-
- public QueueItem[] NowPlayingQueue { get; set; }
- public bool HasCustomDeviceName { get; set; }
- public string PlaylistItemId { get; set; }
- public string ServerId { get; set; }
- public string UserPrimaryImageTag { get; set; }
}
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
index 2e328ba63..de35acbba 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
@@ -7,14 +7,25 @@ using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Subtitles
{
+ /// <summary>
+ /// Subtitle writer for the WebVTT format.
+ /// </summary>
public class VttWriter : ISubtitleWriter
{
+ /// <inheritdoc />
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
{
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{
writer.WriteLine("WEBVTT");
writer.WriteLine(string.Empty);
+ writer.WriteLine("REGION");
+ writer.WriteLine("id:subtitle");
+ writer.WriteLine("width:80%");
+ writer.WriteLine("lines:3");
+ writer.WriteLine("regionanchor:50%,100%");
+ writer.WriteLine("viewportanchor:50%,90%");
+ writer.WriteLine(string.Empty);
foreach (var trackEvent in info.TrackEvents)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -22,13 +33,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks);
var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks);
- // make sure the start and end times are different and seqential
+ // make sure the start and end times are different and sequential
if (endTime.TotalMilliseconds <= startTime.TotalMilliseconds)
{
endTime = startTime.Add(TimeSpan.FromMilliseconds(1));
}
- writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff}", startTime, endTime);
+ writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff} region:subtitle", startTime, endTime);
var text = trackEvent.Text;
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index b5e8d5589..22a42322a 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -20,6 +20,11 @@ namespace MediaBrowser.Model.Configuration
public bool EnableUPnP { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether to enable prometheus metrics exporting.
+ /// </summary>
+ public bool EnableMetrics { get; set; }
+
+ /// <summary>
/// Gets or sets the public mapped port.
/// </summary>
/// <value>The public mapped port.</value>
@@ -44,17 +49,24 @@ namespace MediaBrowser.Model.Configuration
public int HttpsPortNumber { get; set; }
/// <summary>
- /// Gets or sets a value indicating whether [use HTTPS].
+ /// Gets or sets a value indicating whether to use HTTPS.
/// </summary>
- /// <value><c>true</c> if [use HTTPS]; otherwise, <c>false</c>.</value>
+ /// <remarks>
+ /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be
+ /// provided for <see cref="CertificatePath"/> and <see cref="CertificatePassword"/>.
+ /// </remarks>
public bool EnableHttps { get; set; }
+
public bool EnableNormalizedItemByNameIds { get; set; }
/// <summary>
- /// Gets or sets the value pointing to the file system where the ssl certificate is located..
+ /// Gets or sets the filesystem path of an X.509 certificate to use for SSL.
/// </summary>
- /// <value>The value pointing to the file system where the ssl certificate is located..</value>
public string CertificatePath { get; set; }
+
+ /// <summary>
+ /// Gets or sets the password required to access the X.509 certificate data in the file specified by <see cref="CertificatePath"/>.
+ /// </summary>
public string CertificatePassword { get; set; }
/// <summary>
@@ -64,8 +76,11 @@ namespace MediaBrowser.Model.Configuration
public bool IsPortAuthorized { get; set; }
public bool AutoRunWebApp { get; set; }
+
public bool EnableRemoteAccess { get; set; }
+
public bool CameraUploadUpgraded { get; set; }
+
public bool CollectionsUpgraded { get; set; }
/// <summary>
@@ -81,6 +96,7 @@ namespace MediaBrowser.Model.Configuration
/// </summary>
/// <value>The metadata path.</value>
public string MetadataPath { get; set; }
+
public string MetadataNetworkPath { get; set; }
/// <summary>
@@ -203,15 +219,26 @@ namespace MediaBrowser.Model.Configuration
public int RemoteClientBitrateLimit { get; set; }
public bool EnableFolderView { get; set; }
+
public bool EnableGroupingIntoCollections { get; set; }
+
public bool DisplaySpecialsWithinSeasons { get; set; }
+
public string[] LocalNetworkSubnets { get; set; }
+
public string[] LocalNetworkAddresses { get; set; }
+
public string[] CodecsUsed { get; set; }
+
public bool IgnoreVirtualInterfaces { get; set; }
+
public bool EnableExternalContentInSuggestions { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the server should force connections over HTTPS.
+ /// </summary>
public bool RequireHttps { get; set; }
- public bool IsBehindProxy { get; set; }
+
public bool EnableNewOmdbSupport { get; set; }
public string[] RemoteIPFilter { get; set; }
@@ -246,6 +273,7 @@ namespace MediaBrowser.Model.Configuration
PublicHttpsPort = DefaultHttpsPort;
HttpServerPortNumber = DefaultHttpPort;
HttpsPortNumber = DefaultHttpsPort;
+ EnableMetrics = false;
EnableHttps = false;
EnableDashboardResponseCaching = true;
EnableCaseSensitiveItemIds = true;
diff --git a/MediaBrowser.Model/Dlna/CodecProfile.cs b/MediaBrowser.Model/Dlna/CodecProfile.cs
index 756e500dd..7bb961deb 100644
--- a/MediaBrowser.Model/Dlna/CodecProfile.cs
+++ b/MediaBrowser.Model/Dlna/CodecProfile.cs
@@ -1,8 +1,8 @@
#pragma warning disable CS1591
using System;
+using System.Linq;
using System.Xml.Serialization;
-using MediaBrowser.Model.Extensions;
namespace MediaBrowser.Model.Dlna
{
@@ -57,7 +57,7 @@ namespace MediaBrowser.Model.Dlna
foreach (var val in codec)
{
- if (ListHelper.ContainsIgnoreCase(codecs, val))
+ if (codecs.Contains(val, StringComparer.OrdinalIgnoreCase))
{
return true;
}
diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
index 7423efaf6..0c3bd8882 100644
--- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs
+++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
@@ -1,8 +1,8 @@
#pragma warning disable CS1591
using System;
+using System.Linq;
using System.Globalization;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.Model.Dlna
@@ -167,9 +167,7 @@ namespace MediaBrowser.Model.Dlna
switch (condition.Condition)
{
case ProfileConditionType.EqualsAny:
- {
- return ListHelper.ContainsIgnoreCase(expected.Split('|'), currentValue);
- }
+ return expected.Split('|').Contains(currentValue, StringComparer.OrdinalIgnoreCase);
case ProfileConditionType.Equals:
return string.Equals(currentValue, expected, StringComparison.OrdinalIgnoreCase);
case ProfileConditionType.NotEquals:
diff --git a/MediaBrowser.Model/Dlna/ContainerProfile.cs b/MediaBrowser.Model/Dlna/ContainerProfile.cs
index e6691c513..cc2417a70 100644
--- a/MediaBrowser.Model/Dlna/ContainerProfile.cs
+++ b/MediaBrowser.Model/Dlna/ContainerProfile.cs
@@ -1,8 +1,8 @@
#pragma warning disable CS1591
using System;
+using System.Linq;
using System.Xml.Serialization;
-using MediaBrowser.Model.Extensions;
namespace MediaBrowser.Model.Dlna
{
@@ -45,7 +45,7 @@ namespace MediaBrowser.Model.Dlna
public static bool ContainsContainer(string profileContainers, string inputContainer)
{
var isNegativeList = false;
- if (profileContainers != null && profileContainers.StartsWith("-"))
+ if (profileContainers != null && profileContainers.StartsWith("-", StringComparison.Ordinal))
{
isNegativeList = true;
profileContainers = profileContainers.Substring(1);
@@ -72,7 +72,7 @@ namespace MediaBrowser.Model.Dlna
foreach (var container in allInputContainers)
{
- if (ListHelper.ContainsIgnoreCase(profileContainers, container))
+ if (profileContainers.Contains(container, StringComparer.OrdinalIgnoreCase))
{
return false;
}
@@ -86,7 +86,7 @@ namespace MediaBrowser.Model.Dlna
foreach (var container in allInputContainers)
{
- if (ListHelper.ContainsIgnoreCase(profileContainers, container))
+ if (profileContainers.Contains(container, StringComparer.OrdinalIgnoreCase))
{
return true;
}
diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs
index 0cefbbe01..3813ac5eb 100644
--- a/MediaBrowser.Model/Dlna/DeviceProfile.cs
+++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs
@@ -1,8 +1,8 @@
#pragma warning disable CS1591
using System;
+using System.Linq;
using System.Xml.Serialization;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.Model.Dlna
@@ -93,14 +93,14 @@ namespace MediaBrowser.Model.Dlna
public DeviceProfile()
{
- DirectPlayProfiles = new DirectPlayProfile[] { };
- TranscodingProfiles = new TranscodingProfile[] { };
- ResponseProfiles = new ResponseProfile[] { };
- CodecProfiles = new CodecProfile[] { };
- ContainerProfiles = new ContainerProfile[] { };
+ DirectPlayProfiles = Array.Empty<DirectPlayProfile>();
+ TranscodingProfiles = Array.Empty<TranscodingProfile>();
+ ResponseProfiles = Array.Empty<ResponseProfile>();
+ CodecProfiles = Array.Empty<CodecProfile>();
+ ContainerProfiles = Array.Empty<ContainerProfile>();
SubtitleProfiles = Array.Empty<SubtitleProfile>();
- XmlRootAttributes = new XmlAttribute[] { };
+ XmlRootAttributes = Array.Empty<XmlAttribute>();
SupportedMediaTypes = "Audio,Photo,Video";
MaxStreamingBitrate = 8000000;
@@ -129,13 +129,14 @@ namespace MediaBrowser.Model.Dlna
continue;
}
- if (!ListHelper.ContainsIgnoreCase(i.GetAudioCodecs(), audioCodec ?? string.Empty))
+ if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase))
{
continue;
}
return i;
}
+
return null;
}
@@ -155,7 +156,7 @@ namespace MediaBrowser.Model.Dlna
continue;
}
- if (!ListHelper.ContainsIgnoreCase(i.GetAudioCodecs(), audioCodec ?? string.Empty))
+ if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase))
{
continue;
}
@@ -185,7 +186,7 @@ namespace MediaBrowser.Model.Dlna
}
var audioCodecs = i.GetAudioCodecs();
- if (audioCodecs.Length > 0 && !ListHelper.ContainsIgnoreCase(audioCodecs, audioCodec ?? string.Empty))
+ if (audioCodecs.Length > 0 && !audioCodecs.Contains(audioCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase))
{
continue;
}
@@ -288,13 +289,13 @@ namespace MediaBrowser.Model.Dlna
}
var audioCodecs = i.GetAudioCodecs();
- if (audioCodecs.Length > 0 && !ListHelper.ContainsIgnoreCase(audioCodecs, audioCodec ?? string.Empty))
+ if (audioCodecs.Length > 0 && !audioCodecs.Contains(audioCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase))
{
continue;
}
var videoCodecs = i.GetVideoCodecs();
- if (videoCodecs.Length > 0 && !ListHelper.ContainsIgnoreCase(videoCodecs, videoCodec ?? string.Empty))
+ if (videoCodecs.Length > 0 && !videoCodecs.Contains(videoCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase))
{
continue;
}
diff --git a/MediaBrowser.Model/Dlna/SubtitleProfile.cs b/MediaBrowser.Model/Dlna/SubtitleProfile.cs
index 6a8f655ac..9c28019aa 100644
--- a/MediaBrowser.Model/Dlna/SubtitleProfile.cs
+++ b/MediaBrowser.Model/Dlna/SubtitleProfile.cs
@@ -1,7 +1,8 @@
#pragma warning disable CS1591
+using System;
+using System.Linq;
using System.Xml.Serialization;
-using MediaBrowser.Model.Extensions;
namespace MediaBrowser.Model.Dlna
{
@@ -40,7 +41,7 @@ namespace MediaBrowser.Model.Dlna
}
var languages = GetLanguages();
- return languages.Length == 0 || ListHelper.ContainsIgnoreCase(languages, subLanguage);
+ return languages.Length == 0 || languages.Contains(subLanguage, StringComparer.OrdinalIgnoreCase);
}
}
}
diff --git a/MediaBrowser.Model/Dto/PublicUserDto.cs b/MediaBrowser.Model/Dto/PublicUserDto.cs
new file mode 100644
index 000000000..b6bfaf2e9
--- /dev/null
+++ b/MediaBrowser.Model/Dto/PublicUserDto.cs
@@ -0,0 +1,48 @@
+using System;
+
+namespace MediaBrowser.Model.Dto
+{
+ /// <summary>
+ /// Class PublicUserDto. Its goal is to show only public information about a user
+ /// </summary>
+ public class PublicUserDto : IItemDto
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the primary image tag.
+ /// </summary>
+ /// <value>The primary image tag.</value>
+ public string PrimaryImageTag { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance has password.
+ /// </summary>
+ /// <value><c>true</c> if this instance has password; otherwise, <c>false</c>.</value>
+ public bool HasPassword { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance has configured password.
+ /// Note that in this case this method should not be here, but it is necessary when changing password at the
+ /// first login.
+ /// </summary>
+ /// <value><c>true</c> if this instance has configured password; otherwise, <c>false</c>.</value>
+ public bool HasConfiguredPassword { get; set; }
+
+ /// <summary>
+ /// Gets or sets the primary image aspect ratio.
+ /// </summary>
+ /// <value>The primary image aspect ratio.</value>
+ public double? PrimaryImageAspectRatio { get; set; }
+
+ /// <inheritdoc />
+ public override string ToString()
+ {
+ return Name ?? base.ToString();
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
index cd387bd54..922eb4ca7 100644
--- a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
+++ b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
@@ -20,7 +20,7 @@ namespace MediaBrowser.Model.Entities
}
/// <summary>
- /// Gets a provider id
+ /// Gets a provider id.
/// </summary>
/// <param name="instance">The instance.</param>
/// <param name="provider">The provider.</param>
@@ -31,7 +31,7 @@ namespace MediaBrowser.Model.Entities
}
/// <summary>
- /// Gets a provider id
+ /// Gets a provider id.
/// </summary>
/// <param name="instance">The instance.</param>
/// <param name="name">The name.</param>
@@ -53,7 +53,7 @@ namespace MediaBrowser.Model.Entities
}
/// <summary>
- /// Sets a provider id
+ /// Sets a provider id.
/// </summary>
/// <param name="instance">The instance.</param>
/// <param name="name">The name.</param>
@@ -89,7 +89,7 @@ namespace MediaBrowser.Model.Entities
}
/// <summary>
- /// Sets a provider id
+ /// Sets a provider id.
/// </summary>
/// <param name="instance">The instance.</param>
/// <param name="provider">The provider.</param>
diff --git a/MediaBrowser.Model/Extensions/ListHelper.cs b/MediaBrowser.Model/Extensions/ListHelper.cs
deleted file mode 100644
index 90ce6f2e5..000000000
--- a/MediaBrowser.Model/Extensions/ListHelper.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Model.Extensions
-{
- // TODO: @bond remove
- public static class ListHelper
- {
- public static bool ContainsIgnoreCase(string[] list, string value)
- {
- if (value == null)
- {
- throw new ArgumentNullException(nameof(value));
- }
-
- foreach (var item in list)
- {
- if (string.Equals(item, value, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
- return false;
- }
- }
-}
diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs
index 06efedb30..fe2fbe7e4 100644
--- a/MediaBrowser.Model/Net/MimeTypes.cs
+++ b/MediaBrowser.Model/Net/MimeTypes.cs
@@ -17,115 +17,132 @@ namespace MediaBrowser.Model.Net
/// </summary>
private static readonly HashSet<string> _videoFileExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
- ".mkv",
- ".m2t",
- ".m2ts",
+ ".3gp",
+ ".asf",
+ ".avi",
+ ".divx",
+ ".dvr-ms",
+ ".f4v",
+ ".flv",
".img",
".iso",
+ ".m2t",
+ ".m2ts",
+ ".m2v",
+ ".m4v",
".mk3d",
- ".ts",
- ".rmvb",
+ ".mkv",
".mov",
- ".avi",
+ ".mp4",
".mpg",
".mpeg",
- ".wmv",
- ".mp4",
- ".divx",
- ".dvr-ms",
- ".wtv",
+ ".mts",
+ ".ogg",
".ogm",
".ogv",
- ".asf",
- ".m4v",
- ".flv",
- ".f4v",
- ".3gp",
+ ".rec",
+ ".ts",
+ ".rmvb",
".webm",
- ".mts",
- ".m2v",
- ".rec"
+ ".wmv",
+ ".wtv",
};
// http://en.wikipedia.org/wiki/Internet_media_type
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
+ // http://www.iana.org/assignments/media-types/media-types.xhtml
// Add more as needed
private static readonly Dictionary<string, string> _mimeTypeLookup = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
// Type application
+ { ".7z", "application/x-7z-compressed" },
+ { ".azw", "application/vnd.amazon.ebook" },
+ { ".azw3", "application/vnd.amazon.ebook" },
{ ".cbz", "application/x-cbz" },
{ ".cbr", "application/epub+zip" },
{ ".eot", "application/vnd.ms-fontobject" },
{ ".epub", "application/epub+zip" },
{ ".js", "application/x-javascript" },
{ ".json", "application/json" },
+ { ".m3u8", "application/x-mpegURL" },
{ ".map", "application/x-javascript" },
+ { ".mobi", "application/x-mobipocket-ebook" },
{ ".pdf", "application/pdf" },
+ { ".rar", "application/vnd.rar" },
+ { ".srt", "application/x-subrip" },
{ ".ttml", "application/ttml+xml" },
- { ".m3u8", "application/x-mpegURL" },
- { ".mobi", "application/x-mobipocket-ebook" },
- { ".xml", "application/xml" },
{ ".wasm", "application/wasm" },
+ { ".xml", "application/xml" },
+ { ".zip", "application/zip" },
// Type image
+ { ".bmp", "image/bmp" },
+ { ".gif", "image/gif" },
+ { ".ico", "image/vnd.microsoft.icon" },
{ ".jpg", "image/jpeg" },
{ ".jpeg", "image/jpeg" },
- { ".tbn", "image/jpeg" },
{ ".png", "image/png" },
- { ".gif", "image/gif" },
- { ".tiff", "image/tiff" },
- { ".webp", "image/webp" },
- { ".ico", "image/vnd.microsoft.icon" },
{ ".svg", "image/svg+xml" },
{ ".svgz", "image/svg+xml" },
+ { ".tbn", "image/jpeg" },
+ { ".tif", "image/tiff" },
+ { ".tiff", "image/tiff" },
+ { ".webp", "image/webp" },
// Type font
{ ".ttf" , "font/ttf" },
{ ".woff" , "font/woff" },
+ { ".woff2" , "font/woff2" },
// Type text
{ ".ass", "text/x-ssa" },
{ ".ssa", "text/x-ssa" },
{ ".css", "text/css" },
{ ".csv", "text/csv" },
+ { ".rtf", "text/rtf" },
{ ".txt", "text/plain" },
{ ".vtt", "text/vtt" },
// Type video
- { ".mpg", "video/mpeg" },
- { ".ogv", "video/ogg" },
- { ".mov", "video/quicktime" },
- { ".webm", "video/webm" },
- { ".mkv", "video/x-matroska" },
- { ".wmv", "video/x-ms-wmv" },
- { ".flv", "video/x-flv" },
- { ".avi", "video/x-msvideo" },
- { ".asf", "video/x-ms-asf" },
- { ".m4v", "video/x-m4v" },
- { ".m4s", "video/mp4" },
{ ".3gp", "video/3gpp" },
{ ".3g2", "video/3gpp2" },
+ { ".asf", "video/x-ms-asf" },
+ { ".avi", "video/x-msvideo" },
+ { ".flv", "video/x-flv" },
+ { ".mp4", "video/mp4" },
+ { ".m4s", "video/mp4" },
+ { ".m4v", "video/x-m4v" },
+ { ".mpegts", "video/mp2t" },
+ { ".mpg", "video/mpeg" },
+ { ".mkv", "video/x-matroska" },
+ { ".mov", "video/quicktime" },
{ ".mpd", "video/vnd.mpeg.dash.mpd" },
+ { ".ogv", "video/ogg" },
{ ".ts", "video/mp2t" },
- { ".mpegts", "video/mp2t" },
+ { ".webm", "video/webm" },
+ { ".wmv", "video/x-ms-wmv" },
// Type audio
- { ".mp3", "audio/mpeg" },
- { ".m4a", "audio/mp4" },
{ ".aac", "audio/mp4" },
- { ".webma", "audio/webm" },
- { ".wav", "audio/wav" },
- { ".wma", "audio/x-ms-wma" },
- { ".ogg", "audio/ogg" },
- { ".oga", "audio/ogg" },
- { ".opus", "audio/ogg" },
{ ".ac3", "audio/ac3" },
+ { ".ape", "audio/x-ape" },
{ ".dsf", "audio/dsf" },
- { ".m4b", "audio/m4b" },
- { ".xsp", "audio/xsp" },
{ ".dsp", "audio/dsp" },
{ ".flac", "audio/flac" },
- { ".ape", "audio/x-ape" },
+ { ".m4a", "audio/mp4" },
+ { ".m4b", "audio/m4b" },
+ { ".mid", "audio/midi" },
+ { ".midi", "audio/midi" },
+ { ".mp3", "audio/mpeg" },
+ { ".oga", "audio/ogg" },
+ { ".ogg", "audio/ogg" },
+ { ".opus", "audio/ogg" },
+ { ".vorbis", "audio/vorbis" },
+ { ".wav", "audio/wav" },
+ { ".webma", "audio/webm" },
+ { ".wma", "audio/x-ms-wma" },
{ ".wv", "audio/x-wavpack" },
+ { ".xsp", "audio/xsp" },
};
private static readonly Dictionary<string, string> _extensionLookup = CreateExtensionLookup();
diff --git a/MediaBrowser.Model/Net/WebSocketMessage.cs b/MediaBrowser.Model/Net/WebSocketMessage.cs
index 7575224d4..03f03e4cc 100644
--- a/MediaBrowser.Model/Net/WebSocketMessage.cs
+++ b/MediaBrowser.Model/Net/WebSocketMessage.cs
@@ -1,5 +1,8 @@
+
#pragma warning disable CS1591
+using System;
+
namespace MediaBrowser.Model.Net
{
/// <summary>
@@ -13,7 +16,9 @@ namespace MediaBrowser.Model.Net
/// </summary>
/// <value>The type of the message.</value>
public string MessageType { get; set; }
- public string MessageId { get; set; }
+
+ public Guid MessageId { get; set; }
+
public string ServerId { get; set; }
/// <summary>
@@ -22,5 +27,4 @@ namespace MediaBrowser.Model.Net
/// <value>The data.</value>
public T Data { get; set; }
}
-
}
diff --git a/MediaBrowser.Model/Notifications/NotificationOptions.cs b/MediaBrowser.Model/Notifications/NotificationOptions.cs
index 79a128e9b..9c54bd70e 100644
--- a/MediaBrowser.Model/Notifications/NotificationOptions.cs
+++ b/MediaBrowser.Model/Notifications/NotificationOptions.cs
@@ -1,7 +1,7 @@
#pragma warning disable CS1591
using System;
-using MediaBrowser.Model.Extensions;
+using System.Linq;
using MediaBrowser.Model.Users;
namespace MediaBrowser.Model.Notifications
@@ -81,8 +81,12 @@ namespace MediaBrowser.Model.Notifications
{
foreach (NotificationOption i in Options)
{
- if (string.Equals(type, i.Type, StringComparison.OrdinalIgnoreCase)) return i;
+ if (string.Equals(type, i.Type, StringComparison.OrdinalIgnoreCase))
+ {
+ return i;
+ }
}
+
return null;
}
@@ -98,7 +102,7 @@ namespace MediaBrowser.Model.Notifications
NotificationOption opt = GetOptions(notificationType);
return opt == null ||
- !ListHelper.ContainsIgnoreCase(opt.DisabledServices, service);
+ !opt.DisabledServices.Contains(service, StringComparer.OrdinalIgnoreCase);
}
public bool IsEnabledToMonitorUser(string type, Guid userId)
@@ -106,7 +110,7 @@ namespace MediaBrowser.Model.Notifications
NotificationOption opt = GetOptions(type);
return opt != null && opt.Enabled &&
- !ListHelper.ContainsIgnoreCase(opt.DisabledMonitorUsers, userId.ToString(""));
+ !opt.DisabledMonitorUsers.Contains(userId.ToString(""), StringComparer.OrdinalIgnoreCase);
}
public bool IsEnabledToSendToUser(string type, string userId, UserPolicy userPolicy)
@@ -125,7 +129,7 @@ namespace MediaBrowser.Model.Notifications
return true;
}
- return ListHelper.ContainsIgnoreCase(opt.SendToUsers, userId);
+ return opt.SendToUsers.Contains(userId, StringComparer.OrdinalIgnoreCase);
}
return false;
diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs
index 3d543039e..f2c5aa1e3 100644
--- a/MediaBrowser.Model/System/SystemInfo.cs
+++ b/MediaBrowser.Model/System/SystemInfo.cs
@@ -116,24 +116,6 @@ namespace MediaBrowser.Model.System
public string TranscodingTempPath { get; set; }
/// <summary>
- /// Gets or sets the HTTP server port number.
- /// </summary>
- /// <value>The HTTP server port number.</value>
- public int HttpServerPortNumber { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [enable HTTPS].
- /// </summary>
- /// <value><c>true</c> if [enable HTTPS]; otherwise, <c>false</c>.</value>
- public bool SupportsHttps { get; set; }
-
- /// <summary>
- /// Gets or sets the HTTPS server port number.
- /// </summary>
- /// <value>The HTTPS server port number.</value>
- public int HttpsPortNumber { get; set; }
-
- /// <summary>
/// Gets or sets a value indicating whether this instance has update available.
/// </summary>
/// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value>
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
index 37160dd2c..f0328e8d8 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
@@ -11,13 +11,10 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Plugins.Omdb
{
- public class OmdbEpisodeProvider :
- IRemoteMetadataProvider<Episode, EpisodeInfo>,
- IHasOrder
+ public class OmdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
{
private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClient _httpClient;
@@ -26,16 +23,27 @@ namespace MediaBrowser.Providers.Plugins.Omdb
private readonly IServerConfigurationManager _configurationManager;
private readonly IApplicationHost _appHost;
- public OmdbEpisodeProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClient httpClient, ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
+ public OmdbEpisodeProvider(
+ IJsonSerializer jsonSerializer,
+ IApplicationHost appHost,
+ IHttpClient httpClient,
+ ILibraryManager libraryManager,
+ IFileSystem fileSystem,
+ IServerConfigurationManager configurationManager)
{
_jsonSerializer = jsonSerializer;
_httpClient = httpClient;
_fileSystem = fileSystem;
_configurationManager = configurationManager;
_appHost = appHost;
- _itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClient, logger, libraryManager, fileSystem, configurationManager);
+ _itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClient, libraryManager, fileSystem, configurationManager);
}
+ // After TheTvDb
+ public int Order => 1;
+
+ public string Name => "The Open Movie Database";
+
public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
{
return _itemProvider.GetSearchResults(searchInfo, "episode", cancellationToken);
@@ -66,10 +74,6 @@ namespace MediaBrowser.Providers.Plugins.Omdb
return result;
}
- // After TheTvDb
- public int Order => 1;
-
- public string Name => "The Open Movie Database";
public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
{
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
index 3aadda5d0..64a75955a 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
@@ -17,7 +17,6 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Plugins.Omdb
{
@@ -26,22 +25,27 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClient _httpClient;
- private readonly ILogger _logger;
private readonly ILibraryManager _libraryManager;
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
private readonly IApplicationHost _appHost;
- public OmdbItemProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClient httpClient, ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
+ public OmdbItemProvider(
+ IJsonSerializer jsonSerializer,
+ IApplicationHost appHost,
+ IHttpClient httpClient,
+ ILibraryManager libraryManager,
+ IFileSystem fileSystem,
+ IServerConfigurationManager configurationManager)
{
_jsonSerializer = jsonSerializer;
_httpClient = httpClient;
- _logger = logger;
_libraryManager = libraryManager;
_fileSystem = fileSystem;
_configurationManager = configurationManager;
_appHost = appHost;
}
+
// After primary option
public int Order => 2;
@@ -80,7 +84,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var parsedName = _libraryManager.ParseName(name);
var yearInName = parsedName.Year;
name = parsedName.Name;
- year = year ?? yearInName;
+ year ??= yearInName;
}
if (string.IsNullOrWhiteSpace(imdbId))
@@ -312,6 +316,5 @@ namespace MediaBrowser.Providers.Plugins.Omdb
/// <value>The results.</value>
public List<SearchResult> Search { get; set; }
}
-
}
}
diff --git a/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs
index 7195dc42a..6e3c26c26 100644
--- a/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs
@@ -27,9 +27,6 @@ namespace MediaBrowser.Providers.Tmdb.TV
public class TmdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder
{
private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}?api_key={1}&append_to_response=credits,images,keywords,external_ids,videos,content_ratings";
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
- internal static TmdbSeriesProvider Current { get; private set; }
private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem;
@@ -39,6 +36,10 @@ namespace MediaBrowser.Providers.Tmdb.TV
private readonly IHttpClient _httpClient;
private readonly ILibraryManager _libraryManager;
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+ internal static TmdbSeriesProvider Current { get; private set; }
+
public TmdbSeriesProvider(
IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
@@ -217,10 +218,9 @@ namespace MediaBrowser.Providers.Tmdb.TV
var series = seriesResult.Item;
series.Name = seriesInfo.Name;
+ series.OriginalTitle = seriesInfo.Original_Name;
series.SetProviderId(MetadataProviders.Tmdb, seriesInfo.Id.ToString(_usCulture));
- //series.VoteCount = seriesInfo.vote_count;
-
string voteAvg = seriesInfo.Vote_Average.ToString(CultureInfo.InvariantCulture);
if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out float rating))
@@ -240,7 +240,7 @@ namespace MediaBrowser.Providers.Tmdb.TV
series.Genres = seriesInfo.Genres.Select(i => i.Name).ToArray();
}
- //series.HomePageUrl = seriesInfo.homepage;
+ series.HomePageUrl = seriesInfo.Homepage;
series.RunTimeTicks = seriesInfo.Episode_Run_Time.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault();
@@ -308,29 +308,61 @@ namespace MediaBrowser.Providers.Tmdb.TV
seriesResult.ResetPeople();
var tmdbImageUrl = settings.images.GetImageUrl("original");
- if (seriesInfo.Credits != null && seriesInfo.Credits.Cast != null)
+ if (seriesInfo.Credits != null)
{
- foreach (var actor in seriesInfo.Credits.Cast.OrderBy(a => a.Order))
+ if (seriesInfo.Credits.Cast != null)
{
- var personInfo = new PersonInfo
+ foreach (var actor in seriesInfo.Credits.Cast.OrderBy(a => a.Order))
{
- Name = actor.Name.Trim(),
- Role = actor.Character,
- Type = PersonType.Actor,
- SortOrder = actor.Order
- };
+ var personInfo = new PersonInfo
+ {
+ Name = actor.Name.Trim(),
+ Role = actor.Character,
+ Type = PersonType.Actor,
+ SortOrder = actor.Order
+ };
- if (!string.IsNullOrWhiteSpace(actor.Profile_Path))
- {
- personInfo.ImageUrl = tmdbImageUrl + actor.Profile_Path;
+ if (!string.IsNullOrWhiteSpace(actor.Profile_Path))
+ {
+ personInfo.ImageUrl = tmdbImageUrl + actor.Profile_Path;
+ }
+
+ if (actor.Id > 0)
+ {
+ personInfo.SetProviderId(MetadataProviders.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
+ }
+
+ seriesResult.AddPerson(personInfo);
}
+ }
- if (actor.Id > 0)
+ if (seriesInfo.Credits.Crew != null)
+ {
+ var keepTypes = new[]
{
- personInfo.SetProviderId(MetadataProviders.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
- }
+ PersonType.Director,
+ PersonType.Writer,
+ PersonType.Producer
+ };
- seriesResult.AddPerson(personInfo);
+ foreach (var person in seriesInfo.Credits.Crew)
+ {
+ // Normalize this
+ var type = TmdbUtils.MapCrewToPersonType(person);
+
+ if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
+ && !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ seriesResult.AddPerson(new PersonInfo
+ {
+ Name = person.Name.Trim(),
+ Role = person.Job,
+ Type = type
+ });
+ }
}
}
}
diff --git a/MediaBrowser.Providers/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Tmdb/TmdbUtils.cs
index 035b99c1a..7dacc7404 100644
--- a/MediaBrowser.Providers/Tmdb/TmdbUtils.cs
+++ b/MediaBrowser.Providers/Tmdb/TmdbUtils.cs
@@ -4,18 +4,51 @@ using MediaBrowser.Providers.Tmdb.Models.General;
namespace MediaBrowser.Providers.Tmdb
{
+ /// <summary>
+ /// Utilities for the TMDb provider
+ /// </summary>
public static class TmdbUtils
{
+ /// <summary>
+ /// URL of the TMDB instance to use.
+ /// </summary>
public const string BaseTmdbUrl = "https://www.themoviedb.org/";
+
+ /// <summary>
+ /// URL of the TMDB API instance to use.
+ /// </summary>
public const string BaseTmdbApiUrl = "https://api.themoviedb.org/";
+
+ /// <summary>
+ /// Name of the provider.
+ /// </summary>
public const string ProviderName = "TheMovieDb";
+
+ /// <summary>
+ /// API key to use when performing an API call.
+ /// </summary>
public const string ApiKey = "4219e299c89411838049ab0dab19ebd5";
+
+ /// <summary>
+ /// Value of the Accept header for requests to the provider.
+ /// </summary>
public const string AcceptHeader = "application/json,image/*";
+ /// <summary>
+ /// Maps the TMDB provided roles for crew members to Jellyfin roles.
+ /// </summary>
+ /// <param name="crew">Crew member to map against the Jellyfin person types.</param>
+ /// <returns>The Jellyfin person type.</returns>
public static string MapCrewToPersonType(Crew crew)
{
if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase)
- && crew.Job.IndexOf("producer", StringComparison.InvariantCultureIgnoreCase) != -1)
+ && crew.Job.Contains("director", StringComparison.InvariantCultureIgnoreCase))
+ {
+ return PersonType.Director;
+ }
+
+ if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase)
+ && crew.Job.Contains("producer", StringComparison.InvariantCultureIgnoreCase))
{
return PersonType.Producer;
}
diff --git a/MediaBrowser.sln b/MediaBrowser.sln
index 1c84622ac..6b9fed83b 100644
--- a/MediaBrowser.sln
+++ b/MediaBrowser.sln
@@ -1,7 +1,9 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
+Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.3
MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Controller", "MediaBrowser.Controller\MediaBrowser.Controller.csproj", "{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Api", "MediaBrowser.Api\MediaBrowser.Api.csproj", "{4FD51AC5-2C16-4308-A993-C3A84F3B4582}"
@@ -36,31 +38,38 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Naming", "Emby.Naming\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.MediaEncoding", "MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj", "{960295EE-4AF4-4440-A525-B4C295B01A61}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{41093F42-C7CC-4D07-956B-6182CBEDE2EC}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
+ jellyfin.ruleset = jellyfin.ruleset
SharedVersion.cs = SharedVersion.cs
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing.Skia", "Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj", "{154872D9-6C12-4007-96E3-8F70A58386CE}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Api", "Jellyfin.Api\Jellyfin.Api.csproj", "{DFBEFB4C-DA19-4143-98B7-27320C7F7163}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Api", "Jellyfin.Api\Jellyfin.Api.csproj", "{DFBEFB4C-DA19-4143-98B7-27320C7F7163}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}"
+ ProjectSection(SolutionItems) = preProject
+ tests\jellyfin-tests.ruleset = tests\jellyfin-tests.ruleset
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Common.Tests", "tests\Jellyfin.Common.Tests\Jellyfin.Common.Tests.csproj", "{DF194677-DFD3-42AF-9F75-D44D5A416478}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.MediaEncoding.Tests", "tests\Jellyfin.MediaEncoding.Tests\Jellyfin.MediaEncoding.Tests.csproj", "{28464062-0939-4AA7-9F7B-24DDDA61A7C0}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Common.Tests", "tests\Jellyfin.Common.Tests\Jellyfin.Common.Tests.csproj", "{DF194677-DFD3-42AF-9F75-D44D5A416478}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Naming.Tests", "tests\Jellyfin.Naming.Tests\Jellyfin.Naming.Tests.csproj", "{3998657B-1CCC-49DD-A19F-275DC8495F57}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.MediaEncoding.Tests", "tests\Jellyfin.MediaEncoding.Tests\Jellyfin.MediaEncoding.Tests.csproj", "{28464062-0939-4AA7-9F7B-24DDDA61A7C0}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Api.Tests", "tests\Jellyfin.Api.Tests\Jellyfin.Api.Tests.csproj", "{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Naming.Tests", "tests\Jellyfin.Naming.Tests\Jellyfin.Naming.Tests.csproj", "{3998657B-1CCC-49DD-A19F-275DC8495F57}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementations.Tests", "tests\Jellyfin.Server.Implementations.Tests\Jellyfin.Server.Implementations.Tests.csproj", "{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Api.Tests", "tests\Jellyfin.Api.Tests\Jellyfin.Api.Tests.csproj", "{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Controller.Tests", "tests\Jellyfin.Controller.Tests\Jellyfin.Controller.Tests.csproj", "{462584F7-5023-4019-9EAC-B98CA458C0A0}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Implementations.Tests", "tests\Jellyfin.Server.Implementations.Tests\Jellyfin.Server.Implementations.Tests.csproj", "{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Data", "Jellyfin.Data\Jellyfin.Data.csproj", "{F03299F2-469F-40EF-A655-3766F97A5702}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Controller.Tests", "tests\Jellyfin.Controller.Tests\Jellyfin.Controller.Tests.csproj", "{462584F7-5023-4019-9EAC-B98CA458C0A0}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Api.Tests", "tests\MediaBrowser.Api.Tests\MediaBrowser.Api.Tests.csproj", "{7C93C84F-105C-48E5-A878-406FA0A5B296}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -112,10 +121,6 @@ Global
{713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.Build.0 = Release|Any CPU
- {88AE38DF-19D7-406F-A6A9-09527719A21E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {88AE38DF-19D7-406F-A6A9-09527719A21E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {88AE38DF-19D7-406F-A6A9-09527719A21E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {88AE38DF-19D7-406F-A6A9-09527719A21E}.Release|Any CPU.Build.0 = Release|Any CPU
{E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E383961B-9356-4D5D-8233-9A1079D03055}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -176,6 +181,18 @@ Global
{462584F7-5023-4019-9EAC-B98CA458C0A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{462584F7-5023-4019-9EAC-B98CA458C0A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{462584F7-5023-4019-9EAC-B98CA458C0A0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F03299F2-469F-40EF-A655-3766F97A5702}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F03299F2-469F-40EF-A655-3766F97A5702}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F03299F2-469F-40EF-A655-3766F97A5702}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F03299F2-469F-40EF-A655-3766F97A5702}.Release|Any CPU.Build.0 = Release|Any CPU
+ {22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7C93C84F-105C-48E5-A878-406FA0A5B296}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7C93C84F-105C-48E5-A878-406FA0A5B296}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7C93C84F-105C-48E5-A878-406FA0A5B296}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7C93C84F-105C-48E5-A878-406FA0A5B296}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -208,5 +225,6 @@ Global
{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+ {7C93C84F-105C-48E5-A878-406FA0A5B296} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
EndGlobalSection
EndGlobal
diff --git a/SharedVersion.cs b/SharedVersion.cs
index d741f379d..6981c1ca9 100644
--- a/SharedVersion.cs
+++ b/SharedVersion.cs
@@ -1,4 +1,4 @@
using System.Reflection;
-[assembly: AssemblyVersion("10.5.0")]
-[assembly: AssemblyFileVersion("10.5.0")]
+[assembly: AssemblyVersion("10.6.0")]
+[assembly: AssemblyFileVersion("10.6.0")]
diff --git a/build b/build
index 95d5d5c49..c07a74de4 100755..120000
--- a/build
+++ b/build
@@ -1,197 +1 @@
-#!/usr/bin/env bash
-
-# build - build Jellyfin binaries or packages
-
-set -o errexit
-set -o pipefail
-
-# The list of possible package actions (except 'clean')
-declare -a actions=( 'build' 'package' 'sign' 'publish' )
-
-# The list of possible platforms, based on directories under 'deployment/'
-declare -a platforms=( $(
- find deployment/ -maxdepth 1 -mindepth 1 -type d -exec basename {} \; | sort
-) )
-
-# The list of standard dependencies required by all build scripts; individual
-# action scripts may specify their own dependencies
-declare -a dependencies=( 'tar' 'zip' )
-
-usage() {
- echo -e "build - build Jellyfin binaries or packages"
- echo -e ""
- echo -e "Usage:"
- echo -e " $ build --list-platforms"
- echo -e " $ build --list-actions <platform>"
- echo -e " $ build [-k/--keep-artifacts] [-b/--web-branch <web_branch>] <platform> <action>"
- echo -e ""
- echo -e "The 'keep-artifacts' option preserves build artifacts, e.g. Docker images for system package builds."
- echo -e "The web_branch defaults to the same branch name as the current main branch or can be 'local' to not touch the submodule branching."
- echo -e "To build all platforms, use 'all'."
- echo -e "To perform all build actions, use 'all'."
- echo -e "Build output files are collected at '../bin/<platform>'."
-}
-
-# Show usage on stderr with exit 1 on argless
-if [[ -z $1 ]]; then
- usage >&2
- exit 1
-fi
-# Show usage if -h or --help are specified in the args
-if [[ $@ =~ '-h' || $@ =~ '--help' ]]; then
- usage
- exit 0
-fi
-
-# List all available platforms then exit
-if [[ $1 == '--list-platforms' ]]; then
- echo -e "Available platforms:"
- for platform in ${platforms[@]}; do
- echo -e " ${platform}"
- done
- exit 0
-fi
-
-# List all available actions for a given platform then exit
-if [[ $1 == '--list-actions' ]]; then
- platform="$2"
- if [[ ! " ${platforms[@]} " =~ " ${platform} " ]]; then
- echo "ERROR: Platform ${platform} does not exist."
- exit 1
- fi
- echo -e "Available actions for platform ${platform}:"
- for action in ${actions[@]}; do
- if [[ -f deployment/${platform}/${action}.sh ]]; then
- echo -e " ${action}"
- fi
- done
- exit 0
-fi
-
-# Parse keep-artifacts option
-if [[ $1 == '-k' || $1 == '--keep-artifacts' ]]; then
- keep_artifacts="y"
- shift 1
-else
- keep_artifacts="n"
-fi
-
-# Parse branch option
-if [[ $1 == '-b' || $1 == '--web-branch' ]]; then
- web_branch="$2"
- shift 2
-else
- web_branch="$( git branch 2>/dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/' )"
-fi
-
-# Parse platform option
-if [[ -n $1 ]]; then
- cli_platform="$1"
- shift
-else
- echo "ERROR: A platform must be specified. Use 'all' to specify all platforms."
- exit 1
-fi
-if [[ ${cli_platform} == 'all' ]]; then
- declare -a platform=( ${platforms[@]} )
-else
- if [[ ! " ${platforms[@]} " =~ " ${cli_platform} " ]]; then
- echo "ERROR: Platform ${cli_platform} is invalid. Use the '--list-platforms' option to list available platforms."
- exit 1
- else
- declare -a platform=( "${cli_platform}" )
- fi
-fi
-
-# Parse action option
-if [[ -n $1 ]]; then
- cli_action="$1"
- shift
-else
- echo "ERROR: An action must be specified. Use 'all' to specify all actions."
- exit 1
-fi
-if [[ ${cli_action} == 'all' ]]; then
- declare -a action=( ${actions[@]} )
-else
- if [[ ! " ${actions[@]} " =~ " ${cli_action} " ]]; then
- echo "ERROR: Action ${cli_action} is invalid. Use the '--list-actions <platform>' option to list available actions."
- exit 1
- else
- declare -a action=( "${cli_action}" )
- fi
-fi
-
-# Verify required utilities are installed
-missing_deps=()
-for utility in ${dependencies[@]}; do
- if ! which ${utility} &>/dev/null; then
- missing_deps+=( ${utility} )
- fi
-done
-
-# Error if we're missing anything
-if [[ ${#missing_deps[@]} -gt 0 ]]; then
- echo -e "ERROR: This script requires the following missing utilities:"
- for utility in ${missing_deps[@]}; do
- echo -e " ${utility}"
- done
- exit 1
-fi
-
-# Parse platform-specific dependencies
-for target_platform in ${platform[@]}; do
- # Read platform-specific dependencies
- if [[ -f deployment/${target_platform}/dependencies.txt ]]; then
- platform_dependencies="$( grep -v '^#' deployment/${target_platform}/dependencies.txt )"
-
- # Verify required utilities are installed
- missing_deps=()
- for utility in ${platform_dependencies[@]}; do
- if ! which ${utility} &>/dev/null; then
- missing_deps+=( ${utility} )
- fi
- done
-
- # Error if we're missing anything
- if [[ ${#missing_deps[@]} -gt 0 ]]; then
- echo -e "ERROR: The ${target_platform} platform requires the following utilities:"
- for utility in ${missing_deps[@]}; do
- echo -e " ${utility}"
- done
- exit 1
- fi
- fi
-done
-
-# Execute each platform and action in order, if said action is enabled
-pushd deployment/
-for target_platform in ${platform[@]}; do
- echo -e "> Processing platform ${target_platform}"
- date_start=$( date +%s )
- pushd ${target_platform}
- cleanup() {
- echo -e ">> Processing action clean"
- if [[ -f clean.sh && -x clean.sh ]]; then
- ./clean.sh ${keep_artifacts}
- fi
- }
- trap cleanup EXIT INT
- for target_action in ${action[@]}; do
- echo -e ">> Processing action ${target_action}"
- if [[ -f ${target_action}.sh && -x ${target_action}.sh ]]; then
- ./${target_action}.sh web_branch=${web_branch}
- fi
- done
- if [[ -d pkg-dist/ ]]; then
- echo -e ">> Collecting build artifacts"
- target_dir="../../../bin/${target_platform}"
- mkdir -p ${target_dir}
- mv pkg-dist/* ${target_dir}/
- fi
- cleanup
- date_end=$( date +%s )
- echo -e "> Completed platform ${target_platform} in $( expr ${date_end} - ${date_start} ) seconds."
- popd
-done
-popd
+build.sh \ No newline at end of file
diff --git a/build.sh b/build.sh
new file mode 100755
index 000000000..1db02af98
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,114 @@
+#!/usr/bin/env bash
+
+# build.sh - Build Jellyfin binary packages
+# Part of the Jellyfin Project
+
+set -o errexit
+set -o pipefail
+
+usage() {
+ echo -e "build.sh - Build Jellyfin binary packages"
+ echo -e "Usage:"
+ echo -e " $0 -t/--type <BUILD_TYPE> -p/--platform <PLATFORM> [-k/--keep-artifacts] [-l/--list-platforms]"
+ echo -e "Notes:"
+ echo -e " * BUILD_TYPE can be one of: [native, docker] and must be specified"
+ echo -e " * native: Build using the build script in the host OS"
+ echo -e " * docker: Build using the build script in a standardized Docker container"
+ echo -e " * PLATFORM can be any platform shown by -l/--list-platforms and must be specified"
+ echo -e " * If -k/--keep-artifacts is specified, transient artifacts (e.g. Docker containers) will be"
+ echo -e " retained after the build is finished; the source directory will still be cleaned"
+ echo -e " * If -l/--list-platforms is specified, all other arguments are ignored; the script will print"
+ echo -e " the list of supported platforms and exit"
+}
+
+list_platforms() {
+ declare -a platforms
+ platforms=(
+ $( find deployment -maxdepth 1 -mindepth 1 -name "build.*" | awk -F'.' '{ $1=""; printf $2; if ($3 != ""){ printf "." $3; }; if ($4 != ""){ printf "." $4; }; print ""; }' | sort )
+ )
+ echo -e "Valid platforms:"
+ echo
+ for platform in ${platforms[@]}; do
+ echo -e "* ${platform} : $( grep '^#=' deployment/build.${platform} | sed 's/^#= //' )"
+ done
+}
+
+do_build_native() {
+ if [[ ! -f $( which dpkg ) || $( dpkg --print-architecture | head -1 ) != "${PLATFORM##*.}" ]]; then
+ echo "Cross-building is not supported for native builds, use 'docker' builds on amd64 for cross-building."
+ exit 1
+ fi
+ export IS_DOCKER=NO
+ deployment/build.${PLATFORM}
+}
+
+do_build_docker() {
+ if [[ -f $( which dpkg ) && $( dpkg --print-architecture | head -1 ) != "amd64" ]]; then
+ echo "Docker-based builds only support amd64-based cross-building; use a 'native' build instead."
+ exit 1
+ fi
+ if [[ ! -f deployment/Dockerfile.${PLATFORM} ]]; then
+ echo "Missing Dockerfile for platform ${PLATFORM}"
+ exit 1
+ fi
+ if [[ ${KEEP_ARTIFACTS} == YES ]]; then
+ docker_args=""
+ else
+ docker_args="--rm"
+ fi
+
+ docker build . -t "jellyfin-builder.${PLATFORM}" -f deployment/Dockerfile.${PLATFORM}
+ mkdir -p ${ARTIFACT_DIR}
+ docker run $docker_args -v "${SOURCE_DIR}:/jellyfin" -v "${ARTIFACT_DIR}:/dist" "jellyfin-builder.${PLATFORM}"
+}
+
+while [[ $# -gt 0 ]]; do
+ key="$1"
+ case $key in
+ -t|--type)
+ BUILD_TYPE="$2"
+ shift # past argument
+ shift # past value
+ ;;
+ -p|--platform)
+ PLATFORM="$2"
+ shift # past argument
+ shift # past value
+ ;;
+ -k|--keep-artifacts)
+ KEEP_ARTIFACTS=YES
+ shift # past argument
+ ;;
+ -l|--list-platforms)
+ list_platforms
+ exit 0
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ *) # unknown option
+ echo "Unknown option $1"
+ usage
+ exit 1
+ ;;
+ esac
+done
+
+if [[ -z ${BUILD_TYPE} || -z ${PLATFORM} ]]; then
+ usage
+ exit 1
+fi
+
+export SOURCE_DIR="$( pwd )"
+export ARTIFACT_DIR="${SOURCE_DIR}/../bin/${PLATFORM}"
+
+# Determine build type
+case ${BUILD_TYPE} in
+ native)
+ do_build_native
+ ;;
+ docker)
+ do_build_docker
+ ;;
+esac
diff --git a/build.yaml b/build.yaml
index 123f77fb8..9e590e5a0 100644
--- a/build.yaml
+++ b/build.yaml
@@ -1,18 +1,17 @@
---
# We just wrap `build` so this is really it
name: "jellyfin"
-version: "10.5.0"
+version: "10.6.0"
packages:
- - debian-package-x64
- - debian-package-armhf
- - debian-package-arm64
- - ubuntu-package-x64
- - ubuntu-package-armhf
- - ubuntu-package-arm64
- - fedora-package-x64
- - centos-package-x64
- - linux-x64
+ - debian.amd64
+ - debian.arm64
+ - debian.armhf
+ - ubuntu.amd64
+ - ubuntu.arm64
+ - ubuntu.armhf
+ - fedora.amd64
+ - centos.amd64
+ - linux.amd64
+ - windows.amd64
- macos
- portable
- - win-x64
- - win-x86
diff --git a/bump_version b/bump_version
index 106dd7a78..46b7f86e0 100755
--- a/bump_version
+++ b/bump_version
@@ -10,10 +10,6 @@ usage() {
echo -e ""
echo -e "Usage:"
echo -e " $ bump_version <new_version>"
- echo -e ""
- echo -e "The web_branch defaults to the same branch name as the current main branch."
- echo -e "This helps facilitate releases where both branches would be called release-X.Y.Z"
- echo -e "and would already be created before running this script."
}
if [[ -z $1 ]]; then
@@ -54,37 +50,26 @@ else
new_version_deb="${new_version}-1"
fi
-# Set the Dockerfile web version to the specified new_version
-old_version="$(
- grep "JELLYFIN_WEB_VERSION=" Dockerfile \
- | sed -E 's/ARG JELLYFIN_WEB_VERSION=v([0-9\.]+[-a-z0-9]*)/\1/'
-)"
-echo $old_version
-
-old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
-sed -i "s/${old_version_sed}/${new_version}/g" Dockerfile*
+# Update the metapackage equivs file
+debian_equivs_file="debian/metapackage/jellyfin"
+sed -i "s/${old_version_sed}/${new_version}/g" ${debian_equivs_file}
# Write out a temporary Debian changelog with our new stuff appended and some templated formatting
-debian_changelog_file="deployment/debian-package-x64/pkg-src/changelog"
+debian_changelog_file="debian/changelog"
debian_changelog_temp="$( mktemp )"
# Create new temp file with our changelog
-echo -e "### DEBIAN PACKAGE CHANGELOG: Verify this file looks correct or edit accordingly, then delete this line, write, and exit.
-jellyfin (${new_version_deb}) unstable; urgency=medium
+echo -e "jellyfin (${new_version_deb}) unstable; urgency=medium
* New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version}
-- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 )
" >> ${debian_changelog_temp}
cat ${debian_changelog_file} >> ${debian_changelog_temp}
-# Edit the file to verify
-$EDITOR ${debian_changelog_temp}
# Move into place
mv ${debian_changelog_temp} ${debian_changelog_file}
-# Clean up
-rm -f ${debian_changelog_temp}
# Write out a temporary Yum changelog with our new stuff prepended and some templated formatting
-fedora_spec_file="deployment/fedora-package-x64/pkg-src/jellyfin.spec"
+fedora_spec_file="fedora/jellyfin.spec"
fedora_changelog_temp="$( mktemp )"
fedora_spec_temp_dir="$( mktemp -d )"
fedora_spec_temp="${fedora_spec_temp_dir}/jellyfin.spec.tmp"
@@ -98,21 +83,18 @@ sed -i "s/${old_version_sed}/${new_version_sed}/g" xx00
# Remove the header from xx01
sed -i '/^%changelog/d' xx01
# Create new temp file with our changelog
-echo -e "### YUM SPEC CHANGELOG: Verify this file looks correct or edit accordingly, then delete this line, write, and exit.
-%changelog
+echo -e "%changelog
* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version}" >> ${fedora_changelog_temp}
cat xx01 >> ${fedora_changelog_temp}
-# Edit the file to verify
-$EDITOR ${fedora_changelog_temp}
# Reassembble
cat xx00 ${fedora_changelog_temp} > ${fedora_spec_temp}
popd
# Move into place
mv ${fedora_spec_temp} ${fedora_spec_file}
# Clean up
-rm -rf ${fedora_changelog_temp} ${fedora_spec_temp_dir}
+rm -rf ${fedora_spec_temp_dir}
# Stage the changed files for commit
-git add ${shared_version_file} ${build_file} ${debian_changelog_file} ${fedora_spec_file} Dockerfile*
+git add ${shared_version_file} ${build_file} ${debian_equivs_file} ${debian_changelog_file} ${fedora_spec_file}
git status
diff --git a/deployment/debian-package-x64/pkg-src/bin/restart.sh b/debian/bin/restart.sh
index 9b64b6d72..9b64b6d72 100755
--- a/deployment/debian-package-x64/pkg-src/bin/restart.sh
+++ b/debian/bin/restart.sh
diff --git a/deployment/debian-package-x64/pkg-src/changelog b/debian/changelog
index 51c482237..35fb65957 100644
--- a/deployment/debian-package-x64/pkg-src/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+jellyfin-server (10.6.0-1) unstable; urgency=medium
+
+ * Forthcoming stable release
+
+ -- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 23 Mar 2020 14:46:05 -0400
+
jellyfin (10.5.0-1) unstable; urgency=medium
* New upstream version 10.5.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.5.0
diff --git a/deployment/debian-package-x64/pkg-src/compat b/debian/compat
index 45a4fb75d..45a4fb75d 100644
--- a/deployment/debian-package-x64/pkg-src/compat
+++ b/debian/compat
diff --git a/deployment/debian-package-x64/pkg-src/conf/jellyfin b/debian/conf/jellyfin
index c6e595f15..64c98520c 100644
--- a/deployment/debian-package-x64/pkg-src/conf/jellyfin
+++ b/debian/conf/jellyfin
@@ -18,6 +18,9 @@ JELLYFIN_CONFIG_DIR="/etc/jellyfin"
JELLYFIN_LOG_DIR="/var/log/jellyfin"
JELLYFIN_CACHE_DIR="/var/cache/jellyfin"
+# web client path, installed by the jellyfin-web package
+JELLYFIN_WEB_OPT="--webdir=/usr/share/jellyfin/web"
+
# Restart script for in-app server control
JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh"
@@ -37,4 +40,4 @@ JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
# Application username
JELLYFIN_USER="jellyfin"
# Full application command
-JELLYFIN_ARGS="$JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLFIN_NOWEBAPP_OPT"
+JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLFIN_NOWEBAPP_OPT"
diff --git a/deployment/debian-package-x64/pkg-src/conf/jellyfin-sudoers b/debian/conf/jellyfin-sudoers
index b481ba4ad..b481ba4ad 100644
--- a/deployment/debian-package-x64/pkg-src/conf/jellyfin-sudoers
+++ b/debian/conf/jellyfin-sudoers
diff --git a/deployment/debian-package-x64/pkg-src/conf/jellyfin.service.conf b/debian/conf/jellyfin.service.conf
index 1b69dd74e..1b69dd74e 100644
--- a/deployment/debian-package-x64/pkg-src/conf/jellyfin.service.conf
+++ b/debian/conf/jellyfin.service.conf
diff --git a/deployment/debian-package-x64/pkg-src/conf/logging.json b/debian/conf/logging.json
index f32b2089e..f32b2089e 100644
--- a/deployment/debian-package-x64/pkg-src/conf/logging.json
+++ b/debian/conf/logging.json
diff --git a/deployment/debian-package-x64/pkg-src/control b/debian/control
index 13fd3ecab..896d8286b 100644
--- a/deployment/debian-package-x64/pkg-src/control
+++ b/debian/control
@@ -1,4 +1,4 @@
-Source: jellyfin
+Source: jellyfin-server
Section: misc
Priority: optional
Maintainer: Jellyfin Team <team@jellyfin.org>
@@ -8,24 +8,23 @@ Build-Depends: debhelper (>= 9),
libcurl4-openssl-dev,
libfontconfig1-dev,
libfreetype6-dev,
- libssl-dev,
- wget,
- npm | nodejs
+ libssl-dev
Standards-Version: 3.9.4
-Homepage: https://jellyfin.media/
+Homepage: https://jellyfin.org/
Vcs-Git: https://github.org/jellyfin/jellyfin.git
Vcs-Browser: https://github.org/jellyfin/jellyfin
-Package: jellyfin
+Package: jellyfin-server
Replaces: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server
Breaks: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server
Conflicts: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server
Architecture: any
Depends: at,
libsqlite3-0,
- jellyfin-ffmpeg,
+ jellyfin-ffmpeg (>= 4.2.1-2),
libfontconfig1,
libfreetype6,
libssl1.1
-Description: Jellyfin is a home media server.
- It is built on top of other popular open source technologies such as Service Stack, jQuery, jQuery mobile, and Mono. It features a REST-based api with built-in documentation to facilitate client development. We also have client libraries for our api to enable rapid development.
+Recommends: jellyfin-web
+Description: Jellyfin is the Free Software Media System.
+ This package provides the Jellyfin server backend and API.
diff --git a/deployment/debian-package-x64/pkg-src/copyright b/debian/copyright
index 0d7a2a600..0d7a2a600 100644
--- a/deployment/debian-package-x64/pkg-src/copyright
+++ b/debian/copyright
diff --git a/deployment/debian-package-x64/pkg-src/gbp.conf b/debian/gbp.conf
index 60b3d2872..60b3d2872 100644
--- a/deployment/debian-package-x64/pkg-src/gbp.conf
+++ b/debian/gbp.conf
diff --git a/deployment/debian-package-x64/pkg-src/install b/debian/install
index 994322d14..994322d14 100644
--- a/deployment/debian-package-x64/pkg-src/install
+++ b/debian/install
diff --git a/deployment/debian-package-x64/pkg-src/jellyfin.init b/debian/jellyfin.init
index 7f5642bac..7f5642bac 100644
--- a/deployment/debian-package-x64/pkg-src/jellyfin.init
+++ b/debian/jellyfin.init
diff --git a/deployment/debian-package-x64/pkg-src/jellyfin.service b/debian/jellyfin.service
index 1305e238b..f1a8f4652 100644
--- a/deployment/debian-package-x64/pkg-src/jellyfin.service
+++ b/debian/jellyfin.service
@@ -6,7 +6,7 @@ After = network.target
Type = simple
EnvironmentFile = /etc/default/jellyfin
User = jellyfin
-ExecStart = /usr/bin/jellyfin ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT}
+ExecStart = /usr/bin/jellyfin ${JELLYFIN_WEB_OPT} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT}
Restart = on-failure
TimeoutSec = 15
diff --git a/deployment/debian-package-x64/pkg-src/jellyfin.upstart b/debian/jellyfin.upstart
index ef5bc9bca..ef5bc9bca 100644
--- a/deployment/debian-package-x64/pkg-src/jellyfin.upstart
+++ b/debian/jellyfin.upstart
diff --git a/debian/metapackage/jellyfin b/debian/metapackage/jellyfin
new file mode 100644
index 000000000..9a41eafae
--- /dev/null
+++ b/debian/metapackage/jellyfin
@@ -0,0 +1,13 @@
+Source: jellyfin
+Section: misc
+Priority: optional
+Homepage: https://jellyfin.org
+Standards-Version: 3.9.2
+
+Package: jellyfin
+Version: 10.6.0
+Maintainer: Jellyfin Packaging Team <packaging@jellyfin.org>
+Depends: jellyfin-server, jellyfin-web
+Description: Provides the Jellyfin Free Software Media System
+ Provides the full Jellyfin experience, including both the server and web interface.
+
diff --git a/deployment/debian-package-x64/pkg-src/po/POTFILES.in b/debian/po/POTFILES.in
index cef83a340..cef83a340 100644
--- a/deployment/debian-package-x64/pkg-src/po/POTFILES.in
+++ b/debian/po/POTFILES.in
diff --git a/deployment/debian-package-x64/pkg-src/po/templates.pot b/debian/po/templates.pot
index 2cdcae417..2cdcae417 100644
--- a/deployment/debian-package-x64/pkg-src/po/templates.pot
+++ b/debian/po/templates.pot
diff --git a/deployment/debian-package-x64/pkg-src/postinst b/debian/postinst
index 860222e05..860222e05 100644
--- a/deployment/debian-package-x64/pkg-src/postinst
+++ b/debian/postinst
diff --git a/deployment/debian-package-x64/pkg-src/postrm b/debian/postrm
index 1d00a984e..1d00a984e 100644
--- a/deployment/debian-package-x64/pkg-src/postrm
+++ b/debian/postrm
diff --git a/deployment/debian-package-x64/pkg-src/preinst b/debian/preinst
index 2713fb9b8..2713fb9b8 100644
--- a/deployment/debian-package-x64/pkg-src/preinst
+++ b/debian/preinst
diff --git a/deployment/debian-package-x64/pkg-src/prerm b/debian/prerm
index e965cb7d7..e965cb7d7 100644
--- a/deployment/debian-package-x64/pkg-src/prerm
+++ b/debian/prerm
diff --git a/deployment/debian-package-x64/pkg-src/rules b/debian/rules
index c2d57dfb2..2a5d41a69 100755
--- a/deployment/debian-package-x64/pkg-src/rules
+++ b/debian/rules
@@ -2,8 +2,6 @@
CONFIG := Release
TERM := xterm
SHELL := /bin/bash
-WEB_TARGET := $(CURDIR)/MediaBrowser.WebDashboard/jellyfin-web
-WEB_VERSION := $(shell sed -n -e 's/^version: "\(.*\)"/\1/p' $(CURDIR)/build.yaml)
HOST_ARCH := $(shell arch)
BUILD_ARCH := ${DEB_HOST_MULTIARCH}
@@ -41,25 +39,12 @@ override_dh_auto_test:
override_dh_clistrip:
override_dh_auto_build:
- echo $(WEB_VERSION)
- # Clone down and build Web frontend
- mkdir -p $(WEB_TARGET)
- wget -O web-src.tgz https://github.com/jellyfin/jellyfin-web/archive/v$(WEB_VERSION).tar.gz || wget -O web-src.tgz https://github.com/jellyfin/jellyfin-web/archive/master.tar.gz
- mkdir -p $(CURDIR)/web
- tar -xzf web-src.tgz -C $(CURDIR)/web/ --strip 1
- cd $(CURDIR)/web/ && npm install yarn
- cd $(CURDIR)/web/ && node_modules/yarn/bin/yarn install
- mv $(CURDIR)/web/dist/* $(WEB_TARGET)/
- # Build the application
dotnet publish --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) \
"-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server
override_dh_auto_clean:
dotnet clean -maxcpucount:1 --configuration $(CONFIG) Jellyfin.Server || true
- rm -f '$(CURDIR)/web-src.tgz'
rm -rf '$(CURDIR)/usr'
- rm -rf '$(CURDIR)/web'
- rm -rf '$(WEB_TARGET)'
# Force the service name to jellyfin even if we're building jellyfin-nightly
override_dh_installinit:
diff --git a/deployment/debian-package-x64/pkg-src/source.lintian-overrides b/debian/source.lintian-overrides
index aeb332f13..aeb332f13 100644
--- a/deployment/debian-package-x64/pkg-src/source.lintian-overrides
+++ b/debian/source.lintian-overrides
diff --git a/deployment/debian-package-x64/pkg-src/source/format b/debian/source/format
index d3827e75a..d3827e75a 100644
--- a/deployment/debian-package-x64/pkg-src/source/format
+++ b/debian/source/format
diff --git a/deployment/debian-package-x64/pkg-src/source/options b/debian/source/options
index 17b5373d5..17b5373d5 100644
--- a/deployment/debian-package-x64/pkg-src/source/options
+++ b/debian/source/options
diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64
new file mode 100644
index 000000000..39788cc0e
--- /dev/null
+++ b/deployment/Dockerfile.centos.amd64
@@ -0,0 +1,32 @@
+FROM centos:7
+# Docker build arguments
+ARG SOURCE_DIR=/jellyfin
+ARG ARTIFACT_DIR=/dist
+ARG SDK_VERSION=3.1
+# Docker run environment
+ENV SOURCE_DIR=/jellyfin
+ENV ARTIFACT_DIR=/dist
+ENV IS_DOCKER=YES
+
+# Prepare CentOS environment
+RUN yum update -y \
+ && yum install -y epel-release \
+ && yum install -y @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git
+
+# Install DotNET SDK
+RUN rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm \
+ && rpmdev-setuptree \
+ && yum install -y dotnet-sdk-${SDK_VERSION}
+
+# Create symlinks and directories
+RUN ln -sf ${SOURCE_DIR}/deployment/build.centos.amd64 /build.sh \
+ && mkdir -p ${SOURCE_DIR}/SPECS \
+ && ln -s ${SOURCE_DIR}/fedora/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \
+ && mkdir -p ${SOURCE_DIR}/SOURCES \
+ && ln -s ${SOURCE_DIR}/fedora ${SOURCE_DIR}/SOURCES
+
+VOLUME ${SOURCE_DIR}/
+
+VOLUME ${ARTIFACT_DIR}/
+
+ENTRYPOINT ["/build.sh"]
diff --git a/deployment/win-x64/Dockerfile b/deployment/Dockerfile.debian.amd64
index 8a3374954..b5a038048 100644
--- a/deployment/win-x64/Dockerfile
+++ b/deployment/Dockerfile.debian.amd64
@@ -1,7 +1,6 @@
FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
-ARG PLATFORM_DIR=/jellyfin/deployment/win-x64
ARG ARTIFACT_DIR=/dist
ARG SDK_VERSION=3.1
# Docker run environment
@@ -9,10 +8,11 @@ ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV DEB_BUILD_OPTIONS=noddebs
ENV ARCH=amd64
+ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 zip
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
@@ -21,17 +21,11 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
-# Install yarn package manager
-RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
- && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
- && apt update \
- && apt install -y yarn
+# Link to build script
+RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.amd64 /build.sh
-# Link to docker-build script
-RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
+VOLUME ${SOURCE_DIR}/
VOLUME ${ARTIFACT_DIR}/
-COPY . ${SOURCE_DIR}/
-
-ENTRYPOINT ["/docker-build.sh"]
+ENTRYPOINT ["/build.sh"]
diff --git a/deployment/debian-package-arm64/Dockerfile.amd64 b/deployment/Dockerfile.debian.arm64
index b63e08b7d..cfe562df3 100644
--- a/deployment/debian-package-arm64/Dockerfile.amd64
+++ b/deployment/Dockerfile.debian.arm64
@@ -1,7 +1,6 @@
FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
-ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-arm64
ARG ARTIFACT_DIR=/dist
ARG SDK_VERSION=3.1
# Docker run environment
@@ -9,10 +8,11 @@ ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV DEB_BUILD_OPTIONS=noddebs
ENV ARCH=amd64
+ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget npm devscripts mmv
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
@@ -29,15 +29,11 @@ RUN dpkg --add-architecture arm64 \
&& cd cross-gcc-packages-amd64/cross-gcc-8-arm64 \
&& apt-get install -y gcc-8-source libstdc++-8-dev-arm64-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 libfreetype6-dev:arm64 libssl-dev:arm64 liblttng-ust0:arm64 libstdc++-8-dev:arm64
-# Link to docker-build script
-RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
+# Link to build script
+RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.arm64 /build.sh
-# Link to Debian source dir; mkdir needed or it fails, can't force dest
-RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
+VOLUME ${SOURCE_DIR}/
VOLUME ${ARTIFACT_DIR}/
-COPY . ${SOURCE_DIR}/
-
-ENTRYPOINT ["/docker-build.sh"]
-#ENTRYPOINT ["/bin/bash"]
+ENTRYPOINT ["/build.sh"]
diff --git a/deployment/debian-package-armhf/Dockerfile.amd64 b/deployment/Dockerfile.debian.armhf
index 1b64b5314..ea8c8c8e6 100644
--- a/deployment/debian-package-armhf/Dockerfile.amd64
+++ b/deployment/Dockerfile.debian.armhf
@@ -1,7 +1,6 @@
FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
-ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-armhf
ARG ARTIFACT_DIR=/dist
ARG SDK_VERSION=3.1
# Docker run environment
@@ -9,10 +8,11 @@ ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV DEB_BUILD_OPTIONS=noddebs
ENV ARCH=amd64
+ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget npm devscripts mmv
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
@@ -29,14 +29,11 @@ RUN dpkg --add-architecture armhf \
&& cd cross-gcc-packages-amd64/cross-gcc-8-armhf \
&& apt-get install -y gcc-8-source libstdc++-8-dev-armhf-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip binutils-arm-linux-gnueabihf libc6-dev:armhf linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf libfontconfig1-dev:armhf libfreetype6-dev:armhf libssl-dev:armhf liblttng-ust0:armhf libstdc++-8-dev:armhf
-# Link to docker-build script
-RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
+# Link to build script
+RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.armhf /build.sh
-# Link to Debian source dir; mkdir needed or it fails, can't force dest
-RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
+VOLUME ${SOURCE_DIR}/
VOLUME ${ARTIFACT_DIR}/
-COPY . ${SOURCE_DIR}/
-
-ENTRYPOINT ["/docker-build.sh"]
+ENTRYPOINT ["/build.sh"]
diff --git a/deployment/fedora-package-x64/Dockerfile b/deployment/Dockerfile.fedora.amd64
index 87120f3a0..01b99deb6 100644
--- a/deployment/fedora-package-x64/Dockerfile
+++ b/deployment/Dockerfile.fedora.amd64
@@ -1,18 +1,16 @@
FROM fedora:31
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
-ARG PLATFORM_DIR=/jellyfin/deployment/fedora-package-x64
ARG ARTIFACT_DIR=/dist
ARG SDK_VERSION=3.1
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
+ENV IS_DOCKER=YES
# Prepare Fedora environment
-RUN dnf update -y
-
-# Install build dependencies
-RUN dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel nodejs-yarn
+RUN dnf update -y \
+ && dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel
# Install DotNET SDK
RUN rpm --import https://packages.microsoft.com/keys/microsoft.asc \
@@ -20,14 +18,14 @@ RUN rpm --import https://packages.microsoft.com/keys/microsoft.asc \
&& dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION}
# Create symlinks and directories
-RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \
+RUN ln -sf ${SOURCE_DIR}/deployment/build.fedora.amd64 /build.sh \
&& mkdir -p ${SOURCE_DIR}/SPECS \
- && ln -s ${PLATFORM_DIR}/pkg-src/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \
+ && ln -s ${SOURCE_DIR}/fedora/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \
&& mkdir -p ${SOURCE_DIR}/SOURCES \
- && ln -s ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/SOURCES
+ && ln -s ${SOURCE_DIR}/fedora ${SOURCE_DIR}/SOURCES
-VOLUME ${ARTIFACT_DIR}/
+VOLUME ${SOURCE_DIR}/
-COPY . ${SOURCE_DIR}/
+VOLUME ${ARTIFACT_DIR}/
-ENTRYPOINT ["/docker-build.sh"]
+ENTRYPOINT ["/build.sh"]
diff --git a/deployment/macos/Dockerfile b/deployment/Dockerfile.linux.amd64
index b522df884..d8bec9214 100644
--- a/deployment/macos/Dockerfile
+++ b/deployment/Dockerfile.linux.amd64
@@ -1,7 +1,6 @@
FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
-ARG PLATFORM_DIR=/jellyfin/deployment/macos
ARG ARTIFACT_DIR=/dist
ARG SDK_VERSION=3.1
# Docker run environment
@@ -9,6 +8,7 @@ ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV DEB_BUILD_OPTIONS=noddebs
ENV ARCH=amd64
+ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update \
@@ -21,17 +21,11 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
-# Install yarn package manager
-RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
- && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
- && apt update \
- && apt install -y yarn
-
# Link to docker-build script
-RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
+RUN ln -sf ${SOURCE_DIR}/deployment/build.linux.amd64 /build.sh
-VOLUME ${ARTIFACT_DIR}/
+VOLUME ${SOURCE_DIR}/
-COPY . ${SOURCE_DIR}/
+VOLUME ${ARTIFACT_DIR}/
-ENTRYPOINT ["/docker-build.sh"]
+ENTRYPOINT ["/build.sh"]
diff --git a/deployment/portable/Dockerfile b/deployment/Dockerfile.macos
index 965eb82b8..ba5da4019 100644
--- a/deployment/portable/Dockerfile
+++ b/deployment/Dockerfile.macos
@@ -1,7 +1,6 @@
FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
-ARG PLATFORM_DIR=/jellyfin/deployment/portable
ARG ARTIFACT_DIR=/dist
ARG SDK_VERSION=3.1
# Docker run environment
@@ -9,6 +8,7 @@ ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV DEB_BUILD_OPTIONS=noddebs
ENV ARCH=amd64
+ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update \
@@ -21,17 +21,11 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
-# Install yarn package manager
-RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
- && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
- && apt update \
- && apt install -y yarn
-
# Link to docker-build script
-RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
+RUN ln -sf ${SOURCE_DIR}/deployment/build.macos /build.sh
-VOLUME ${ARTIFACT_DIR}/
+VOLUME ${SOURCE_DIR}/
-COPY . ${SOURCE_DIR}/
+VOLUME ${ARTIFACT_DIR}/
-ENTRYPOINT ["/docker-build.sh"]
+ENTRYPOINT ["/build.sh"]
diff --git a/deployment/linux-x64/Dockerfile b/deployment/Dockerfile.portable
index c47057546..2893e140d 100644
--- a/deployment/linux-x64/Dockerfile
+++ b/deployment/Dockerfile.portable
@@ -1,14 +1,13 @@
FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
-ARG PLATFORM_DIR=/jellyfin/deployment/linux-x64
ARG ARTIFACT_DIR=/dist
ARG SDK_VERSION=3.1
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV DEB_BUILD_OPTIONS=noddebs
-ENV ARCH=amd64
+ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update \
@@ -21,17 +20,11 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
-# Install yarn package manager
-RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
- && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
- && apt update \
- && apt install -y yarn
-
# Link to docker-build script
-RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
+RUN ln -sf ${SOURCE_DIR}/deployment/build.portable /build.sh
-VOLUME ${ARTIFACT_DIR}/
+VOLUME ${SOURCE_DIR}/
-COPY . ${SOURCE_DIR}/
+VOLUME ${ARTIFACT_DIR}/
-ENTRYPOINT ["/docker-build.sh"]
+ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
new file mode 100644
index 000000000..e61be4efc
--- /dev/null
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -0,0 +1,31 @@
+FROM ubuntu:bionic
+# Docker build arguments
+ARG SOURCE_DIR=/jellyfin
+ARG ARTIFACT_DIR=/dist
+ARG SDK_VERSION=3.1
+# Docker run environment
+ENV SOURCE_DIR=/jellyfin
+ENV ARTIFACT_DIR=/dist
+ENV DEB_BUILD_OPTIONS=noddebs
+ENV ARCH=amd64
+ENV IS_DOCKER=YES
+
+# Prepare Debian build environment
+RUN apt-get update \
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
+
+# Install dotnet repository
+# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
+RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+ && mkdir -p dotnet-sdk \
+ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
+ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
+
+# Link to build script
+RUN ln -sf ${SOURCE_DIR}/deployment/build.ubuntu.amd64 /build.sh
+
+VOLUME ${SOURCE_DIR}/
+
+VOLUME ${ARTIFACT_DIR}/
+
+ENTRYPOINT ["/build.sh"]
diff --git a/deployment/ubuntu-package-arm64/Dockerfile.amd64 b/deployment/Dockerfile.ubuntu.arm64
index b11994a18..f91b91cd4 100644
--- a/deployment/ubuntu-package-arm64/Dockerfile.amd64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -1,7 +1,6 @@
FROM ubuntu:bionic
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
-ARG PLATFORM_DIR=/jellyfin/deployment/ubuntu-package-arm64
ARG ARTIFACT_DIR=/dist
ARG SDK_VERSION=3.1
# Docker run environment
@@ -9,6 +8,7 @@ ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV DEB_BUILD_OPTIONS=noddebs
ENV ARCH=amd64
+ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update \
@@ -21,12 +21,6 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
-# Install npm package manager
-RUN wget -q -O- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - \
- && echo "deb https://deb.nodesource.com/node_10.x $(lsb_release -s -c) main" > /etc/apt/sources.list.d/npm.list \
- && apt update \
- && apt install -y nodejs
-
# Prepare the cross-toolchain
RUN rm /etc/apt/sources.list \
&& export CODENAME="$( lsb_release -c -s )" \
@@ -46,14 +40,11 @@ RUN rm /etc/apt/sources.list \
&& ln -fs /usr/share/zoneinfo/America/Toronto /etc/localtime \
&& apt-get install -y gcc-6-source libstdc++6-arm64-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 libfreetype6-dev:arm64 liblttng-ust0:arm64 libstdc++6:arm64 libssl-dev:arm64
-# Link to docker-build script
-RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
+# Link to build script
+RUN ln -sf ${SOURCE_DIR}/deployment/build.ubuntu.arm64 /build.sh
-# Link to Debian source dir; mkdir needed or it fails, can't force dest
-RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
+VOLUME ${SOURCE_DIR}/
VOLUME ${ARTIFACT_DIR}/
-COPY . ${SOURCE_DIR}/
-
-ENTRYPOINT ["/docker-build.sh"]
+ENTRYPOINT ["/build.sh"]
diff --git a/deployment/ubuntu-package-armhf/Dockerfile.amd64 b/deployment/Dockerfile.ubuntu.armhf
index e475b1438..85414614c 100644
--- a/deployment/ubuntu-package-armhf/Dockerfile.amd64
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -1,7 +1,6 @@
FROM ubuntu:bionic
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
-ARG PLATFORM_DIR=/jellyfin/deployment/ubuntu-package-armhf
ARG ARTIFACT_DIR=/dist
ARG SDK_VERSION=3.1
# Docker run environment
@@ -9,6 +8,7 @@ ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV DEB_BUILD_OPTIONS=noddebs
ENV ARCH=amd64
+ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update \
@@ -21,12 +21,6 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
-# Install npm package manager
-RUN wget -q -O- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - \
- && echo "deb https://deb.nodesource.com/node_10.x $(lsb_release -s -c) main" > /etc/apt/sources.list.d/npm.list \
- && apt update \
- && apt install -y nodejs
-
# Prepare the cross-toolchain
RUN rm /etc/apt/sources.list \
&& export CODENAME="$( lsb_release -c -s )" \
@@ -46,14 +40,11 @@ RUN rm /etc/apt/sources.list \
&& ln -fs /usr/share/zoneinfo/America/Toronto /etc/localtime \
&& apt-get install -y gcc-6-source libstdc++6-armhf-cross binutils-arm-linux-gnueabihf bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:armhf linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf libfontconfig1-dev:armhf libfreetype6-dev:armhf liblttng-ust0:armhf libstdc++6:armhf libssl-dev:armhf
-# Link to docker-build script
-RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
+# Link to build script
+RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.armhf /build.sh
-# Link to Debian source dir; mkdir needed or it fails, can't force dest
-RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
+VOLUME ${SOURCE_DIR}/
VOLUME ${ARTIFACT_DIR}/
-COPY . ${SOURCE_DIR}/
-
-ENTRYPOINT ["/docker-build.sh"]
+ENTRYPOINT ["/build.sh"]
diff --git a/deployment/win-x86/Dockerfile b/deployment/Dockerfile.windows.amd64
index f8dc5be83..0397a023e 100644
--- a/deployment/win-x86/Dockerfile
+++ b/deployment/Dockerfile.windows.amd64
@@ -1,14 +1,13 @@
FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
-ARG PLATFORM_DIR=/jellyfin/deployment/win-x86
ARG ARTIFACT_DIR=/dist
ARG SDK_VERSION=3.1
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV DEB_BUILD_OPTIONS=noddebs
-ENV ARCH=amd64
+ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update \
@@ -21,17 +20,11 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
-# Install yarn package manager
-RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
- && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
- && apt update \
- && apt install -y yarn
-
# Link to docker-build script
-RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
+RUN ln -sf ${SOURCE_DIR}/deployment/build.windows.amd64 /build.sh
-VOLUME ${ARTIFACT_DIR}/
+VOLUME ${SOURCE_DIR}/
-COPY . ${SOURCE_DIR}/
+VOLUME ${ARTIFACT_DIR}/
-ENTRYPOINT ["/docker-build.sh"]
+ENTRYPOINT ["/build.sh"]
diff --git a/deployment/README.md b/deployment/README.md
deleted file mode 100644
index a805f59ca..000000000
--- a/deployment/README.md
+++ /dev/null
@@ -1,62 +0,0 @@
-# Jellyfin Packaging
-
-This directory contains the packaging configuration of Jellyfin for multiple platforms. The specification is below; all package platforms must follow the specification to be compatable with the central `build` script.
-
-## Package List
-
-### Operating System Packages
-
-* `debian-package-x64`: Package for Debian and Ubuntu amd64 systems.
-* `fedora-package-x64`: Package for Fedora, CentOS, and Red Hat Enterprise Linux amd64 systems.
-
-### Portable Builds (archives)
-
-* `linux-x64`: Portable binary archive for generic Linux amd64 systems.
-* `macos`: Portable binary archive for MacOS amd64 systems.
-* `win-x64`: Portable binary archive for Windows amd64 systems.
-* `win-x86`: Portable binary archive for Windows i386 systems.
-
-### Other Builds
-
-These builds are not necessarily run from the `build` script, but are present for other platforms.
-
-* `portable`: Compiled `.dll` for use with .NET Core runtime on any system.
-* `docker`: Docker manifests for auto-publishing.
-* `unraid`: unRaid Docker template; not built by `build` but imported into unRaid directly.
-* `windows`: Support files and scripts for Windows CI build.
-
-## Package Specification
-
-### Dependencies
-
-* If a platform requires additional build dependencies, the required binary names, i.e. to validate `which <binary>`, should be specified in a `dependencies.txt` file inside the platform directory.
-
-* Each dependency should be present on its own line.
-
-### Action Scripts
-
-* Actions are defined in BASH scripts with the name `<action>.sh` within the platform directory.
-
-* The list of valid actions are:
-
- 1. `build`: Builds a set of binaries.
- 2. `package`: Assembles the compiled binaries into a package.
- 3. `sign`: Performs signing actions on a package.
- 4. `publish`: Performs a publishing action for a package.
- 5. `clean`: Cleans up any artifacts from the previous actions.
-
-* All package actions are optional, however at least one should generate output files, and any that do should contain a `clean` action.
-
-* Actions are executed in the order specified above, and later actions may depend on former actions.
-
-* Actions except for `clean` should `set -o errexit` to terminate on failed actions.
-
-* The `clean` action should always `exit 0` even if no work is done or it fails.
-
-* The `clean` action can be passed a variable as argument 1, named `keep_artifacts`, containing either the value `y` or `n`. It is indended to handle situations when the user runs `build --keep-artifacts` and should be handled intelligently. Usually, this is used to preserve Docker images while still removing temporary directories.
-
-### Output Files
-
-* Upon completion of the defined actions, at least one output file must be created in the `<platform>/pkg-dist` directory.
-
-* Output files will be moved to the directory `jellyfin-build/<platform>` one directory above the repository root upon completion.
diff --git a/deployment/build.centos.amd64 b/deployment/build.centos.amd64
new file mode 100755
index 000000000..939bbc45a
--- /dev/null
+++ b/deployment/build.centos.amd64
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+#= CentOS/RHEL 7+ amd64 .rpm
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+# Build RPM
+make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS
+rpmbuild --rebuild -bb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm
+
+# Move the artifacts out
+mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+fi
+
+rm -f fedora/jellyfin*.tar.gz
+
+popd
diff --git a/deployment/build.debian.amd64 b/deployment/build.debian.amd64
new file mode 100755
index 000000000..f44c6a7d1
--- /dev/null
+++ b/deployment/build.debian.amd64
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+#= Debian 10+ amd64 .deb
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ # Remove build-dep for dotnet-sdk-3.1, since it's installed manually
+ cp -a debian/control /tmp/control.orig
+ sed -i '/dotnet-sdk-3.1,/d' debian/control
+fi
+
+# Build DEB
+dpkg-buildpackage -us -uc --pre-clean --post-clean
+
+mkdir -p ${ARTIFACT_DIR}/
+mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}/
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ cp -a /tmp/control.orig debian/control
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+fi
+
+popd
diff --git a/deployment/build.debian.arm64 b/deployment/build.debian.arm64
new file mode 100755
index 000000000..0127671f3
--- /dev/null
+++ b/deployment/build.debian.arm64
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+#= Debian 10+ arm64 .deb
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ # Remove build-dep for dotnet-sdk-3.1, since it's installed manually
+ cp -a debian/control /tmp/control.orig
+ sed -i '/dotnet-sdk-3.1,/d' debian/control
+fi
+
+# Build DEB
+export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
+dpkg-buildpackage -us -uc -a arm64 --pre-clean --post-clean
+
+mkdir -p ${ARTIFACT_DIR}/
+mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}/
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ cp -a /tmp/control.orig debian/control
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+fi
+
+popd
diff --git a/deployment/build.debian.armhf b/deployment/build.debian.armhf
new file mode 100755
index 000000000..02e3db4fc
--- /dev/null
+++ b/deployment/build.debian.armhf
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+#= Debian 10+ arm64 .deb
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ # Remove build-dep for dotnet-sdk-3.1, since it's installed manually
+ cp -a debian/control /tmp/control.orig
+ sed -i '/dotnet-sdk-3.1,/d' debian/control
+fi
+
+# Build DEB
+export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
+dpkg-buildpackage -us -uc -a armhf --pre-clean --post-clean
+
+mkdir -p ${ARTIFACT_DIR}/
+mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}/
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ cp -a /tmp/control.orig debian/control
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+fi
+
+popd
diff --git a/deployment/build.fedora.amd64 b/deployment/build.fedora.amd64
new file mode 100755
index 000000000..8ac99decc
--- /dev/null
+++ b/deployment/build.fedora.amd64
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+#= Fedora 29+ amd64 .rpm
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+# Build RPM
+make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS
+rpmbuild -rb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm
+
+# Move the artifacts out
+mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+fi
+
+rm -f fedora/jellyfin*.tar.gz
+
+popd
diff --git a/deployment/build.linux.amd64 b/deployment/build.linux.amd64
new file mode 100755
index 000000000..0cbbd05cf
--- /dev/null
+++ b/deployment/build.linux.amd64
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+#= Generic Linux amd64 .tar.gz
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+# Get version
+version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+
+# Build archives
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-x64 --output dist/jellyfin-server_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"
+tar -czf jellyfin-server_${version}_linux-amd64.tar.gz -C dist jellyfin-server_${version}
+rm -rf dist/jellyfin-server_${version}
+
+# Move the artifacts out
+mkdir -p ${ARTIFACT_DIR}/
+mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+fi
+
+popd
diff --git a/deployment/build.macos b/deployment/build.macos
new file mode 100755
index 000000000..16be29eee
--- /dev/null
+++ b/deployment/build.macos
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+#= MacOS 10.13+ .tar.gz
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+# Get version
+version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+
+# Build archives
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-x64 --output dist/jellyfin-server_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"
+tar -czf jellyfin-server_${version}_macos-amd64.tar.gz -C dist jellyfin-server_${version}
+rm -rf dist/jellyfin-server_${version}
+
+# Move the artifacts out
+mkdir -p ${ARTIFACT_DIR}/
+mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+fi
+
+popd
diff --git a/deployment/build.portable b/deployment/build.portable
new file mode 100755
index 000000000..1e8a4ab62
--- /dev/null
+++ b/deployment/build.portable
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+#= Portable .NET DLL .tar.gz
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+# Get version
+version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+
+# Build archives
+dotnet publish Jellyfin.Server --configuration Release --output dist/jellyfin-server_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"
+tar -czf jellyfin-server_${version}_portable.tar.gz -C dist jellyfin-server_${version}
+rm -rf dist/jellyfin-server_${version}
+
+# Move the artifacts out
+mkdir -p ${ARTIFACT_DIR}/
+mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+fi
+
+popd
diff --git a/deployment/build.ubuntu.amd64 b/deployment/build.ubuntu.amd64
new file mode 100755
index 000000000..107ddbe02
--- /dev/null
+++ b/deployment/build.ubuntu.amd64
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+#= Ubuntu 18.04+ amd64 .deb
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ # Remove build-dep for dotnet-sdk-3.1, since it's installed manually
+ cp -a debian/control /tmp/control.orig
+ sed -i '/dotnet-sdk-3.1,/d' debian/control
+fi
+
+# Build DEB
+dpkg-buildpackage -us -uc --pre-clean --post-clean
+
+mkdir -p ${ARTIFACT_DIR}/
+mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}/
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ cp -a /tmp/control.orig debian/control
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+fi
+
+popd
diff --git a/deployment/build.ubuntu.arm64 b/deployment/build.ubuntu.arm64
new file mode 100755
index 000000000..b13868f44
--- /dev/null
+++ b/deployment/build.ubuntu.arm64
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+#= Ubuntu 18.04+ arm64 .deb
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ # Remove build-dep for dotnet-sdk-3.1, since it's installed manually
+ cp -a debian/control /tmp/control.orig
+ sed -i '/dotnet-sdk-3.1,/d' debian/control
+fi
+
+# Build DEB
+export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
+dpkg-buildpackage -us -uc -a arm64 --pre-clean --post-clean
+
+mkdir -p ${ARTIFACT_DIR}/
+mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}/
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ cp -a /tmp/control.orig debian/control
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+fi
+
+popd
diff --git a/deployment/build.ubuntu.armhf b/deployment/build.ubuntu.armhf
new file mode 100755
index 000000000..0b4dd308a
--- /dev/null
+++ b/deployment/build.ubuntu.armhf
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+#= Ubuntu 18.04+ arm64 .deb
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ # Remove build-dep for dotnet-sdk-3.1, since it's installed manually
+ cp -a debian/control /tmp/control.orig
+ sed -i '/dotnet-sdk-3.1,/d' debian/control
+fi
+
+# Build DEB
+export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
+dpkg-buildpackage -us -uc -a armhf --pre-clean --post-clean
+
+mkdir -p ${ARTIFACT_DIR}/
+mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}/
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ cp -a /tmp/control.orig debian/control
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+fi
+
+popd
diff --git a/deployment/build.windows.amd64 b/deployment/build.windows.amd64
new file mode 100755
index 000000000..39bd41f99
--- /dev/null
+++ b/deployment/build.windows.amd64
@@ -0,0 +1,54 @@
+#!/bin/bash
+
+#= Windows 7+ amd64 (x64) .zip
+
+set -o errexit
+set -o xtrace
+
+# Version variables
+NSSM_VERSION="nssm-2.24-101-g897c7ad"
+NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip"
+FFMPEG_VERSION="ffmpeg-4.2.1-win64-static"
+FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win64/static/${FFMPEG_VERSION}.zip"
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+# Get version
+version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+
+output_dir="dist/jellyfin-server_${version}"
+
+# Build binary
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime win-x64 --output ${output_dir}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"
+
+# Prepare addins
+addin_build_dir="$( mktemp -d )"
+wget ${NSSM_URL} -O ${addin_build_dir}/nssm.zip
+wget ${FFMPEG_URL} -O ${addin_build_dir}/ffmpeg.zip
+unzip ${addin_build_dir}/nssm.zip -d ${addin_build_dir}
+cp ${addin_build_dir}/${NSSM_VERSION}/win64/nssm.exe ${output_dir}/nssm.exe
+unzip ${addin_build_dir}/ffmpeg.zip -d ${addin_build_dir}
+cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffmpeg.exe ${output_dir}/ffmpeg.exe
+cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffprobe.exe ${output_dir}/ffprobe.exe
+rm -rf ${addin_build_dir}
+
+# Prepare scripts
+cp ${SOURCE_DIR}/windows/legacy/install-jellyfin.ps1 ${output_dir}/install-jellyfin.ps1
+cp ${SOURCE_DIR}/windows/legacy/install.bat ${output_dir}/install.bat
+
+# Create zip package
+pushd dist
+zip -qr jellyfin-server_${version}.portable.zip jellyfin-server_${version}
+popd
+rm -rf ${output_dir}
+
+# Move the artifacts out
+mkdir -p ${ARTIFACT_DIR}/
+mv dist/jellyfin[-_]*.zip ${ARTIFACT_DIR}/
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+fi
+
+popd
diff --git a/deployment/centos-package-x64/Dockerfile b/deployment/centos-package-x64/Dockerfile
deleted file mode 100644
index 08219a2e4..000000000
--- a/deployment/centos-package-x64/Dockerfile
+++ /dev/null
@@ -1,39 +0,0 @@
-FROM centos:7
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG PLATFORM_DIR=/jellyfin/deployment/centos-package-x64
-ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=3.1
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-
-# Prepare CentOS environment
-RUN yum update -y \
- && yum install -y epel-release
-
-# Install build dependencies
-RUN yum install -y @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git
-
-# Install recent NodeJS and Yarn
-RUN curl -fSsLo /etc/yum.repos.d/yarn.repo https://dl.yarnpkg.com/rpm/yarn.repo \
- && rpm -i https://rpm.nodesource.com/pub_10.x/el/7/x86_64/nodesource-release-el7-1.noarch.rpm \
- && yum install -y yarn
-
-# Install DotNET SDK
-RUN rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm \
- && rpmdev-setuptree \
- && yum install -y dotnet-sdk-${SDK_VERSION}
-
-# Create symlinks and directories
-RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \
- && mkdir -p ${SOURCE_DIR}/SPECS \
- && ln -s ${PLATFORM_DIR}/pkg-src/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \
- && mkdir -p ${SOURCE_DIR}/SOURCES \
- && ln -s ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/SOURCES
-
-VOLUME ${ARTIFACT_DIR}/
-
-COPY . ${SOURCE_DIR}/
-
-ENTRYPOINT ["/docker-build.sh"]
diff --git a/deployment/centos-package-x64/clean.sh b/deployment/centos-package-x64/clean.sh
deleted file mode 100755
index 31455de0d..000000000
--- a/deployment/centos-package-x64/clean.sh
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env bash
-
-keep_artifacts="${1}"
-
-WORKDIR="$( pwd )"
-VERSION="$( grep -A1 '^Version:' ${WORKDIR}/pkg-src/jellyfin.spec | awk '{ print $NF }' )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-package_source_dir="${WORKDIR}/pkg-src"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-centos-build"
-
-rm -f "${package_source_dir}/jellyfin-${VERSION}.tar.gz" &>/dev/null \
- || sudo rm -f "${package_source_dir}/jellyfin-${VERSION}.tar.gz" &>/dev/null
-
-rm -rf "${package_temporary_dir}" &>/dev/null \
- || sudo rm -rf "${package_temporary_dir}" &>/dev/null
-
-rm -rf "${output_dir}" &>/dev/null \
- || sudo rm -rf "${output_dir}" &>/dev/null
-
-if [[ ${keep_artifacts} == 'n' ]]; then
- docker_sudo=""
- if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo=sudo
- fi
- ${docker_sudo} docker image rm ${image_name} --force
-fi
diff --git a/deployment/centos-package-x64/dependencies.txt b/deployment/centos-package-x64/dependencies.txt
deleted file mode 100644
index bdb967096..000000000
--- a/deployment/centos-package-x64/dependencies.txt
+++ /dev/null
@@ -1 +0,0 @@
-docker
diff --git a/deployment/centos-package-x64/docker-build.sh b/deployment/centos-package-x64/docker-build.sh
deleted file mode 100755
index 62dd144e5..000000000
--- a/deployment/centos-package-x64/docker-build.sh
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/bin/bash
-
-# Builds the RPM inside the Docker container
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd ${SOURCE_DIR}
-
-# Build RPM
-make -f .copr/Makefile srpm outdir=/root/rpmbuild/SRPMS
-rpmbuild --rebuild -bb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm
-
-# Move the artifacts out
-mkdir -p ${ARTIFACT_DIR}/rpm
-mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/rpm/
-chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
diff --git a/deployment/centos-package-x64/package.sh b/deployment/centos-package-x64/package.sh
deleted file mode 100755
index 1b983f49d..000000000
--- a/deployment/centos-package-x64/package.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env bash
-
-args="${@}"
-declare -a docker_envvars
-for arg in ${args}; do
- docker_envvars+=("-e ${arg}")
-done
-
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-centos-build"
-
-# Determine if sudo should be used for Docker
-if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo="sudo"
-else
- docker_sudo=""
-fi
-
-# Prepare temporary package dir
-mkdir -p "${package_temporary_dir}"
-# Set up the build environment Docker image
-${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
-# Build the RPMs and copy out to ${package_temporary_dir}
-${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
-# Move the RPMs to the output directory
-mkdir -p "${output_dir}"
-mv "${package_temporary_dir}"/rpm/* "${output_dir}"
diff --git a/deployment/centos-package-x64/pkg-src b/deployment/centos-package-x64/pkg-src
deleted file mode 120000
index 3ff4d3cbf..000000000
--- a/deployment/centos-package-x64/pkg-src
+++ /dev/null
@@ -1 +0,0 @@
-../fedora-package-x64/pkg-src/ \ No newline at end of file
diff --git a/deployment/debian-package-arm64/Dockerfile.arm64 b/deployment/debian-package-arm64/Dockerfile.arm64
deleted file mode 100644
index 9ca486844..000000000
--- a/deployment/debian-package-arm64/Dockerfile.arm64
+++ /dev/null
@@ -1,34 +0,0 @@
-FROM debian:10
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-arm64
-ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=3.1
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV DEB_BUILD_OPTIONS=noddebs
-ENV ARCH=arm64
-
-# Prepare Debian build environment
-RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget npm devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev liblttng-ust0
-
-# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/5a4c8f96-1c73-401c-a6de-8e100403188a/0ce6ab39747e2508366d498f9c0a0669/dotnet-sdk-3.1.100-linux-arm64.tar.gz -O dotnet-sdk.tar.gz \
- && mkdir -p dotnet-sdk \
- && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
- && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
-
-# Link to docker-build script
-RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
-
-# Link to Debian source dir; mkdir needed or it fails, can't force dest
-RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
-
-VOLUME ${ARTIFACT_DIR}/
-
-COPY . ${SOURCE_DIR}/
-
-ENTRYPOINT ["/docker-build.sh"]
diff --git a/deployment/debian-package-arm64/clean.sh b/deployment/debian-package-arm64/clean.sh
deleted file mode 100755
index e7bfdf8b4..000000000
--- a/deployment/debian-package-arm64/clean.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env bash
-
-keep_artifacts="${1}"
-
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-debian_arm64-build"
-
-rm -rf "${package_temporary_dir}" &>/dev/null \
- || sudo rm -rf "${package_temporary_dir}" &>/dev/null
-
-rm -rf "${output_dir}" &>/dev/null \
- || sudo rm -rf "${output_dir}" &>/dev/null
-
-if [[ ${keep_artifacts} == 'n' ]]; then
- docker_sudo=""
- if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo=sudo
- fi
- ${docker_sudo} docker image rm ${image_name} --force
-fi
diff --git a/deployment/debian-package-arm64/dependencies.txt b/deployment/debian-package-arm64/dependencies.txt
deleted file mode 100644
index bdb967096..000000000
--- a/deployment/debian-package-arm64/dependencies.txt
+++ /dev/null
@@ -1 +0,0 @@
-docker
diff --git a/deployment/debian-package-arm64/docker-build.sh b/deployment/debian-package-arm64/docker-build.sh
deleted file mode 100755
index 67ab6bd74..000000000
--- a/deployment/debian-package-arm64/docker-build.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/bin/bash
-
-# Builds the DEB inside the Docker container
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd ${SOURCE_DIR}
-
-# Remove build-dep for dotnet-sdk-3.1, since it's not a package in this image
-sed -i '/dotnet-sdk-3.1,/d' debian/control
-
-# Build DEB
-export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
-dpkg-buildpackage -us -uc -aarm64
-
-# Move the artifacts out
-mkdir -p ${ARTIFACT_DIR}/deb
-mv /jellyfin[-_]* ${ARTIFACT_DIR}/deb/
-chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
diff --git a/deployment/debian-package-arm64/package.sh b/deployment/debian-package-arm64/package.sh
deleted file mode 100755
index 209198218..000000000
--- a/deployment/debian-package-arm64/package.sh
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/usr/bin/env bash
-
-args="${@}"
-declare -a docker_envvars
-for arg in ${args}; do
- docker_envvars+=("-e ${arg}")
-done
-
-ARCH="$( arch )"
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-debian_arm64-build"
-
-# Determine if sudo should be used for Docker
-if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo="sudo"
-else
- docker_sudo=""
-fi
-
-# Determine which Dockerfile to use
-case $ARCH in
- 'x86_64')
- DOCKERFILE="Dockerfile.amd64"
- ;;
- 'armv7l')
- DOCKERFILE="Dockerfile.arm64"
- ;;
-esac
-
-# Prepare temporary package dir
-mkdir -p "${package_temporary_dir}"
-# Set up the build environment Docker image
-${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
-# Build the DEBs and copy out to ${package_temporary_dir}
-${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
-# Move the DEBs to the output directory
-mkdir -p "${output_dir}"
-mv "${package_temporary_dir}"/deb/* "${output_dir}"
diff --git a/deployment/debian-package-arm64/pkg-src b/deployment/debian-package-arm64/pkg-src
deleted file mode 120000
index 4c695fea1..000000000
--- a/deployment/debian-package-arm64/pkg-src
+++ /dev/null
@@ -1 +0,0 @@
-../debian-package-x64/pkg-src \ No newline at end of file
diff --git a/deployment/debian-package-armhf/Dockerfile.armhf b/deployment/debian-package-armhf/Dockerfile.armhf
deleted file mode 100644
index dd398b5aa..000000000
--- a/deployment/debian-package-armhf/Dockerfile.armhf
+++ /dev/null
@@ -1,34 +0,0 @@
-FROM debian:10
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-armhf
-ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=3.1
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV DEB_BUILD_OPTIONS=noddebs
-ENV ARCH=armhf
-
-# Prepare Debian build environment
-RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget npm devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev liblttng-ust0
-
-# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/67766a96-eb8c-4cd2-bca4-ea63d2cc115c/7bf13840aa2ed88793b7315d5e0d74e6/dotnet-sdk-3.1.100-linux-arm.tar.gz -O dotnet-sdk.tar.gz \
- && mkdir -p dotnet-sdk \
- && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
- && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
-
-# Link to docker-build script
-RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
-
-# Link to Debian source dir; mkdir needed or it fails, can't force dest
-RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
-
-VOLUME ${ARTIFACT_DIR}/
-
-COPY . ${SOURCE_DIR}/
-
-ENTRYPOINT ["/docker-build.sh"]
diff --git a/deployment/debian-package-armhf/clean.sh b/deployment/debian-package-armhf/clean.sh
deleted file mode 100755
index 35a3d3e9a..000000000
--- a/deployment/debian-package-armhf/clean.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env bash
-
-keep_artifacts="${1}"
-
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-debian_armhf-build"
-
-rm -rf "${package_temporary_dir}" &>/dev/null \
- || sudo rm -rf "${package_temporary_dir}" &>/dev/null
-
-rm -rf "${output_dir}" &>/dev/null \
- || sudo rm -rf "${output_dir}" &>/dev/null
-
-if [[ ${keep_artifacts} == 'n' ]]; then
- docker_sudo=""
- if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo=sudo
- fi
- ${docker_sudo} docker image rm ${image_name} --force
-fi
diff --git a/deployment/debian-package-armhf/dependencies.txt b/deployment/debian-package-armhf/dependencies.txt
deleted file mode 100644
index bdb967096..000000000
--- a/deployment/debian-package-armhf/dependencies.txt
+++ /dev/null
@@ -1 +0,0 @@
-docker
diff --git a/deployment/debian-package-armhf/docker-build.sh b/deployment/debian-package-armhf/docker-build.sh
deleted file mode 100755
index 1bd7fb291..000000000
--- a/deployment/debian-package-armhf/docker-build.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/bin/bash
-
-# Builds the DEB inside the Docker container
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd ${SOURCE_DIR}
-
-# Remove build-dep for dotnet-sdk-3.1, since it's not a package in this image
-sed -i '/dotnet-sdk-3.1,/d' debian/control
-
-# Build DEB
-export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
-dpkg-buildpackage -us -uc -aarmhf
-
-# Move the artifacts out
-mkdir -p ${ARTIFACT_DIR}/deb
-mv /jellyfin[-_]* ${ARTIFACT_DIR}/deb/
-chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
diff --git a/deployment/debian-package-armhf/package.sh b/deployment/debian-package-armhf/package.sh
deleted file mode 100755
index 4a27dd828..000000000
--- a/deployment/debian-package-armhf/package.sh
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/usr/bin/env bash
-
-args="${@}"
-declare -a docker_envvars
-for arg in ${args}; do
- docker_envvars+=("-e ${arg}")
-done
-
-ARCH="$( arch )"
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-debian_armhf-build"
-
-# Determine if sudo should be used for Docker
-if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo="sudo"
-else
- docker_sudo=""
-fi
-
-# Determine which Dockerfile to use
-case $ARCH in
- 'x86_64')
- DOCKERFILE="Dockerfile.amd64"
- ;;
- 'armv7l')
- DOCKERFILE="Dockerfile.armhf"
- ;;
-esac
-
-# Prepare temporary package dir
-mkdir -p "${package_temporary_dir}"
-# Set up the build environment Docker image
-${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
-# Build the DEBs and copy out to ${package_temporary_dir}
-${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
-# Move the DEBs to the output directory
-mkdir -p "${output_dir}"
-mv "${package_temporary_dir}"/deb/* "${output_dir}"
diff --git a/deployment/debian-package-armhf/pkg-src b/deployment/debian-package-armhf/pkg-src
deleted file mode 120000
index 0bb6d5524..000000000
--- a/deployment/debian-package-armhf/pkg-src
+++ /dev/null
@@ -1 +0,0 @@
-../debian-package-x64/pkg-src/ \ No newline at end of file
diff --git a/deployment/debian-package-x64/Dockerfile b/deployment/debian-package-x64/Dockerfile
deleted file mode 100644
index e863d1edf..000000000
--- a/deployment/debian-package-x64/Dockerfile
+++ /dev/null
@@ -1,34 +0,0 @@
-FROM debian:10
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-x64
-ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=3.1
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV DEB_BUILD_OPTIONS=noddebs
-ENV ARCH=amd64
-
-# Prepare Debian build environment
-RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget npm devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
-
-# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
- && mkdir -p dotnet-sdk \
- && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
- && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
-
-# Link to docker-build script
-RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
-
-# Link to Debian source dir; mkdir needed or it fails, can't force dest
-RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
-
-VOLUME ${ARTIFACT_DIR}/
-
-COPY . ${SOURCE_DIR}/
-
-ENTRYPOINT ["/docker-build.sh"]
diff --git a/deployment/debian-package-x64/clean.sh b/deployment/debian-package-x64/clean.sh
deleted file mode 100755
index 4e507bcb2..000000000
--- a/deployment/debian-package-x64/clean.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env bash
-
-keep_artifacts="${1}"
-
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-debian-build"
-
-rm -rf "${package_temporary_dir}" &>/dev/null \
- || sudo rm -rf "${package_temporary_dir}" &>/dev/null
-
-rm -rf "${output_dir}" &>/dev/null \
- || sudo rm -rf "${output_dir}" &>/dev/null
-
-if [[ ${keep_artifacts} == 'n' ]]; then
- docker_sudo=""
- if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo=sudo
- fi
- ${docker_sudo} docker image rm ${image_name} --force
-fi
diff --git a/deployment/debian-package-x64/dependencies.txt b/deployment/debian-package-x64/dependencies.txt
deleted file mode 100644
index bdb967096..000000000
--- a/deployment/debian-package-x64/dependencies.txt
+++ /dev/null
@@ -1 +0,0 @@
-docker
diff --git a/deployment/debian-package-x64/docker-build.sh b/deployment/debian-package-x64/docker-build.sh
deleted file mode 100755
index 962a522eb..000000000
--- a/deployment/debian-package-x64/docker-build.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-
-# Builds the DEB inside the Docker container
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd ${SOURCE_DIR}
-
-# Remove build-dep for dotnet-sdk-3.1, since it's not a package in this image
-sed -i '/dotnet-sdk-3.1,/d' debian/control
-
-# Build DEB
-dpkg-buildpackage -us -uc
-
-# Move the artifacts out
-mkdir -p ${ARTIFACT_DIR}/deb
-mv /jellyfin[-_]* ${ARTIFACT_DIR}/deb/
-chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
diff --git a/deployment/debian-package-x64/package.sh b/deployment/debian-package-x64/package.sh
deleted file mode 100755
index 5a416959a..000000000
--- a/deployment/debian-package-x64/package.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env bash
-
-args="${@}"
-declare -a docker_envvars
-for arg in ${args}; do
- docker_envvars+=("-e ${arg}")
-done
-
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-debian-build"
-
-# Determine if sudo should be used for Docker
-if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo="sudo"
-else
- docker_sudo=""
-fi
-
-# Prepare temporary package dir
-mkdir -p "${package_temporary_dir}"
-# Set up the build environment Docker image
-${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
-# Build the DEBs and copy out to ${package_temporary_dir}
-${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
-# Move the DEBs to the output directory
-mkdir -p "${output_dir}"
-mv "${package_temporary_dir}"/deb/* "${output_dir}"
diff --git a/deployment/fedora-package-x64/clean.sh b/deployment/fedora-package-x64/clean.sh
deleted file mode 100755
index 700c8f1bb..000000000
--- a/deployment/fedora-package-x64/clean.sh
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env bash
-
-keep_artifacts="${1}"
-
-WORKDIR="$( pwd )"
-VERSION="$( grep -A1 '^Version:' ${WORKDIR}/pkg-src/jellyfin.spec | awk '{ print $NF }' )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-package_source_dir="${WORKDIR}/pkg-src"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-fedora-build"
-
-rm -f "${package_source_dir}/jellyfin-${VERSION}.tar.gz" &>/dev/null \
- || sudo rm -f "${package_source_dir}/jellyfin-${VERSION}.tar.gz" &>/dev/null
-
-rm -rf "${package_temporary_dir}" &>/dev/null \
- || sudo rm -rf "${package_temporary_dir}" &>/dev/null
-
-rm -rf "${output_dir}" &>/dev/null \
- || sudo rm -rf "${output_dir}" &>/dev/null
-
-if [[ ${keep_artifacts} == 'n' ]]; then
- docker_sudo=""
- if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo=sudo
- fi
- ${docker_sudo} docker image rm ${image_name} --force
-fi
diff --git a/deployment/fedora-package-x64/dependencies.txt b/deployment/fedora-package-x64/dependencies.txt
deleted file mode 100644
index bdb967096..000000000
--- a/deployment/fedora-package-x64/dependencies.txt
+++ /dev/null
@@ -1 +0,0 @@
-docker
diff --git a/deployment/fedora-package-x64/docker-build.sh b/deployment/fedora-package-x64/docker-build.sh
deleted file mode 100755
index 740e8d35c..000000000
--- a/deployment/fedora-package-x64/docker-build.sh
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/bin/bash
-
-# Builds the RPM inside the Docker container
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd ${SOURCE_DIR}
-
-# Build RPM
-make -f .copr/Makefile srpm outdir=/root/rpmbuild/SRPMS
-rpmbuild -rb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm
-
-# Move the artifacts out
-mkdir -p ${ARTIFACT_DIR}/rpm
-mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/rpm/
-chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
diff --git a/deployment/fedora-package-x64/package.sh b/deployment/fedora-package-x64/package.sh
deleted file mode 100755
index ae6962dd1..000000000
--- a/deployment/fedora-package-x64/package.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env bash
-
-args="${@}"
-declare -a docker_envvars
-for arg in ${args}; do
- docker_envvars+=("-e ${arg}")
-done
-
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-fedora-build"
-
-# Determine if sudo should be used for Docker
-if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo="sudo"
-else
- docker_sudo=""
-fi
-
-# Prepare temporary package dir
-mkdir -p "${package_temporary_dir}"
-# Set up the build environment Docker image
-${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
-# Build the RPMs and copy out to ${package_temporary_dir}
-${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
-# Move the RPMs to the output directory
-mkdir -p "${output_dir}"
-mv "${package_temporary_dir}"/rpm/* "${output_dir}"
diff --git a/deployment/linux-x64/clean.sh b/deployment/linux-x64/clean.sh
deleted file mode 100755
index c07501a7b..000000000
--- a/deployment/linux-x64/clean.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env bash
-
-keep_artifacts="${1}"
-
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-linux-build"
-
-rm -rf "${package_temporary_dir}" &>/dev/null \
- || sudo rm -rf "${package_temporary_dir}" &>/dev/null
-
-rm -rf "${output_dir}" &>/dev/null \
- || sudo rm -rf "${output_dir}" &>/dev/null
-
-if [[ ${keep_artifacts} == 'n' ]]; then
- docker_sudo=""
- if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo=sudo
- fi
- ${docker_sudo} docker image rm ${image_name} --force
-fi
diff --git a/deployment/linux-x64/dependencies.txt b/deployment/linux-x64/dependencies.txt
deleted file mode 100644
index bdb967096..000000000
--- a/deployment/linux-x64/dependencies.txt
+++ /dev/null
@@ -1 +0,0 @@
-docker
diff --git a/deployment/linux-x64/docker-build.sh b/deployment/linux-x64/docker-build.sh
deleted file mode 100755
index e33328a36..000000000
--- a/deployment/linux-x64/docker-build.sh
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/bin/bash
-
-# Builds the TAR archive inside the Docker container
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd ${SOURCE_DIR}
-
-# Clone down and build Web frontend
-web_build_dir="$( mktemp -d )"
-web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web"
-git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/
-pushd ${web_build_dir}
-if [[ -n ${web_branch} ]]; then
- checkout -b origin/${web_branch}
-fi
-yarn install
-mkdir -p ${web_target}
-mv dist/* ${web_target}/
-popd
-rm -rf ${web_build_dir}
-
-# Get version
-version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
-
-# Build archives
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-x64 --output /dist/jellyfin_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"
-tar -cvzf /jellyfin_${version}.portable.tar.gz -C /dist jellyfin_${version}
-rm -rf /dist/jellyfin_${version}
-
-# Move the artifacts out
-mkdir -p ${ARTIFACT_DIR}/
-mv /jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/
-chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
diff --git a/deployment/linux-x64/package.sh b/deployment/linux-x64/package.sh
deleted file mode 100755
index dfe8a9aa4..000000000
--- a/deployment/linux-x64/package.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env bash
-
-args="${@}"
-declare -a docker_envvars
-for arg in ${args}; do
- docker_envvars+=("-e ${arg}")
-done
-
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-linux-build"
-
-# Determine if sudo should be used for Docker
-if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo="sudo"
-else
- docker_sudo=""
-fi
-
-# Prepare temporary package dir
-mkdir -p "${package_temporary_dir}"
-# Set up the build environment Docker image
-${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
-# Build the DEBs and copy out to ${package_temporary_dir}
-${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
-# Move the DEBs to the output directory
-mkdir -p "${output_dir}"
-mv "${package_temporary_dir}"/* "${output_dir}"
diff --git a/deployment/macos/clean.sh b/deployment/macos/clean.sh
deleted file mode 100755
index c07501a7b..000000000
--- a/deployment/macos/clean.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env bash
-
-keep_artifacts="${1}"
-
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-linux-build"
-
-rm -rf "${package_temporary_dir}" &>/dev/null \
- || sudo rm -rf "${package_temporary_dir}" &>/dev/null
-
-rm -rf "${output_dir}" &>/dev/null \
- || sudo rm -rf "${output_dir}" &>/dev/null
-
-if [[ ${keep_artifacts} == 'n' ]]; then
- docker_sudo=""
- if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo=sudo
- fi
- ${docker_sudo} docker image rm ${image_name} --force
-fi
diff --git a/deployment/macos/dependencies.txt b/deployment/macos/dependencies.txt
deleted file mode 100644
index bdb967096..000000000
--- a/deployment/macos/dependencies.txt
+++ /dev/null
@@ -1 +0,0 @@
-docker
diff --git a/deployment/macos/docker-build.sh b/deployment/macos/docker-build.sh
deleted file mode 100755
index f9417388d..000000000
--- a/deployment/macos/docker-build.sh
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/bin/bash
-
-# Builds the TAR archive inside the Docker container
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd ${SOURCE_DIR}
-
-# Clone down and build Web frontend
-web_build_dir="$( mktemp -d )"
-web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web"
-git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/
-pushd ${web_build_dir}
-if [[ -n ${web_branch} ]]; then
- checkout -b origin/${web_branch}
-fi
-yarn install
-mkdir -p ${web_target}
-mv dist/* ${web_target}/
-popd
-rm -rf ${web_build_dir}
-
-# Get version
-version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
-
-# Build archives
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-x64 --output /dist/jellyfin_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"
-tar -cvzf /jellyfin_${version}.portable.tar.gz -C /dist jellyfin_${version}
-rm -rf /dist/jellyfin_${version}
-
-# Move the artifacts out
-mkdir -p ${ARTIFACT_DIR}/
-mv /jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/
-chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
diff --git a/deployment/macos/package.sh b/deployment/macos/package.sh
deleted file mode 100755
index 464c0d382..000000000
--- a/deployment/macos/package.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env bash
-
-args="${@}"
-declare -a docker_envvars
-for arg in ${args}; do
- docker_envvars+=("-e ${arg}")
-done
-
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-macos-build"
-
-# Determine if sudo should be used for Docker
-if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo="sudo"
-else
- docker_sudo=""
-fi
-
-# Prepare temporary package dir
-mkdir -p "${package_temporary_dir}"
-# Set up the build environment Docker image
-${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
-# Build the DEBs and copy out to ${package_temporary_dir}
-${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
-# Move the DEBs to the output directory
-mkdir -p "${output_dir}"
-mv "${package_temporary_dir}"/* "${output_dir}"
diff --git a/deployment/portable/clean.sh b/deployment/portable/clean.sh
deleted file mode 100755
index c07501a7b..000000000
--- a/deployment/portable/clean.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env bash
-
-keep_artifacts="${1}"
-
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-linux-build"
-
-rm -rf "${package_temporary_dir}" &>/dev/null \
- || sudo rm -rf "${package_temporary_dir}" &>/dev/null
-
-rm -rf "${output_dir}" &>/dev/null \
- || sudo rm -rf "${output_dir}" &>/dev/null
-
-if [[ ${keep_artifacts} == 'n' ]]; then
- docker_sudo=""
- if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo=sudo
- fi
- ${docker_sudo} docker image rm ${image_name} --force
-fi
diff --git a/deployment/portable/dependencies.txt b/deployment/portable/dependencies.txt
deleted file mode 100644
index bdb967096..000000000
--- a/deployment/portable/dependencies.txt
+++ /dev/null
@@ -1 +0,0 @@
-docker
diff --git a/deployment/portable/docker-build.sh b/deployment/portable/docker-build.sh
deleted file mode 100755
index 094190bbf..000000000
--- a/deployment/portable/docker-build.sh
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/bin/bash
-
-# Builds the TAR archive inside the Docker container
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd ${SOURCE_DIR}
-
-# Clone down and build Web frontend
-web_build_dir="$( mktemp -d )"
-web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web"
-git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/
-pushd ${web_build_dir}
-if [[ -n ${web_branch} ]]; then
- checkout -b origin/${web_branch}
-fi
-yarn install
-mkdir -p ${web_target}
-mv dist/* ${web_target}/
-popd
-rm -rf ${web_build_dir}
-
-# Get version
-version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
-
-# Build archives
-dotnet publish Jellyfin.Server --configuration Release --output /dist/jellyfin_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
-tar -cvzf /jellyfin_${version}.portable.tar.gz -C /dist jellyfin_${version}
-rm -rf /dist/jellyfin_${version}
-
-# Move the artifacts out
-mkdir -p ${ARTIFACT_DIR}/
-mv /jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/
-chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
diff --git a/deployment/portable/package.sh b/deployment/portable/package.sh
deleted file mode 100755
index 0ceb54dda..000000000
--- a/deployment/portable/package.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env bash
-
-args="${@}"
-declare -a docker_envvars
-for arg in ${args}; do
- docker_envvars+=("-e ${arg}")
-done
-
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-portable-build"
-
-# Determine if sudo should be used for Docker
-if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo="sudo"
-else
- docker_sudo=""
-fi
-
-# Prepare temporary package dir
-mkdir -p "${package_temporary_dir}"
-# Set up the build environment Docker image
-${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
-# Build the DEBs and copy out to ${package_temporary_dir}
-${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
-# Move the DEBs to the output directory
-mkdir -p "${output_dir}"
-mv "${package_temporary_dir}"/* "${output_dir}"
diff --git a/deployment/ubuntu-package-arm64/Dockerfile.arm64 b/deployment/ubuntu-package-arm64/Dockerfile.arm64
deleted file mode 100644
index 8f004b2f1..000000000
--- a/deployment/ubuntu-package-arm64/Dockerfile.arm64
+++ /dev/null
@@ -1,40 +0,0 @@
-FROM ubuntu:bionic
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG PLATFORM_DIR=/jellyfin/deployment/ubuntu-package-arm64
-ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=3.1
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV DEB_BUILD_OPTIONS=noddebs
-ENV ARCH=arm64
-
-# Prepare Debian build environment
-RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0 libssl-dev
-
-# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/5a4c8f96-1c73-401c-a6de-8e100403188a/0ce6ab39747e2508366d498f9c0a0669/dotnet-sdk-3.1.100-linux-arm64.tar.gz -O dotnet-sdk.tar.gz \
- && mkdir -p dotnet-sdk \
- && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
- && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
-
-# Install npm package manager
-RUN wget -q -O- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - \
- && echo "deb https://deb.nodesource.com/node_10.x $(lsb_release -s -c) main" > /etc/apt/sources.list.d/npm.list \
- && apt update \
- && apt install -y nodejs
-
-# Link to docker-build script
-RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
-
-# Link to Debian source dir; mkdir needed or it fails, can't force dest
-RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
-
-VOLUME ${ARTIFACT_DIR}/
-
-COPY . ${SOURCE_DIR}/
-
-ENTRYPOINT ["/docker-build.sh"]
diff --git a/deployment/ubuntu-package-arm64/clean.sh b/deployment/ubuntu-package-arm64/clean.sh
deleted file mode 100755
index 82d427f9e..000000000
--- a/deployment/ubuntu-package-arm64/clean.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env bash
-
-keep_artifacts="${1}"
-
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-ubuntu-build"
-
-rm -rf "${package_temporary_dir}" &>/dev/null \
- || sudo rm -rf "${package_temporary_dir}" &>/dev/null
-
-rm -rf "${output_dir}" &>/dev/null \
- || sudo rm -rf "${output_dir}" &>/dev/null
-
-if [[ ${keep_artifacts} == 'n' ]]; then
- docker_sudo=""
- if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo=sudo
- fi
- ${docker_sudo} docker image rm ${image_name} --force
-fi
diff --git a/deployment/ubuntu-package-arm64/dependencies.txt b/deployment/ubuntu-package-arm64/dependencies.txt
deleted file mode 100644
index bdb967096..000000000
--- a/deployment/ubuntu-package-arm64/dependencies.txt
+++ /dev/null
@@ -1 +0,0 @@
-docker
diff --git a/deployment/ubuntu-package-arm64/docker-build.sh b/deployment/ubuntu-package-arm64/docker-build.sh
deleted file mode 100755
index 67ab6bd74..000000000
--- a/deployment/ubuntu-package-arm64/docker-build.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/bin/bash
-
-# Builds the DEB inside the Docker container
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd ${SOURCE_DIR}
-
-# Remove build-dep for dotnet-sdk-3.1, since it's not a package in this image
-sed -i '/dotnet-sdk-3.1,/d' debian/control
-
-# Build DEB
-export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
-dpkg-buildpackage -us -uc -aarm64
-
-# Move the artifacts out
-mkdir -p ${ARTIFACT_DIR}/deb
-mv /jellyfin[-_]* ${ARTIFACT_DIR}/deb/
-chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
diff --git a/deployment/ubuntu-package-arm64/package.sh b/deployment/ubuntu-package-arm64/package.sh
deleted file mode 100755
index d1140a727..000000000
--- a/deployment/ubuntu-package-arm64/package.sh
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/usr/bin/env bash
-
-args="${@}"
-declare -a docker_envvars
-for arg in ${args}; do
- docker_envvars+=("-e ${arg}")
-done
-
-ARCH="$( arch )"
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-ubuntu_arm64-build"
-
-# Determine if sudo should be used for Docker
-if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo="sudo"
-else
- docker_sudo=""
-fi
-
-# Determine which Dockerfile to use
-case $ARCH in
- 'x86_64')
- DOCKERFILE="Dockerfile.amd64"
- ;;
- 'armv7l')
- DOCKERFILE="Dockerfile.arm64"
- ;;
-esac
-
-# Prepare temporary package dir
-mkdir -p "${package_temporary_dir}"
-# Set up the build environment Docker image
-${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
-# Build the DEBs and copy out to ${package_temporary_dir}
-${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
-# Move the DEBs to the output directory
-mkdir -p "${output_dir}"
-mv "${package_temporary_dir}"/deb/* "${output_dir}"
diff --git a/deployment/ubuntu-package-arm64/pkg-src b/deployment/ubuntu-package-arm64/pkg-src
deleted file mode 120000
index 4c695fea1..000000000
--- a/deployment/ubuntu-package-arm64/pkg-src
+++ /dev/null
@@ -1 +0,0 @@
-../debian-package-x64/pkg-src \ No newline at end of file
diff --git a/deployment/ubuntu-package-armhf/Dockerfile.armhf b/deployment/ubuntu-package-armhf/Dockerfile.armhf
deleted file mode 100644
index 0e71fa693..000000000
--- a/deployment/ubuntu-package-armhf/Dockerfile.armhf
+++ /dev/null
@@ -1,40 +0,0 @@
-FROM ubuntu:bionic
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG PLATFORM_DIR=/jellyfin/deployment/ubuntu-package-armhf
-ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=3.1
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV DEB_BUILD_OPTIONS=noddebs
-ENV ARCH=armhf
-
-# Prepare Debian build environment
-RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0 libssl-dev
-
-# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/67766a96-eb8c-4cd2-bca4-ea63d2cc115c/7bf13840aa2ed88793b7315d5e0d74e6/dotnet-sdk-3.1.100-linux-arm.tar.gz -O dotnet-sdk.tar.gz \
- && mkdir -p dotnet-sdk \
- && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
- && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
-
-# Install npm package manager
-RUN wget -q -O- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - \
- && echo "deb https://deb.nodesource.com/node_10.x $(lsb_release -s -c) main" > /etc/apt/sources.list.d/npm.list \
- && apt update \
- && apt install -y nodejs
-
-# Link to docker-build script
-RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
-
-# Link to Debian source dir; mkdir needed or it fails, can't force dest
-RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
-
-VOLUME ${ARTIFACT_DIR}/
-
-COPY . ${SOURCE_DIR}/
-
-ENTRYPOINT ["/docker-build.sh"]
diff --git a/deployment/ubuntu-package-armhf/clean.sh b/deployment/ubuntu-package-armhf/clean.sh
deleted file mode 100755
index 82d427f9e..000000000
--- a/deployment/ubuntu-package-armhf/clean.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env bash
-
-keep_artifacts="${1}"
-
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-ubuntu-build"
-
-rm -rf "${package_temporary_dir}" &>/dev/null \
- || sudo rm -rf "${package_temporary_dir}" &>/dev/null
-
-rm -rf "${output_dir}" &>/dev/null \
- || sudo rm -rf "${output_dir}" &>/dev/null
-
-if [[ ${keep_artifacts} == 'n' ]]; then
- docker_sudo=""
- if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo=sudo
- fi
- ${docker_sudo} docker image rm ${image_name} --force
-fi
diff --git a/deployment/ubuntu-package-armhf/dependencies.txt b/deployment/ubuntu-package-armhf/dependencies.txt
deleted file mode 100644
index bdb967096..000000000
--- a/deployment/ubuntu-package-armhf/dependencies.txt
+++ /dev/null
@@ -1 +0,0 @@
-docker
diff --git a/deployment/ubuntu-package-armhf/docker-build.sh b/deployment/ubuntu-package-armhf/docker-build.sh
deleted file mode 100755
index 1bd7fb291..000000000
--- a/deployment/ubuntu-package-armhf/docker-build.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/bin/bash
-
-# Builds the DEB inside the Docker container
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd ${SOURCE_DIR}
-
-# Remove build-dep for dotnet-sdk-3.1, since it's not a package in this image
-sed -i '/dotnet-sdk-3.1,/d' debian/control
-
-# Build DEB
-export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
-dpkg-buildpackage -us -uc -aarmhf
-
-# Move the artifacts out
-mkdir -p ${ARTIFACT_DIR}/deb
-mv /jellyfin[-_]* ${ARTIFACT_DIR}/deb/
-chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
diff --git a/deployment/ubuntu-package-armhf/package.sh b/deployment/ubuntu-package-armhf/package.sh
deleted file mode 100755
index 2ceb3e816..000000000
--- a/deployment/ubuntu-package-armhf/package.sh
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/usr/bin/env bash
-
-args="${@}"
-declare -a docker_envvars
-for arg in ${args}; do
- docker_envvars+=("-e ${arg}")
-done
-
-ARCH="$( arch )"
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-ubuntu_armhf-build"
-
-# Determine if sudo should be used for Docker
-if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo="sudo"
-else
- docker_sudo=""
-fi
-
-# Determine which Dockerfile to use
-case $ARCH in
- 'x86_64')
- DOCKERFILE="Dockerfile.amd64"
- ;;
- 'armv7l')
- DOCKERFILE="Dockerfile.armhf"
- ;;
-esac
-
-# Prepare temporary package dir
-mkdir -p "${package_temporary_dir}"
-# Set up the build environment Docker image
-${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
-# Build the DEBs and copy out to ${package_temporary_dir}
-${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
-# Move the DEBs to the output directory
-mkdir -p "${output_dir}"
-mv "${package_temporary_dir}"/deb/* "${output_dir}"
diff --git a/deployment/ubuntu-package-armhf/pkg-src b/deployment/ubuntu-package-armhf/pkg-src
deleted file mode 120000
index 4c695fea1..000000000
--- a/deployment/ubuntu-package-armhf/pkg-src
+++ /dev/null
@@ -1 +0,0 @@
-../debian-package-x64/pkg-src \ No newline at end of file
diff --git a/deployment/ubuntu-package-x64/Dockerfile b/deployment/ubuntu-package-x64/Dockerfile
deleted file mode 100644
index e2dda6392..000000000
--- a/deployment/ubuntu-package-x64/Dockerfile
+++ /dev/null
@@ -1,36 +0,0 @@
-FROM ubuntu:bionic
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG PLATFORM_DIR=/jellyfin/deployment/ubuntu-package-x64
-ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=3.1
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV DEB_BUILD_OPTIONS=noddebs
-ENV ARCH=amd64
-
-# Prepare Ubuntu build environment
-RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev liblttng-ust0 \
- && ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \
- && mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
-
-# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
- && mkdir -p dotnet-sdk \
- && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
- && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
-
-# Install npm package manager
-RUN wget -q -O- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - \
- && echo "deb https://deb.nodesource.com/node_10.x $(lsb_release -s -c) main" > /etc/apt/sources.list.d/npm.list \
- && apt update \
- && apt install -y nodejs
-
-VOLUME ${ARTIFACT_DIR}/
-
-COPY . ${SOURCE_DIR}/
-
-ENTRYPOINT ["/docker-build.sh"]
diff --git a/deployment/ubuntu-package-x64/clean.sh b/deployment/ubuntu-package-x64/clean.sh
deleted file mode 100755
index 82d427f9e..000000000
--- a/deployment/ubuntu-package-x64/clean.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env bash
-
-keep_artifacts="${1}"
-
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-ubuntu-build"
-
-rm -rf "${package_temporary_dir}" &>/dev/null \
- || sudo rm -rf "${package_temporary_dir}" &>/dev/null
-
-rm -rf "${output_dir}" &>/dev/null \
- || sudo rm -rf "${output_dir}" &>/dev/null
-
-if [[ ${keep_artifacts} == 'n' ]]; then
- docker_sudo=""
- if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo=sudo
- fi
- ${docker_sudo} docker image rm ${image_name} --force
-fi
diff --git a/deployment/ubuntu-package-x64/dependencies.txt b/deployment/ubuntu-package-x64/dependencies.txt
deleted file mode 100644
index bdb967096..000000000
--- a/deployment/ubuntu-package-x64/dependencies.txt
+++ /dev/null
@@ -1 +0,0 @@
-docker
diff --git a/deployment/ubuntu-package-x64/docker-build.sh b/deployment/ubuntu-package-x64/docker-build.sh
deleted file mode 100755
index 962a522eb..000000000
--- a/deployment/ubuntu-package-x64/docker-build.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-
-# Builds the DEB inside the Docker container
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd ${SOURCE_DIR}
-
-# Remove build-dep for dotnet-sdk-3.1, since it's not a package in this image
-sed -i '/dotnet-sdk-3.1,/d' debian/control
-
-# Build DEB
-dpkg-buildpackage -us -uc
-
-# Move the artifacts out
-mkdir -p ${ARTIFACT_DIR}/deb
-mv /jellyfin[-_]* ${ARTIFACT_DIR}/deb/
-chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
diff --git a/deployment/ubuntu-package-x64/package.sh b/deployment/ubuntu-package-x64/package.sh
deleted file mode 100755
index 08c003778..000000000
--- a/deployment/ubuntu-package-x64/package.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env bash
-
-args="${@}"
-declare -a docker_envvars
-for arg in ${args}; do
- docker_envvars+=("-e ${arg}")
-done
-
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-ubuntu-build"
-
-# Determine if sudo should be used for Docker
-if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo="sudo"
-else
- docker_sudo=""
-fi
-
-# Prepare temporary package dir
-mkdir -p "${package_temporary_dir}"
-# Set up the build environment Docker image
-${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
-# Build the DEBs and copy out to ${package_temporary_dir}
-${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
-# Move the DEBs to the output directory
-mkdir -p "${output_dir}"
-mv "${package_temporary_dir}"/deb/* "${output_dir}"
diff --git a/deployment/ubuntu-package-x64/pkg-src b/deployment/ubuntu-package-x64/pkg-src
deleted file mode 120000
index 0bb6d5524..000000000
--- a/deployment/ubuntu-package-x64/pkg-src
+++ /dev/null
@@ -1 +0,0 @@
-../debian-package-x64/pkg-src/ \ No newline at end of file
diff --git a/deployment/win-x64/clean.sh b/deployment/win-x64/clean.sh
deleted file mode 100755
index 6c183f337..000000000
--- a/deployment/win-x64/clean.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env bash
-
-keep_artifacts="${1}"
-
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-windows-x64-build"
-
-rm -rf "${package_temporary_dir}" &>/dev/null \
- || sudo rm -rf "${package_temporary_dir}" &>/dev/null
-
-rm -rf "${output_dir}" &>/dev/null \
- || sudo rm -rf "${output_dir}" &>/dev/null
-
-if [[ ${keep_artifacts} == 'n' ]]; then
- docker_sudo=""
- if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo=sudo
- fi
- ${docker_sudo} docker image rm ${image_name} --force
-fi
diff --git a/deployment/win-x64/dependencies.txt b/deployment/win-x64/dependencies.txt
deleted file mode 100644
index bdb967096..000000000
--- a/deployment/win-x64/dependencies.txt
+++ /dev/null
@@ -1 +0,0 @@
-docker
diff --git a/deployment/win-x64/docker-build.sh b/deployment/win-x64/docker-build.sh
deleted file mode 100755
index 79e5fb0bc..000000000
--- a/deployment/win-x64/docker-build.sh
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/bin/bash
-
-# Builds the ZIP archive inside the Docker container
-
-set -o errexit
-set -o xtrace
-
-# Version variables
-NSSM_VERSION="nssm-2.24-101-g897c7ad"
-NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip"
-FFMPEG_VERSION="ffmpeg-4.2.1-win64-static"
-FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win64/static/${FFMPEG_VERSION}.zip"
-
-# Move to source directory
-pushd ${SOURCE_DIR}
-
-# Clone down and build Web frontend
-web_build_dir="$( mktemp -d )"
-web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web"
-git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/
-pushd ${web_build_dir}
-if [[ -n ${web_branch} ]]; then
- checkout -b origin/${web_branch}
-fi
-yarn install
-mkdir -p ${web_target}
-mv dist/* ${web_target}/
-popd
-rm -rf ${web_build_dir}
-
-# Get version
-version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
-
-# Build binary
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime win-x64 --output /dist/jellyfin_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"
-
-# Prepare addins
-addin_build_dir="$( mktemp -d )"
-wget ${NSSM_URL} -O ${addin_build_dir}/nssm.zip
-wget ${FFMPEG_URL} -O ${addin_build_dir}/ffmpeg.zip
-unzip ${addin_build_dir}/nssm.zip -d ${addin_build_dir}
-cp ${addin_build_dir}/${NSSM_VERSION}/win64/nssm.exe /dist/jellyfin_${version}/nssm.exe
-unzip ${addin_build_dir}/ffmpeg.zip -d ${addin_build_dir}
-cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffmpeg.exe /dist/jellyfin_${version}/ffmpeg.exe
-cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffprobe.exe /dist/jellyfin_${version}/ffprobe.exe
-rm -rf ${addin_build_dir}
-
-# Prepare scripts
-cp ${SOURCE_DIR}/deployment/windows/legacy/install-jellyfin.ps1 /dist/jellyfin_${version}/install-jellyfin.ps1
-cp ${SOURCE_DIR}/deployment/windows/legacy/install.bat /dist/jellyfin_${version}/install.bat
-
-# Create zip package
-pushd /dist
-zip -r /jellyfin_${version}.portable.zip jellyfin_${version}
-popd
-rm -rf /dist/jellyfin_${version}
-
-# Move the artifacts out
-mkdir -p ${ARTIFACT_DIR}/
-mv /jellyfin[-_]*.zip ${ARTIFACT_DIR}/
-chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
diff --git a/deployment/win-x64/package.sh b/deployment/win-x64/package.sh
deleted file mode 100755
index a8ab190fa..000000000
--- a/deployment/win-x64/package.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env bash
-
-args="${@}"
-declare -a docker_envvars
-for arg in ${args}; do
- docker_envvars+=("-e ${arg}")
-done
-
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-windows-x64-build"
-
-# Determine if sudo should be used for Docker
-if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo="sudo"
-else
- docker_sudo=""
-fi
-
-# Prepare temporary package dir
-mkdir -p "${package_temporary_dir}"
-# Set up the build environment Docker image
-${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
-# Build the DEBs and copy out to ${package_temporary_dir}
-${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
-# Move the DEBs to the output directory
-mkdir -p "${output_dir}"
-mv "${package_temporary_dir}"/* "${output_dir}"
diff --git a/deployment/win-x86/clean.sh b/deployment/win-x86/clean.sh
deleted file mode 100755
index 8b78c5e4b..000000000
--- a/deployment/win-x86/clean.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env bash
-
-keep_artifacts="${1}"
-
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-windows-x86-build"
-
-rm -rf "${package_temporary_dir}" &>/dev/null \
- || sudo rm -rf "${package_temporary_dir}" &>/dev/null
-
-rm -rf "${output_dir}" &>/dev/null \
- || sudo rm -rf "${output_dir}" &>/dev/null
-
-if [[ ${keep_artifacts} == 'n' ]]; then
- docker_sudo=""
- if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo=sudo
- fi
- ${docker_sudo} docker image rm ${image_name} --force
-fi
diff --git a/deployment/win-x86/dependencies.txt b/deployment/win-x86/dependencies.txt
deleted file mode 100644
index bdb967096..000000000
--- a/deployment/win-x86/dependencies.txt
+++ /dev/null
@@ -1 +0,0 @@
-docker
diff --git a/deployment/win-x86/docker-build.sh b/deployment/win-x86/docker-build.sh
deleted file mode 100755
index 977dcf78f..000000000
--- a/deployment/win-x86/docker-build.sh
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/bin/bash
-
-# Builds the ZIP archive inside the Docker container
-
-set -o errexit
-set -o xtrace
-
-# Version variables
-NSSM_VERSION="nssm-2.24-101-g897c7ad"
-NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip"
-FFMPEG_VERSION="ffmpeg-4.2.1-win32-static"
-FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win32/static/${FFMPEG_VERSION}.zip"
-
-# Move to source directory
-pushd ${SOURCE_DIR}
-
-# Clone down and build Web frontend
-web_build_dir="$( mktemp -d )"
-web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web"
-git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/
-pushd ${web_build_dir}
-if [[ -n ${web_branch} ]]; then
- checkout -b origin/${web_branch}
-fi
-yarn install
-mkdir -p ${web_target}
-mv dist/* ${web_target}/
-popd
-rm -rf ${web_build_dir}
-
-# Get version
-version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
-
-# Build binary
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime win-x86 --output /dist/jellyfin_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"
-
-# Prepare addins
-addin_build_dir="$( mktemp -d )"
-wget ${NSSM_URL} -O ${addin_build_dir}/nssm.zip
-wget ${FFMPEG_URL} -O ${addin_build_dir}/ffmpeg.zip
-unzip ${addin_build_dir}/nssm.zip -d ${addin_build_dir}
-cp ${addin_build_dir}/${NSSM_VERSION}/win64/nssm.exe /dist/jellyfin_${version}/nssm.exe
-unzip ${addin_build_dir}/ffmpeg.zip -d ${addin_build_dir}
-cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffmpeg.exe /dist/jellyfin_${version}/ffmpeg.exe
-cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffprobe.exe /dist/jellyfin_${version}/ffprobe.exe
-rm -rf ${addin_build_dir}
-
-# Prepare scripts
-cp ${SOURCE_DIR}/deployment/windows/legacy/install-jellyfin.ps1 /dist/jellyfin_${version}/install-jellyfin.ps1
-cp ${SOURCE_DIR}/deployment/windows/legacy/install.bat /dist/jellyfin_${version}/install.bat
-
-# Create zip package
-pushd /dist
-zip -r /jellyfin_${version}.portable.zip jellyfin_${version}
-popd
-rm -rf /dist/jellyfin_${version}
-
-# Move the artifacts out
-mkdir -p ${ARTIFACT_DIR}/
-mv /jellyfin[-_]*.zip ${ARTIFACT_DIR}/
-chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
diff --git a/deployment/win-x86/package.sh b/deployment/win-x86/package.sh
deleted file mode 100755
index 65e7e2928..000000000
--- a/deployment/win-x86/package.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env bash
-
-args="${@}"
-declare -a docker_envvars
-for arg in ${args}; do
- docker_envvars+=("-e ${arg}")
-done
-
-WORKDIR="$( pwd )"
-
-package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
-output_dir="${WORKDIR}/pkg-dist"
-current_user="$( whoami )"
-image_name="jellyfin-windows-x86-build"
-
-# Determine if sudo should be used for Docker
-if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
- && [[ ! ${EUID:-1000} -eq 0 ]] \
- && [[ ! ${USER} == "root" ]] \
- && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
- docker_sudo="sudo"
-else
- docker_sudo=""
-fi
-
-# Prepare temporary package dir
-mkdir -p "${package_temporary_dir}"
-# Set up the build environment Docker image
-${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
-# Build the DEBs and copy out to ${package_temporary_dir}
-${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars}
-# Move the DEBs to the output directory
-mkdir -p "${output_dir}"
-mv "${package_temporary_dir}"/* "${output_dir}"
diff --git a/deployment/fedora-package-x64/pkg-src/.gitignore b/fedora/.gitignore
index 6019b98c2..6019b98c2 100644
--- a/deployment/fedora-package-x64/pkg-src/.gitignore
+++ b/fedora/.gitignore
diff --git a/fedora/Makefile b/fedora/Makefile
new file mode 100644
index 000000000..97904ddd3
--- /dev/null
+++ b/fedora/Makefile
@@ -0,0 +1,26 @@
+VERSION := $(shell sed -ne '/^Version:/s/.* *//p' fedora/jellyfin.spec)
+
+srpm:
+ cd fedora/; \
+ SOURCE_DIR=.. \
+ WORKDIR="$${PWD}"; \
+ tar \
+ --transform "s,^\.,jellyfin-server-$(VERSION)," \
+ --exclude='.git*' \
+ --exclude='**/.git' \
+ --exclude='**/.hg' \
+ --exclude='**/.vs' \
+ --exclude='**/.vscode' \
+ --exclude='deployment' \
+ --exclude='**/bin' \
+ --exclude='**/obj' \
+ --exclude='**/.nuget' \
+ --exclude='*.deb' \
+ --exclude='*.rpm' \
+ --exclude='jellyfin-server-$(VERSION).tar.gz' \
+ -czf "jellyfin-server-$(VERSION).tar.gz" \
+ -C $${SOURCE_DIR} ./
+ cd fedora/; \
+ rpmbuild -bs jellyfin.spec \
+ --define "_sourcedir $$PWD/" \
+ --define "_srcrpmdir $(outdir)"
diff --git a/deployment/fedora-package-x64/pkg-src/README.md b/fedora/README.md
index 7ed6f7efc..7ed6f7efc 100644
--- a/deployment/fedora-package-x64/pkg-src/README.md
+++ b/fedora/README.md
diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin-firewalld.xml b/fedora/jellyfin-firewalld.xml
index 538c5d65f..538c5d65f 100644
--- a/deployment/fedora-package-x64/pkg-src/jellyfin-firewalld.xml
+++ b/fedora/jellyfin-firewalld.xml
diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.env b/fedora/jellyfin.env
index de48f13af..bf64acd3f 100644
--- a/deployment/fedora-package-x64/pkg-src/jellyfin.env
+++ b/fedora/jellyfin.env
@@ -20,6 +20,9 @@ JELLYFIN_CONFIG_DIR="/etc/jellyfin"
JELLYFIN_LOG_DIR="/var/log/jellyfin"
JELLYFIN_CACHE_DIR="/var/cache/jellyfin"
+# web client path, installed by the jellyfin-web package
+JELLYFIN_WEB_OPT="--webdir=/usr/share/jellyfin-web"
+
# In-App service control
JELLYFIN_RESTART_OPT="--restartpath=/usr/libexec/jellyfin/restart.sh"
diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.override.conf b/fedora/jellyfin.override.conf
index 8652450bb..8652450bb 100644
--- a/deployment/fedora-package-x64/pkg-src/jellyfin.override.conf
+++ b/fedora/jellyfin.override.conf
diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.service b/fedora/jellyfin.service
index f3dc594b1..b092ebf2f 100644
--- a/deployment/fedora-package-x64/pkg-src/jellyfin.service
+++ b/fedora/jellyfin.service
@@ -5,7 +5,7 @@ Description=Jellyfin is a free software media system that puts you in control of
[Service]
EnvironmentFile=/etc/sysconfig/jellyfin
WorkingDirectory=/var/lib/jellyfin
-ExecStart=/usr/bin/jellyfin ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT}
+ExecStart=/usr/bin/jellyfin ${JELLYFIN_WEB_OPT} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT}
TimeoutSec=15
Restart=on-failure
User=jellyfin
diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.spec b/fedora/jellyfin.spec
index 33c6f6f64..9311864a6 100644
--- a/deployment/fedora-package-x64/pkg-src/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -7,15 +7,13 @@
%endif
Name: jellyfin
-Version: 10.5.0
+Version: 10.6.0
Release: 1%{?dist}
-Summary: The Free Software Media Browser
-License: GPLv2
-URL: https://jellyfin.media
+Summary: The Free Software Media System
+License: GPLv3
+URL: https://jellyfin.org
# Jellyfin Server tarball created by `make -f .copr/Makefile srpm`, real URL ends with `v%{version}.tar.gz`
-Source0: https://github.com/%{name}/%{name}/archive/%{name}-%{version}.tar.gz
-# Jellyfin Webinterface downloaded by `make -f .copr/Makefile srpm`, real URL ends with `v%{version}.tar.gz`
-Source1: https://github.com/%{name}/%{name}-web/archive/%{name}-web-%{version}.tar.gz
+Source0: jellyfin-server-%{version}.tar.gz
Source11: jellyfin.service
Source12: jellyfin.env
Source13: jellyfin.sudoers
@@ -25,43 +23,30 @@ Source16: jellyfin-firewalld.xml
%{?systemd_requires}
BuildRequires: systemd
-Requires(pre): shadow-utils
-BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel, glibc-devel, libicu-devel, git
-%if 0%{?fedora}
-BuildRequires: nodejs-yarn, git
-%else
-# Requirements not packaged in main repos
-# From https://rpm.nodesource.com/pub_10.x/el/7/x86_64/
-BuildRequires: nodejs >= 10 yarn
-%endif
-Requires: libcurl, fontconfig, freetype, openssl, glibc libicu
+BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel, glibc-devel, libicu-devel
# Requirements not packaged in main repos
# COPR @dotnet-sig/dotnet or
# https://packages.microsoft.com/rhel/7/prod/
BuildRequires: dotnet-runtime-3.1, dotnet-sdk-3.1
-# RPMfusion free
-Requires: ffmpeg
-
+Requires: %{name}-server = %{version}-%{release}, %{name}-web >= 10.6, %{name}-web < 10.7
# Disable Automatic Dependency Processing
AutoReqProv: no
%description
Jellyfin is a free software media system that puts you in control of managing and streaming your media.
+%package server
+# RPMfusion free
+Summary: The Free Software Media System Server backend
+Requires(pre): shadow-utils
+Requires: ffmpeg
+Requires: libcurl, fontconfig, freetype, openssl, glibc libicu
+
+%description server
+The Jellyfin media server backend.
%prep
-%autosetup -n %{name}-%{version} -b 0 -b 1
-web_build_dir="$(mktemp -d)"
-web_target="$PWD/MediaBrowser.WebDashboard/jellyfin-web"
-pushd ../jellyfin-web-%{version} || pushd ../jellyfin-web-master
-%if 0%{?fedora}
-nodejs-yarn install
-%else
-yarn install
-%endif
-mkdir -p ${web_target}
-mv dist/* ${web_target}/
-popd
+%autosetup -n jellyfin-server-%{version} -b 0
%build
@@ -70,81 +55,76 @@ export DOTNET_CLI_TELEMETRY_OPTOUT=1
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} \
"-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server
-%{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/%{name}/LICENSE
-%{__install} -D -m 0644 %{SOURCE15} %{buildroot}%{_sysconfdir}/systemd/system/%{name}.service.d/override.conf
-%{__install} -D -m 0644 Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/%{name}/logging.json
+%{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/jellyfin/LICENSE
+%{__install} -D -m 0644 %{SOURCE15} %{buildroot}%{_sysconfdir}/systemd/system/jellyfin.service.d/override.conf
+%{__install} -D -m 0644 Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json
%{__mkdir} -p %{buildroot}%{_bindir}
tee %{buildroot}%{_bindir}/jellyfin << EOF
#!/bin/sh
-exec %{_libdir}/%{name}/%{name} \${@}
+exec %{_libdir}/jellyfin/jellyfin \${@}
EOF
%{__mkdir} -p %{buildroot}%{_sharedstatedir}/jellyfin
-%{__mkdir} -p %{buildroot}%{_sysconfdir}/%{name}
+%{__mkdir} -p %{buildroot}%{_sysconfdir}/jellyfin
%{__mkdir} -p %{buildroot}%{_var}/log/jellyfin
%{__mkdir} -p %{buildroot}%{_var}/cache/jellyfin
-%{__install} -D -m 0644 %{SOURCE11} %{buildroot}%{_unitdir}/%{name}.service
-%{__install} -D -m 0644 %{SOURCE12} %{buildroot}%{_sysconfdir}/sysconfig/%{name}
-%{__install} -D -m 0600 %{SOURCE13} %{buildroot}%{_sysconfdir}/sudoers.d/%{name}-sudoers
-%{__install} -D -m 0755 %{SOURCE14} %{buildroot}%{_libexecdir}/%{name}/restart.sh
-%{__install} -D -m 0644 %{SOURCE16} %{buildroot}%{_prefix}/lib/firewalld/services/%{name}.xml
+%{__install} -D -m 0644 %{SOURCE11} %{buildroot}%{_unitdir}/jellyfin.service
+%{__install} -D -m 0644 %{SOURCE12} %{buildroot}%{_sysconfdir}/sysconfig/jellyfin
+%{__install} -D -m 0600 %{SOURCE13} %{buildroot}%{_sysconfdir}/sudoers.d/jellyfin-sudoers
+%{__install} -D -m 0755 %{SOURCE14} %{buildroot}%{_libexecdir}/jellyfin/restart.sh
+%{__install} -D -m 0644 %{SOURCE16} %{buildroot}%{_prefix}/lib/firewalld/services/jellyfin.xml
-%files
-%{_libdir}/%{name}/jellyfin-web/*
-%attr(755,root,root) %{_bindir}/%{name}
-%{_libdir}/%{name}/*.json
-%{_libdir}/%{name}/*.dll
-%{_libdir}/%{name}/*.so
-%{_libdir}/%{name}/*.a
-%{_libdir}/%{name}/createdump
+%files server
+%attr(755,root,root) %{_bindir}/jellyfin
+%{_libdir}/jellyfin/*.json
+%{_libdir}/jellyfin/*.dll
+%{_libdir}/jellyfin/*.so
+%{_libdir}/jellyfin/*.a
+%{_libdir}/jellyfin/createdump
# Needs 755 else only root can run it since binary build by dotnet is 722
-%attr(755,root,root) %{_libdir}/%{name}/jellyfin
-%{_libdir}/%{name}/SOS_README.md
-%{_unitdir}/%{name}.service
-%{_libexecdir}/%{name}/restart.sh
-%{_prefix}/lib/firewalld/services/%{name}.xml
-%attr(755,jellyfin,jellyfin) %dir %{_sysconfdir}/%{name}
-%config %{_sysconfdir}/sysconfig/%{name}
-%config(noreplace) %attr(600,root,root) %{_sysconfdir}/sudoers.d/%{name}-sudoers
-%config(noreplace) %{_sysconfdir}/systemd/system/%{name}.service.d/override.conf
-%config(noreplace) %attr(644,jellyfin,jellyfin) %{_sysconfdir}/%{name}/logging.json
+%attr(755,root,root) %{_libdir}/jellyfin/jellyfin
+%{_libdir}/jellyfin/SOS_README.md
+%{_unitdir}/jellyfin.service
+%{_libexecdir}/jellyfin/restart.sh
+%{_prefix}/lib/firewalld/services/jellyfin.xml
+%attr(755,jellyfin,jellyfin) %dir %{_sysconfdir}/jellyfin
+%config %{_sysconfdir}/sysconfig/jellyfin
+%config(noreplace) %attr(600,root,root) %{_sysconfdir}/sudoers.d/jellyfin-sudoers
+%config(noreplace) %{_sysconfdir}/systemd/system/jellyfin.service.d/override.conf
+%config(noreplace) %attr(644,jellyfin,jellyfin) %{_sysconfdir}/jellyfin/logging.json
%attr(750,jellyfin,jellyfin) %dir %{_sharedstatedir}/jellyfin
%attr(-,jellyfin,jellyfin) %dir %{_var}/log/jellyfin
%attr(750,jellyfin,jellyfin) %dir %{_var}/cache/jellyfin
-%if 0%{?fedora}
-%license LICENSE
-%else
-%{_datadir}/licenses/%{name}/LICENSE
-%endif
+%{_datadir}/licenses/jellyfin/LICENSE
-%pre
+%pre server
getent group jellyfin >/dev/null || groupadd -r jellyfin
getent passwd jellyfin >/dev/null || \
useradd -r -g jellyfin -d %{_sharedstatedir}/jellyfin -s /sbin/nologin \
-c "Jellyfin default user" jellyfin
exit 0
-%post
+%post server
# Move existing configuration cache and logs to their new locations and symlink them.
if [ $1 -gt 1 ] ; then
service_state=$(systemctl is-active jellyfin.service)
if [ "${service_state}" = "active" ]; then
systemctl stop jellyfin.service
fi
- if [ ! -L %{_sharedstatedir}/%{name}/config ]; then
- mv %{_sharedstatedir}/%{name}/config/* %{_sysconfdir}/%{name}/
- rmdir %{_sharedstatedir}/%{name}/config
- ln -sf %{_sysconfdir}/%{name} %{_sharedstatedir}/%{name}/config
+ if [ ! -L %{_sharedstatedir}/jellyfin/config ]; then
+ mv %{_sharedstatedir}/jellyfin/config/* %{_sysconfdir}/jellyfin/
+ rmdir %{_sharedstatedir}/jellyfin/config
+ ln -sf %{_sysconfdir}/jellyfin %{_sharedstatedir}/jellyfin/config
fi
- if [ ! -L %{_sharedstatedir}/%{name}/logs ]; then
- mv %{_sharedstatedir}/%{name}/logs/* %{_var}/log/jellyfin
- rmdir %{_sharedstatedir}/%{name}/logs
- ln -sf %{_var}/log/jellyfin %{_sharedstatedir}/%{name}/logs
+ if [ ! -L %{_sharedstatedir}/jellyfin/logs ]; then
+ mv %{_sharedstatedir}/jellyfin/logs/* %{_var}/log/jellyfin
+ rmdir %{_sharedstatedir}/jellyfin/logs
+ ln -sf %{_var}/log/jellyfin %{_sharedstatedir}/jellyfin/logs
fi
- if [ ! -L %{_sharedstatedir}/%{name}/cache ]; then
- mv %{_sharedstatedir}/%{name}/cache/* %{_var}/cache/jellyfin
- rmdir %{_sharedstatedir}/%{name}/cache
- ln -sf %{_var}/cache/jellyfin %{_sharedstatedir}/%{name}/cache
+ if [ ! -L %{_sharedstatedir}/jellyfin/cache ]; then
+ mv %{_sharedstatedir}/jellyfin/cache/* %{_var}/cache/jellyfin
+ rmdir %{_sharedstatedir}/jellyfin/cache
+ ln -sf %{_var}/cache/jellyfin %{_sharedstatedir}/jellyfin/cache
fi
if [ "${service_state}" = "active" ]; then
systemctl start jellyfin.service
@@ -152,13 +132,15 @@ if [ $1 -gt 1 ] ; then
fi
%systemd_post jellyfin.service
-%preun
+%preun server
%systemd_preun jellyfin.service
-%postun
+%postun server
%systemd_postun_with_restart jellyfin.service
%changelog
+* Mon Mar 23 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
+- Forthcoming stable release
* Fri Oct 11 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.5.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.5.0
* Sat Aug 31 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.sudoers b/fedora/jellyfin.sudoers
index dd245af4b..dd245af4b 100644
--- a/deployment/fedora-package-x64/pkg-src/jellyfin.sudoers
+++ b/fedora/jellyfin.sudoers
diff --git a/deployment/fedora-package-x64/pkg-src/restart.sh b/fedora/restart.sh
index 9b64b6d72..9e53efecd 100755
--- a/deployment/fedora-package-x64/pkg-src/restart.sh
+++ b/fedora/restart.sh
@@ -24,13 +24,13 @@ cmd="$( get_service_command )"
echo "Detected service control platform '$cmd'; using it to restart Jellyfin..."
case $cmd in
'systemctl')
- echo "sleep 2; /usr/bin/sudo $( which systemctl ) restart jellyfin" | at now
+ echo "sleep 2; /usr/bin/sudo $( which systemctl ) restart jellyfin" | at now
;;
'service')
- echo "sleep 2; /usr/bin/sudo $( which service ) jellyfin restart" | at now
+ echo "sleep 2; /usr/bin/sudo $( which service ) jellyfin restart" | at now
;;
'sysv')
- echo "sleep 2; /usr/bin/sudo /etc/init.d/jellyfin restart" | at now
+ echo "sleep 2; /usr/bin/sudo /etc/init.d/jellyfin restart" | at now
;;
esac
exit 0
diff --git a/tests/Jellyfin.Common.Tests/Extensions/StringExtensionsTests.cs b/tests/Jellyfin.Common.Tests/Extensions/StringExtensionsTests.cs
new file mode 100644
index 000000000..8bf613f05
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Extensions/StringExtensionsTests.cs
@@ -0,0 +1,43 @@
+using System;
+using MediaBrowser.Common.Extensions;
+using Xunit;
+
+namespace Jellyfin.Common.Tests.Extensions
+{
+ public class StringExtensionsTests
+ {
+ [Theory]
+ [InlineData("", 'q', "")]
+ [InlineData("Banana split", ' ', "Banana")]
+ [InlineData("Banana split", 'q', "Banana split")]
+ public void LeftPart_ValidArgsCharNeedle_Correct(string str, char needle, string expectedResult)
+ {
+ var result = str.AsSpan().LeftPart(needle).ToString();
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Theory]
+ [InlineData("", "", "")]
+ [InlineData("", "q", "")]
+ [InlineData("Banana split", "", "")]
+ [InlineData("Banana split", " ", "Banana")]
+ [InlineData("Banana split test", " split", "Banana")]
+ public void LeftPart_ValidArgsWithoutStringComparison_Correct(string str, string needle, string expectedResult)
+ {
+ var result = str.AsSpan().LeftPart(needle).ToString();
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Theory]
+ [InlineData("", "", StringComparison.Ordinal, "")]
+ [InlineData("Banana split", " ", StringComparison.Ordinal, "Banana")]
+ [InlineData("Banana split test", " split", StringComparison.Ordinal, "Banana")]
+ [InlineData("Banana split test", " Split", StringComparison.Ordinal, "Banana split test")]
+ [InlineData("Banana split test", " Splït", StringComparison.InvariantCultureIgnoreCase, "Banana split test")]
+ public void LeftPart_ValidArgs_Correct(string str, string needle, StringComparison stringComparison, string expectedResult)
+ {
+ var result = str.AsSpan().LeftPart(needle, stringComparison).ToString();
+ Assert.Equal(expectedResult, result);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs b/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs
new file mode 100644
index 000000000..51633e157
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs
@@ -0,0 +1,19 @@
+using System;
+using MediaBrowser.Model.Extensions;
+using Xunit;
+
+namespace Jellyfin.Model.Tests.Extensions
+{
+ public class StringHelperTests
+ {
+ [Theory]
+ [InlineData("", "")]
+ [InlineData("banana", "Banana")]
+ [InlineData("Banana", "Banana")]
+ [InlineData("ä", "Ä")]
+ public void StringHelper_ValidArgs_Success(string input, string expectedResult)
+ {
+ Assert.Equal(expectedResult, StringHelper.FirstToUpper(input));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
new file mode 100644
index 000000000..f6c327498
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
@@ -0,0 +1,21 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp3.1</TargetFramework>
+ <IsPackable>false</IsPackable>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
+ <PackageReference Include="xunit" Version="2.4.1" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
+ <PackageReference Include="coverlet.collector" Version="1.2.1" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="../../MediaBrowser.Model/MediaBrowser.Model.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs b/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs
index 9a4b0b542..c9a295a4c 100644
--- a/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs
@@ -6,61 +6,45 @@ namespace Jellyfin.Naming.Tests.Music
{
public class MultiDiscAlbumTests
{
- [Fact]
- public void TestMultiDiscAlbums()
+ private readonly NamingOptions _namingOptions = new NamingOptions();
+
+ [Theory]
+ [InlineData("", false)]
+ [InlineData("C:/", false)]
+ [InlineData("/home/", false)]
+ [InlineData(@"blah blah", false)]
+ [InlineData(@"D:/music/weezer/03 Pinkerton", false)]
+ [InlineData(@"D:/music/michael jackson/Bad (2012 Remaster)", false)]
+ [InlineData(@"cd1", true)]
+ [InlineData(@"disc18", true)]
+ [InlineData(@"disk10", true)]
+ [InlineData(@"vol7", true)]
+ [InlineData(@"volume1", true)]
+ [InlineData(@"cd 1", true)]
+ [InlineData(@"disc 1", true)]
+ [InlineData(@"disk 1", true)]
+ [InlineData(@"disk", false)]
+ [InlineData(@"disk ·", false)]
+ [InlineData(@"disk a", false)]
+ [InlineData(@"disk volume", false)]
+ [InlineData(@"disc disc", false)]
+ [InlineData(@"disk disc 6", false)]
+ [InlineData(@"cd - 1", true)]
+ [InlineData(@"disc- 1", true)]
+ [InlineData(@"disk - 1", true)]
+ [InlineData(@"Disc 01 (Hugo Wolf · 24 Lieder)", true)]
+ [InlineData(@"Disc 04 (Encores and Folk Songs)", true)]
+ [InlineData(@"Disc04 (Encores and Folk Songs)", true)]
+ [InlineData(@"Disc 04(Encores and Folk Songs)", true)]
+ [InlineData(@"Disc04(Encores and Folk Songs)", true)]
+ [InlineData(@"D:/Video/MBTestLibrary/VideoTest/music/.38 special/anth/Disc 2", true)]
+ [InlineData(@"[1985] Opportunities (Let's make lots of money) (1985)", false)]
+ [InlineData(@"Blah 04(Encores and Folk Songs)", false)]
+ public void AlbumParser_MultidiscPath_Identifies(string path, bool result)
{
- Assert.False(IsMultiDiscAlbumFolder(@"blah blah"));
- Assert.False(IsMultiDiscAlbumFolder(@"D:/music/weezer/03 Pinkerton"));
- Assert.False(IsMultiDiscAlbumFolder(@"D:/music/michael jackson/Bad (2012 Remaster)"));
+ var parser = new AlbumParser(_namingOptions);
- Assert.True(IsMultiDiscAlbumFolder(@"cd1"));
- Assert.True(IsMultiDiscAlbumFolder(@"disc18"));
- Assert.True(IsMultiDiscAlbumFolder(@"disk10"));
- Assert.True(IsMultiDiscAlbumFolder(@"vol7"));
- Assert.True(IsMultiDiscAlbumFolder(@"volume1"));
-
- Assert.True(IsMultiDiscAlbumFolder(@"cd 1"));
- Assert.True(IsMultiDiscAlbumFolder(@"disc 1"));
- Assert.True(IsMultiDiscAlbumFolder(@"disk 1"));
-
- Assert.False(IsMultiDiscAlbumFolder(@"disk"));
- Assert.False(IsMultiDiscAlbumFolder(@"disk ·"));
- Assert.False(IsMultiDiscAlbumFolder(@"disk a"));
-
- Assert.False(IsMultiDiscAlbumFolder(@"disk volume"));
- Assert.False(IsMultiDiscAlbumFolder(@"disc disc"));
- Assert.False(IsMultiDiscAlbumFolder(@"disk disc 6"));
-
- Assert.True(IsMultiDiscAlbumFolder(@"cd - 1"));
- Assert.True(IsMultiDiscAlbumFolder(@"disc- 1"));
- Assert.True(IsMultiDiscAlbumFolder(@"disk - 1"));
-
- Assert.True(IsMultiDiscAlbumFolder(@"Disc 01 (Hugo Wolf · 24 Lieder)"));
- Assert.True(IsMultiDiscAlbumFolder(@"Disc 04 (Encores and Folk Songs)"));
- Assert.True(IsMultiDiscAlbumFolder(@"Disc04 (Encores and Folk Songs)"));
- Assert.True(IsMultiDiscAlbumFolder(@"Disc 04(Encores and Folk Songs)"));
- Assert.True(IsMultiDiscAlbumFolder(@"Disc04(Encores and Folk Songs)"));
-
- Assert.True(IsMultiDiscAlbumFolder(@"D:/Video/MBTestLibrary/VideoTest/music/.38 special/anth/Disc 2"));
- }
-
- [Fact]
- public void TestMultiDiscAlbums1()
- {
- Assert.False(IsMultiDiscAlbumFolder(@"[1985] Opportunities (Let's make lots of money) (1985)"));
- }
-
- [Fact]
- public void TestMultiDiscAlbums2()
- {
- Assert.False(IsMultiDiscAlbumFolder(@"Blah 04(Encores and Folk Songs)"));
- }
-
- private bool IsMultiDiscAlbumFolder(string path)
- {
- var parser = new AlbumParser(new NamingOptions());
-
- return parser.IsMultiPart(path);
+ Assert.Equal(result, parser.IsMultiPart(path));
}
}
}
diff --git a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs
index 41da889c2..40d80607c 100644
--- a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs
@@ -1,4 +1,5 @@
-using Emby.Naming.Common;
+using System;
+using Emby.Naming.Common;
using Emby.Naming.Subtitles;
using Xunit;
@@ -6,28 +7,19 @@ namespace Jellyfin.Naming.Tests.Subtitles
{
public class SubtitleParserTests
{
- private SubtitleParser GetParser()
- {
- var options = new NamingOptions();
-
- return new SubtitleParser(options);
- }
-
- [Fact]
- public void TestSubtitles()
- {
- Test("The Skin I Live In (2011).srt", null, false, false);
- Test("The Skin I Live In (2011).eng.srt", "eng", false, false);
- Test("The Skin I Live In (2011).eng.default.srt", "eng", true, false);
- Test("The Skin I Live In (2011).eng.forced.srt", "eng", false, true);
- Test("The Skin I Live In (2011).eng.foreign.srt", "eng", false, true);
- Test("The Skin I Live In (2011).eng.default.foreign.srt", "eng", true, true);
- Test("The Skin I Live In (2011).default.foreign.eng.srt", "eng", true, true);
- }
+ private readonly NamingOptions _namingOptions = new NamingOptions();
- private void Test(string input, string language, bool isDefault, bool isForced)
+ [Theory]
+ [InlineData("The Skin I Live In (2011).srt", null, false, false)]
+ [InlineData("The Skin I Live In (2011).eng.srt", "eng", false, false)]
+ [InlineData("The Skin I Live In (2011).eng.default.srt", "eng", true, false)]
+ [InlineData("The Skin I Live In (2011).eng.forced.srt", "eng", false, true)]
+ [InlineData("The Skin I Live In (2011).eng.foreign.srt", "eng", false, true)]
+ [InlineData("The Skin I Live In (2011).eng.default.foreign.srt", "eng", true, true)]
+ [InlineData("The Skin I Live In (2011).default.foreign.eng.srt", "eng", true, true)]
+ public void SubtitleParser_ValidFileName_Parses(string input, string language, bool isDefault, bool isForced)
{
- var parser = GetParser();
+ var parser = new SubtitleParser(_namingOptions);
var result = parser.ParseFile(input);
@@ -35,5 +27,20 @@ namespace Jellyfin.Naming.Tests.Subtitles
Assert.Equal(isDefault, result.IsDefault);
Assert.Equal(isForced, result.IsForced);
}
+
+ [Theory]
+ [InlineData("The Skin I Live In (2011).mp4")]
+ public void SubtitleParser_InvalidFileName_ReturnsNull(string input)
+ {
+ var parser = new SubtitleParser(_namingOptions);
+
+ Assert.Null(parser.ParseFile(input));
+ }
+
+ [Fact]
+ public void SubtitleParser_EmptyFileName_ThrowsArgumentException()
+ {
+ Assert.Throws<ArgumentException>(() => new SubtitleParser(_namingOptions).ParseFile(string.Empty));
+ }
}
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/BaseVideoTest.cs b/tests/Jellyfin.Naming.Tests/Video/BaseVideoTest.cs
deleted file mode 100644
index 0c2978aca..000000000
--- a/tests/Jellyfin.Naming.Tests/Video/BaseVideoTest.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using Emby.Naming.Common;
-using Emby.Naming.Video;
-
-namespace Jellyfin.Naming.Tests.Video
-{
- public abstract class BaseVideoTest
- {
- private readonly NamingOptions _namingOptions = new NamingOptions();
-
- protected VideoResolver GetParser()
- => new VideoResolver(_namingOptions);
- }
-}
diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
index 49cb2387b..917d8fb3a 100644
--- a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
@@ -46,6 +46,7 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("Maximum Ride - 2016 - WEBDL-1080p - x264 AC3.mkv", "Maximum Ride", 2016)]
// FIXME: [InlineData("Robin Hood [Multi-Subs] [2018].mkv", "Robin Hood", 2018)]
[InlineData(@"3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv", "3.Days.to.Kill", 2014)] // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again
+ [InlineData("3 days to kill (2005).mkv", "3 days to kill", 2005)]
public void CleanDateTimeTest(string input, string expectedName, int? expectedYear)
{
input = Path.GetFileName(input);
diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
index a64d17349..a2722a175 100644
--- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
@@ -5,7 +5,7 @@ using Xunit;
namespace Jellyfin.Naming.Tests.Video
{
- public class ExtraTests : BaseVideoTest
+ public class ExtraTests
{
private readonly NamingOptions _videoOptions = new NamingOptions();
diff --git a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs
index ed3112936..d2b3d6ff0 100644
--- a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs
@@ -4,26 +4,26 @@ using Xunit;
namespace Jellyfin.Naming.Tests.Video
{
- public class Format3DTests : BaseVideoTest
+ public class Format3DTests
{
+ private readonly NamingOptions _namingOptions = new NamingOptions();
+
[Fact]
public void TestKodiFormat3D()
{
- var options = new NamingOptions();
-
- Test("Super movie.3d.mp4", false, null, options);
- Test("Super movie.3d.hsbs.mp4", true, "hsbs", options);
- Test("Super movie.3d.sbs.mp4", true, "sbs", options);
- Test("Super movie.3d.htab.mp4", true, "htab", options);
- Test("Super movie.3d.tab.mp4", true, "tab", options);
- Test("Super movie 3d hsbs.mp4", true, "hsbs", options);
+ Test("Super movie.3d.mp4", false, null);
+ Test("Super movie.3d.hsbs.mp4", true, "hsbs");
+ Test("Super movie.3d.sbs.mp4", true, "sbs");
+ Test("Super movie.3d.htab.mp4", true, "htab");
+ Test("Super movie.3d.tab.mp4", true, "tab");
+ Test("Super movie 3d hsbs.mp4", true, "hsbs");
}
[Fact]
public void Test3DName()
{
var result =
- GetParser().ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv");
+ new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv");
Assert.Equal("hsbs", result.Format3D);
Assert.Equal("Oblivion", result.Name);
@@ -34,32 +34,31 @@ namespace Jellyfin.Naming.Tests.Video
{
// These were introduced for Media Browser 3
// Kodi conventions are preferred but these still need to be supported
- var options = new NamingOptions();
- Test("Super movie.3d.mp4", false, null, options);
- Test("Super movie.3d.hsbs.mp4", true, "hsbs", options);
- Test("Super movie.3d.sbs.mp4", true, "sbs", options);
- Test("Super movie.3d.htab.mp4", true, "htab", options);
- Test("Super movie.3d.tab.mp4", true, "tab", options);
+ Test("Super movie.3d.mp4", false, null);
+ Test("Super movie.3d.hsbs.mp4", true, "hsbs");
+ Test("Super movie.3d.sbs.mp4", true, "sbs");
+ Test("Super movie.3d.htab.mp4", true, "htab");
+ Test("Super movie.3d.tab.mp4", true, "tab");
- Test("Super movie.hsbs.mp4", true, "hsbs", options);
- Test("Super movie.sbs.mp4", true, "sbs", options);
- Test("Super movie.htab.mp4", true, "htab", options);
- Test("Super movie.tab.mp4", true, "tab", options);
- Test("Super movie.sbs3d.mp4", true, "sbs3d", options);
- Test("Super movie.3d.mvc.mp4", true, "mvc", options);
+ Test("Super movie.hsbs.mp4", true, "hsbs");
+ Test("Super movie.sbs.mp4", true, "sbs");
+ Test("Super movie.htab.mp4", true, "htab");
+ Test("Super movie.tab.mp4", true, "tab");
+ Test("Super movie.sbs3d.mp4", true, "sbs3d");
+ Test("Super movie.3d.mvc.mp4", true, "mvc");
- Test("Super movie [3d].mp4", false, null, options);
- Test("Super movie [hsbs].mp4", true, "hsbs", options);
- Test("Super movie [fsbs].mp4", true, "fsbs", options);
- Test("Super movie [ftab].mp4", true, "ftab", options);
- Test("Super movie [htab].mp4", true, "htab", options);
- Test("Super movie [sbs3d].mp4", true, "sbs3d", options);
+ Test("Super movie [3d].mp4", false, null);
+ Test("Super movie [hsbs].mp4", true, "hsbs");
+ Test("Super movie [fsbs].mp4", true, "fsbs");
+ Test("Super movie [ftab].mp4", true, "ftab");
+ Test("Super movie [htab].mp4", true, "htab");
+ Test("Super movie [sbs3d].mp4", true, "sbs3d");
}
- private void Test(string input, bool is3D, string format3D, NamingOptions options)
+ private void Test(string input, bool is3D, string? format3D)
{
- var parser = new Format3DParser(options);
+ var parser = new Format3DParser(_namingOptions);
var result = parser.Parse(input);
diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
index b8fbb2cb2..03fe32b6e 100644
--- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
@@ -8,6 +8,8 @@ namespace Jellyfin.Naming.Tests.Video
{
public class MultiVersionTests
{
+ private readonly NamingOptions _namingOptions = new NamingOptions();
+
// FIXME
// [Fact]
public void TestMultiEdition1()
@@ -430,8 +432,7 @@ namespace Jellyfin.Naming.Tests.Video
private VideoListResolver GetResolver()
{
- var options = new NamingOptions();
- return new VideoListResolver(options);
+ return new VideoListResolver(_namingOptions);
}
}
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
index 3e0cbaf0c..3630a07e4 100644
--- a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
@@ -6,8 +6,10 @@ using Xunit;
namespace Jellyfin.Naming.Tests.Video
{
- public class StackTests : BaseVideoTest
+ public class StackTests
{
+ private readonly NamingOptions _namingOptions = new NamingOptions();
+
[Fact]
public void TestSimpleStack()
{
@@ -446,7 +448,7 @@ namespace Jellyfin.Naming.Tests.Video
private StackResolver GetResolver()
{
- return new StackResolver(new NamingOptions());
+ return new StackResolver(_namingOptions);
}
}
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
index 8d5ced9a4..e31d97e2e 100644
--- a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
@@ -4,8 +4,10 @@ using Xunit;
namespace Jellyfin.Naming.Tests.Video
{
- public class StubTests : BaseVideoTest
+ public class StubTests
{
+ private readonly NamingOptions _namingOptions = new NamingOptions();
+
[Fact]
public void TestStubs()
{
@@ -27,16 +29,14 @@ namespace Jellyfin.Naming.Tests.Video
public void TestStubName()
{
var result =
- GetParser().ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc");
+ new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc");
Assert.Equal("Oblivion", result.Name);
}
private void Test(string path, bool isStub, string stubType)
{
- var options = new NamingOptions();
-
- var isStubResult = StubResolver.TryResolveFile(path, options, out var stubTypeResult);
+ var isStubResult = StubResolver.TryResolveFile(path, _namingOptions, out var stubTypeResult);
Assert.Equal(isStub, isStubResult);
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
index ef8a17898..566dc9f7c 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
@@ -8,6 +8,7 @@ namespace Jellyfin.Naming.Tests.Video
{
public class VideoListResolverTests
{
+ private readonly NamingOptions _namingOptions = new NamingOptions();
// FIXME
// [Fact]
public void TestStackAndExtras()
@@ -450,8 +451,7 @@ namespace Jellyfin.Naming.Tests.Video
private VideoListResolver GetResolver()
{
- var options = new NamingOptions();
- return new VideoListResolver(options);
+ return new VideoListResolver(_namingOptions);
}
}
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
index 5a3ce8886..114735cee 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
@@ -1,275 +1,200 @@
-using MediaBrowser.Model.Entities;
+using System.Collections.Generic;
+using Emby.Naming.Common;
+using Emby.Naming.Video;
+using MediaBrowser.Model.Entities;
using Xunit;
namespace Jellyfin.Naming.Tests.Video
{
- public class VideoResolverTests : BaseVideoTest
+ public class VideoResolverTests
{
- // FIXME
- // [Fact]
- public void TestSimpleFile()
- {
- var parser = GetParser();
-
- var result =
- parser.ResolveFile(@"/server/Movies/Brave (2007)/Brave (2006).mkv");
-
- Assert.Equal(2006, result.Year);
- Assert.False(result.IsStub);
- Assert.False(result.Is3D);
- Assert.Equal("Brave", result.Name);
- Assert.Null(result.ExtraType);
- }
-
- // FIXME
- // [Fact]
- public void TestSimpleFile2()
- {
- var parser = GetParser();
-
- var result =
- parser.ResolveFile(@"/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv");
-
- Assert.Equal(1995, result.Year);
- Assert.False(result.IsStub);
- Assert.False(result.Is3D);
- Assert.Equal("Bad Boys", result.Name);
- Assert.Null(result.ExtraType);
- }
-
- // FIXME
- // [Fact]
- public void TestSimpleFileWithNumericName()
- {
- var parser = GetParser();
-
- var result =
- parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006).mkv");
-
- Assert.Equal(2006, result.Year);
- Assert.False(result.IsStub);
- Assert.False(result.Is3D);
- Assert.Equal("300", result.Name);
- Assert.Null(result.ExtraType);
- }
-
- // FIXME
- // [Fact]
- public void TestExtra()
- {
- var parser = GetParser();
-
- var result =
- parser.ResolveFile(@"/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv");
-
- Assert.Equal(2006, result.Year);
- Assert.False(result.IsStub);
- Assert.False(result.Is3D);
- Assert.Equal(ExtraType.Trailer, result.ExtraType);
- Assert.Equal("Brave (2006)-trailer", result.Name);
- }
-
- // FIXME
- // [Fact]
- public void TestExtraWithNumericName()
- {
- var parser = GetParser();
-
- var result =
- parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006)-trailer.mkv");
-
- Assert.Equal(2006, result.Year);
- Assert.False(result.IsStub);
- Assert.False(result.Is3D);
- Assert.Equal("300 (2006)-trailer", result.Name);
- Assert.Equal(ExtraType.Trailer, result.ExtraType);
- }
-
- // FIXME
- // [Fact]
- public void TestStubFileWithNumericName()
- {
- var parser = GetParser();
-
- var result =
- parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006).bluray.disc");
-
- Assert.Equal(2006, result.Year);
- Assert.True(result.IsStub);
- Assert.Equal("bluray", result.StubType);
- Assert.False(result.Is3D);
- Assert.Equal("300", result.Name);
- Assert.Null(result.ExtraType);
- }
-
- // FIXME
- // [Fact]
- public void TestStubFile()
- {
- var parser = GetParser();
-
- var result =
- parser.ResolveFile(@"/server/Movies/Brave (2007)/Brave (2006).bluray.disc");
-
- Assert.Equal(2006, result.Year);
- Assert.True(result.IsStub);
- Assert.Equal("bluray", result.StubType);
- Assert.False(result.Is3D);
- Assert.Equal("Brave", result.Name);
- Assert.Null(result.ExtraType);
- }
-
- // FIXME
- // [Fact]
- public void TestExtraStubWithNumericNameNotSupported()
- {
- var parser = GetParser();
-
- var result =
- parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc");
-
- Assert.Equal(2006, result.Year);
- Assert.True(result.IsStub);
- Assert.Equal("bluray", result.StubType);
- Assert.False(result.Is3D);
- Assert.Equal("300", result.Name);
- Assert.Null(result.ExtraType);
- }
-
- // FIXME
- // [Fact]
- public void TestExtraStubNotSupported()
- {
- // Using a stub for an extra is currently not supported
- var parser = GetParser();
-
- var result =
- parser.ResolveFile(@"/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc");
-
- Assert.Equal(2006, result.Year);
- Assert.True(result.IsStub);
- Assert.Equal("bluray", result.StubType);
- Assert.False(result.Is3D);
- Assert.Equal("brave", result.Name);
- Assert.Null(result.ExtraType);
- }
-
- // FIXME
- // [Fact]
- public void Test3DFileWithNumericName()
- {
- var parser = GetParser();
-
- var result =
- parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv");
-
- Assert.Equal(2006, result.Year);
- Assert.False(result.IsStub);
- Assert.True(result.Is3D);
- Assert.Equal("sbs", result.Format3D);
- Assert.Equal("300", result.Name);
- Assert.Null(result.ExtraType);
- }
-
- // FIXME
- // [Fact]
- public void TestBad3DFileWithNumericName()
- {
- var parser = GetParser();
-
- var result =
- parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv");
-
- Assert.Equal(2006, result.Year);
- Assert.False(result.IsStub);
- Assert.False(result.Is3D);
- Assert.Equal("300", result.Name);
- Assert.Null(result.ExtraType);
- Assert.Null(result.Format3D);
- }
-
- // FIXME
- // [Fact]
- public void Test3DFile()
- {
- var parser = GetParser();
-
- var result =
- parser.ResolveFile(@"/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv");
-
- Assert.Equal(2006, result.Year);
- Assert.False(result.IsStub);
- Assert.True(result.Is3D);
- Assert.Equal("sbs", result.Format3D);
- Assert.Equal("brave", result.Name);
- Assert.Null(result.ExtraType);
- }
-
- [Fact]
- public void TestNameWithoutDate()
- {
- var parser = GetParser();
-
- var result =
- parser.ResolveFile(@"/server/Movies/American Psycho/American.Psycho.mkv");
-
- Assert.Null(result.Year);
- Assert.False(result.IsStub);
- Assert.False(result.Is3D);
- Assert.Null(result.Format3D);
- Assert.Equal("American.Psycho", result.Name);
- Assert.Null(result.ExtraType);
- }
-
- // FIXME
- // [Fact]
- public void TestCleanDateAndStringsSequence()
- {
- var parser = GetParser();
-
- // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again
- var result =
- parser.ResolveFile(@"/server/Movies/3.Days.to.Kill/3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv");
-
- Assert.Equal(2014, result.Year);
- Assert.False(result.IsStub);
- Assert.False(result.Is3D);
- Assert.Null(result.Format3D);
- Assert.Equal("3.Days.to.Kill", result.Name);
- Assert.Null(result.ExtraType);
- }
-
- // FIXME
- // [Fact]
- public void TestCleanDateAndStringsSequence1()
- {
- var parser = GetParser();
-
- // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again
- var result =
- parser.ResolveFile(@"/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv");
-
- Assert.Equal(2005, result.Year);
- Assert.False(result.IsStub);
- Assert.False(result.Is3D);
- Assert.Null(result.Format3D);
- Assert.Equal("3 days to kill", result.Name);
- Assert.Null(result.ExtraType);
- }
-
- [Fact]
- public void TestFolderNameWithExtension()
- {
- var parser = GetParser();
-
- var result =
- parser.ResolveFile(@"/server/Movies/7 Psychos.mkv/7 Psychos.mkv");
-
- Assert.Null(result.Year);
- Assert.False(result.IsStub);
- Assert.False(result.Is3D);
- Assert.Equal("7 Psychos", result.Name);
- Assert.Null(result.ExtraType);
+ private readonly NamingOptions _namingOptions = new NamingOptions();
+
+ public static IEnumerable<object[]> GetResolveFileTestData()
+ {
+ yield return new object[]
+ {
+ new VideoFileInfo()
+ {
+ Path = @"/server/Movies/7 Psychos.mkv/7 Psychos.mkv",
+ Container = "mkv",
+ Name = "7 Psychos"
+ }
+ };
+ yield return new object[]
+ {
+ new VideoFileInfo()
+ {
+ Path = @"/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv",
+ Container = "mkv",
+ Name = "3 days to kill",
+ Year = 2005
+ }
+ };
+ yield return new object[]
+ {
+ new VideoFileInfo()
+ {
+ Path = @"/server/Movies/American Psycho/American.Psycho.mkv",
+ Container = "mkv",
+ Name = "American.Psycho",
+ }
+ };
+ yield return new object[]
+ {
+ new VideoFileInfo()
+ {
+ Path = @"/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv",
+ Container = "mkv",
+ Name = "brave",
+ Year = 2006,
+ Is3D = true,
+ Format3D = "sbs",
+ }
+ };
+ yield return new object[]
+ {
+ new VideoFileInfo()
+ {
+ Path = @"/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv",
+ Container = "mkv",
+ Name = "300",
+ Year = 2006
+ }
+ };
+ yield return new object[]
+ {
+ new VideoFileInfo()
+ {
+ Path = @"/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv",
+ Container = "mkv",
+ Name = "300",
+ Year = 2006,
+ Is3D = true,
+ Format3D = "sbs",
+ }
+ };
+ yield return new object[]
+ {
+ new VideoFileInfo()
+ {
+ Path = @"/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc",
+ Container = "disc",
+ Name = "brave",
+ Year = 2006,
+ IsStub = true,
+ StubType = "bluray",
+ }
+ };
+ yield return new object[]
+ {
+ new VideoFileInfo()
+ {
+ Path = @"/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc",
+ Container = "disc",
+ Name = "300",
+ Year = 2006,
+ IsStub = true,
+ StubType = "bluray",
+ }
+ };
+ yield return new object[]
+ {
+ new VideoFileInfo()
+ {
+ Path = @"/server/Movies/Brave (2007)/Brave (2006).bluray.disc",
+ Container = "disc",
+ Name = "Brave",
+ Year = 2006,
+ IsStub = true,
+ StubType = "bluray",
+ }
+ };
+ yield return new object[]
+ {
+ new VideoFileInfo()
+ {
+ Path = @"/server/Movies/300 (2007)/300 (2006).bluray.disc",
+ Container = "disc",
+ Name = "300",
+ Year = 2006,
+ IsStub = true,
+ StubType = "bluray",
+ }
+ };
+ yield return new object[]
+ {
+ new VideoFileInfo()
+ {
+ Path = @"/server/Movies/300 (2007)/300 (2006)-trailer.mkv",
+ Container = "mkv",
+ Name = "300",
+ Year = 2006,
+ ExtraType = ExtraType.Trailer,
+ }
+ };
+ yield return new object[]
+ {
+ new VideoFileInfo()
+ {
+ Path = @"/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv",
+ Container = "mkv",
+ Name = "Brave",
+ Year = 2006,
+ ExtraType = ExtraType.Trailer,
+ }
+ };
+ yield return new object[]
+ {
+ new VideoFileInfo()
+ {
+ Path = @"/server/Movies/300 (2007)/300 (2006).mkv",
+ Container = "mkv",
+ Name = "300",
+ Year = 2006
+ }
+ };
+ yield return new object[]
+ {
+ new VideoFileInfo()
+ {
+ Path = @"/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv",
+ Container = "mkv",
+ Name = "Bad Boys",
+ Year = 1995,
+ }
+ };
+ yield return new object[]
+ {
+ new VideoFileInfo()
+ {
+ Path = @"/server/Movies/Brave (2007)/Brave (2006).mkv",
+ Container = "mkv",
+ Name = "Brave",
+ Year = 2006,
+ }
+ };
+ }
+
+
+ [Theory]
+ [MemberData(nameof(GetResolveFileTestData))]
+ public void ResolveFile_ValidFileName_Success(VideoFileInfo expectedResult)
+ {
+ var result = new VideoResolver(_namingOptions).ResolveFile(expectedResult.Path);
+
+ Assert.NotNull(result);
+ Assert.Equal(result.Path, expectedResult.Path);
+ Assert.Equal(result.Container, expectedResult.Container);
+ Assert.Equal(result.Name, expectedResult.Name);
+ Assert.Equal(result.Year, expectedResult.Year);
+ Assert.Equal(result.ExtraType, expectedResult.ExtraType);
+ Assert.Equal(result.Format3D, expectedResult.Format3D);
+ Assert.Equal(result.Is3D, expectedResult.Is3D);
+ Assert.Equal(result.IsStub, expectedResult.IsStub);
+ Assert.Equal(result.StubType, expectedResult.StubType);
+ Assert.Equal(result.IsDirectory, expectedResult.IsDirectory);
+ Assert.Equal(result.FileNameWithoutExtension, expectedResult.FileNameWithoutExtension);
}
}
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/HttpServer/ResponseFilterTests.cs b/tests/Jellyfin.Server.Implementations.Tests/HttpServer/ResponseFilterTests.cs
new file mode 100644
index 000000000..39bd94b59
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/HttpServer/ResponseFilterTests.cs
@@ -0,0 +1,18 @@
+using Emby.Server.Implementations.HttpServer;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.HttpServer
+{
+ public class ResponseFilterTests
+ {
+ [Theory]
+ [InlineData(null, null)]
+ [InlineData("", "")]
+ [InlineData("This is a clean string.", "This is a clean string.")]
+ [InlineData("This isn't \n\ra clean string.", "This isn't a clean string.")]
+ public void RemoveControlCharacters_ValidArgs_Correct(string? input, string? result)
+ {
+ Assert.Equal(result, ResponseFilter.RemoveControlCharacters(input));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
new file mode 100644
index 000000000..c771f5f4a
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
@@ -0,0 +1,27 @@
+using System;
+using Emby.Server.Implementations.Library;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Library
+{
+ public class PathExtensionsTests
+ {
+ [Theory]
+ [InlineData("Superman: Red Son [imdbid=tt10985510]", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son - tt10985510", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son", "imdbid", null)]
+ public void GetAttributeValue_ValidArgs_Correct(string input, string attribute, string? expectedResult)
+ {
+ Assert.Equal(expectedResult, PathExtensions.GetAttributeValue(input, attribute));
+ }
+
+ [Theory]
+ [InlineData("", "")]
+ [InlineData("Superman: Red Son [imdbid=tt10985510]", "")]
+ [InlineData("", "imdbid")]
+ public void GetAttributeValue_EmptyString_ThrowsArgumentException(string input, string attribute)
+ {
+ Assert.Throws<ArgumentException>(() => PathExtensions.GetAttributeValue(input, attribute));
+ }
+ }
+}
diff --git a/tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs b/tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs
new file mode 100644
index 000000000..34698fe25
--- /dev/null
+++ b/tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs
@@ -0,0 +1,49 @@
+using System.Text.Json;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Branding;
+using Xunit;
+
+namespace MediaBrowser.Api.Tests
+{
+ public sealed class BrandingServiceTests : IClassFixture<JellyfinApplicationFactory>
+ {
+ private readonly JellyfinApplicationFactory _factory;
+
+ public BrandingServiceTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task GetConfiguration_ReturnsCorrectResponse()
+ {
+ // Arrange
+ var client = _factory.CreateClient();
+
+ // Act
+ var response = await client.GetAsync("/Branding/Configuration");
+
+ // Assert
+ response.EnsureSuccessStatusCode();
+ Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType.ToString());
+ var responseBody = await response.Content.ReadAsStreamAsync();
+ _ = await JsonSerializer.DeserializeAsync<BrandingOptions>(responseBody);
+ }
+
+ [Theory]
+ [InlineData("/Branding/Css")]
+ [InlineData("/Branding/Css.css")]
+ public async Task GetCss_ReturnsCorrectResponse(string url)
+ {
+ // Arrange
+ var client = _factory.CreateClient();
+
+ // Act
+ var response = await client.GetAsync(url);
+
+ // Assert
+ response.EnsureSuccessStatusCode();
+ Assert.Equal("text/css", response.Content.Headers.ContentType.ToString());
+ }
+ }
+}
diff --git a/tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs b/tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs
new file mode 100644
index 000000000..c39ed07de
--- /dev/null
+++ b/tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Concurrent;
+using System.IO;
+using Emby.Server.Implementations;
+using Emby.Server.Implementations.IO;
+using Emby.Server.Implementations.Networking;
+using Jellyfin.Drawing.Skia;
+using Jellyfin.Server;
+using MediaBrowser.Common;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Serilog;
+using Serilog.Extensions.Logging;
+
+namespace MediaBrowser.Api.Tests
+{
+ /// <summary>
+ /// Factory for bootstrapping the Jellyfin application in memory for functional end to end tests.
+ /// </summary>
+ public class JellyfinApplicationFactory : WebApplicationFactory<Startup>
+ {
+ private static readonly string _testPathRoot = Path.Combine(Path.GetTempPath(), "jellyfin-test-data");
+ private static readonly ConcurrentBag<IDisposable> _disposableComponents = new ConcurrentBag<IDisposable>();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="JellyfinApplicationFactory"/> class.
+ /// </summary>
+ public JellyfinApplicationFactory()
+ {
+ // Perform static initialization that only needs to happen once per test-run
+ Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger();
+ Program.PerformStaticInitialization();
+ }
+
+ /// <inheritdoc/>
+ protected override IWebHostBuilder CreateWebHostBuilder()
+ {
+ return new WebHostBuilder();
+ }
+
+ /// <inheritdoc/>
+ protected override void ConfigureWebHost(IWebHostBuilder builder)
+ {
+ // Specify the startup command line options
+ var commandLineOpts = new StartupOptions
+ {
+ NoWebClient = true,
+ NoAutoRunWebApp = true
+ };
+
+ // Use a temporary directory for the application paths
+ var webHostPathRoot = Path.Combine(_testPathRoot, "test-host-" + Path.GetFileNameWithoutExtension(Path.GetRandomFileName()));
+ Directory.CreateDirectory(Path.Combine(webHostPathRoot, "logs"));
+ Directory.CreateDirectory(Path.Combine(webHostPathRoot, "config"));
+ Directory.CreateDirectory(Path.Combine(webHostPathRoot, "cache"));
+ Directory.CreateDirectory(Path.Combine(webHostPathRoot, "jellyfin-web"));
+ var appPaths = new ServerApplicationPaths(
+ webHostPathRoot,
+ Path.Combine(webHostPathRoot, "logs"),
+ Path.Combine(webHostPathRoot, "config"),
+ Path.Combine(webHostPathRoot, "cache"),
+ Path.Combine(webHostPathRoot, "jellyfin-web"));
+
+ // Create the logging config file
+ // TODO: We shouldn't need to do this since we are only logging to console
+ Program.InitLoggingConfigFile(appPaths).GetAwaiter().GetResult();
+
+ // Create a copy of the application configuration to use for startup
+ var startupConfig = Program.CreateAppConfiguration(commandLineOpts, appPaths);
+
+ ILoggerFactory loggerFactory = new SerilogLoggerFactory();
+ _disposableComponents.Add(loggerFactory);
+
+ // Create the app host and initialize it
+ var appHost = new CoreAppHost(
+ appPaths,
+ loggerFactory,
+ commandLineOpts,
+ new ManagedFileSystem(loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
+ new NetworkManager(loggerFactory.CreateLogger<NetworkManager>()));
+ _disposableComponents.Add(appHost);
+ var serviceCollection = new ServiceCollection();
+ appHost.Init(serviceCollection);
+
+ // Configure the web host builder
+ Program.ConfigureWebHostBuilder(builder, appHost, serviceCollection, commandLineOpts, startupConfig, appPaths);
+ }
+
+ /// <inheritdoc/>
+ protected override TestServer CreateServer(IWebHostBuilder builder)
+ {
+ // Create the test server using the base implementation
+ var testServer = base.CreateServer(builder);
+
+ // Finish initializing the app host
+ var appHost = (CoreAppHost)testServer.Services.GetRequiredService<IApplicationHost>();
+ appHost.ServiceProvider = testServer.Services;
+ appHost.InitializeServices().GetAwaiter().GetResult();
+ appHost.RunStartupTasksAsync().GetAwaiter().GetResult();
+
+ return testServer;
+ }
+
+ /// <inheritdoc/>
+ protected override void Dispose(bool disposing)
+ {
+ foreach (var disposable in _disposableComponents)
+ {
+ disposable.Dispose();
+ }
+
+ _disposableComponents.Clear();
+
+ base.Dispose(disposing);
+ }
+ }
+}
diff --git a/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj
new file mode 100644
index 000000000..f30e48690
--- /dev/null
+++ b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj
@@ -0,0 +1,33 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp3.1</TargetFramework>
+ <IsPackable>false</IsPackable>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.3" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
+ <PackageReference Include="xunit" Version="2.4.1" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
+ <PackageReference Include="coverlet.collector" Version="1.2.1" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\Jellyfin.Server\Jellyfin.Server.csproj" />
+ <ProjectReference Include="..\..\MediaBrowser.Api\MediaBrowser.Api.csproj" />
+ </ItemGroup>
+
+ <!-- Code Analyzers-->
+ <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+ </ItemGroup>
+
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+
+</Project>
diff --git a/tests/jellyfin-tests.ruleset b/tests/jellyfin-tests.ruleset
new file mode 100644
index 000000000..5a113e955
--- /dev/null
+++ b/tests/jellyfin-tests.ruleset
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RuleSet Name="Rules for MediaBrowser.Api.Tests" Description="Code analysis rules for MediaBrowser.Api.Tests.csproj" ToolsVersion="14.0">
+
+ <!-- Include the solution default RuleSet. The rules in this file will override the defaults. -->
+ <Include Path="../jellyfin.ruleset" Action="Default" />
+
+ <!-- StyleCop Analyzer Rules -->
+ <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
+ <!-- SA0001: XML comment analysis is disabled due to project configuration -->
+ <Rule Id="SA0001" Action="None" />
+ </Rules>
+
+ <!-- FxCop Analyzer Rules -->
+ <Rules AnalyzerId="Microsoft.CodeAnalysis.FxCopAnalyzers" RuleNamespace="Microsoft.Design">
+ <!-- CA1707: Identifiers should not contain underscores -->
+ <Rule Id="CA1707" Action="None" />
+ <!-- CA2007: Consider calling ConfigureAwait on the awaited task -->
+ <Rule Id="CA2007" Action="None" />
+ <!-- CA2234: Pass system uri objects instead of strings -->
+ <Rule Id="CA2234" Action="Info" />
+ </Rules>
+</RuleSet>
diff --git a/windows/build-jellyfin.ps1 b/windows/build-jellyfin.ps1
new file mode 100644
index 000000000..c762137a7
--- /dev/null
+++ b/windows/build-jellyfin.ps1
@@ -0,0 +1,190 @@
+[CmdletBinding()]
+param(
+ [switch]$MakeNSIS,
+ [switch]$InstallNSIS,
+ [switch]$InstallFFMPEG,
+ [switch]$InstallNSSM,
+ [switch]$SkipJellyfinBuild,
+ [switch]$GenerateZip,
+ [string]$InstallLocation = "./dist/jellyfin-win-nsis",
+ [string]$UXLocation = "../jellyfin-ux",
+ [switch]$InstallTrayApp,
+ [ValidateSet('Debug','Release')][string]$BuildType = 'Release',
+ [ValidateSet('Quiet','Minimal', 'Normal')][string]$DotNetVerbosity = 'Minimal',
+ [ValidateSet('win','win7', 'win8','win81','win10')][string]$WindowsVersion = 'win',
+ [ValidateSet('x64','x86', 'arm', 'arm64')][string]$Architecture = 'x64'
+)
+
+$ProgressPreference = 'SilentlyContinue' # Speedup all downloads by hiding progress bars.
+
+#PowershellCore and *nix check to make determine which temp dir to use.
+if(($PSVersionTable.PSEdition -eq 'Core') -and (-not $IsWindows)){
+ $TempDir = mktemp -d
+}else{
+ $TempDir = $env:Temp
+}
+#Create staging dir
+New-Item -ItemType Directory -Force -Path $InstallLocation
+$ResolvedInstallLocation = Resolve-Path $InstallLocation
+$ResolvedUXLocation = Resolve-Path $UXLocation
+
+function Build-JellyFin {
+ if(($Architecture -eq 'arm64') -and ($WindowsVersion -ne 'win10')){
+ Write-Error "arm64 only supported with Windows10 Version"
+ exit
+ }
+ if(($Architecture -eq 'arm') -and ($WindowsVersion -notin @('win10','win81','win8'))){
+ Write-Error "arm only supported with Windows 8 or higher"
+ exit
+ }
+ Write-Verbose "windowsversion-Architecture: $windowsversion-$Architecture"
+ Write-Verbose "InstallLocation: $ResolvedInstallLocation"
+ Write-Verbose "DotNetVerbosity: $DotNetVerbosity"
+ dotnet publish --self-contained -c $BuildType --output $ResolvedInstallLocation -v $DotNetVerbosity -p:GenerateDocumentationFile=false -p:DebugSymbols=false -p:DebugType=none --runtime `"$windowsversion-$Architecture`" Jellyfin.Server
+}
+
+function Install-FFMPEG {
+ param(
+ [string]$ResolvedInstallLocation,
+ [string]$Architecture,
+ [string]$FFMPEGVersionX86 = "ffmpeg-4.2.1-win32-shared"
+ )
+ Write-Verbose "Checking Architecture"
+ if($Architecture -notin @('x86','x64')){
+ Write-Warning "No builds available for your selected architecture of $Architecture"
+ Write-Warning "FFMPEG will not be installed"
+ }elseif($Architecture -eq 'x64'){
+ Write-Verbose "Downloading 64 bit FFMPEG"
+ Invoke-WebRequest -Uri https://repo.jellyfin.org/releases/server/windows/ffmpeg/jellyfin-ffmpeg.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose
+ }else{
+ Write-Verbose "Downloading 32 bit FFMPEG"
+ Invoke-WebRequest -Uri https://ffmpeg.zeranoe.com/builds/win32/shared/$FFMPEGVersionX86.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose
+ }
+
+ Expand-Archive "$tempdir/ffmpeg.zip" -DestinationPath "$tempdir/ffmpeg/" -Force | Write-Verbose
+ if($Architecture -eq 'x64'){
+ Write-Verbose "Copying Binaries to Jellyfin location"
+ Get-ChildItem "$tempdir/ffmpeg" | ForEach-Object {
+ Copy-Item $_.FullName -Destination $installLocation | Write-Verbose
+ }
+ }else{
+ Write-Verbose "Copying Binaries to Jellyfin location"
+ Get-ChildItem "$tempdir/ffmpeg/$FFMPEGVersionX86/bin" | ForEach-Object {
+ Copy-Item $_.FullName -Destination $installLocation | Write-Verbose
+ }
+ }
+ Remove-Item "$tempdir/ffmpeg/" -Recurse -Force -ErrorAction Continue | Write-Verbose
+ Remove-Item "$tempdir/ffmpeg.zip" -Force -ErrorAction Continue | Write-Verbose
+}
+
+function Install-NSSM {
+ param(
+ [string]$ResolvedInstallLocation,
+ [string]$Architecture
+ )
+ Write-Verbose "Checking Architecture"
+ if($Architecture -notin @('x86','x64')){
+ Write-Warning "No builds available for your selected architecture of $Architecture"
+ Write-Warning "NSSM will not be installed"
+ }else{
+ Write-Verbose "Downloading NSSM"
+ # [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+ # Temporary workaround, file is hosted in an azure blob with a custom domain in front for brevity
+ Invoke-WebRequest -Uri http://files.evilt.win/nssm/nssm-2.24-101-g897c7ad.zip -UseBasicParsing -OutFile "$tempdir/nssm.zip" | Write-Verbose
+ }
+
+ Expand-Archive "$tempdir/nssm.zip" -DestinationPath "$tempdir/nssm/" -Force | Write-Verbose
+ if($Architecture -eq 'x64'){
+ Write-Verbose "Copying Binaries to Jellyfin location"
+ Get-ChildItem "$tempdir/nssm/nssm-2.24-101-g897c7ad/win64" | ForEach-Object {
+ Copy-Item $_.FullName -Destination $installLocation | Write-Verbose
+ }
+ }else{
+ Write-Verbose "Copying Binaries to Jellyfin location"
+ Get-ChildItem "$tempdir/nssm/nssm-2.24-101-g897c7ad/win32" | ForEach-Object {
+ Copy-Item $_.FullName -Destination $installLocation | Write-Verbose
+ }
+ }
+ Remove-Item "$tempdir/nssm/" -Recurse -Force -ErrorAction Continue | Write-Verbose
+ Remove-Item "$tempdir/nssm.zip" -Force -ErrorAction Continue | Write-Verbose
+}
+
+function Make-NSIS {
+ param(
+ [string]$ResolvedInstallLocation
+ )
+
+ $env:InstallLocation = $ResolvedInstallLocation
+ if($InstallNSIS.IsPresent -or ($InstallNSIS -eq $true)){
+ & "$tempdir/nsis/nsis-3.04/makensis.exe" /D$Architecture /DUXPATH=$ResolvedUXLocation ".\deployment\windows\jellyfin.nsi"
+ } else {
+ & "makensis" /D$Architecture /DUXPATH=$ResolvedUXLocation ".\deployment\windows\jellyfin.nsi"
+ }
+ Copy-Item .\deployment\windows\jellyfin_*.exe $ResolvedInstallLocation\..\
+}
+
+
+function Install-NSIS {
+ Write-Verbose "Downloading NSIS"
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+ Invoke-WebRequest -Uri https://nchc.dl.sourceforge.net/project/nsis/NSIS%203/3.04/nsis-3.04.zip -UseBasicParsing -OutFile "$tempdir/nsis.zip" | Write-Verbose
+
+ Expand-Archive "$tempdir/nsis.zip" -DestinationPath "$tempdir/nsis/" -Force | Write-Verbose
+}
+
+function Cleanup-NSIS {
+ Remove-Item "$tempdir/nsis/" -Recurse -Force -ErrorAction Continue | Write-Verbose
+ Remove-Item "$tempdir/nsis.zip" -Force -ErrorAction Continue | Write-Verbose
+}
+
+function Install-TrayApp {
+ param(
+ [string]$ResolvedInstallLocation,
+ [string]$Architecture
+ )
+ Write-Verbose "Checking Architecture"
+ if($Architecture -ne 'x64'){
+ Write-Warning "No builds available for your selected architecture of $Architecture"
+ Write-Warning "The tray app will not be available."
+ }else{
+ Write-Verbose "Downloading Tray App and copying to Jellyfin location"
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+ Invoke-WebRequest -Uri https://github.com/jellyfin/jellyfin-windows-tray/releases/latest/download/JellyfinTray.exe -UseBasicParsing -OutFile "$installLocation/JellyfinTray.exe" | Write-Verbose
+ }
+}
+
+if(-not $SkipJellyfinBuild.IsPresent -and -not ($InstallNSIS -eq $true)){
+ Write-Verbose "Starting Build Process: Selected Environment is $WindowsVersion-$Architecture"
+ Build-JellyFin
+}
+if($InstallFFMPEG.IsPresent -or ($InstallFFMPEG -eq $true)){
+ Write-Verbose "Starting FFMPEG Install"
+ Install-FFMPEG $ResolvedInstallLocation $Architecture
+}
+if($InstallNSSM.IsPresent -or ($InstallNSSM -eq $true)){
+ Write-Verbose "Starting NSSM Install"
+ Install-NSSM $ResolvedInstallLocation $Architecture
+}
+if($InstallTrayApp.IsPresent -or ($InstallTrayApp -eq $true)){
+ Write-Verbose "Downloading Windows Tray App"
+ Install-TrayApp $ResolvedInstallLocation $Architecture
+}
+#Copy-Item .\deployment\windows\install-jellyfin.ps1 $ResolvedInstallLocation\install-jellyfin.ps1
+#Copy-Item .\deployment\windows\install.bat $ResolvedInstallLocation\install.bat
+Copy-Item .\LICENSE $ResolvedInstallLocation\LICENSE
+if($InstallNSIS.IsPresent -or ($InstallNSIS -eq $true)){
+ Write-Verbose "Installing NSIS"
+ Install-NSIS
+}
+if($MakeNSIS.IsPresent -or ($MakeNSIS -eq $true)){
+ Write-Verbose "Starting NSIS Package creation"
+ Make-NSIS $ResolvedInstallLocation
+}
+if($InstallNSIS.IsPresent -or ($InstallNSIS -eq $true)){
+ Write-Verbose "Cleanup NSIS"
+ Cleanup-NSIS
+}
+if($GenerateZip.IsPresent -or ($GenerateZip -eq $true)){
+ Compress-Archive -Path $ResolvedInstallLocation -DestinationPath "$ResolvedInstallLocation/jellyfin.zip" -Force
+}
+Write-Verbose "Finished"
diff --git a/windows/dependencies.txt b/windows/dependencies.txt
new file mode 100644
index 000000000..16f77cce7
--- /dev/null
+++ b/windows/dependencies.txt
@@ -0,0 +1,2 @@
+dotnet
+nsis
diff --git a/windows/dialogs/confirmation.nsddef b/windows/dialogs/confirmation.nsddef
new file mode 100644
index 000000000..969ebacd6
--- /dev/null
+++ b/windows/dialogs/confirmation.nsddef
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+This file was created by NSISDialogDesigner 1.4.4.0
+http://coolsoft.altervista.org/nsisdialogdesigner
+Do not edit manually!
+-->
+<Dialog Name="confirmation" Title="Confirmation Page" Subtitle="Please confirm your choices for Jellyfin Server installation" GenerateShowFunction="False">
+ <HeaderCustomScript>!include "helpers\StrSlash.nsh"</HeaderCustomScript>
+ <CreateFunctionCustomScript>${StrSlash} '$0' $INSTDIR
+
+ ${StrSlash} '$1' $_JELLYFINDATADIR_
+
+ ${NSD_SetText} $hCtl_confirmation_ConfirmRichText "{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1043\viewkind4\uc1 \
+ \pard\widctlpar\sa160\sl252\slmult1\b The installer will proceed based on the following inputs gathered on earlier screens.\par \
+ Installation Folder:\b0 $0\line\b \
+ Service install:\b0 $_INSTALLSERVICE_\line\b \
+ Service start:\b0 $_SERVICESTART_\line\b \
+ Service account:\b0 $_SERVICEACCOUNTTYPE_\line\b \
+ Jellyfin Data Folder:\b0 $1\par \
+\
+ \pard\sa200\sl276\slmult1\f1\lang1043\par \
+ }"</CreateFunctionCustomScript>
+ <RichText Name="ConfirmRichText" Location="12, 12" Size="426, 204" TabIndex="0" ExStyle="WS_EX_STATICEDGE" />
+</Dialog>
diff --git a/windows/dialogs/confirmation.nsdinc b/windows/dialogs/confirmation.nsdinc
new file mode 100644
index 000000000..f00e9b43a
--- /dev/null
+++ b/windows/dialogs/confirmation.nsdinc
@@ -0,0 +1,61 @@
+; =========================================================
+; This file was generated by NSISDialogDesigner 1.4.4.0
+; http://coolsoft.altervista.org/nsisdialogdesigner
+;
+; Do not edit it manually, use NSISDialogDesigner instead!
+; Modified by EraYaN (2019-09-01)
+; =========================================================
+
+; handle variables
+Var hCtl_confirmation
+Var hCtl_confirmation_ConfirmRichText
+
+; HeaderCustomScript
+!include "helpers\StrSlash.nsh"
+
+
+
+; dialog create function
+Function fnc_confirmation_Create
+
+ ; === confirmation (type: Dialog) ===
+ nsDialogs::Create 1018
+ Pop $hCtl_confirmation
+ ${If} $hCtl_confirmation == error
+ Abort
+ ${EndIf}
+ !insertmacro MUI_HEADER_TEXT "Confirmation Page" "Please confirm your choices for Jellyfin Server installation"
+
+ ; === ConfirmRichText (type: RichText) ===
+ nsDialogs::CreateControl /NOUNLOAD "RichEdit20A" ${ES_READONLY}|${WS_VISIBLE}|${WS_CHILD}|${WS_TABSTOP}|${WS_VSCROLL}|${ES_MULTILINE}|${ES_WANTRETURN} ${WS_EX_STATICEDGE} 8u 7u 280u 126u ""
+ Pop $hCtl_confirmation_ConfirmRichText
+ ${NSD_AddExStyle} $hCtl_confirmation_ConfirmRichText ${WS_EX_STATICEDGE}
+
+ ; CreateFunctionCustomScript
+ ${StrSlash} '$0' $INSTDIR
+
+ ${StrSlash} '$1' $_JELLYFINDATADIR_
+
+ ${If} $_INSTALLSERVICE_ == "Yes"
+ ${NSD_SetText} $hCtl_confirmation_ConfirmRichText "{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1043\viewkind4\uc1 \
+ \pard\widctlpar\sa160\sl252\slmult1\b The installer will proceed based on the following inputs gathered on earlier screens.\par \
+ Installation Folder:\b0 $0\line\b \
+ Service install:\b0 $_INSTALLSERVICE_\line\b \
+ Service start:\b0 $_SERVICESTART_\line\b \
+ Service account:\b0 $_SERVICEACCOUNTTYPE_\line\b \
+ Jellyfin Data Folder:\b0 $1\par \
+ \
+ \pard\sa200\sl276\slmult1\f1\lang1043\par \
+ }"
+ ${Else}
+ ${NSD_SetText} $hCtl_confirmation_ConfirmRichText "{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1043\viewkind4\uc1 \
+ \pard\widctlpar\sa160\sl252\slmult1\b The installer will proceed based on the following inputs gathered on earlier screens.\par \
+ Installation Folder:\b0 $0\line\b \
+ Service install:\b0 $_INSTALLSERVICE_\line\b \
+ Jellyfin Data Folder:\b0 $1\par \
+ \
+ \pard\sa200\sl276\slmult1\f1\lang1043\par \
+ }"
+ ${EndIf}
+
+FunctionEnd
diff --git a/windows/dialogs/service-config.nsddef b/windows/dialogs/service-config.nsddef
new file mode 100644
index 000000000..3509ada24
--- /dev/null
+++ b/windows/dialogs/service-config.nsddef
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+This file was created by NSISDialogDesigner 1.4.4.0
+http://coolsoft.altervista.org/nsisdialogdesigner
+Do not edit manually!
+-->
+<Dialog Name="service_config" Title="CoOnfigure the service" Subtitle="This controls what type of access the server gets to this system." GenerateShowFunction="False">
+ <CheckBox Name="StartServiceAfterInstall" Location="12, 192" Size="426, 24" Text="Start Service after Install" Checked="True" TabIndex="0" />
+ <Label Name="LocalSystemAccountLabel" Location="12, 115" Size="426, 46" Text="The Local System account has full access to every resource and file on the system. This can have very real security implications, do not use unless absolutely neseccary." TabIndex="1" />
+ <Label Name="NetworkServiceAccountLabel" Location="12, 39" Size="426, 46" Text="The NetworkService account is a predefined local account used by the service control manager. It is the recommended way to install the Jellyfin Server service." TabIndex="2" />
+ <RadioButton Name="UseLocalSystemAccount" Location="12, 88" Size="426, 24" Text="Use Local System account" TabIndex="3" />
+ <RadioButton Name="UseNetworkServiceAccount" Location="12, 12" Size="426, 24" Text="Use Network Service account (Recommended)" Font="Microsoft Sans Serif, 8.25pt, style=Bold" Checked="True" TabIndex="4" />
+</Dialog> \ No newline at end of file
diff --git a/windows/dialogs/service-config.nsdinc b/windows/dialogs/service-config.nsdinc
new file mode 100644
index 000000000..58c350f2e
--- /dev/null
+++ b/windows/dialogs/service-config.nsdinc
@@ -0,0 +1,56 @@
+; =========================================================
+; This file was generated by NSISDialogDesigner 1.4.4.0
+; http://coolsoft.altervista.org/nsisdialogdesigner
+;
+; Do not edit it manually, use NSISDialogDesigner instead!
+; =========================================================
+
+; handle variables
+Var hCtl_service_config
+Var hCtl_service_config_StartServiceAfterInstall
+Var hCtl_service_config_LocalSystemAccountLabel
+Var hCtl_service_config_NetworkServiceAccountLabel
+Var hCtl_service_config_UseLocalSystemAccount
+Var hCtl_service_config_UseNetworkServiceAccount
+Var hCtl_service_config_Font1
+
+
+; dialog create function
+Function fnc_service_config_Create
+
+ ; custom font definitions
+ CreateFont $hCtl_service_config_Font1 "Microsoft Sans Serif" "8.25" "700"
+
+ ; === service_config (type: Dialog) ===
+ nsDialogs::Create 1018
+ Pop $hCtl_service_config
+ ${If} $hCtl_service_config == error
+ Abort
+ ${EndIf}
+ !insertmacro MUI_HEADER_TEXT "Configure the service" "This controls what type of access the server gets to this system."
+
+ ; === StartServiceAfterInstall (type: Checkbox) ===
+ ${NSD_CreateCheckbox} 8u 118u 280u 15u "Start Service after Install"
+ Pop $hCtl_service_config_StartServiceAfterInstall
+ ${NSD_Check} $hCtl_service_config_StartServiceAfterInstall
+
+ ; === LocalSystemAccountLabel (type: Label) ===
+ ${NSD_CreateLabel} 8u 71u 280u 28u "The Local System account has full access to every resource and file on the system. This can have very real security implications, do not use unless absolutely neseccary."
+ Pop $hCtl_service_config_LocalSystemAccountLabel
+
+ ; === NetworkServiceAccountLabel (type: Label) ===
+ ${NSD_CreateLabel} 8u 24u 280u 28u "The NetworkService account is a predefined local account used by the service control manager. It is the recommended way to install the Jellyfin Server service."
+ Pop $hCtl_service_config_NetworkServiceAccountLabel
+
+ ; === UseLocalSystemAccount (type: RadioButton) ===
+ ${NSD_CreateRadioButton} 8u 54u 280u 15u "Use Local System account"
+ Pop $hCtl_service_config_UseLocalSystemAccount
+ ${NSD_AddStyle} $hCtl_service_config_UseLocalSystemAccount ${WS_GROUP}
+
+ ; === UseNetworkServiceAccount (type: RadioButton) ===
+ ${NSD_CreateRadioButton} 8u 7u 280u 15u "Use Network Service account (Recommended)"
+ Pop $hCtl_service_config_UseNetworkServiceAccount
+ SendMessage $hCtl_service_config_UseNetworkServiceAccount ${WM_SETFONT} $hCtl_service_config_Font1 0
+ ${NSD_Check} $hCtl_service_config_UseNetworkServiceAccount
+
+FunctionEnd
diff --git a/windows/dialogs/setuptype.nsddef b/windows/dialogs/setuptype.nsddef
new file mode 100644
index 000000000..b55ceeaaa
--- /dev/null
+++ b/windows/dialogs/setuptype.nsddef
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+This file was created by NSISDialogDesigner 1.4.4.0
+http://coolsoft.altervista.org/nsisdialogdesigner
+Do not edit manually!
+-->
+<Dialog Name="setuptype" Title="Setup Type" Subtitle="Control how Jellyfin is installed.">
+ <Label Name="InstallasaServiceLabel" Location="12, 115" Size="426, 46" Text="Install Jellyfin as a service. This method is recommended for Advanced Users. Additional setup is required to access network shares." TabIndex="0" />
+ <RadioButton Name="InstallasaService" Location="12, 88" Size="426, 24" Text="Install as a Service (Advanced Users)" TabIndex="1" />
+ <Label Name="BasicInstallLabel" Location="12, 39" Size="426, 46" Text="The basic install will run Jellyfin in your current user account.$\nThis is recommended for new users and those with existing Jellyfin installs older than 10.4." TabIndex="2" />
+ <RadioButton Name="BasicInstall" Location="12, 12" Size="426, 24" Text="Basic Install (Recommended)" Font="Microsoft Sans Serif, 8.25pt, style=Bold" Checked="True" TabIndex="3" />
+</Dialog> \ No newline at end of file
diff --git a/windows/dialogs/setuptype.nsdinc b/windows/dialogs/setuptype.nsdinc
new file mode 100644
index 000000000..8746ad2cc
--- /dev/null
+++ b/windows/dialogs/setuptype.nsdinc
@@ -0,0 +1,50 @@
+; =========================================================
+; This file was generated by NSISDialogDesigner 1.4.4.0
+; http://coolsoft.altervista.org/nsisdialogdesigner
+;
+; Do not edit it manually, use NSISDialogDesigner instead!
+; =========================================================
+
+; handle variables
+Var hCtl_setuptype
+Var hCtl_setuptype_InstallasaServiceLabel
+Var hCtl_setuptype_InstallasaService
+Var hCtl_setuptype_BasicInstallLabel
+Var hCtl_setuptype_BasicInstall
+Var hCtl_setuptype_Font1
+
+
+; dialog create function
+Function fnc_setuptype_Create
+
+ ; custom font definitions
+ CreateFont $hCtl_setuptype_Font1 "Microsoft Sans Serif" "8.25" "700"
+
+ ; === setuptype (type: Dialog) ===
+ nsDialogs::Create 1018
+ Pop $hCtl_setuptype
+ ${If} $hCtl_setuptype == error
+ Abort
+ ${EndIf}
+ !insertmacro MUI_HEADER_TEXT "Setup Type" "Control how Jellyfin is installed."
+
+ ; === InstallasaServiceLabel (type: Label) ===
+ ${NSD_CreateLabel} 8u 71u 280u 28u "Install Jellyfin as a service. This method is recommended for Advanced Users. Additional setup is required to access network shares."
+ Pop $hCtl_setuptype_InstallasaServiceLabel
+
+ ; === InstallasaService (type: RadioButton) ===
+ ${NSD_CreateRadioButton} 8u 54u 280u 15u "Install as a Service (Advanced Users)"
+ Pop $hCtl_setuptype_InstallasaService
+ ${NSD_AddStyle} $hCtl_setuptype_InstallasaService ${WS_GROUP}
+
+ ; === BasicInstallLabel (type: Label) ===
+ ${NSD_CreateLabel} 8u 24u 280u 28u "The basic install will run Jellyfin in your current user account.$\nThis is recommended for new users and those with existing Jellyfin installs older than 10.4."
+ Pop $hCtl_setuptype_BasicInstallLabel
+
+ ; === BasicInstall (type: RadioButton) ===
+ ${NSD_CreateRadioButton} 8u 7u 280u 15u "Basic Install (Recommended)"
+ Pop $hCtl_setuptype_BasicInstall
+ SendMessage $hCtl_setuptype_BasicInstall ${WM_SETFONT} $hCtl_setuptype_Font1 0
+ ${NSD_Check} $hCtl_setuptype_BasicInstall
+
+FunctionEnd
diff --git a/windows/helpers/ShowError.nsh b/windows/helpers/ShowError.nsh
new file mode 100644
index 000000000..6e09b1e40
--- /dev/null
+++ b/windows/helpers/ShowError.nsh
@@ -0,0 +1,10 @@
+; Show error
+!macro ShowError TEXT RETRYLABEL
+ MessageBox MB_ABORTRETRYIGNORE|MB_ICONSTOP "${TEXT}" IDIGNORE +2 IDRETRY ${RETRYLABEL}
+ Abort
+!macroend
+
+!macro ShowErrorFinal TEXT
+ MessageBox MB_OK|MB_ICONSTOP "${TEXT}"
+ Abort
+!macroend
diff --git a/windows/helpers/StrSlash.nsh b/windows/helpers/StrSlash.nsh
new file mode 100644
index 000000000..b8aa771aa
--- /dev/null
+++ b/windows/helpers/StrSlash.nsh
@@ -0,0 +1,47 @@
+; Adapted from: https://nsis.sourceforge.io/Another_String_Replace_(and_Slash/BackSlash_Converter) (2019-08-31)
+
+!macro _StrSlashConstructor out in
+ Push "${in}"
+ Push "\"
+ Call StrSlash
+ Pop ${out}
+!macroend
+
+!define StrSlash '!insertmacro "_StrSlashConstructor"'
+
+; Push $filenamestring (e.g. 'c:\this\and\that\filename.htm')
+; Push "\"
+; Call StrSlash
+; Pop $R0
+; ;Now $R0 contains 'c:/this/and/that/filename.htm'
+Function StrSlash
+ Exch $R3 ; $R3 = needle ("\" or "/")
+ Exch
+ Exch $R1 ; $R1 = String to replacement in (haystack)
+ Push $R2 ; Replaced haystack
+ Push $R4 ; $R4 = not $R3 ("/" or "\")
+ Push $R6
+ Push $R7 ; Scratch reg
+ StrCpy $R2 ""
+ StrLen $R6 $R1
+ StrCpy $R4 "\"
+ StrCmp $R3 "/" loop
+ StrCpy $R4 "/"
+loop:
+ StrCpy $R7 $R1 1
+ StrCpy $R1 $R1 $R6 1
+ StrCmp $R7 $R3 found
+ StrCpy $R2 "$R2$R7"
+ StrCmp $R1 "" done loop
+found:
+ StrCpy $R2 "$R2$R4"
+ StrCmp $R1 "" done loop
+done:
+ StrCpy $R3 $R2
+ Pop $R7
+ Pop $R6
+ Pop $R4
+ Pop $R2
+ Pop $R1
+ Exch $R3
+FunctionEnd
diff --git a/windows/jellyfin.nsi b/windows/jellyfin.nsi
new file mode 100644
index 000000000..fada62d98
--- /dev/null
+++ b/windows/jellyfin.nsi
@@ -0,0 +1,575 @@
+!verbose 3
+SetCompressor /SOLID bzip2
+ShowInstDetails show
+ShowUninstDetails show
+Unicode True
+
+;--------------------------------
+!define SF_USELECTED 0 ; used to check selected options status, rest are inherited from Sections.nsh
+
+ !include "MUI2.nsh"
+ !include "Sections.nsh"
+ !include "LogicLib.nsh"
+
+ !include "helpers\ShowError.nsh"
+
+; Global variables that we'll use
+ Var _JELLYFINVERSION_
+ Var _JELLYFINDATADIR_
+ Var _SETUPTYPE_
+ Var _INSTALLSERVICE_
+ Var _SERVICESTART_
+ Var _SERVICEACCOUNTTYPE_
+ Var _EXISTINGINSTALLATION_
+ Var _EXISTINGSERVICE_
+ Var _MAKESHORTCUTS_
+ Var _FOLDEREXISTS_
+;
+!ifdef x64
+ !define ARCH "x64"
+ !define NAMESUFFIX "(64 bit)"
+ !define INSTALL_DIRECTORY "$PROGRAMFILES64\Jellyfin\Server"
+!endif
+
+!ifdef x84
+ !define ARCH "x86"
+ !define NAMESUFFIX "(32 bit)"
+ !define INSTALL_DIRECTORY "$PROGRAMFILES32\Jellyfin\Server"
+!endif
+
+!ifndef ARCH
+ !error "Set the Arch with /Dx86 or /Dx64"
+!endif
+
+;--------------------------------
+
+ !define REG_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\JellyfinServer" ;Registry to show up in Add/Remove Programs
+ !define REG_CONFIG_KEY "Software\Jellyfin\Server" ;Registry to store all configuration
+
+ !getdllversion "$%InstallLocation%\jellyfin.dll" ver_ ;Align installer version with jellyfin.dll version
+
+ Name "Jellyfin Server ${ver_1}.${ver_2}.${ver_3} ${NAMESUFFIX}" ; This is referred in various header text labels
+ OutFile "jellyfin_${ver_1}.${ver_2}.${ver_3}_windows-${ARCH}.exe" ; Naming convention jellyfin_{version}_windows-{arch].exe
+ BrandingText "Jellyfin Server ${ver_1}.${ver_2}.${ver_3} Installer" ; This shows in just over the buttons
+
+; installer attributes, these show up in details tab on installer properties
+ VIProductVersion "${ver_1}.${ver_2}.${ver_3}.0" ; VIProductVersion format, should be X.X.X.X
+ VIFileVersion "${ver_1}.${ver_2}.${ver_3}.0" ; VIFileVersion format, should be X.X.X.X
+ VIAddVersionKey "ProductName" "Jellyfin Server"
+ VIAddVersionKey "FileVersion" "${ver_1}.${ver_2}.${ver_3}.0"
+ VIAddVersionKey "LegalCopyright" "(c) 2019 Jellyfin Contributors. Code released under the GNU General Public License"
+ VIAddVersionKey "FileDescription" "Jellyfin Server: The Free Software Media System"
+
+;TODO, check defaults
+ InstallDir ${INSTALL_DIRECTORY} ;Default installation folder
+ InstallDirRegKey HKLM "${REG_CONFIG_KEY}" "InstallFolder" ;Read the registry for install folder,
+
+ RequestExecutionLevel admin ; ask it upfront for service control, and installing in priv folders
+
+ CRCCheck on ; make sure the installer wasn't corrupted while downloading
+
+ !define MUI_ABORTWARNING ;Prompts user in case of aborting install
+
+; TODO: Replace with nice Jellyfin Icons
+!ifdef UXPATH
+ !define MUI_ICON "${UXPATH}\branding\NSIS\modern-install.ico" ; Installer Icon
+ !define MUI_UNICON "${UXPATH}\branding\NSIS\modern-install.ico" ; Uninstaller Icon
+
+ !define MUI_HEADERIMAGE
+ !define MUI_HEADERIMAGE_BITMAP "${UXPATH}\branding\NSIS\installer-header.bmp"
+ !define MUI_WELCOMEFINISHPAGE_BITMAP "${UXPATH}\branding\NSIS\installer-right.bmp"
+ !define MUI_UNWELCOMEFINISHPAGE_BITMAP "${UXPATH}\branding\NSIS\installer-right.bmp"
+!endif
+
+;--------------------------------
+;Pages
+
+; Welcome Page
+ !define MUI_WELCOMEPAGE_TEXT "The installer will ask for details to install Jellyfin Server."
+ !insertmacro MUI_PAGE_WELCOME
+; License Page
+ !insertmacro MUI_PAGE_LICENSE "$%InstallLocation%\LICENSE" ; picking up generic GPL
+
+; Setup Type Page
+ Page custom ShowSetupTypePage SetupTypePage_Config
+
+; Components Page
+ !define MUI_PAGE_CUSTOMFUNCTION_PRE HideComponentsPage
+ !insertmacro MUI_PAGE_COMPONENTS
+ !define MUI_PAGE_CUSTOMFUNCTION_PRE HideInstallDirectoryPage ; Controls when to hide / show
+ !define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Install folder" ; shows just above the folder selection dialog
+ !insertmacro MUI_PAGE_DIRECTORY
+
+; Data folder Page
+ !define MUI_PAGE_CUSTOMFUNCTION_PRE HideDataDirectoryPage ; Controls when to hide / show
+ !define MUI_PAGE_HEADER_TEXT "Choose Data Location"
+ !define MUI_PAGE_HEADER_SUBTEXT "Choose the folder in which to install the Jellyfin Server data."
+ !define MUI_DIRECTORYPAGE_TEXT_TOP "The installer will set the following folder for Jellyfin Server data. To install in a different folder, click Browse and select another folder. Please make sure the folder exists and is accessible. Click Next to continue."
+ !define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Data folder"
+ !define MUI_DIRECTORYPAGE_VARIABLE $_JELLYFINDATADIR_
+ !insertmacro MUI_PAGE_DIRECTORY
+
+; Custom Dialogs
+ !include "dialogs\setuptype.nsdinc"
+ !include "dialogs\service-config.nsdinc"
+ !include "dialogs\confirmation.nsdinc"
+
+; Select service account type
+ #!define MUI_PAGE_CUSTOMFUNCTION_PRE HideServiceConfigPage ; Controls when to hide / show (This does not work for Page, might need to go PageEx)
+ #!define MUI_PAGE_CUSTOMFUNCTION_SHOW fnc_service_config_Show
+ #!define MUI_PAGE_CUSTOMFUNCTION_LEAVE ServiceConfigPage_Config
+ #!insertmacro MUI_PAGE_CUSTOM ServiceAccountType
+ Page custom ShowServiceConfigPage ServiceConfigPage_Config
+
+; Confirmation Page
+ Page custom ShowConfirmationPage ; just letting the user know what they chose to install
+
+; Actual Installion Page
+ !insertmacro MUI_PAGE_INSTFILES
+
+ !insertmacro MUI_UNPAGE_CONFIRM
+ !insertmacro MUI_UNPAGE_INSTFILES
+ #!insertmacro MUI_UNPAGE_FINISH
+
+;--------------------------------
+;Languages; Add more languages later here if needed
+ !insertmacro MUI_LANGUAGE "English"
+
+;--------------------------------
+;Installer Sections
+Section "!Jellyfin Server (required)" InstallJellyfinServer
+ SectionIn RO ; Mandatory section, isn't this the whole purpose to run the installer.
+
+ StrCmp "$_EXISTINGINSTALLATION_" "Yes" RunUninstaller CarryOn ; Silently uninstall in case of previous installation
+
+ RunUninstaller:
+ DetailPrint "Looking for uninstaller at $INSTDIR"
+ FindFirst $0 $1 "$INSTDIR\Uninstall.exe"
+ FindClose $0
+ StrCmp $1 "" CarryOn ; the registry key was there but uninstaller was not found
+
+ DetailPrint "Silently running the uninstaller at $INSTDIR"
+ ExecWait '"$INSTDIR\Uninstall.exe" /S _?=$INSTDIR' $0
+ DetailPrint "Uninstall finished, $0"
+
+ CarryOn:
+ ${If} $_EXISTINGSERVICE_ == 'Yes'
+ ExecWait '"$INSTDIR\nssm.exe" stop JellyfinServer' $0
+ ${If} $0 <> 0
+ MessageBox MB_OK|MB_ICONSTOP "Could not stop the Jellyfin Server service."
+ Abort
+ ${EndIf}
+ DetailPrint "Stopped Jellyfin Server service, $0"
+ ${EndIf}
+
+ SetOutPath "$INSTDIR"
+
+ File "/oname=icon.ico" "${UXPATH}\branding\NSIS\modern-install.ico"
+ File /r $%InstallLocation%\*
+
+
+; Write the InstallFolder, DataFolder, Network Service info into the registry for later use
+ WriteRegExpandStr HKLM "${REG_CONFIG_KEY}" "InstallFolder" "$INSTDIR"
+ WriteRegExpandStr HKLM "${REG_CONFIG_KEY}" "DataFolder" "$_JELLYFINDATADIR_"
+ WriteRegStr HKLM "${REG_CONFIG_KEY}" "ServiceAccountType" "$_SERVICEACCOUNTTYPE_"
+
+ !getdllversion "$%InstallLocation%\jellyfin.dll" ver_
+ StrCpy $_JELLYFINVERSION_ "${ver_1}.${ver_2}.${ver_3}" ;
+
+; Write the uninstall keys for Windows
+ WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayName" "Jellyfin Server $_JELLYFINVERSION_ ${NAMESUFFIX}"
+ WriteRegExpandStr HKLM "${REG_UNINST_KEY}" "UninstallString" '"$INSTDIR\Uninstall.exe"'
+ WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayIcon" '"$INSTDIR\Uninstall.exe",0'
+ WriteRegStr HKLM "${REG_UNINST_KEY}" "Publisher" "The Jellyfin Project"
+ WriteRegStr HKLM "${REG_UNINST_KEY}" "URLInfoAbout" "https://jellyfin.org/"
+ WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayVersion" "$_JELLYFINVERSION_"
+ WriteRegDWORD HKLM "${REG_UNINST_KEY}" "NoModify" 1
+ WriteRegDWORD HKLM "${REG_UNINST_KEY}" "NoRepair" 1
+
+;Create uninstaller
+ WriteUninstaller "$INSTDIR\Uninstall.exe"
+SectionEnd
+
+Section "Jellyfin Server Service" InstallService
+${If} $_INSTALLSERVICE_ == "Yes" ; Only run this if we're going to install the service!
+ ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0
+ DetailPrint "Jellyfin Server service statuscode, $0"
+ ${If} $0 == 0
+ InstallRetry:
+ ExecWait '"$INSTDIR\nssm.exe" install JellyfinServer "$INSTDIR\jellyfin.exe" --service --datadir \"$_JELLYFINDATADIR_\"' $0
+ ${If} $0 <> 0
+ !insertmacro ShowError "Could not install the Jellyfin Server service." InstallRetry
+ ${EndIf}
+ DetailPrint "Jellyfin Server Service install, $0"
+ ${Else}
+ DetailPrint "Jellyfin Server Service exists, updating..."
+
+ ConfigureApplicationRetry:
+ ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Application "$INSTDIR\jellyfin.exe"' $0
+ ${If} $0 <> 0
+ !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureApplicationRetry
+ ${EndIf}
+ DetailPrint "Jellyfin Server Service setting (Application), $0"
+
+ ConfigureAppParametersRetry:
+ ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer AppParameters --service --datadir \"$_JELLYFINDATADIR_\"' $0
+ ${If} $0 <> 0
+ !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureAppParametersRetry
+ ${EndIf}
+ DetailPrint "Jellyfin Server Service setting (AppParameters), $0"
+ ${EndIf}
+
+
+ Sleep 3000 ; Give time for Windows to catchup
+ ConfigureStartRetry:
+ ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Start SERVICE_DELAYED_AUTO_START' $0
+ ${If} $0 <> 0
+ !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureStartRetry
+ ${EndIf}
+ DetailPrint "Jellyfin Server Service setting (Start), $0"
+
+ ConfigureDescriptionRetry:
+ ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Description "Jellyfin Server: The Free Software Media System"' $0
+ ${If} $0 <> 0
+ !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureDescriptionRetry
+ ${EndIf}
+ DetailPrint "Jellyfin Server Service setting (Description), $0"
+ ConfigureDisplayNameRetry:
+ ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer DisplayName "Jellyfin Server"' $0
+ ${If} $0 <> 0
+ !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureDisplayNameRetry
+
+ ${EndIf}
+ DetailPrint "Jellyfin Server Service setting (DisplayName), $0"
+
+ Sleep 3000
+ ${If} $_SERVICEACCOUNTTYPE_ == "NetworkService" ; the default install using NSSM is Local System
+ ConfigureNetworkServiceRetry:
+ ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Objectname "Network Service"' $0
+ ${If} $0 <> 0
+ !insertmacro ShowError "Could not configure the Jellyfin Server service account." ConfigureNetworkServiceRetry
+ ${EndIf}
+ DetailPrint "Jellyfin Server service account change, $0"
+ ${EndIf}
+
+ Sleep 3000
+ ConfigureDefaultAppExit:
+ ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer AppExit Default Exit' $0
+ ${If} $0 <> 0
+ !insertmacro ShowError "Could not configure the Jellyfin Server service app exit action." ConfigureDefaultAppExit
+ ${EndIf}
+ DetailPrint "Jellyfin Server service exit action set, $0"
+${EndIf}
+
+SectionEnd
+
+Section "-start service" StartService
+${If} $_SERVICESTART_ == "Yes"
+${AndIf} $_INSTALLSERVICE_ == "Yes"
+ StartRetry:
+ ExecWait '"$INSTDIR\nssm.exe" start JellyfinServer' $0
+ ${If} $0 <> 0
+ !insertmacro ShowError "Could not start the Jellyfin Server service." StartRetry
+ ${EndIf}
+ DetailPrint "Jellyfin Server service start, $0"
+${EndIf}
+SectionEnd
+
+Section "Create Shortcuts" CreateWinShortcuts
+ ${If} $_MAKESHORTCUTS_ == "Yes"
+ CreateDirectory "$SMPROGRAMS\Jellyfin Server"
+ CreateShortCut "$SMPROGRAMS\Jellyfin Server\Jellyfin (View Console).lnk" "$INSTDIR\jellyfin.exe" "--datadir $\"$_JELLYFINDATADIR_$\"" "$INSTDIR\icon.ico" 0 SW_SHOWMAXIMIZED
+ CreateShortCut "$SMPROGRAMS\Jellyfin Server\Jellyfin Tray App.lnk" "$INSTDIR\jellyfintray.exe" "" "$INSTDIR\icon.ico" 0
+ ;CreateShortCut "$DESKTOP\Jellyfin Server.lnk" "$INSTDIR\jellyfin.exe" "--datadir $\"$_JELLYFINDATADIR_$\"" "$INSTDIR\icon.ico" 0 SW_SHOWMINIMIZED
+ CreateShortCut "$DESKTOP\Jellyfin Server\Jellyfin Server.lnk" "$INSTDIR\jellyfintray.exe" "" "$INSTDIR\icon.ico" 0
+ ${EndIf}
+SectionEnd
+
+;--------------------------------
+;Descriptions
+
+;Language strings
+ LangString DESC_InstallJellyfinServer ${LANG_ENGLISH} "Install Jellyfin Server"
+ LangString DESC_InstallService ${LANG_ENGLISH} "Install As a Service"
+
+;Assign language strings to sections
+ !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
+ !insertmacro MUI_DESCRIPTION_TEXT ${InstallJellyfinServer} $(DESC_InstallJellyfinServer)
+ !insertmacro MUI_DESCRIPTION_TEXT ${InstallService} $(DESC_InstallService)
+ !insertmacro MUI_FUNCTION_DESCRIPTION_END
+
+;--------------------------------
+;Uninstaller Section
+
+Section "Uninstall"
+
+ ReadRegStr $INSTDIR HKLM "${REG_CONFIG_KEY}" "InstallFolder" ; read the installation folder
+ ReadRegStr $_JELLYFINDATADIR_ HKLM "${REG_CONFIG_KEY}" "DataFolder" ; read the data folder
+ ReadRegStr $_SERVICEACCOUNTTYPE_ HKLM "${REG_CONFIG_KEY}" "ServiceAccountType" ; read the account name
+
+ DetailPrint "Jellyfin Install location: $INSTDIR"
+ DetailPrint "Jellyfin Data folder: $_JELLYFINDATADIR_"
+
+ MessageBox MB_YESNO|MB_ICONINFORMATION "Do you want to retain the Jellyfin Server data folder? The media will not be touched. $\r$\nIf unsure choose YES." /SD IDYES IDYES PreserveData
+
+ RMDir /r /REBOOTOK "$_JELLYFINDATADIR_"
+
+ PreserveData:
+
+ ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0
+ DetailPrint "Jellyfin Server service statuscode, $0"
+ IntCmp $0 0 NoServiceUninstall ; service doesn't exist, may be run from desktop shortcut
+
+ Sleep 3000 ; Give time for Windows to catchup
+
+ UninstallStopRetry:
+ ExecWait '"$INSTDIR\nssm.exe" stop JellyfinServer' $0
+ ${If} $0 <> 0
+ !insertmacro ShowError "Could not stop the Jellyfin Server service." UninstallStopRetry
+ ${EndIf}
+ DetailPrint "Stopped Jellyfin Server service, $0"
+
+ UninstallRemoveRetry:
+ ExecWait '"$INSTDIR\nssm.exe" remove JellyfinServer confirm' $0
+ ${If} $0 <> 0
+ !insertmacro ShowError "Could not remove the Jellyfin Server service." UninstallRemoveRetry
+ ${EndIf}
+ DetailPrint "Removed Jellyfin Server service, $0"
+
+ Sleep 3000 ; Give time for Windows to catchup
+
+ NoServiceUninstall: ; existing install was present but no service was detected. Remove shortcuts if account is set to none
+ ${If} $_SERVICEACCOUNTTYPE_ == "None"
+ RMDir /r "$SMPROGRAMS\Jellyfin Server"
+ Delete "$DESKTOP\Jellyfin Server.lnk"
+ DetailPrint "Removed old shortcuts..."
+ ${EndIf}
+
+ Delete "$INSTDIR\*.*"
+ RMDir /r /REBOOTOK "$INSTDIR\jellyfin-web"
+ Delete "$INSTDIR\Uninstall.exe"
+ RMDir /r /REBOOTOK "$INSTDIR"
+
+ DeleteRegKey HKLM "Software\Jellyfin"
+ DeleteRegKey HKLM "${REG_UNINST_KEY}"
+
+SectionEnd
+
+Function .onInit
+; Setting up defaults
+ StrCpy $_INSTALLSERVICE_ "Yes"
+ StrCpy $_SERVICESTART_ "Yes"
+ StrCpy $_SERVICEACCOUNTTYPE_ "NetworkService"
+ StrCpy $_EXISTINGINSTALLATION_ "No"
+ StrCpy $_EXISTINGSERVICE_ "No"
+ StrCpy $_MAKESHORTCUTS_ "No"
+
+ SetShellVarContext current
+ StrCpy $_JELLYFINDATADIR_ "$%ProgramData%\Jellyfin\Server"
+
+ System::Call 'kernel32::CreateMutex(p 0, i 0, t "JellyfinServerMutex") p .r1 ?e'
+ Pop $R0
+
+ StrCmp $R0 0 +3
+ !insertmacro ShowErrorFinal "The installer is already running."
+
+;Detect if Jellyfin is already installed.
+; In case it is installed, let the user choose either
+; 1. Exit installer
+; 2. Upgrade without messing with data
+; 2a. Don't ask for any details, uninstall and install afresh with old settings
+
+; Read Registry for previous installation
+ ClearErrors
+ ReadRegStr "$0" HKLM "${REG_CONFIG_KEY}" "InstallFolder"
+ IfErrors NoExisitingInstall
+
+ DetailPrint "Existing Jellyfin Server detected at: $0"
+ StrCpy "$INSTDIR" "$0" ; set the location fro registry as new default
+
+ StrCpy $_EXISTINGINSTALLATION_ "Yes" ; Set our flag to be used later
+ SectionSetText ${InstallJellyfinServer} "Upgrade Jellyfin Server (required)" ; Change install text to "Upgrade"
+
+ ; check if service was run using Network Service account
+ ClearErrors
+ ReadRegStr $_SERVICEACCOUNTTYPE_ HKLM "${REG_CONFIG_KEY}" "ServiceAccountType" ; in case of error _SERVICEACCOUNTTYPE_ will be NetworkService as default
+
+ ClearErrors
+ ReadRegStr $_JELLYFINDATADIR_ HKLM "${REG_CONFIG_KEY}" "DataFolder" ; in case of error, the default holds
+
+ ; Hide sections which will not be needed in case of previous install
+ ; SectionSetText ${InstallService} ""
+
+; check if there is a service called Jellyfin, there should be
+; hack : nssm statuscode Jellyfin will return non zero return code in case it exists
+ ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0
+ DetailPrint "Jellyfin Server service statuscode, $0"
+ IntCmp $0 0 NoService ; service doesn't exist, may be run from desktop shortcut
+
+ ; if service was detected, set defaults going forward.
+ StrCpy $_EXISTINGSERVICE_ "Yes"
+ StrCpy $_INSTALLSERVICE_ "Yes"
+ StrCpy $_SERVICESTART_ "Yes"
+ StrCpy $_MAKESHORTCUTS_ "No"
+ SectionSetText ${CreateWinShortcuts} ""
+
+
+ NoService: ; existing install was present but no service was detected
+ ${If} $_SERVICEACCOUNTTYPE_ == "None"
+ StrCpy $_SETUPTYPE_ "Basic"
+ StrCpy $_INSTALLSERVICE_ "No"
+ StrCpy $_SERVICESTART_ "No"
+ StrCpy $_MAKESHORTCUTS_ "Yes"
+ ${EndIf}
+
+; Let the user know that we'll upgrade and provide an option to quit.
+ MessageBox MB_OKCANCEL|MB_ICONINFORMATION "Existing installation of Jellyfin Server was detected, it'll be upgraded, settings will be retained. \
+ $\r$\nClick OK to proceed, Cancel to exit installer." /SD IDOK IDOK ProceedWithUpgrade
+ Quit ; Quit if the user is not sure about upgrade
+
+ ProceedWithUpgrade:
+
+ NoExisitingInstall: ; by this time, the variables have been correctly set to reflect previous install details
+
+FunctionEnd
+
+Function HideInstallDirectoryPage
+ ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder
+ Abort
+ ${EndIf}
+FunctionEnd
+
+Function HideDataDirectoryPage
+ ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder
+ Abort
+ ${EndIf}
+FunctionEnd
+
+Function HideServiceConfigPage
+ ${If} $_INSTALLSERVICE_ == "No" ; Not running as a service, don't ask for service type
+ ${OrIf} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder
+ Abort
+ ${EndIf}
+FunctionEnd
+
+Function HideConfirmationPage
+ ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder
+ Abort
+ ${EndIf}
+FunctionEnd
+
+Function HideSetupTypePage
+ ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for SetupType
+ Abort
+ ${EndIf}
+FunctionEnd
+
+Function HideComponentsPage
+ ${If} $_SETUPTYPE_ == "Basic" ; Basic installation chosen, don't show components choice
+ Abort
+ ${EndIf}
+FunctionEnd
+
+; Setup Type dialog show function
+Function ShowSetupTypePage
+ Call HideSetupTypePage
+ Call fnc_setuptype_Create
+ nsDialogs::Show
+FunctionEnd
+
+; Service Config dialog show function
+Function ShowServiceConfigPage
+ Call HideServiceConfigPage
+ Call fnc_service_config_Create
+ nsDialogs::Show
+FunctionEnd
+
+; Confirmation dialog show function
+Function ShowConfirmationPage
+ Call HideConfirmationPage
+ Call fnc_confirmation_Create
+ nsDialogs::Show
+FunctionEnd
+
+; Declare temp variables to read the options from the custom page.
+Var StartServiceAfterInstall
+Var UseNetworkServiceAccount
+Var UseLocalSystemAccount
+Var BasicInstall
+
+
+Function SetupTypePage_Config
+${NSD_GetState} $hCtl_setuptype_BasicInstall $BasicInstall
+ IfFileExists "$LOCALAPPDATA\Jellyfin" folderfound foldernotfound ; if the folder exists, use this, otherwise, go with new default
+ folderfound:
+ StrCpy $_FOLDEREXISTS_ "Yes"
+ Goto InstallCheck
+ foldernotfound:
+ StrCpy $_FOLDEREXISTS_ "No"
+ Goto InstallCheck
+
+InstallCheck:
+${If} $BasicInstall == 1
+ StrCpy $_SETUPTYPE_ "Basic"
+ StrCpy $_INSTALLSERVICE_ "No"
+ StrCpy $_SERVICESTART_ "No"
+ StrCpy $_SERVICEACCOUNTTYPE_ "None"
+ StrCpy $_MAKESHORTCUTS_ "Yes"
+ ${If} $_FOLDEREXISTS_ == "Yes"
+ StrCpy $_JELLYFINDATADIR_ "$LOCALAPPDATA\Jellyfin\"
+ ${EndIf}
+${Else}
+ StrCpy $_SETUPTYPE_ "Advanced"
+ StrCpy $_INSTALLSERVICE_ "Yes"
+ StrCpy $_MAKESHORTCUTS_ "No"
+ ${If} $_FOLDEREXISTS_ == "Yes"
+ MessageBox MB_OKCANCEL|MB_ICONINFORMATION "An existing data folder was detected.\
+ $\r$\nBasic Setup is highly recommended.\
+ $\r$\nIf you proceed, you will need to set up Jellyfin again." IDOK GoAhead IDCANCEL GoBack
+ GoBack:
+ Abort
+ ${EndIf}
+ GoAhead:
+ StrCpy $_JELLYFINDATADIR_ "$%ProgramData%\Jellyfin\Server"
+ SectionSetText ${CreateWinShortcuts} ""
+${EndIf}
+
+FunctionEnd
+
+Function ServiceConfigPage_Config
+${NSD_GetState} $hCtl_service_config_StartServiceAfterInstall $StartServiceAfterInstall
+${If} $StartServiceAfterInstall == 1
+ StrCpy $_SERVICESTART_ "Yes"
+${Else}
+ StrCpy $_SERVICESTART_ "No"
+${EndIf}
+${NSD_GetState} $hCtl_service_config_UseNetworkServiceAccount $UseNetworkServiceAccount
+${NSD_GetState} $hCtl_service_config_UseLocalSystemAccount $UseLocalSystemAccount
+
+${If} $UseNetworkServiceAccount == 1
+ StrCpy $_SERVICEACCOUNTTYPE_ "NetworkService"
+${ElseIf} $UseLocalSystemAccount == 1
+ StrCpy $_SERVICEACCOUNTTYPE_ "LocalSystem"
+${Else}
+ !insertmacro ShowErrorFinal "Service account type not properly configured."
+${EndIf}
+
+FunctionEnd
+
+; This function handles the choices during component selection
+Function .onSelChange
+
+; If we are not installing service, we don't need to set the NetworkService account or StartService
+ SectionGetFlags ${InstallService} $0
+ ${If} $0 = ${SF_SELECTED}
+ StrCpy $_INSTALLSERVICE_ "Yes"
+ ${Else}
+ StrCpy $_INSTALLSERVICE_ "No"
+ StrCpy $_SERVICESTART_ "No"
+ StrCpy $_SERVICEACCOUNTTYPE_ "None"
+ ${EndIf}
+FunctionEnd
+
+Function .onInstSuccess
+ #ExecShell "open" "http://localhost:8096"
+FunctionEnd
diff --git a/windows/legacy/install-jellyfin.ps1 b/windows/legacy/install-jellyfin.ps1
new file mode 100644
index 000000000..e909a0468
--- /dev/null
+++ b/windows/legacy/install-jellyfin.ps1
@@ -0,0 +1,460 @@
+[CmdletBinding()]
+
+param(
+ [Switch]$Quiet,
+ [Switch]$InstallAsService,
+ [System.Management.Automation.pscredential]$ServiceUser,
+ [switch]$CreateDesktopShorcut,
+ [switch]$LaunchJellyfin,
+ [switch]$MigrateEmbyLibrary,
+ [string]$InstallLocation,
+ [string]$EmbyLibraryLocation,
+ [string]$JellyfinLibraryLocation
+)
+<# This form was created using POSHGUI.com a free online gui designer for PowerShell
+.NAME
+ Install-Jellyfin
+#>
+
+#This doesn't need to be used by default anymore, but I am keeping it in as a function for future use.
+function Elevate-Window {
+ # Get the ID and security principal of the current user account
+ $myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent()
+ $myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($myWindowsID)
+
+ # Get the security principal for the Administrator role
+ $adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator
+
+ # Check to see if we are currently running "as Administrator"
+ if ($myWindowsPrincipal.IsInRole($adminRole))
+ {
+ # We are running "as Administrator" - so change the title and background color to indicate this
+ $Host.UI.RawUI.WindowTitle = $myInvocation.MyCommand.Definition + "(Elevated)"
+ $Host.UI.RawUI.BackgroundColor = "DarkBlue"
+ clear-host
+ }
+ else
+ {
+ # We are not running "as Administrator" - so relaunch as administrator
+
+ # Create a new process object that starts PowerShell
+ $newProcess = new-object System.Diagnostics.ProcessStartInfo "PowerShell";
+
+ # Specify the current script path and name as a parameter
+ $newProcess.Arguments = $myInvocation.MyCommand.Definition;
+
+ # Indicate that the process should be elevated
+ $newProcess.Verb = "runas";
+
+ # Start the new process
+ [System.Diagnostics.Process]::Start($newProcess);
+
+ # Exit from the current, unelevated, process
+ exit
+ }
+}
+
+#FIXME The install methods should be a function that takes all the params, the quiet flag should be a paramset
+
+if($Quiet.IsPresent -or $Quiet -eq $true){
+ if([string]::IsNullOrEmpty($JellyfinLibraryLocation)){
+ $Script:JellyfinDataDir = "$env:LOCALAPPDATA\jellyfin\"
+ }else{
+ $Script:JellyfinDataDir = $JellyfinLibraryLocation
+ }
+ if([string]::IsNullOrEmpty($InstallLocation)){
+ $Script:DefaultJellyfinInstallDirectory = "$env:Appdata\jellyfin\"
+ }else{
+ $Script:DefaultJellyfinInstallDirectory = $InstallLocation
+ }
+
+ if([string]::IsNullOrEmpty($EmbyLibraryLocation)){
+ $Script:defaultEmbyDataDir = "$env:Appdata\Emby-Server\data\"
+ }else{
+ $Script:defaultEmbyDataDir = $EmbyLibraryLocation
+ }
+
+ if($InstallAsService.IsPresent -or $InstallAsService -eq $true){
+ $Script:InstallAsService = $true
+ }else{$Script:InstallAsService = $false}
+ if($null -eq $ServiceUser){
+ $Script:InstallServiceAsUser = $false
+ }else{
+ $Script:InstallServiceAsUser = $true
+ $Script:UserCredentials = $ServiceUser
+ $Script:JellyfinDataDir = "$env:HOMEDRIVE\Users\$($Script:UserCredentials.UserName)\Appdata\Local\jellyfin\"}
+ if($CreateDesktopShorcut.IsPresent -or $CreateDesktopShorcut -eq $true) {$Script:CreateShortcut = $true}else{$Script:CreateShortcut = $false}
+ if($MigrateEmbyLibrary.IsPresent -or $MigrateEmbyLibrary -eq $true){$Script:MigrateLibrary = $true}else{$Script:MigrateLibrary = $false}
+ if($LaunchJellyfin.IsPresent -or $LaunchJellyfin -eq $true){$Script:StartJellyfin = $true}else{$Script:StartJellyfin = $false}
+
+ if(-not (Test-Path $Script:DefaultJellyfinInstallDirectory)){
+ mkdir $Script:DefaultJellyfinInstallDirectory
+ }
+ Copy-Item -Path $PSScriptRoot/* -DestinationPath "$Script:DefaultJellyfinInstallDirectory/" -Force -Recurse
+ if($Script:InstallAsService){
+ if($Script:InstallServiceAsUser){
+ &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" install Jellyfin `"$Script:DefaultJellyfinInstallDirectory\jellyfin.exe`" --datadir `"$Script:JellyfinDataDir`"
+ Start-Sleep -Milliseconds 500
+ &sc.exe config Jellyfin obj=".\$($Script:UserCredentials.UserName)" password="$($Script:UserCredentials.GetNetworkCredential().Password)"
+ &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START
+ }else{
+ &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" install Jellyfin `"$Script:DefaultJellyfinInstallDirectory\jellyfin.exe`" --datadir `"$Script:JellyfinDataDir`"
+ Start-Sleep -Milliseconds 500
+ #&"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin ObjectName $Script:UserCredentials.UserName $Script:UserCredentials.GetNetworkCredential().Password
+ #Set-Service -Name Jellyfin -Credential $Script:UserCredentials
+ &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START
+ }
+ }
+ if($Script:MigrateLibrary){
+ Copy-Item -Path $Script:defaultEmbyDataDir/config -Destination $Script:JellyfinDataDir -force -Recurse
+ Copy-Item -Path $Script:defaultEmbyDataDir/cache -Destination $Script:JellyfinDataDir -force -Recurse
+ Copy-Item -Path $Script:defaultEmbyDataDir/data -Destination $Script:JellyfinDataDir -force -Recurse
+ Copy-Item -Path $Script:defaultEmbyDataDir/metadata -Destination $Script:JellyfinDataDir -force -Recurse
+ Copy-Item -Path $Script:defaultEmbyDataDir/root -Destination $Script:JellyfinDataDir -force -Recurse
+ }
+ if($Script:CreateShortcut){
+ $WshShell = New-Object -comObject WScript.Shell
+ $Shortcut = $WshShell.CreateShortcut("$Home\Desktop\Jellyfin.lnk")
+ $Shortcut.TargetPath = "$Script:DefaultJellyfinInstallDirectory\jellyfin.exe"
+ $Shortcut.Save()
+ }
+ if($Script:StartJellyfin){
+ if($Script:InstallAsService){
+ Get-Service Jellyfin | Start-Service
+ }else{
+ Start-Process -FilePath $Script:DefaultJellyfinInstallDirectory\jellyfin.exe -PassThru
+ }
+ }
+}else{
+
+}
+Add-Type -AssemblyName System.Windows.Forms
+[System.Windows.Forms.Application]::EnableVisualStyles()
+
+$Script:JellyFinDataDir = "$env:LOCALAPPDATA\jellyfin\"
+$Script:DefaultJellyfinInstallDirectory = "$env:Appdata\jellyfin\"
+$Script:defaultEmbyDataDir = "$env:Appdata\Emby-Server\"
+$Script:InstallAsService = $False
+$Script:InstallServiceAsUser = $false
+$Script:CreateShortcut = $false
+$Script:MigrateLibrary = $false
+$Script:StartJellyfin = $false
+
+function InstallJellyfin {
+ Write-Host "Install as service: $Script:InstallAsService"
+ Write-Host "Install as serviceuser: $Script:InstallServiceAsUser"
+ Write-Host "Create Shortcut: $Script:CreateShortcut"
+ Write-Host "MigrateLibrary: $Script:MigrateLibrary"
+ $GUIElementsCollection | ForEach-Object {
+ $_.Enabled = $false
+ }
+ Write-Host "Making Jellyfin directory"
+ $ProgressBar.Minimum = 1
+ $ProgressBar.Maximum = 100
+ $ProgressBar.Value = 1
+ if($Script:DefaultJellyfinInstallDirectory -ne $InstallLocationBox.Text){
+ Write-Host "Custom Install Location Chosen: $($InstallLocationBox.Text)"
+ $Script:DefaultJellyfinInstallDirectory = $InstallLocationBox.Text
+ }
+ if($Script:JellyfinDataDir -ne $CustomLibraryBox.Text){
+ Write-Host "Custom Library Location Chosen: $($CustomLibraryBox.Text)"
+ $Script:JellyfinDataDir = $CustomLibraryBox.Text
+ }
+ if(-not (Test-Path $Script:DefaultJellyfinInstallDirectory)){
+ mkdir $Script:DefaultJellyfinInstallDirectory
+ }
+ Write-Host "Copying Jellyfin Data"
+ $progressbar.Value = 10
+ Copy-Item -Path $PSScriptRoot/* -Destination $Script:DefaultJellyfinInstallDirectory/ -Force -Recurse
+ Write-Host "Finished Copying"
+ $ProgressBar.Value = 50
+ if($Script:InstallAsService){
+ if($Script:InstallServiceAsUser){
+ Write-Host "Installing Service as user $($Script:UserCredentials.UserName)"
+ &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" install Jellyfin `"$Script:DefaultJellyfinInstallDirectory\jellyfin.exe`" --datadir `"$Script:JellyfinDataDir`"
+ Start-Sleep -Milliseconds 2000
+ &sc.exe config Jellyfin obj=".\$($Script:UserCredentials.UserName)" password="$($Script:UserCredentials.GetNetworkCredential().Password)"
+ &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START
+ }else{
+ Write-Host "Installing Service as LocalSystem"
+ &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" install Jellyfin `"$Script:DefaultJellyfinInstallDirectory\jellyfin.exe`" --datadir `"$Script:JellyfinDataDir`"
+ Start-Sleep -Milliseconds 2000
+ &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START
+ }
+ }
+ $progressbar.Value = 60
+ if($Script:MigrateLibrary){
+ if($Script:defaultEmbyDataDir -ne $LibraryLocationBox.Text){
+ Write-Host "Custom location defined for emby library: $($LibraryLocationBox.Text)"
+ $Script:defaultEmbyDataDir = $LibraryLocationBox.Text
+ }
+ Write-Host "Copying emby library from $Script:defaultEmbyDataDir to $Script:JellyFinDataDir"
+ Write-Host "This could take a while depending on the size of your library. Please be patient"
+ Write-Host "Copying config"
+ Copy-Item -Path $Script:defaultEmbyDataDir/config -Destination $Script:JellyfinDataDir -force -Recurse
+ Write-Host "Copying cache"
+ Copy-Item -Path $Script:defaultEmbyDataDir/cache -Destination $Script:JellyfinDataDir -force -Recurse
+ Write-Host "Copying data"
+ Copy-Item -Path $Script:defaultEmbyDataDir/data -Destination $Script:JellyfinDataDir -force -Recurse
+ Write-Host "Copying metadata"
+ Copy-Item -Path $Script:defaultEmbyDataDir/metadata -Destination $Script:JellyfinDataDir -force -Recurse
+ Write-Host "Copying root dir"
+ Copy-Item -Path $Script:defaultEmbyDataDir/root -Destination $Script:JellyfinDataDir -force -Recurse
+ }
+ $progressbar.Value = 80
+ if($Script:CreateShortcut){
+ Write-Host "Creating Shortcut"
+ $WshShell = New-Object -comObject WScript.Shell
+ $Shortcut = $WshShell.CreateShortcut("$Home\Desktop\Jellyfin.lnk")
+ $Shortcut.TargetPath = "$Script:DefaultJellyfinInstallDirectory\jellyfin.exe"
+ $Shortcut.Save()
+ }
+ $ProgressBar.Value = 90
+ if($Script:StartJellyfin){
+ if($Script:InstallAsService){
+ Write-Host "Starting Jellyfin Service"
+ Get-Service Jellyfin | Start-Service
+ }else{
+ Write-Host "Starting Jellyfin"
+ Start-Process -FilePath $Script:DefaultJellyfinInstallDirectory\jellyfin.exe -PassThru
+ }
+ }
+ $progressbar.Value = 100
+ Write-Host Finished
+ $wshell = New-Object -ComObject Wscript.Shell
+ $wshell.Popup("Operation Completed",0,"Done",0x1)
+ $InstallForm.Close()
+}
+function ServiceBoxCheckChanged {
+ if($InstallAsServiceCheck.Checked){
+ $Script:InstallAsService = $true
+ $ServiceUserLabel.Visible = $true
+ $ServiceUserLabel.Enabled = $true
+ $ServiceUserBox.Visible = $true
+ $ServiceUserBox.Enabled = $true
+ }else{
+ $Script:InstallAsService = $false
+ $ServiceUserLabel.Visible = $false
+ $ServiceUserLabel.Enabled = $false
+ $ServiceUserBox.Visible = $false
+ $ServiceUserBox.Enabled = $false
+ }
+}
+function UserSelect {
+ if($ServiceUserBox.Text -eq 'Local System')
+ {
+ $Script:InstallServiceAsUser = $false
+ $Script:UserCredentials = $null
+ $ServiceUserBox.Items.RemoveAt(1)
+ $ServiceUserBox.Items.Add("Custom User")
+ }elseif($ServiceUserBox.Text -eq 'Custom User'){
+ $Script:InstallServiceAsUser = $true
+ $Script:UserCredentials = Get-Credential -Message "Please enter the credentials of the user you with to run Jellyfin Service as" -UserName $env:USERNAME
+ $ServiceUserBox.Items[1] = "$($Script:UserCredentials.UserName)"
+ }
+}
+function CreateShortcutBoxCheckChanged {
+ if($CreateShortcutCheck.Checked){
+ $Script:CreateShortcut = $true
+ }else{
+ $Script:CreateShortcut = $False
+ }
+}
+function StartJellyFinBoxCheckChanged {
+ if($StartProgramCheck.Checked){
+ $Script:StartJellyfin = $true
+ }else{
+ $Script:StartJellyfin = $false
+ }
+}
+
+function CustomLibraryCheckChanged {
+ if($CustomLibraryCheck.Checked){
+ $Script:UseCustomLibrary = $true
+ $CustomLibraryBox.Enabled = $true
+ }else{
+ $Script:UseCustomLibrary = $false
+ $CustomLibraryBox.Enabled = $false
+ }
+}
+
+function MigrateLibraryCheckboxChanged {
+
+ if($MigrateLibraryCheck.Checked){
+ $Script:MigrateLibrary = $true
+ $LibraryMigrationLabel.Visible = $true
+ $LibraryMigrationLabel.Enabled = $true
+ $LibraryLocationBox.Visible = $true
+ $LibraryLocationBox.Enabled = $true
+ }else{
+ $Script:MigrateLibrary = $false
+ $LibraryMigrationLabel.Visible = $false
+ $LibraryMigrationLabel.Enabled = $false
+ $LibraryLocationBox.Visible = $false
+ $LibraryLocationBox.Enabled = $false
+ }
+
+}
+
+
+#region begin GUI{
+
+$InstallForm = New-Object system.Windows.Forms.Form
+$InstallForm.ClientSize = '320,240'
+$InstallForm.text = "Terrible Jellyfin Installer"
+$InstallForm.TopMost = $false
+
+$GUIElementsCollection = @()
+
+$InstallButton = New-Object system.Windows.Forms.Button
+$InstallButton.text = "Install"
+$InstallButton.width = 60
+$InstallButton.height = 30
+$InstallButton.location = New-Object System.Drawing.Point(5,5)
+$InstallButton.Font = 'Microsoft Sans Serif,10'
+$GUIElementsCollection += $InstallButton
+
+$ProgressBar = New-Object system.Windows.Forms.ProgressBar
+$ProgressBar.width = 245
+$ProgressBar.height = 30
+$ProgressBar.location = New-Object System.Drawing.Point(70,5)
+
+$InstallLocationLabel = New-Object system.Windows.Forms.Label
+$InstallLocationLabel.text = "Install Location"
+$InstallLocationLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
+$InstallLocationLabel.AutoSize = $true
+$InstallLocationLabel.width = 100
+$InstallLocationLabel.height = 20
+$InstallLocationLabel.location = New-Object System.Drawing.Point(5,50)
+$InstallLocationLabel.Font = 'Microsoft Sans Serif,10'
+$GUIElementsCollection += $InstallLocationLabel
+
+$InstallLocationBox = New-Object system.Windows.Forms.TextBox
+$InstallLocationBox.multiline = $false
+$InstallLocationBox.width = 205
+$InstallLocationBox.height = 20
+$InstallLocationBox.location = New-Object System.Drawing.Point(110,50)
+$InstallLocationBox.Text = $Script:DefaultJellyfinInstallDirectory
+$InstallLocationBox.Font = 'Microsoft Sans Serif,10'
+$GUIElementsCollection += $InstallLocationBox
+
+$CustomLibraryCheck = New-Object system.Windows.Forms.CheckBox
+$CustomLibraryCheck.text = "Custom Library Location:"
+$CustomLibraryCheck.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
+$CustomLibraryCheck.AutoSize = $false
+$CustomLibraryCheck.width = 180
+$CustomLibraryCheck.height = 20
+$CustomLibraryCheck.location = New-Object System.Drawing.Point(5,75)
+$CustomLibraryCheck.Font = 'Microsoft Sans Serif,10'
+$GUIElementsCollection += $CustomLibraryCheck
+
+$CustomLibraryBox = New-Object system.Windows.Forms.TextBox
+$CustomLibraryBox.multiline = $false
+$CustomLibraryBox.width = 130
+$CustomLibraryBox.height = 20
+$CustomLibraryBox.location = New-Object System.Drawing.Point(185,75)
+$CustomLibraryBox.Text = $Script:JellyFinDataDir
+$CustomLibraryBox.Font = 'Microsoft Sans Serif,10'
+$CustomLibraryBox.Enabled = $false
+$GUIElementsCollection += $CustomLibraryBox
+
+$InstallAsServiceCheck = New-Object system.Windows.Forms.CheckBox
+$InstallAsServiceCheck.text = "Install as Service"
+$InstallAsServiceCheck.AutoSize = $false
+$InstallAsServiceCheck.width = 140
+$InstallAsServiceCheck.height = 20
+$InstallAsServiceCheck.location = New-Object System.Drawing.Point(5,125)
+$InstallAsServiceCheck.Font = 'Microsoft Sans Serif,10'
+$GUIElementsCollection += $InstallAsServiceCheck
+
+$ServiceUserLabel = New-Object system.Windows.Forms.Label
+$ServiceUserLabel.text = "Run Service As:"
+$ServiceUserLabel.AutoSize = $true
+$ServiceUserLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
+$ServiceUserLabel.width = 100
+$ServiceUserLabel.height = 20
+$ServiceUserLabel.location = New-Object System.Drawing.Point(15,145)
+$ServiceUserLabel.Font = 'Microsoft Sans Serif,10'
+$ServiceUserLabel.Visible = $false
+$ServiceUserLabel.Enabled = $false
+$GUIElementsCollection += $ServiceUserLabel
+
+$ServiceUserBox = New-Object system.Windows.Forms.ComboBox
+$ServiceUserBox.text = "Run Service As"
+$ServiceUserBox.width = 195
+$ServiceUserBox.height = 20
+@('Local System','Custom User') | ForEach-Object {[void] $ServiceUserBox.Items.Add($_)}
+$ServiceUserBox.location = New-Object System.Drawing.Point(120,145)
+$ServiceUserBox.Font = 'Microsoft Sans Serif,10'
+$ServiceUserBox.Visible = $false
+$ServiceUserBox.Enabled = $false
+$ServiceUserBox.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList
+$GUIElementsCollection += $ServiceUserBox
+
+$MigrateLibraryCheck = New-Object system.Windows.Forms.CheckBox
+$MigrateLibraryCheck.text = "Import Emby/Old JF Library"
+$MigrateLibraryCheck.AutoSize = $false
+$MigrateLibraryCheck.width = 160
+$MigrateLibraryCheck.height = 20
+$MigrateLibraryCheck.location = New-Object System.Drawing.Point(5,170)
+$MigrateLibraryCheck.Font = 'Microsoft Sans Serif,10'
+$GUIElementsCollection += $MigrateLibraryCheck
+
+$LibraryMigrationLabel = New-Object system.Windows.Forms.Label
+$LibraryMigrationLabel.text = "Emby/Old JF Library Path"
+$LibraryMigrationLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
+$LibraryMigrationLabel.AutoSize = $false
+$LibraryMigrationLabel.width = 120
+$LibraryMigrationLabel.height = 20
+$LibraryMigrationLabel.location = New-Object System.Drawing.Point(15,190)
+$LibraryMigrationLabel.Font = 'Microsoft Sans Serif,10'
+$LibraryMigrationLabel.Visible = $false
+$LibraryMigrationLabel.Enabled = $false
+$GUIElementsCollection += $LibraryMigrationLabel
+
+$LibraryLocationBox = New-Object system.Windows.Forms.TextBox
+$LibraryLocationBox.multiline = $false
+$LibraryLocationBox.width = 175
+$LibraryLocationBox.height = 20
+$LibraryLocationBox.location = New-Object System.Drawing.Point(140,190)
+$LibraryLocationBox.Text = $Script:defaultEmbyDataDir
+$LibraryLocationBox.Font = 'Microsoft Sans Serif,10'
+$LibraryLocationBox.Visible = $false
+$LibraryLocationBox.Enabled = $false
+$GUIElementsCollection += $LibraryLocationBox
+
+$CreateShortcutCheck = New-Object system.Windows.Forms.CheckBox
+$CreateShortcutCheck.text = "Desktop Shortcut"
+$CreateShortcutCheck.AutoSize = $false
+$CreateShortcutCheck.width = 150
+$CreateShortcutCheck.height = 20
+$CreateShortcutCheck.location = New-Object System.Drawing.Point(5,215)
+$CreateShortcutCheck.Font = 'Microsoft Sans Serif,10'
+$GUIElementsCollection += $CreateShortcutCheck
+
+$StartProgramCheck = New-Object system.Windows.Forms.CheckBox
+$StartProgramCheck.text = "Start Jellyfin"
+$StartProgramCheck.AutoSize = $false
+$StartProgramCheck.width = 160
+$StartProgramCheck.height = 20
+$StartProgramCheck.location = New-Object System.Drawing.Point(160,215)
+$StartProgramCheck.Font = 'Microsoft Sans Serif,10'
+$GUIElementsCollection += $StartProgramCheck
+
+$InstallForm.controls.AddRange($GUIElementsCollection)
+$InstallForm.Controls.Add($ProgressBar)
+
+#region gui events {
+$InstallButton.Add_Click({ InstallJellyfin })
+$CustomLibraryCheck.Add_CheckedChanged({CustomLibraryCheckChanged})
+$InstallAsServiceCheck.Add_CheckedChanged({ServiceBoxCheckChanged})
+$ServiceUserBox.Add_SelectedValueChanged({ UserSelect })
+$MigrateLibraryCheck.Add_CheckedChanged({MigrateLibraryCheckboxChanged})
+$CreateShortcutCheck.Add_CheckedChanged({CreateShortcutBoxCheckChanged})
+$StartProgramCheck.Add_CheckedChanged({StartJellyFinBoxCheckChanged})
+#endregion events }
+
+#endregion GUI }
+
+
+[void]$InstallForm.ShowDialog()
diff --git a/windows/legacy/install.bat b/windows/legacy/install.bat
new file mode 100644
index 000000000..e21479a79
--- /dev/null
+++ b/windows/legacy/install.bat
@@ -0,0 +1 @@
+powershell.exe -executionpolicy Bypass -file install-jellyfin.ps1