diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c67c292372..4ce0d7583a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -33,7 +33,7 @@ "libfontconfig1" ] }, - "ghcr.io/devcontainers/features/docker-in-docker:2": { + "ghcr.io/devcontainers/features/docker-in-docker:3": { "dockerDashComposeVersion": "v2" }, "ghcr.io/devcontainers/features/github-cli:1": {}, diff --git a/.devcontainer/install-ffmpeg.sh b/.devcontainer/install-ffmpeg.sh index 1e58e6ef44..1344634630 100644 --- a/.devcontainer/install-ffmpeg.sh +++ b/.devcontainer/install-ffmpeg.sh @@ -15,7 +15,7 @@ sudo apt-get install software-properties-common -y sudo add-apt-repository universe -y sudo mkdir -p /etc/apt/keyrings -curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/jellyfin.gpg +curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | sudo gpg --batch --yes --dearmor -o /etc/apt/keyrings/jellyfin.gpg export VERSION_OS="$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release )" export VERSION_CODENAME="$( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release )" export DPKG_ARCHITECTURE="$( dpkg --print-architecture )" diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml index 45235be712..a7e644a55f 100644 --- a/.github/ISSUE_TEMPLATE/issue report.yml +++ b/.github/ISSUE_TEMPLATE/issue report.yml @@ -87,6 +87,7 @@ body: label: Jellyfin Server version description: What version of Jellyfin are you using? options: + - 10.11.9 - 10.11.8 - 10.11.7 - 10.11.6 diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index b14e5958ab..cf4cc1c7f1 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -32,13 +32,13 @@ jobs: dotnet-version: '10.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 diff --git a/.github/workflows/ci-format.yml b/.github/workflows/ci-format.yml new file mode 100644 index 0000000000..c2cca262bf --- /dev/null +++ b/.github/workflows/ci-format.yml @@ -0,0 +1,25 @@ +name: Format +on: + push: + branches: + - master + # Run formatter against the forked branch, but + # do not allow access to secrets + # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflows-in-forked-repositories + pull_request: + +env: + SDK_VERSION: "10.0.x" + +jobs: + format-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + with: + dotnet-version: ${{ env.SDK_VERSION }} + + - name: Run DotNet Format + run: dotnet format --verify-no-changes --verbosity minimal diff --git a/.github/workflows/issue-stale.yml b/.github/workflows/issue-stale.yml index 339fcf569e..d6372ef6f4 100644 --- a/.github/workflows/issue-stale.yml +++ b/.github/workflows/issue-stale.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 + - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} ascending: true diff --git a/.github/workflows/pull-request-conflict.yml b/.github/workflows/pull-request-conflict.yml index b003636a6e..32628ac912 100644 --- a/.github/workflows/pull-request-conflict.yml +++ b/.github/workflows/pull-request-conflict.yml @@ -4,7 +4,7 @@ on: push: branches: - master - pull_request: + pull_request_target: issue_comment: permissions: {} diff --git a/.github/workflows/pull-request-stale.yaml b/.github/workflows/pull-request-stale.yaml index e114276c28..6f225a4714 100644 --- a/.github/workflows/pull-request-stale.yaml +++ b/.github/workflows/pull-request-stale.yaml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 + - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} ascending: true diff --git a/.gitignore b/.gitignore index d5a0367bff..e399f1fc47 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ local.properties .classpath .settings/ .loadpath +*.lscache # External tool builders .externalToolBuilders/ diff --git a/Directory.Packages.props b/Directory.Packages.props index e722bed973..f568f7e781 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,11 +9,11 @@ - + - + @@ -74,7 +74,7 @@ - + diff --git a/Emby.Naming/ExternalFiles/ExternalPathParser.cs b/Emby.Naming/ExternalFiles/ExternalPathParser.cs index 3461b3c0d6..8e7da5db42 100644 --- a/Emby.Naming/ExternalFiles/ExternalPathParser.cs +++ b/Emby.Naming/ExternalFiles/ExternalPathParser.cs @@ -70,7 +70,7 @@ namespace Emby.Naming.ExternalFiles if (lastSeparator == -1) { - break; + break; } string currentSlice = languageString[lastSeparator..]; diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index ef5fa8bef9..aa19948e36 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.AppBase } else { - _configurationFactories = [.._configurationFactories, factory]; + _configurationFactories = [.. _configurationFactories, factory]; } _configurationStores = _configurationFactories diff --git a/Emby.Server.Implementations/Chapters/ChapterManager.cs b/Emby.Server.Implementations/Chapters/ChapterManager.cs index 8a4721ce62..69cbe533c6 100644 --- a/Emby.Server.Implementations/Chapters/ChapterManager.cs +++ b/Emby.Server.Implementations/Chapters/ChapterManager.cs @@ -240,15 +240,15 @@ public class ChapterManager : IChapterManager public void SaveChapters(BaseItem item, IReadOnlyList chapters) { if (!Supports(item)) - { - _logger.LogWarning("Attempted to save chapters for unsupported item type {Type}: {Name} ({Id})", item.GetType().Name, item.Name, item.Id); - return; - } + { + _logger.LogWarning("Attempted to save chapters for unsupported item type {Type}: {Name} ({Id})", item.GetType().Name, item.Name, item.Id); + return; + } // Remove any chapters that are outside of the runtime of the item var validChapters = chapters.Where(c => c.StartPositionTicks < item.RunTimeTicks).ToList(); _chapterRepository.SaveChapters(item.Id, validChapters); -} + } /// public ChapterInfo? GetChapter(Guid baseItemId, int index) diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index cfa3e7c31d..7591359ea4 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.Library '[' => ']', '(' => ')', '{' => '}', - _ => '\0' + _ => '\0' }; if (attributeCloser != '\0' && (str[attributeEnd] == '=' || str[attributeEnd] == '-')) { diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index c81a0adb89..769d721665 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -31,7 +31,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// /// The logger. /// The naming options. - public SeriesResolver(ILogger logger, NamingOptions namingOptions) + public SeriesResolver(ILogger logger, NamingOptions namingOptions) { _logger = logger; _namingOptions = namingOptions; diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index 1eaf1d79e8..17af935562 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -107,5 +107,6 @@ "TaskMoveTrickplayImagesDescription": "ينقل ملفات معاينات التنقل الحالية وفقاً لإعدادات المكتبة.", "CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم", "CleanupUserDataTaskDescription": "ينظف جميع بيانات المستخدم (مثل حالة المشاهدة وحالة المفضلة وغيرها) للمحتوى الذي لم يعد موجوداً لمدة 90 يوماً على الأقل.", - "Original": "فريد" + "Original": "فريد", + "LyricDownloadFailureFromForItem": "فشل تحميل الكلمات من {0} إلى {1}" } diff --git a/Emby.Server.Implementations/Localization/Core/br.json b/Emby.Server.Implementations/Localization/Core/br.json new file mode 100644 index 0000000000..cedc87e5a6 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/br.json @@ -0,0 +1,63 @@ +{ + "Artists": "Arzourien", + "AuthenticationSucceededWithUserName": "{0} kennasket gant berzh", + "Books": "Levrioù", + "ChapterNameValue": "Pennad {0}", + "Collections": "Dastumadegoù", + "Default": "Dre ziouer", + "External": "Diavaez", + "FailedLoginAttemptWithUserName": "Kennaskañ c'hwitet gant {0}", + "Favorites": "Sinedoù", + "Folders": "Teuliadoù", + "Forced": "Rediet", + "Genres": "Doareoù", + "HeaderContinueWatching": "Kenderc'hel da sellet", + "HeaderFavoriteEpisodes": "Rannoù Karetañ", + "HeaderFavoriteShows": "Heuliadennoù Karetañ", + "HeaderLiveTV": "TV war-eeun", + "HeaderNextUp": "Da c'houde", + "HearingImpaired": "Tud fall o C'hleved", + "HomeVideos": "Videoioù Personel", + "Inherit": "Hêrezhiñ", + "LabelIpAddressValue": "Chomlec'h IP : {0}", + "LabelRunningTimeValue": "Padelezh : {0}", + "Latest": "Diwezhañ", + "AppDeviceValues": "Arload : {0}, Trobarzhell : {1}", + "LyricDownloadFailureFromForItem": "C'hwitet eo pellgargañ ar c'homzoù eus {0} evit {1}", + "MixedContent": "Danvez mesket", + "Movies": "Filmoù", + "Music": "Sonerezh", + "MusicVideos": "Videoioù Sonerezh", + "NameInstallFailed": "{0} c'hwitet war ar staliadur", + "NameSeasonNumber": "Koulzad {0}", + "NameSeasonUnknown": "Koulzad Dianav", + "NewVersionIsAvailable": "Ur stumm Servijer Jellyfin nevez a c'haller pellgargañ.", + "NotificationOptionApplicationUpdateAvailable": "Hizivadur an arload zo da gaout", + "NotificationOptionApplicationUpdateInstalled": "Hizivadur an arload staliet", + "NotificationOptionAudioPlayback": "Lenn aodio lañset", + "NotificationOptionAudioPlaybackStopped": "Lenn aodio ehanet", + "Original": "Orin", + "Photos": "Fotoioù", + "Shows": "Heuliadennoù", + "Undefined": "Dianav", + "TasksMaintenanceCategory": "Trezalc’h", + "TasksLibraryCategory": "Levraoueg", + "TasksApplicationCategory": "Arload", + "NotificationOptionInstallationFailed": "C'hwitet war staliañ", + "NotificationOptionPluginError": "Fazi Askouezh", + "NotificationOptionPluginInstalled": "Askouezh staliet", + "NotificationOptionPluginUninstalled": "Askouezh distaliet", + "ScheduledTaskFailedWithName": "c'hwitadenn war {0}", + "TvShows": "Heuliadennoù TV", + "VersionNumber": "Stumm {0}", + "TasksChannelsCategory": "Chadennoù enlinenn", + "TaskAudioNormalization": "Normalizadur an aodio", + "TaskRefreshPeople": "Freskaat ar gomedianed", + "TaskUpdatePlugins": "Hizivaat an askouezhioù", + "TaskRefreshChannels": "Freskaat ar chadennoù", + "TaskOptimizeDatabase": "Gwellekaat an diaz roadennoù", + "TaskKeyframeExtractor": "Eztenner skeudennoù-alc'hwez", + "NotificationOptionCameraImageUploaded": "Karget eo skeudenn ar benveg", + "NotificationOptionNewLibraryContent": "Danvez nevez ouzhpennet", + "NotificationOptionPluginUpdateInstalled": "Staliet eo hizivadur an askouezh" +} diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 1465a4d5e1..6c81726ee6 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -107,5 +107,6 @@ "TaskMoveTrickplayImagesDescription": "Mou els fitxers existents d'imatges de previsualització segons la configuració de la mediateca.", "CleanupUserDataTaskDescription": "Neteja totes les dades d'usuari (estat de la visualització, estat dels preferits, etc.) del contingut multimèdia que no ha estat present durant almenys 90 dies.", "CleanupUserDataTask": "Tasca de neteja de dades d'usuari", - "Original": "Original" + "Original": "Original", + "LyricDownloadFailureFromForItem": "No s'han pogut descarregar les lletres des de {0} per a {1}" } diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 9e3d4456a8..28f0e2df97 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -107,5 +107,6 @@ "TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny.", "CleanupUserDataTaskDescription": "Odstraní všechna uživatelská data (stav zhlédnutí, oblíbené atd.) z médií, které již neexistují více než 90 dní.", "CleanupUserDataTask": "Pročistit uživatelská data", - "Original": "Originál" + "Original": "Originál", + "LyricDownloadFailureFromForItem": "Nepodařilo se stáhnout texty pro {1} ze služby {0}" } diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index e30528b3cd..697d9c090f 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -106,5 +106,7 @@ "TaskMoveTrickplayImagesDescription": "Flyt eksisterende trickplay-billeder jævnfør biblioteksindstillinger.", "TaskExtractMediaSegmentsDescription": "Udtrækker eller henter mediesegmenter fra plugins som understøtter MediaSegment.", "CleanupUserDataTask": "Brugerdata oprydningsopgave", - "CleanupUserDataTaskDescription": "Rydder alle brugerdata (eks. visning- og favoritstatus) fra medier, der har været utilgængelige i mindst 90 dage." + "CleanupUserDataTaskDescription": "Rydder alle brugerdata (eks. visning- og favoritstatus) fra medier, der har været utilgængelige i mindst 90 dage.", + "LyricDownloadFailureFromForItem": "Sangtekster kunne ikke downloades fra {0} til {1}", + "Original": "Original" } diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index d61ce396cb..35efcf74d3 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -107,5 +107,6 @@ "TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay", "CleanupUserDataTask": "Tarea de limpieza de datos del usuario", "CleanupUserDataTaskDescription": "Limpia todos los datos del usuario (estado de visualización, favoritos, etc.) de los medios que ya no están disponibles desde hace al menos 90 días.", - "Original": "Original" + "Original": "Original", + "LyricDownloadFailureFromForItem": "No se pudieron descargar las letras desde {0} para {1}." } diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json index b530f19fa9..e6bf1f25b5 100644 --- a/Emby.Server.Implementations/Localization/Core/et.json +++ b/Emby.Server.Implementations/Localization/Core/et.json @@ -106,5 +106,7 @@ "TaskExtractMediaSegmentsDescription": "Eraldab või võtab meedialõigud MediaSegment'i toega pluginatest.", "TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht", "CleanupUserDataTask": "Puhasta kasutajaandmed", - "CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud." + "CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud.", + "LyricDownloadFailureFromForItem": "Laulusõnade hankimine teenusest {0} loole {1} nurjus", + "Original": "Algne" } diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index d08f652e58..d080eb0236 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -106,5 +106,7 @@ "TaskMoveTrickplayImages": "Siirrä Trickplay-kuvien sijainti", "TaskMoveTrickplayImagesDescription": "Siirtää olemassa olevia trickplay-tiedostoja kirjaston asetusten mukaan.", "CleanupUserDataTask": "Käyttäjätietojen puhdistustehtävä", - "CleanupUserDataTaskDescription": "Puhdistaa kaikki käyttäjätiedot (katselutila, suosikit ym.) medioista, joita ei ole ollut saatavilla yli 90 päivään." + "CleanupUserDataTaskDescription": "Puhdistaa kaikki käyttäjätiedot (katselutila, suosikit ym.) medioista, joita ei ole ollut saatavilla yli 90 päivään.", + "LyricDownloadFailureFromForItem": "Sanoitusten lataus kohteesta {0} kappaleelle {1} epäonnistui", + "Original": "Alkuperäinen" } diff --git a/Emby.Server.Implementations/Localization/Core/ga.json b/Emby.Server.Implementations/Localization/Core/ga.json index 8badf551b3..1ee606cc64 100644 --- a/Emby.Server.Implementations/Localization/Core/ga.json +++ b/Emby.Server.Implementations/Localization/Core/ga.json @@ -107,5 +107,6 @@ "TaskDownloadMissingSubtitles": "Íosluchtaigh fotheidil ar iarraidh", "CleanupUserDataTask": "Tasc glantacháin sonraí úsáideora", "CleanupUserDataTaskDescription": "Glanann sé gach sonraí úsáideora (stádas faire, stádas is fearr leat srl.) ó mheáin nach bhfuil i láthair a thuilleadh ar feadh 90 lá ar a laghad.", - "Original": "Bunaidh" + "Original": "Bunaidh", + "LyricDownloadFailureFromForItem": "Theip ar liricí a íoslódáil ó {0} do {1}" } diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index e1c1ec109d..1995d7a4cf 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -1,5 +1,5 @@ { - "AppDeviceValues": "Program: {0}, Eszköz: {1}", + "AppDeviceValues": "alkalmazás: {0}, eszköz: {1}", "Artists": "Előadók", "AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve", "Books": "Könyvek", @@ -107,5 +107,6 @@ "TaskExtractMediaSegmentsDescription": "Kinyeri vagy megszerzi a médiaszegmenseket a MediaSegment támogatással rendelkező bővítményekből.", "CleanupUserDataTaskDescription": "Legalább 90 napja nem elérhető médiákhoz kapcsolódó összes felhasználói adat (pl. megtekintési állapot, kedvencek) törlése.", "CleanupUserDataTask": "Felhasználói adatok tisztítása feladat", - "Original": "Eredeti" + "Original": "Eredeti", + "LyricDownloadFailureFromForItem": "Dalszöveg letöltése {0}-tól {1}-hez sikertelen" } diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index 6053aec896..f13944e6be 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -107,5 +107,6 @@ "TaskExtractMediaSegments": "Scansiona Segmento Media", "CleanupUserDataTask": "Task di pulizia dei dati utente", "CleanupUserDataTaskDescription": "Pulisce tutti i dati utente (stato di visione, status preferiti, ecc.) dai contenuti non più presenti da almeno 90 giorni.", - "Original": "Originale" + "Original": "Originale", + "LyricDownloadFailureFromForItem": "Scaricamento dei testi non riuscito da {0} per {1}" } diff --git a/Emby.Server.Implementations/Localization/Core/ka.json b/Emby.Server.Implementations/Localization/Core/ka.json index c0b7a196f7..5245d89948 100644 --- a/Emby.Server.Implementations/Localization/Core/ka.json +++ b/Emby.Server.Implementations/Localization/Core/ka.json @@ -2,7 +2,7 @@ "Genres": "ჟანრები", "TasksApplicationCategory": "აპლიკაცია", "AppDeviceValues": "აპლიკაცია: {0}, მოწყობილობა: {1}", - "Artists": "არტისტი", + "Artists": "შემსრულებლები", "AuthenticationSucceededWithUserName": "{0} -ის ავთენტიკაცია წარმატებულია", "Books": "წიგნები", "Forced": "იძულებითი", @@ -22,7 +22,7 @@ "HearingImpaired": "სმენადაქვეითებული", "LabelRunningTimeValue": "ხანგრძლივობა: {0}", "MixedContent": "შერეული შემცველობა", - "MusicVideos": "მუსიკალური ვიდეოები", + "MusicVideos": "მუსიკის ვიდეოები", "NotificationOptionInstallationFailed": "დაყენების შეცდომა", "NotificationOptionApplicationUpdateInstalled": "აპლიკაციის განახლება დაყენებულია", "NotificationOptionAudioPlayback": "აუდიოს დაკვრა დაწყებულია", @@ -33,22 +33,22 @@ "TasksChannelsCategory": "ინტერნეტ-არხები", "TaskRefreshChannelsDescription": "ინტერნეტ-არხის ინფორმაციის განახლება.", "Collections": "კოლექციები", - "Default": "ნაგულისხმები", + "Default": "ნაგულისხმევი", "Favorites": "რჩეულები", "Folders": "საქაღალდეები", "HeaderFavoriteShows": "რჩეული სერიალები", - "HeaderLiveTV": "ლაივ ტელევიზია", + "HeaderLiveTV": "ცოცხალი ტელევიზია", "HeaderNextUp": "შემდეგი", "HomeVideos": "სახლის ვიდეოები", "NameSeasonNumber": "სეზონი {0}", "NameSeasonUnknown": "სეზონი უცნობია", - "NotificationOptionPluginError": "მოდულის შეცდომა", - "NotificationOptionPluginInstalled": "მოდული დაყენებულია", + "NotificationOptionPluginError": "დამატების შეცდომა", + "NotificationOptionPluginInstalled": "დამატება დაყენებულია", "NotificationOptionPluginUninstalled": "მოდული წაიშალა", - "ScheduledTaskFailedWithName": "{0} ვერ შესრულდა", + "ScheduledTaskFailedWithName": "{0} ჩავარდა", "TvShows": "სატელევიზიო სერიალები", "TaskRefreshPeople": "ხალხის განახლება", - "TaskUpdatePlugins": "მოდულების განახლება", + "TaskUpdatePlugins": "დამატებების განახლება", "TaskRefreshChannels": "არხების განახლება", "TaskOptimizeDatabase": "მონაცემთა ბაზის ოპტიმიზაცია", "TaskKeyframeExtractor": "საკვანძო კადრის გამომღები", @@ -106,5 +106,7 @@ "TaskMoveTrickplayImages": "Trickplay სურათების მიგრაცია", "TaskMoveTrickplayImagesDescription": "გადააქვს trickplay ფაილები ბიბლიოთეკის პარამეტრებზე დაყრდნობით.", "CleanupUserDataTask": "მომხმარებლების მონაცემების გასუფთავება", - "CleanupUserDataTaskDescription": "ასუფთავებს მომხმარებლების მონაცემებს (ყურების სტატუსი, ფავორიტები ანდ ა.შ) მედია ელემენტებისთვის რომლების 90 დღეზე მეტია აღარ არსებობენ." + "CleanupUserDataTaskDescription": "ასუფთავებს მომხმარებლების მონაცემებს (ყურების სტატუსი, ფავორიტები ანდ ა.შ) მედია ელემენტებისთვის რომლების 90 დღეზე მეტია აღარ არსებობენ.", + "LyricDownloadFailureFromForItem": "{1}-ისთვის {0}-დან ლირიკის გადმოწერა ჩავარდა", + "Original": "ორიგინალი" } diff --git a/Emby.Server.Implementations/Localization/Core/mi.json b/Emby.Server.Implementations/Localization/Core/mi.json index 000e82ebdf..74fae52dca 100644 --- a/Emby.Server.Implementations/Localization/Core/mi.json +++ b/Emby.Server.Implementations/Localization/Core/mi.json @@ -2,5 +2,14 @@ "AppDeviceValues": "Taupānga: {0}, Pūrere: {1}", "Artists": "Kaiwaiata", "AuthenticationSucceededWithUserName": "{0} has been successfully authenticated", - "Books": "Ngā pukapuka" + "Books": "Ngā pukapuka", + "Default": "Taunoa", + "Collections": "Kohinga", + "External": "Waho", + "Folders": "Kōpaki", + "Forced": "Kaha", + "Music": "Waiata", + "Movies": "Kiriata", + "Latest": "Hou", + "Inherit": "Riro" } diff --git a/Emby.Server.Implementations/Localization/Core/ml.json b/Emby.Server.Implementations/Localization/Core/ml.json index c43a5a7431..dbf2ed4648 100644 --- a/Emby.Server.Implementations/Localization/Core/ml.json +++ b/Emby.Server.Implementations/Localization/Core/ml.json @@ -98,5 +98,10 @@ "TaskAudioNormalization": "സാധാരണ ശബ്ദ നിലയിലെത്തിലെത്തിക്കുക", "TaskAudioNormalizationDescription": "സാധാരണ ശബ്ദ നിലയിലെത്തിലെത്തിക്കുന്ന ഡാറ്റയ്ക്കായി ഫയലുകൾ സ്കാൻ ചെയ്യുക.", "TaskRefreshTrickplayImages": "ട്രിക്ക് പ്ലേ ചിത്രങ്ങൾ സൃഷ്ടിക്കുക", - "TaskRefreshTrickplayImagesDescription": "പ്രവർത്തനക്ഷമമാക്കിയ ലൈബ്രറികളിൽ വീഡിയോകൾക്കായി ട്രിക്ക്പ്ലേ പ്രിവ്യൂകൾ സൃഷ്ടിക്കുന്നു." + "TaskRefreshTrickplayImagesDescription": "പ്രവർത്തനക്ഷമമാക്കിയ ലൈബ്രറികളിൽ വീഡിയോകൾക്കായി ട്രിക്ക്പ്ലേ പ്രിവ്യൂകൾ സൃഷ്ടിക്കുന്നു.", + "Original": "ഓറിജിനൽ", + "TaskDownloadMissingLyrics": "ഇല്ലാത്ത വരികൾ ഡൗൺലോഡ് ചെയ്യുക", + "TaskDownloadMissingLyricsDescription": "പാട്ടുകളുടെ വരികൾ ഡൗൺലോഡ് ചെയ്യുന്നു", + "TaskExtractMediaSegments": "മീഡിയ സെഗ്‌മെന്റ് സ്‌കാൻ", + "TaskExtractMediaSegmentsDescription": "മീഡിയസെഗ്മെന്റ് പ്രാപ്തമാക്കിയ പ്ലഗിനുകളിൽ നിന്ന് മീഡിയ സെഗ്‌മെന്റുകൾ എക്‌സ്‌ട്രാക്റ്റുചെയ്യുന്നു അല്ലെങ്കിൽ നേടുന്നു." } diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 9aea3adc22..898f5892c9 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -8,7 +8,7 @@ "FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}", "Favorites": "Favorieten", "Folders": "Mappen", - "HeaderContinueWatching": "Verderkijken", + "HeaderContinueWatching": "Verder kijken", "HeaderFavoriteEpisodes": "Favoriete afleveringen", "HeaderFavoriteShows": "Favoriete series", "HeaderLiveTV": "Live-tv", diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index 9b2ef063a6..1db500adf3 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -106,5 +106,7 @@ "TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de mídia de plug-ins habilitados para MediaSegment.", "TaskMoveTrickplayImages": "Migrar o local da imagem do Trickplay", "CleanupUserDataTask": "Tarefa de limpeza de dados do usuário", - "CleanupUserDataTaskDescription": "Limpa todos os dados do usuário (estado de visualização, status de favorito, etc.) de mídias que não estão presentes por pelo menos 90 dias." + "CleanupUserDataTaskDescription": "Limpa todos os dados do usuário (estado de visualização, status de favorito, etc.) de mídias que não estão presentes por pelo menos 90 dias.", + "LyricDownloadFailureFromForItem": "Download das Letras falharam em {0} para o item {1}", + "Original": "Original" } diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index 15b1543d8e..ce338acf34 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -107,5 +107,6 @@ "TaskMoveTrickplayImages": "Migrar a localização da imagem do Trickplay", "CleanupUserDataTask": "Task de limpeza de dados do usuário", "CleanupUserDataTaskDescription": "Remove todos os dados do usuário (progresso, favoritos etc) de mídias que não estão presentes há pelo menos 90 dias.", - "Original": "Original" + "Original": "Original", + "LyricDownloadFailureFromForItem": "Erro ao descarregar letras de {0} para {1}" } diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json index 7b01fbec89..ea83b88951 100644 --- a/Emby.Server.Implementations/Localization/Core/ro.json +++ b/Emby.Server.Implementations/Localization/Core/ro.json @@ -106,5 +106,7 @@ "TaskDownloadMissingLyrics": "Descarcă versurile lipsă", "TaskDownloadMissingLyricsDescription": "Descarcă versuri pentru melodii", "CleanupUserDataTask": "Sarcina de curatare a datelor utilizatorului", - "CleanupUserDataTaskDescription": "Sterge toate datele utilizatorului (starea vizionarii, starea favoritelor etc.) de pe suporturile media care nu mai sunt prezente timp de cel puțin 90 de zile." + "CleanupUserDataTaskDescription": "Sterge toate datele utilizatorului (starea vizionarii, starea favoritelor etc.) de pe suporturile media care nu mai sunt prezente timp de cel puțin 90 de zile.", + "LyricDownloadFailureFromForItem": "Versurile nu au putut fi descărcate din {0} pentru {1}", + "Original": "Original" } diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index d7eccf5f25..40d5e3985d 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -107,5 +107,6 @@ "TaskMoveTrickplayImagesDescription": "Перемещает существующие файлы trickplay в соответствии с настройками медиатеки.", "CleanupUserDataTask": "Задача очистки пользовательских данных", "CleanupUserDataTaskDescription": "Очищает все пользовательские данные (состояние просмотра, статус избранного и т.д.) с медиа, отсутствующих по меньшей мере в течение 90 дней.", - "Original": "Оригинальный" + "Original": "Оригинальный", + "LyricDownloadFailureFromForItem": "Не получилось скачать текст песни с {0} для {1}" } diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index af3fbbaded..7384967122 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -107,5 +107,6 @@ "TaskMoveTrickplayImagesDescription": "Flyttar befintliga trickplay-filer enligt bibliotekets inställningar.", "CleanupUserDataTaskDescription": "Tar bort all användardata (såsom vad du sett, favoriter med mera) för media som inte funnits på enheten på minst 90 dagar.", "CleanupUserDataTask": "Uppgift för rensning av användardata", - "Original": "Original" + "Original": "Original", + "LyricDownloadFailureFromForItem": "Misslyckades att ladda ner låttexter från {0} för {1}" } diff --git a/Emby.Server.Implementations/Localization/Core/uz.json b/Emby.Server.Implementations/Localization/Core/uz.json index 998d799a95..3215733c1a 100644 --- a/Emby.Server.Implementations/Localization/Core/uz.json +++ b/Emby.Server.Implementations/Localization/Core/uz.json @@ -82,5 +82,7 @@ "TaskRefreshChapterImages": "Sahnadan tasvirini chiqarish", "TaskRefreshChapterImagesDescription": "Sahnalarni o'z ichiga olgan videolar uchun eskizlarni yaratadi.", "TaskRefreshLibrary": "Media kutubxonangizni skanerlash", - "TaskCleanLogsDescription": "{0} kundan eski log fayllarni o'chiradi." + "TaskCleanLogsDescription": "{0} kundan eski log fayllarni o'chiradi.", + "Original": "Original", + "LyricDownloadFailureFromForItem": "{0} dan {1} gacha qo'shiq matninin yuklab olishda xatolik ketdi" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index 8c36139f29..18418ae0bc 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -106,5 +106,7 @@ "TaskExtractMediaSegmentsDescription": "从支持 MediaSegment 的插件中提取或获取媒体分段。", "TaskMoveTrickplayImagesDescription": "根据媒体库设置移动现有的进度条预览图文件。", "CleanupUserDataTask": "用户数据清理任务", - "CleanupUserDataTaskDescription": "清理已被删除超过90天的媒体中的所有用户数据(观看状态、收藏夹状态等)。" + "CleanupUserDataTaskDescription": "清理已被删除超过90天的媒体中的所有用户数据(观看状态、收藏夹状态等)。", + "LyricDownloadFailureFromForItem": "无法从 {0} 下载 {1} 的歌词", + "Original": "原始" } diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 0b0b300d30..843e35afcc 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -318,13 +318,13 @@ namespace Emby.Server.Implementations.Localization // A lot of countries don't explicitly have a separate rating for adult content if (ratings.All(x => x.RatingScore?.Score != 1000)) { - ratings.Add(new ParentalRating("XXX", new(1000, null))); + ratings.Add(new ParentalRating("XXX", new(1000, null))); } // A lot of countries don't explicitly have a separate rating for banned content if (ratings.All(x => x.RatingScore?.Score != 1001)) { - ratings.Add(new ParentalRating("Banned", new(1001, null))); + ratings.Add(new ParentalRating("Banned", new(1001, null))); } return [.. ratings.OrderBy(r => r.RatingScore?.Score).ThenBy(r => r.RatingScore?.SubScore)]; diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 91ccb16ef9..f699c99d85 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -564,7 +564,8 @@ namespace Emby.Server.Implementations.Plugins Id = instance.Id, Status = PluginStatus.Active, Name = instance.Name, - Version = instance.Version.ToString() + Version = instance.Version.ToString(), + ImageResourceName = (instance as IHasEmbeddedImage)?.ImageResourceName }) { Instance = instance diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs index 51920c5b14..5e92808f78 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Threading; diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 2885b89e3a..5148b62655 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -386,7 +386,7 @@ namespace Emby.Server.Implementations.Session { if (session is null) { - return; + return; } if (string.IsNullOrEmpty(info.MediaSourceId)) diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs index d140426ddf..11a94648f8 100644 --- a/Emby.Server.Implementations/SystemManager.cs +++ b/Emby.Server.Implementations/SystemManager.cs @@ -89,11 +89,11 @@ public class SystemManager : ISystemManager .GetVirtualFolders() .Where(e => !string.IsNullOrWhiteSpace(e.ItemId)) // this should not be null but for some users it is. .Select(e => new LibraryStorageInfo() - { - Id = Guid.Parse(e.ItemId), - Name = e.Name, - Folders = e.Locations.Select(f => StorageHelper.GetFreeSpaceOf(f)).ToArray() - }); + { + Id = Guid.Parse(e.ItemId), + Name = e.Name, + Folders = e.Locations.Select(f => StorageHelper.GetFreeSpaceOf(f)).ToArray() + }); return new SystemStorageInfo() { diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 590bd05da4..77bb6ee7e7 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -91,18 +91,18 @@ public class AudioController : BaseJellyfinApiController [ProducesAudioFile] public async Task GetAudioStream( [FromRoute, Required] Guid itemId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -112,7 +112,7 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -131,8 +131,8 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -255,18 +255,18 @@ public class AudioController : BaseJellyfinApiController [ProducesAudioFile] public async Task GetAudioStreamByContainer( [FromRoute, Required] Guid itemId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, + [FromRoute, Required][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -276,7 +276,7 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -295,8 +295,8 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index eadb8c9855..2bbfeb40b8 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -1,7 +1,11 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Linq; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Helpers; +using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Dtos; using Jellyfin.Data.Queries; using MediaBrowser.Common.Api; @@ -112,28 +116,31 @@ public class DevicesController : BaseJellyfinApiController } /// - /// Deletes a device. + /// Deletes devices. /// - /// Device Id. + /// Device Ids. /// Device deleted. - /// Device not found. - /// A on success, or a if the device could not be found. + /// A requested device is invalid. + /// A on success, or a if a requested device is invalid. [HttpDelete] [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task DeleteDevice([FromQuery, Required] string id) + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task DeleteDevice([FromQuery] string[] id) { - var existingDevice = _deviceManager.GetDevice(id); - if (existingDevice is null) + var devices = id.Select(_deviceManager.GetDevice).ToArray(); + if (devices.Any(f => f is null)) { - return NotFound(); + return BadRequest(); } - var sessions = _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }); - - foreach (var session in sessions.Items) + foreach (var device in devices) { - await _sessionManager.Logout(session).ConfigureAwait(false); + var sessions = _deviceManager.GetDevices(new DeviceQuery { DeviceId = device!.Id }); + + foreach (var session in sessions.Items) + { + await _sessionManager.Logout(session).ConfigureAwait(false); + } } return NoContent(); diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index c059f5880d..838f48949d 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -167,18 +167,18 @@ public class DynamicHlsController : BaseJellyfinApiController [ProducesPlaylistFile] public async Task GetLiveHlsStream( [FromRoute, Required] Guid itemId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -188,7 +188,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -207,8 +207,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -413,12 +413,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery, Required] string mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -428,7 +428,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -449,8 +449,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -586,12 +586,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery, Required] string mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -602,7 +602,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -621,8 +621,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -753,12 +753,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -768,7 +768,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -789,8 +789,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -922,12 +922,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -938,7 +938,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -957,8 +957,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -1092,7 +1092,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, + [FromRoute, Required][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, [FromQuery, Required] long runtimeTicks, [FromQuery, Required] long actualSegmentLengthTicks, [FromQuery] bool? @static, @@ -1100,12 +1100,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -1115,7 +1115,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -1136,8 +1136,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -1274,7 +1274,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, + [FromRoute, Required][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, [FromQuery, Required] long runtimeTicks, [FromQuery, Required] long actualSegmentLengthTicks, [FromQuery] bool? @static, @@ -1282,12 +1282,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -1298,7 +1298,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -1317,8 +1317,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 234282f7bd..679f6fafc2 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -15,6 +15,7 @@ using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -276,7 +277,7 @@ public class ItemsController : BaseJellyfinApiController && user.GetPreference(PreferenceKind.AllowedTags).Length != 0 && !fields.Contains(ItemFields.Tags)) { - fields = [..fields, ItemFields.Tags]; + fields = [.. fields, ItemFields.Tags]; } var dtoOptions = new DtoOptions { Fields = fields } @@ -285,10 +286,21 @@ public class ItemsController : BaseJellyfinApiController var item = _libraryManager.GetParentItem(parentId, userId); QueryResult result; + Guid[] linkedChildAncestorIds = []; if (includeItemTypes.Length == 1 - && includeItemTypes[0] == BaseItemKind.BoxSet - && item is not BoxSet) + && (includeItemTypes[0] == BaseItemKind.BoxSet || includeItemTypes[0] == BaseItemKind.Playlist) + && item is not BoxSet + && item is not Playlist) { + var itemCollectionType = item is IHasCollectionType hct ? hct.CollectionType : null; + var targetCollectionType = includeItemTypes[0] == BaseItemKind.BoxSet + ? CollectionType.boxsets + : CollectionType.playlists; + if (parentId.HasValue && item is not UserRootFolder && itemCollectionType != targetCollectionType) + { + linkedChildAncestorIds = [parentId.Value]; + } + parentId = null; item = _libraryManager.GetUserRootFolder(); } @@ -438,6 +450,7 @@ public class ItemsController : BaseJellyfinApiController MaxPremiereDate = maxPremiereDate?.ToUniversalTime(), AudioLanguages = audioLanguages, SubtitleLanguages = subtitleLanguages, + LinkedChildAncestorIds = linkedChildAncestorIds, }; if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm)) diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 074cdb24e0..113298c251 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -744,10 +744,12 @@ public class LiveTvController : BaseJellyfinApiController /// Program id. /// Optional. Attach user data. /// Program returned. + /// Program not found. /// An containing the livetv program. [HttpGet("Programs/{programId}")] [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetProgram( [FromRoute, Required] string programId, [FromQuery] Guid? userId) @@ -756,8 +758,14 @@ public class LiveTvController : BaseJellyfinApiController var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); + var result = await _liveTvManager.GetProgram(programId, CancellationToken.None, user).ConfigureAwait(false); - return await _liveTvManager.GetProgram(programId, CancellationToken.None, user).ConfigureAwait(false); + if (result is null) + { + return NotFound(); + } + + return Ok(result); } /// diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 79e6536fb6..0105ecf7a7 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -226,16 +226,32 @@ public class PluginsController : BaseJellyfinApiController return NotFound(); } - var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty); - if (plugin.Manifest.ImagePath is null || !System.IO.File.Exists(imagePath)) + if (!string.IsNullOrEmpty(plugin.Manifest.ImagePath)) { - return NotFound(); + var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath); + if (!System.IO.File.Exists(imagePath)) + { + return NotFound(); + } + + Response.Headers.ContentDisposition = "attachment"; + return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath)); } - Response.Headers.ContentDisposition = "attachment"; + var resourceName = plugin.Manifest.ImageResourceName; + if (!string.IsNullOrEmpty(resourceName) && plugin.Instance is not null) + { + var stream = plugin.Instance.GetType().Assembly.GetManifestResourceStream(resourceName); + if (stream is null) + { + return NotFound(); + } - imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath); - return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath)); + Response.Headers.ContentDisposition = "attachment"; + return File(stream, MimeTypes.GetMimeType(resourceName)); + } + + return NotFound(); } /// diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index d4e9b234c5..2f5ed327c0 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -102,13 +102,13 @@ public class UniversalAudioController : BaseJellyfinApiController [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, [FromQuery] Guid? userId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] int? maxAudioChannels, [FromQuery] int? transcodingAudioChannels, [FromQuery] int? maxStreamingBitrate, [FromQuery] int? audioBitRate, [FromQuery] long? startTimeTicks, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? transcodingContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? transcodingContainer, [FromQuery] MediaStreamProtocol? transcodingProtocol, [FromQuery] int? maxAudioSampleRate, [FromQuery] int? maxAudioBitDepth, diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index c1d06bad36..8b359c48af 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -88,7 +88,7 @@ public class UserViewsController : BaseJellyfinApiController var folders = _userViewManager.GetUserViews(query); var dtoOptions = new DtoOptions(); - dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.PrimaryImageAspectRatio, ItemFields.DisplayPreferencesId]; + dtoOptions.Fields = [.. dtoOptions.Fields, ItemFields.PrimaryImageAspectRatio, ItemFields.DisplayPreferencesId]; var dtos = Array.ConvertAll(folders, i => _dtoService.GetBaseItemDto(i, dtoOptions, user)); diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 2c2cbf1ec6..ed6d3f5bde 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -317,18 +317,18 @@ public class VideosController : BaseJellyfinApiController [ProducesVideoFile] public async Task GetVideoStream( [FromRoute, Required] Guid itemId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -338,7 +338,7 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -359,8 +359,8 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -555,18 +555,18 @@ public class VideosController : BaseJellyfinApiController [ProducesVideoFile] public Task GetVideoStreamByContainer( [FromRoute, Required] Guid itemId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, + [FromRoute, Required][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -576,7 +576,7 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -597,8 +597,8 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index 4034a80887..d123dbc82e 100644 --- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs +++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs @@ -62,12 +62,12 @@ public static class FileStreamResponseHelpers if (response.Headers.TryGetValues(HeaderNames.AcceptRanges, out var acceptRangesHeaders)) { // Prefer upstream server's Accept-Ranges header if available - acceptRangesValue = string.Join(", ", acceptRangesHeaders); - upstreamSupportsRange |= acceptRangesValue.Contains("bytes", StringComparison.OrdinalIgnoreCase); + acceptRangesValue = string.Join(", ", acceptRangesHeaders); + upstreamSupportsRange |= acceptRangesValue.Contains("bytes", StringComparison.OrdinalIgnoreCase); } else if (upstreamSupportsRange) // If we got 206 but no Accept-Ranges header, assume bytes { - acceptRangesValue = "bytes"; + acceptRangesValue = "bytes"; } // Set Accept-Ranges header for the client based on upstream support @@ -76,13 +76,13 @@ public static class FileStreamResponseHelpers // Set Content-Range header if upstream provided it (implies partial content) if (response.Content.Headers.ContentRange is not null) { - httpContext.Response.Headers[HeaderNames.ContentRange] = response.Content.Headers.ContentRange.ToString(); + httpContext.Response.Headers[HeaderNames.ContentRange] = response.Content.Headers.ContentRange.ToString(); } // Set Content-Length header. For partial content, this is the length of the partial segment. if (response.Content.Headers.ContentLength.HasValue) { - httpContext.Response.ContentLength = response.Content.Headers.ContentLength.Value; + httpContext.Response.ContentLength = response.Content.Headers.ContentLength.Value; } // Set Content-Type header diff --git a/Jellyfin.Api/Models/SystemInfoDtos/LibraryStorageDto.cs b/Jellyfin.Api/Models/SystemInfoDtos/LibraryStorageDto.cs index c138324d2e..6e4ba91133 100644 --- a/Jellyfin.Api/Models/SystemInfoDtos/LibraryStorageDto.cs +++ b/Jellyfin.Api/Models/SystemInfoDtos/LibraryStorageDto.cs @@ -10,7 +10,7 @@ namespace Jellyfin.Api.Models.SystemInfoDtos; /// public record LibraryStorageDto { - /// + /// /// Gets or sets the Library Id. /// public required Guid Id { get; set; } diff --git a/Jellyfin.Data/Enums/ActivityLogSortBy.cs b/Jellyfin.Data/Enums/ActivityLogSortBy.cs index d6d44e8c07..a24185e365 100644 --- a/Jellyfin.Data/Enums/ActivityLogSortBy.cs +++ b/Jellyfin.Data/Enums/ActivityLogSortBy.cs @@ -1,4 +1,4 @@ -namespace Jellyfin.Data.Enums; +namespace Jellyfin.Data.Enums; /// /// Activity log sorting options. diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index fe987b9d86..ba24dc3864 100644 --- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -58,9 +58,9 @@ public class ActivityManager : IActivityManager { // TODO switch to LeftJoin in .NET 10. var entries = from a in dbContext.ActivityLogs - join u in dbContext.Users on a.UserId equals u.Id into ugj - from u in ugj.DefaultIfEmpty() - select new ExpandedActivityLog { ActivityLog = a, Username = u.Username }; + join u in dbContext.Users on a.UserId equals u.Id into ugj + from u in ugj.DefaultIfEmpty() + select new ExpandedActivityLog { ActivityLog = a, Username = u.Username }; if (query.HasUserId is not null) { diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs index 09d68e4451..a88904c727 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs @@ -75,9 +75,9 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session eventArgs.DeviceName), notificationType, user.Id) - { - ItemId = eventArgs.Item?.Id.ToString("N", CultureInfo.InvariantCulture), - }) + { + ItemId = eventArgs.Item?.Id.ToString("N", CultureInfo.InvariantCulture), + }) .ConfigureAwait(false); } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs b/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs index 736388e9eb..c64e6ac068 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs @@ -26,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Item; /// /// Handles mapping between BaseItemEntity (database) and BaseItemDto (domain) objects. /// -internal static class BaseItemMapper +public static class BaseItemMapper { /// /// This holds all the types in the running assemblies diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs index bee1bf3f9d..d905775aef 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs @@ -1020,6 +1020,15 @@ public sealed partial class BaseItemRepository baseQuery = baseQuery.Where(e => e.Parents!.AsQueryable().Any(ancestorFilter)); } + if (filter.LinkedChildAncestorIds.Length > 0) + { + // Keep folder-like items (BoxSets, Playlists) whose linked children descend from any of the requested ancestor ids. + var linkedChildAncestorIds = filter.LinkedChildAncestorIds; + baseQuery = baseQuery.Where(e => context.LinkedChildren.Any(lc => + lc.ParentId == e.Id + && lc.Child!.Parents!.Any(a => linkedChildAncestorIds.Contains(a.ParentItemId)))); + } + if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey)) { baseQuery = baseQuery diff --git a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs index 98700f3224..f7d76517e1 100644 --- a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs +++ b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs @@ -55,6 +55,7 @@ public class ChapterRepository : IChapterRepository { using var context = _dbProvider.CreateDbContext(); return context.Chapters.AsNoTracking().Where(e => e.ItemId.Equals(baseItemId)) + .OrderBy(e => e.StartPositionTicks) .Select(e => new { chapter = e, @@ -69,18 +70,16 @@ public class ChapterRepository : IChapterRepository public void SaveChapters(Guid itemId, IReadOnlyList chapters) { using var context = _dbProvider.CreateDbContext(); - using (var transaction = context.Database.BeginTransaction()) + using var transaction = context.Database.BeginTransaction(); + context.Chapters.Where(e => e.ItemId.Equals(itemId)).ExecuteDelete(); + for (var i = 0; i < chapters.Count; i++) { - context.Chapters.Where(e => e.ItemId.Equals(itemId)).ExecuteDelete(); - for (var i = 0; i < chapters.Count; i++) - { - var chapter = chapters[i]; - context.Chapters.Add(Map(chapter, i, itemId)); - } - - context.SaveChanges(); - transaction.Commit(); + var chapter = chapters[i]; + context.Chapters.Add(Map(chapter, i, itemId)); } + + context.SaveChanges(); + transaction.Commit(); } /// diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index 8f8741d00f..b612112d49 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -110,10 +110,10 @@ public class PeopleRepository(IDbContextFactory dbProvider, I using var context = _dbProvider.CreateDbContext(); using var transaction = context.Database.BeginTransaction(); var existingPersons = context.Peoples.Select(e => new - { - item = e, - SelectionKey = e.Name.ToLower() + "-" + e.PersonType - }) + { + item = e, + SelectionKey = e.Name.ToLower() + "-" + e.PersonType + }) .Where(p => personKeys.Contains(p.SelectionKey)) .Select(f => f.item) .ToArray(); diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs index 249df476a0..be98f93dab 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs @@ -81,6 +81,8 @@ public class MediaSegmentManager : IMediaSegmentManager foreach (var provider in providers) { + cancellationToken.ThrowIfCancellationRequested(); + if (!await provider.Supports(baseItem).ConfigureAwait(false)) { _logger.LogDebug("Media Segment provider {ProviderName} does not support item with path {MediaPath}", provider.Name, baseItem.Path); @@ -146,6 +148,15 @@ public class MediaSegmentManager : IMediaSegmentManager await CreateSegmentAsync(segment, providerId).ConfigureAwait(false); } } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception ex) when (cancellationToken.IsCancellationRequested) + { + _logger.LogDebug(ex, "Provider {ProviderName} aborted segment extraction for {MediaPath} due to shutdown", provider.Name, baseItem.Path); + break; + } catch (Exception ex) { _logger.LogError(ex, "Provider {ProviderName} failed to extract segments from {MediaPath}", provider.Name, baseItem.Path); diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index e3fe517c49..8657cb7dbb 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -302,7 +302,7 @@ namespace Jellyfin.Server.Implementations.Security } else if (!escaped && token == '=') { - key = authorizationHeader[start.. i].Trim().ToString(); + key = authorizationHeader[start..i].Trim().ToString(); start = i + 1; } } diff --git a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs index fdc49a9840..c9fd031ef9 100644 --- a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs +++ b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs @@ -68,7 +68,7 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider try { - openApiDocument = _swaggerGenerator.GetSwagger(documentName); + openApiDocument = _swaggerGenerator.GetSwagger(documentName); } catch (Exception ex) { diff --git a/Jellyfin.Server/GlobalSuppressions.cs b/Jellyfin.Server/GlobalSuppressions.cs new file mode 100644 index 0000000000..676747e29f --- /dev/null +++ b/Jellyfin.Server/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Migration files should follow the EFCore standard in regards to naming.", Scope = "namespaceanddescendants", Target = "~N:Jellyfin.Server.Migrations.Routines")] diff --git a/Jellyfin.Server/Migrations/MigrationOptions.cs b/Jellyfin.Server/Migrations/MigrationOptions.cs index c9710f1fd1..cd1b74a613 100644 --- a/Jellyfin.Server/Migrations/MigrationOptions.cs +++ b/Jellyfin.Server/Migrations/MigrationOptions.cs @@ -16,7 +16,7 @@ namespace Jellyfin.Server.Migrations Applied = new List<(Guid Id, string Name)>(); } -// .Net xml serializer can't handle interfaces + // .Net xml serializer can't handle interfaces #pragma warning disable CA1002 // Do not expose generic lists /// /// Gets the list of applied migration routine names. diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/20250420050000_DisableTranscodingThrottling.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs rename to Jellyfin.Server/Migrations/Routines/20250420050000_DisableTranscodingThrottling.cs diff --git a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs b/Jellyfin.Server/Migrations/Routines/20250420060000_CreateUserLoggingConfigFile.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs rename to Jellyfin.Server/Migrations/Routines/20250420060000_CreateUserLoggingConfigFile.cs diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/20250420070000_MigrateActivityLogDb.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs rename to Jellyfin.Server/Migrations/Routines/20250420070000_MigrateActivityLogDb.cs diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs b/Jellyfin.Server/Migrations/Routines/20250420080000_RemoveDuplicateExtras.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs rename to Jellyfin.Server/Migrations/Routines/20250420080000_RemoveDuplicateExtras.cs diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/20250420090000_AddDefaultPluginRepository.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs rename to Jellyfin.Server/Migrations/Routines/20250420090000_AddDefaultPluginRepository.cs diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/20250420100000_MigrateUserDb.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs rename to Jellyfin.Server/Migrations/Routines/20250420100000_MigrateUserDb.cs diff --git a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/20250420110000_ReaddDefaultPluginRepository.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs rename to Jellyfin.Server/Migrations/Routines/20250420110000_ReaddDefaultPluginRepository.cs diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/20250420120000_MigrateDisplayPreferencesDb.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs rename to Jellyfin.Server/Migrations/Routines/20250420120000_MigrateDisplayPreferencesDb.cs diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs b/Jellyfin.Server/Migrations/Routines/20250420130000_RemoveDownloadImagesInAdvance.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs rename to Jellyfin.Server/Migrations/Routines/20250420130000_RemoveDownloadImagesInAdvance.cs diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/20250420140000_MigrateAuthenticationDb.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs rename to Jellyfin.Server/Migrations/Routines/20250420140000_MigrateAuthenticationDb.cs diff --git a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs b/Jellyfin.Server/Migrations/Routines/20250420150000_FixPlaylistOwner.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs rename to Jellyfin.Server/Migrations/Routines/20250420150000_FixPlaylistOwner.cs diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs b/Jellyfin.Server/Migrations/Routines/20250420160000_AddDefaultCastReceivers.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs rename to Jellyfin.Server/Migrations/Routines/20250420160000_AddDefaultCastReceivers.cs diff --git a/Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/20250420170000_UpdateDefaultPluginRepository.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs rename to Jellyfin.Server/Migrations/Routines/20250420170000_UpdateDefaultPluginRepository.cs diff --git a/Jellyfin.Server/Migrations/Routines/FixAudioData.cs b/Jellyfin.Server/Migrations/Routines/20250420180000_FixAudioData.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/FixAudioData.cs rename to Jellyfin.Server/Migrations/Routines/20250420180000_FixAudioData.cs diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs b/Jellyfin.Server/Migrations/Routines/20250420190000_RemoveDuplicatePlaylistChildren.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs rename to Jellyfin.Server/Migrations/Routines/20250420190000_RemoveDuplicatePlaylistChildren.cs diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDbCompatibilityCheck.cs b/Jellyfin.Server/Migrations/Routines/20250420193000_MigrateLibraryDbCompatibilityCheck.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MigrateLibraryDbCompatibilityCheck.cs rename to Jellyfin.Server/Migrations/Routines/20250420193000_MigrateLibraryDbCompatibilityCheck.cs diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/20250420200000_MigrateLibraryDb.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs rename to Jellyfin.Server/Migrations/Routines/20250420200000_MigrateLibraryDb.cs diff --git a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs b/Jellyfin.Server/Migrations/Routines/20250420210000_MoveExtractedFiles.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs rename to Jellyfin.Server/Migrations/Routines/20250420210000_MoveExtractedFiles.cs diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/20250420230000_MoveTrickplayFiles.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs rename to Jellyfin.Server/Migrations/Routines/20250420230000_MoveTrickplayFiles.cs diff --git a/Jellyfin.Server/Migrations/Routines/RefreshInternalDateModified.cs b/Jellyfin.Server/Migrations/Routines/20250420230000_RefreshInternalDateModified.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/RefreshInternalDateModified.cs rename to Jellyfin.Server/Migrations/Routines/20250420230000_RefreshInternalDateModified.cs diff --git a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs b/Jellyfin.Server/Migrations/Routines/20250421000000_MigrateKeyframeData.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs rename to Jellyfin.Server/Migrations/Routines/20250421000000_MigrateKeyframeData.cs diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs b/Jellyfin.Server/Migrations/Routines/20250618010000_MigrateLibraryUserData.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs rename to Jellyfin.Server/Migrations/Routines/20250618010000_MigrateLibraryUserData.cs diff --git a/Jellyfin.Server/Migrations/Routines/FixDates.cs b/Jellyfin.Server/Migrations/Routines/20250620180000_FixDates.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/FixDates.cs rename to Jellyfin.Server/Migrations/Routines/20250620180000_FixDates.cs diff --git a/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs b/Jellyfin.Server/Migrations/Routines/20250730215000_ReseedFolderFlag.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs rename to Jellyfin.Server/Migrations/Routines/20250730215000_ReseedFolderFlag.cs diff --git a/Jellyfin.Server/Migrations/Routines/RefreshCleanNames.cs b/Jellyfin.Server/Migrations/Routines/20251008120000_RefreshCleanNames.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/RefreshCleanNames.cs rename to Jellyfin.Server/Migrations/Routines/20251008120000_RefreshCleanNames.cs diff --git a/Jellyfin.Server/Migrations/Routines/CleanMusicArtist.cs b/Jellyfin.Server/Migrations/Routines/20251009200000_CleanMusicArtist.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/CleanMusicArtist.cs rename to Jellyfin.Server/Migrations/Routines/20251009200000_CleanMusicArtist.cs diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs b/Jellyfin.Server/Migrations/Routines/20260113120000_MigrateLinkedChildren.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs rename to Jellyfin.Server/Migrations/Routines/20260113120000_MigrateLinkedChildren.cs diff --git a/Jellyfin.Server/Migrations/Routines/CleanupOrphanedExtras.cs b/Jellyfin.Server/Migrations/Routines/20260113230000_CleanupOrphanedExtras.cs similarity index 60% rename from Jellyfin.Server/Migrations/Routines/CleanupOrphanedExtras.cs rename to Jellyfin.Server/Migrations/Routines/20260113230000_CleanupOrphanedExtras.cs index 14abaa7317..f4dfa49068 100644 --- a/Jellyfin.Server/Migrations/Routines/CleanupOrphanedExtras.cs +++ b/Jellyfin.Server/Migrations/Routines/20260113230000_CleanupOrphanedExtras.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Database.Implementations; +using Jellyfin.Server.Implementations.Item; using Jellyfin.Server.Migrations.Stages; using Jellyfin.Server.ServerSetupApp; using MediaBrowser.Controller.Channels; @@ -23,7 +24,7 @@ namespace Jellyfin.Server.Migrations.Routines; /// Removes orphaned extras (items with OwnerId pointing to non-existent items). /// Must run before EF migrations that add FK constraints on OwnerId. /// -[JellyfinMigration("2026-01-13T23:00:00", nameof(CleanupOrphanedExtras), Stage = JellyfinMigrationStageTypes.CoreInitialisation)] +[JellyfinMigration("2026-01-13T23:00:00", nameof(CleanupOrphanedExtras), Stage = JellyfinMigrationStageTypes.AppInitialisation)] [JellyfinMigrationBackup(JellyfinDb = true)] public class CleanupOrphanedExtras : IAsyncMigrationRoutine { @@ -37,39 +38,14 @@ public class CleanupOrphanedExtras : IAsyncMigrationRoutine /// The startup logger. /// The database context factory. /// The library manager. - /// The item repository. - /// The item count service. - /// The channel manager. - /// The recordings manager. - /// The media source manager. - /// The media segments manager. - /// The configuration manager. - /// The file system. public CleanupOrphanedExtras( IStartupLogger logger, IDbContextFactory dbContextFactory, - ILibraryManager libraryManager, - IItemRepository itemRepository, - IItemCountService itemCountService, - IChannelManager channelManager, - IRecordingsManager recordingsManager, - IMediaSourceManager mediaSourceManager, - IMediaSegmentManager mediaSegmentManager, - IServerConfigurationManager configurationManager, - IFileSystem fileSystem) + ILibraryManager libraryManager) { _logger = logger; _dbContextFactory = dbContextFactory; _libraryManager = libraryManager; - BaseItem.LibraryManager ??= libraryManager; - BaseItem.ItemRepository ??= itemRepository; - BaseItem.ItemCountService ??= itemCountService; - BaseItem.ChannelManager ??= channelManager; - BaseItem.MediaSourceManager ??= mediaSourceManager; - BaseItem.MediaSegmentManager ??= mediaSegmentManager; - BaseItem.ConfigurationManager ??= configurationManager; - BaseItem.FileSystem ??= fileSystem; - Video.RecordingsManager ??= recordingsManager; } /// @@ -78,12 +54,19 @@ public class CleanupOrphanedExtras : IAsyncMigrationRoutine var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using (context.ConfigureAwait(false)) { + var placeholderOwner = Guid.Parse("00000000-0000-0000-0000-000000000001"); +#pragma warning disable RS0030 // Do not use banned APIs var orphanedItemIds = await context.BaseItems - .Where(b => b.OwnerId.HasValue && !b.OwnerId.Value.Equals(Guid.Empty)) - .Where(b => !context.BaseItems.Any(parent => parent.Id.Equals(b.OwnerId!.Value))) - .Select(b => b.Id) + .Where(b => b.OwnerId.HasValue && b.OwnerId == placeholderOwner) + .Select(b => new + { + b.Id, + b.Path, + b.Type + }) .ToListAsync(cancellationToken) .ConfigureAwait(false); +#pragma warning restore RS0030 // Do not use banned APIs if (orphanedItemIds.Count == 0) { @@ -97,11 +80,16 @@ public class CleanupOrphanedExtras : IAsyncMigrationRoutine var itemsToDelete = new List(); foreach (var itemId in orphanedItemIds) { - var item = _libraryManager.GetItemById(itemId); - if (item is not null) - { - itemsToDelete.Add(item); - } + itemsToDelete.Add(BaseItemMapper.DeserializeBaseItem( + new Database.Implementations.Entities.BaseItemEntity() + { + Id = itemId.Id, + Path = itemId.Path, + Type = itemId.Type + }, + _logger, + null, + true)!); } _libraryManager.DeleteItemsUnsafeFast(itemsToDelete); diff --git a/Jellyfin.Server/Migrations/Routines/FixIncorrectOwnerIdRelationships.cs b/Jellyfin.Server/Migrations/Routines/20260115120000_FixIncorrectOwnerIdRelationships.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/FixIncorrectOwnerIdRelationships.cs rename to Jellyfin.Server/Migrations/Routines/20260115120000_FixIncorrectOwnerIdRelationships.cs diff --git a/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs b/Jellyfin.Server/Migrations/Routines/20260206200000_FixLibrarySubtitleDownloadLanguages.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs rename to Jellyfin.Server/Migrations/Routines/20260206200000_FixLibrarySubtitleDownloadLanguages.cs diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/20260302090000_MigrateRatingLevels.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs rename to Jellyfin.Server/Migrations/Routines/20260302090000_MigrateRatingLevels.cs diff --git a/Jellyfin.Server/Migrations/Routines/MergeDuplicateMusicArtists.cs b/Jellyfin.Server/Migrations/Routines/20260508120000_MergeDuplicateMusicArtists.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MergeDuplicateMusicArtists.cs rename to Jellyfin.Server/Migrations/Routines/20260508120000_MergeDuplicateMusicArtists.cs diff --git a/Jellyfin.Server/Migrations/Routines/MergeDuplicatePeople.cs b/Jellyfin.Server/Migrations/Routines/20260508130000_MergeDuplicatePeople.cs similarity index 96% rename from Jellyfin.Server/Migrations/Routines/MergeDuplicatePeople.cs rename to Jellyfin.Server/Migrations/Routines/20260508130000_MergeDuplicatePeople.cs index d092555139..10433599fa 100644 --- a/Jellyfin.Server/Migrations/Routines/MergeDuplicatePeople.cs +++ b/Jellyfin.Server/Migrations/Routines/20260508130000_MergeDuplicatePeople.cs @@ -284,10 +284,16 @@ public class MergeDuplicatePeople : IAsyncMigrationRoutine return; } - await context.Peoples - .Where(p => idsToDelete.Contains(p.Id)) - .ExecuteDeleteAsync(cancellationToken) - .ConfigureAwait(false); + var idx = 0; + foreach (var item in idsToDelete.Chunk(200)) + { + idx++; // humans count at one + _logger.LogInformation("Remove batch {BatchNo}/{MaxBatches} duplicate Peoples.", idx, idsToDelete.Count / 200); + await context.Peoples + .Where(p => item.Contains(p.Id)) + .ExecuteDeleteAsync(cancellationToken) + .ConfigureAwait(false); + } _logger.LogInformation("Removed {Count} duplicate Peoples rows.", idsToDelete.Count); } diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 05975929db..37bb1abe71 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -90,6 +90,13 @@ public sealed class SetupServer : IDisposable var fileTemplate = await File.ReadAllTextAsync(Path.Combine(AppContext.BaseDirectory, "ServerSetupApp", "index.mstemplate.html")).ConfigureAwait(false); _startupUiRenderer = (await ParserOptionsBuilder.New() .WithTemplate(fileTemplate) + .WithFormatter( + (Version version, int arg) => + { + // version type does not for some stupid reason implement IFormattable which morestachio relies on for ToString support therefor we need to do it manually. + return version.ToString(arg); + }, + "ToString") .WithFormatter( (StartupLogTopic logEntry, IEnumerable children) => { @@ -237,6 +244,7 @@ public sealed class SetupServer : IDisposable }); }); + var version = typeof(Emby.Server.Implementations.ApplicationHost).Assembly.GetName().Version!; app.Run(async (context) => { context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; @@ -250,7 +258,7 @@ public sealed class SetupServer : IDisposable { { "isInReportingMode", _isUnhealthy }, { "retryValue", retryAfterValue }, - { "version", typeof(Emby.Server.Implementations.ApplicationHost).Assembly.GetName().Version! }, + { "version", version }, { "logs", startupLogEntries }, { "networkManagerReady", networkManager is not null }, { "localNetworkRequest", networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress) } diff --git a/Jellyfin.Server/ServerSetupApp/index.mstemplate.html b/Jellyfin.Server/ServerSetupApp/index.mstemplate.html index 890a77619d..5706ce1fac 100644 --- a/Jellyfin.Server/ServerSetupApp/index.mstemplate.html +++ b/Jellyfin.Server/ServerSetupApp/index.mstemplate.html @@ -173,9 +173,9 @@
{{^IF isInReportingMode}} -

Jellyfin Server {{version}} still starting. Please wait.

+

Jellyfin Server {{version.ToString(2)}} still starting. Please wait.

{{#ELSE}} -

Jellyfin Server has encountered an error and was not able to start.

+

Jellyfin Server {{version.ToString(2)}} has encountered an error and was not able to start.

{{/ELSE}} {{/IF}} diff --git a/MediaBrowser.Common/Net/NetworkUtils.cs b/MediaBrowser.Common/Net/NetworkUtils.cs index 71539b8b78..25a1022d4e 100644 --- a/MediaBrowser.Common/Net/NetworkUtils.cs +++ b/MediaBrowser.Common/Net/NetworkUtils.cs @@ -180,9 +180,16 @@ public static partial class NetworkUtils List? tmpResult = null; for (int a = 0; a < values.Length; a++) { + // Skip entries whose '!' polarity doesn't match this pass + var trimmed = values[a].AsSpan().Trim(); + if (trimmed.StartsWith('!') != negated) + { + continue; + } + if (TryParseToSubnet(values[a], out var innerResult, negated)) { - (tmpResult ??= new()).Add(innerResult); + (tmpResult ??= []).Add(innerResult); } else { diff --git a/MediaBrowser.Common/Plugins/LocalPlugin.cs b/MediaBrowser.Common/Plugins/LocalPlugin.cs index 96af423cc3..4723be1001 100644 --- a/MediaBrowser.Common/Plugins/LocalPlugin.cs +++ b/MediaBrowser.Common/Plugins/LocalPlugin.cs @@ -109,7 +109,7 @@ namespace MediaBrowser.Common.Plugins { var inst = Instance?.GetPluginInfo() ?? new PluginInfo(Manifest.Name, Version, Manifest.Description, Manifest.Id, true); inst.Status = Manifest.Status; - inst.HasImage = !string.IsNullOrEmpty(Manifest.ImagePath); + inst.HasImage = !string.IsNullOrEmpty(Manifest.ImagePath) || !string.IsNullOrEmpty(Manifest.ImageResourceName); return inst; } diff --git a/MediaBrowser.Common/Plugins/PluginManifest.cs b/MediaBrowser.Common/Plugins/PluginManifest.cs index e0847ccea4..e749e85899 100644 --- a/MediaBrowser.Common/Plugins/PluginManifest.cs +++ b/MediaBrowser.Common/Plugins/PluginManifest.cs @@ -107,6 +107,15 @@ namespace MediaBrowser.Common.Plugins [JsonPropertyName("imagePath")] public string? ImagePath { get; set; } + /// + /// Gets or sets the name of an embedded resource in the plugin's assembly + /// that should be served as the plugin image. + /// Used by bundled/integrated plugins whose images are shipped inside the assembly + /// rather than on disk. Ignored when is set. + /// + [JsonIgnore] + public string? ImageResourceName { get; set; } + /// /// Gets or sets the collection of assemblies that should be loaded. /// Paths are considered relative to the plugin folder. diff --git a/MediaBrowser.Controller/Entities/Extensions.cs b/MediaBrowser.Controller/Entities/Extensions.cs index c56603a3eb..380041af84 100644 --- a/MediaBrowser.Controller/Entities/Extensions.cs +++ b/MediaBrowser.Controller/Entities/Extensions.cs @@ -34,7 +34,7 @@ namespace MediaBrowser.Controller.Entities } else { - item.RemoteTrailers = [..item.RemoteTrailers, mediaUrl]; + item.RemoteTrailers = [.. item.RemoteTrailers, mediaUrl]; } } } diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 1e5b5aa164..422c40ce5d 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -21,6 +21,7 @@ namespace MediaBrowser.Controller.Entities AlbumArtistIds = []; AlbumIds = []; AncestorIds = []; + LinkedChildAncestorIds = []; ArtistIds = []; BlockUnratedItems = []; BoxSetLibraryFolders = []; @@ -265,6 +266,12 @@ namespace MediaBrowser.Controller.Entities public Guid[] AncestorIds { get; set; } + /// + /// Gets or sets a list of ancestor ids that the item's linked children must descend from. + /// Useful for filtering BoxSets/Playlists to only those that contain items from a specific library. + /// + public Guid[] LinkedChildAncestorIds { get; set; } + public Guid[] TopParentIds { get; set; } public CollectionType?[] PresetViews { get; set; } diff --git a/MediaBrowser.Controller/Entities/TagExtensions.cs b/MediaBrowser.Controller/Entities/TagExtensions.cs index c1e4d1db2f..4ddba9835b 100644 --- a/MediaBrowser.Controller/Entities/TagExtensions.cs +++ b/MediaBrowser.Controller/Entities/TagExtensions.cs @@ -25,7 +25,7 @@ namespace MediaBrowser.Controller.Entities } else { - item.Tags = [..current, name]; + item.Tags = [.. current, name]; } } } diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs index b558ef73d5..c5e7ae4913 100644 --- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs +++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs @@ -117,7 +117,7 @@ namespace MediaBrowser.Controller.Library get { var paths = string.IsNullOrEmpty(Path) ? Array.Empty() : [Path]; - return AdditionalLocations is null ? paths : [..paths, ..AdditionalLocations]; + return AdditionalLocations is null ? paths : [.. paths, .. AdditionalLocations]; } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 65f6b79656..8f6e36bce4 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1267,16 +1267,13 @@ namespace MediaBrowser.Controller.MediaEncoding .Append(_mediaEncoder.GetInputPathArgument(state)); } - // sub2video for external graphical subtitles - if (state.SubtitleStream is not null - && ShouldEncodeSubtitle(state) - && !state.SubtitleStream.IsTextSubtitleStream - && state.SubtitleStream.IsExternal) + if (NeedsExternalSubtitleMuxing(state)) { var subtitlePath = state.SubtitleStream.Path; - var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan()); + var isGraphicalBurnIn = ShouldEncodeSubtitle(state) && !state.SubtitleStream.IsTextSubtitleStream; // dvdsub/vobsub graphical subtitles use .sub+.idx pairs + var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan()); if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase)) { var idxFile = Path.ChangeExtension(subtitlePath, ".idx"); @@ -1307,7 +1304,7 @@ namespace MediaBrowser.Controller.MediaEncoding arg.Append(' ').Append(seekSubParam); } - if (!string.IsNullOrEmpty(canvasArgs)) + if (isGraphicalBurnIn && !string.IsNullOrEmpty(canvasArgs)) { arg.Append(canvasArgs); } @@ -1766,13 +1763,13 @@ namespace MediaBrowser.Controller.MediaEncoding { param += encoderPreset switch { - EncoderPreset.veryslow => " -preset p7", - EncoderPreset.slower => " -preset p6", - EncoderPreset.slow => " -preset p5", - EncoderPreset.medium => " -preset p4", - EncoderPreset.fast => " -preset p3", - EncoderPreset.faster => " -preset p2", - _ => " -preset p1" + EncoderPreset.veryslow => " -preset p7", + EncoderPreset.slower => " -preset p6", + EncoderPreset.slow => " -preset p5", + EncoderPreset.medium => " -preset p4", + EncoderPreset.fast => " -preset p3", + EncoderPreset.faster => " -preset p2", + _ => " -preset p1" }; } else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf) @@ -1782,11 +1779,11 @@ namespace MediaBrowser.Controller.MediaEncoding { param += encoderPreset switch { - EncoderPreset.veryslow => " -quality quality", - EncoderPreset.slower => " -quality quality", - EncoderPreset.slow => " -quality quality", - EncoderPreset.medium => " -quality balanced", - _ => " -quality speed" + EncoderPreset.veryslow => " -quality quality", + EncoderPreset.slower => " -quality quality", + EncoderPreset.slow => " -quality quality", + EncoderPreset.medium => " -quality balanced", + _ => " -quality speed" }; if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) @@ -1806,11 +1803,11 @@ namespace MediaBrowser.Controller.MediaEncoding { param += encoderPreset switch { - EncoderPreset.veryslow => " -prio_speed 0", - EncoderPreset.slower => " -prio_speed 0", - EncoderPreset.slow => " -prio_speed 0", - EncoderPreset.medium => " -prio_speed 0", - _ => " -prio_speed 1" + EncoderPreset.veryslow => " -prio_speed 0", + EncoderPreset.slower => " -prio_speed 0", + EncoderPreset.slow => " -prio_speed 0", + EncoderPreset.medium => " -prio_speed 0", + _ => " -prio_speed 1" }; } @@ -2762,25 +2759,29 @@ namespace MediaBrowser.Controller.MediaEncoding || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase) || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase)) { +#pragma warning disable SA1008 return (inputChannels, outputChannels) switch { - (>= 6, >= 6 or 0) => Math.Min(640000, bitrate), - (> 0, > 0) => Math.Min(outputChannels * 128000, bitrate), - (> 0, _) => Math.Min(inputChannels * 128000, bitrate), + ( >= 6, >= 6 or 0) => Math.Min(640000, bitrate), + ( > 0, > 0) => Math.Min(outputChannels * 128000, bitrate), + ( > 0, _) => Math.Min(inputChannels * 128000, bitrate), (_, _) => Math.Min(384000, bitrate) }; +#pragma warning restore SA1008 } if (string.Equals(audioCodec, "dts", StringComparison.OrdinalIgnoreCase) || string.Equals(audioCodec, "dca", StringComparison.OrdinalIgnoreCase)) { +#pragma warning disable SA1008 return (inputChannels, outputChannels) switch { - (>= 6, >= 6 or 0) => Math.Min(768000, bitrate), - (> 0, > 0) => Math.Min(outputChannels * 136000, bitrate), - (> 0, _) => Math.Min(inputChannels * 136000, bitrate), + ( >= 6, >= 6 or 0) => Math.Min(768000, bitrate), + ( > 0, > 0) => Math.Min(outputChannels * 136000, bitrate), + ( > 0, _) => Math.Min(inputChannels * 136000, bitrate), (_, _) => Math.Min(672000, bitrate) }; +#pragma warning restore SA1008 } // Empty bitrate area is not allow on iOS @@ -3072,11 +3073,8 @@ namespace MediaBrowser.Controller.MediaEncoding int audioStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.AudioStream); if (state.AudioStream.IsExternal) { - bool hasExternalGraphicsSubs = state.SubtitleStream is not null - && ShouldEncodeSubtitle(state) - && state.SubtitleStream.IsExternal - && !state.SubtitleStream.IsTextSubtitleStream; - int externalAudioMapIndex = hasExternalGraphicsSubs ? 2 : 1; + bool hasExternalSubAsInput = NeedsExternalSubtitleMuxing(state); + int externalAudioMapIndex = hasExternalSubAsInput ? 2 : 1; args += string.Format( CultureInfo.InvariantCulture, @@ -3104,12 +3102,31 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (subtitleMethod == SubtitleDeliveryMethod.Embed) { - int subtitleStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.SubtitleStream); + if (state.SubtitleStream.IsExternal) + { + // External subtitle file is added as second FFmpeg input. + // For single-stream files (SRT/ASS/VTT) the in-file index is always 0. + // For multi-stream containers (MKS) we count how many streams from + // the same file appear before the selected one. + var inFileIndex = state.MediaSource.MediaStreams + .Where(s => string.Equals(s.Path, state.SubtitleStream.Path, StringComparison.Ordinal)) + .TakeWhile(s => s.Index != state.SubtitleStream.Index) + .Count(); - args += string.Format( - CultureInfo.InvariantCulture, - " -map 0:{0}", - subtitleStreamIndex); + args += string.Format( + CultureInfo.InvariantCulture, + " -map 1:{0}", + inFileIndex); + } + else + { + int subtitleStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.SubtitleStream); + + args += string.Format( + CultureInfo.InvariantCulture, + " -map 0:{0}", + subtitleStreamIndex); + } } else if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream) { @@ -7886,6 +7903,14 @@ namespace MediaBrowser.Controller.MediaEncoding || (state.BaseRequest.AlwaysBurnInSubtitleWhenTranscoding && !IsCopyCodec(state.OutputVideoCodec)); } + private static bool NeedsExternalSubtitleMuxing(EncodingJobInfo state) + { + return state.SubtitleStream is not null + && state.SubtitleStream.IsExternal + && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed + || (ShouldEncodeSubtitle(state) && !state.SubtitleStream.IsTextSubtitleStream)); + } + public static string GetVideoSyncOption(string videoSync, Version encoderVersion) { if (string.IsNullOrEmpty(videoSync)) diff --git a/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs index 54da218530..9bee653e2e 100644 --- a/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs +++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/Plugins/IHasEmbeddedImage.cs b/MediaBrowser.Controller/Plugins/IHasEmbeddedImage.cs new file mode 100644 index 0000000000..4196cd9f24 --- /dev/null +++ b/MediaBrowser.Controller/Plugins/IHasEmbeddedImage.cs @@ -0,0 +1,17 @@ +namespace MediaBrowser.Controller.Plugins; + +/// +/// Marker interface for integrated/bundled plugins that ship their plugin image as an embedded +/// resource inside the plugin assembly rather than as a file on disk. +/// +/// +/// This interface is intended for plugins compiled into the server. External plugins should +/// continue to declare their image via the imagePath field in meta.json. +/// +public interface IHasEmbeddedImage +{ + /// + /// Gets the name of the embedded resource in this plugin's assembly to serve as the plugin image. + /// + string ImageResourceName { get; } +} diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs index 132765b719..eb38eeb503 100644 --- a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs +++ b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs @@ -141,7 +141,8 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates _logger.LogError("Unable to set playing queue in group {GroupId}.", context.GroupId.ToString()); // Ignore request and return to previous state. - IGroupState newState = prevState switch { + IGroupState newState = prevState switch + { GroupStateType.Playing => new PlayingGroupState(LoggerFactory), GroupStateType.Paused => new PausedGroupState(LoggerFactory), _ => new IdleGroupState(LoggerFactory) diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index cf1423d02d..340d9843ff 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -143,16 +143,16 @@ namespace MediaBrowser.LocalMetadata.Parsers item.Name = reader.ReadNormalizedString(); break; case "CriticRating": - { - var text = reader.ReadElementContentAsString(); - - if (float.TryParse(text, CultureInfo.InvariantCulture, out var value)) { - item.CriticRating = value; - } + var text = reader.ReadElementContentAsString(); - break; - } + if (float.TryParse(text, CultureInfo.InvariantCulture, out var value)) + { + item.CriticRating = value; + } + + break; + } case "SortTitle": item.ForcedSortName = reader.ReadNormalizedString(); @@ -176,55 +176,55 @@ namespace MediaBrowser.LocalMetadata.Parsers break; case "LockedFields": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) { - item.LockedFields = val.Split('|').Select(i => + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) { - if (Enum.TryParse(i, true, out MetadataField field)) + item.LockedFields = val.Split('|').Select(i => { - return (MetadataField?)field; - } + if (Enum.TryParse(i, true, out MetadataField field)) + { + return (MetadataField?)field; + } - return null; - }).Where(i => i.HasValue).Select(i => i!.Value).ToArray(); + return null; + }).Where(i => i.HasValue).Select(i => i!.Value).ToArray(); + } + + break; } - break; - } - case "TagLines": - { - if (!reader.IsEmptyElement) { - using (var subtree = reader.ReadSubtree()) + if (!reader.IsEmptyElement) { - FetchFromTaglinesNode(subtree, item); + using (var subtree = reader.ReadSubtree()) + { + FetchFromTaglinesNode(subtree, item); + } + } + else + { + reader.Read(); } - } - else - { - reader.Read(); - } - break; - } + break; + } case "Countries": - { - if (!reader.IsEmptyElement) { - reader.Skip(); - } - else - { - reader.Read(); - } + if (!reader.IsEmptyElement) + { + reader.Skip(); + } + else + { + reader.Read(); + } - break; - } + break; + } case "ContentRating": case "MPAARating": @@ -307,19 +307,19 @@ namespace MediaBrowser.LocalMetadata.Parsers break; case "Trailers": - { - if (!reader.IsEmptyElement) { - using var subtree = reader.ReadSubtree(); - FetchDataFromTrailersNode(subtree, item); - } - else - { - reader.Read(); - } + if (!reader.IsEmptyElement) + { + using var subtree = reader.ReadSubtree(); + FetchDataFromTrailersNode(subtree, item); + } + else + { + reader.Read(); + } - break; - } + break; + } case "ProductionYear": if (reader.TryReadInt(out var productionYear) && productionYear > 1850) @@ -330,20 +330,20 @@ namespace MediaBrowser.LocalMetadata.Parsers break; case "Rating": case "IMDBrating": - { - var rating = reader.ReadNormalizedString(); - - if (!string.IsNullOrEmpty(rating)) { - // All external meta is saving this as '.' for decimal I believe...but just to be sure - if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val)) - { - item.CommunityRating = val; - } - } + var rating = reader.ReadNormalizedString(); - break; - } + if (!string.IsNullOrEmpty(rating)) + { + // All external meta is saving this as '.' for decimal I believe...but just to be sure + if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val)) + { + item.CommunityRating = val; + } + } + + break; + } case "BirthDate": case "PremiereDate": @@ -370,144 +370,144 @@ namespace MediaBrowser.LocalMetadata.Parsers break; case "Genres": - { - if (!reader.IsEmptyElement) { - using var subtree = reader.ReadSubtree(); - FetchFromGenresNode(subtree, item); - } - else - { - reader.Read(); - } + if (!reader.IsEmptyElement) + { + using var subtree = reader.ReadSubtree(); + FetchFromGenresNode(subtree, item); + } + else + { + reader.Read(); + } - break; - } + break; + } case "Tags": - { - if (!reader.IsEmptyElement) { - using var subtree = reader.ReadSubtree(); - FetchFromTagsNode(subtree, item); - } - else - { - reader.Read(); - } + if (!reader.IsEmptyElement) + { + using var subtree = reader.ReadSubtree(); + FetchFromTagsNode(subtree, item); + } + else + { + reader.Read(); + } - break; - } + break; + } case "Persons": - { - if (!reader.IsEmptyElement) { - using var subtree = reader.ReadSubtree(); - FetchDataFromPersonsNode(subtree, itemResult); - } - else - { - reader.Read(); - } + if (!reader.IsEmptyElement) + { + using var subtree = reader.ReadSubtree(); + FetchDataFromPersonsNode(subtree, itemResult); + } + else + { + reader.Read(); + } - break; - } + break; + } case "Studios": - { - if (!reader.IsEmptyElement) { - using var subtree = reader.ReadSubtree(); - FetchFromStudiosNode(subtree, item); - } - else - { - reader.Read(); - } + if (!reader.IsEmptyElement) + { + using var subtree = reader.ReadSubtree(); + FetchFromStudiosNode(subtree, item); + } + else + { + reader.Read(); + } - break; - } + break; + } case "Shares": - { - if (!reader.IsEmptyElement) { - using var subtree = reader.ReadSubtree(); - if (item is IHasShares hasShares) + if (!reader.IsEmptyElement) { - FetchFromSharesNode(subtree, hasShares); + using var subtree = reader.ReadSubtree(); + if (item is IHasShares hasShares) + { + FetchFromSharesNode(subtree, hasShares); + } + } + else + { + reader.Read(); } - } - else - { - reader.Read(); - } - break; - } + break; + } case "OwnerUserId": - { - var val = reader.ReadNormalizedString(); - - if (Guid.TryParse(val, out var guid) && !guid.Equals(Guid.Empty)) { - if (item is Playlist playlist) - { - playlist.OwnerUserId = guid; - } - } + var val = reader.ReadNormalizedString(); - break; - } + if (Guid.TryParse(val, out var guid) && !guid.Equals(Guid.Empty)) + { + if (item is Playlist playlist) + { + playlist.OwnerUserId = guid; + } + } + + break; + } case "Format3D": - { - var val = reader.ReadNormalizedString(); - - if (item is Video video) { - if (string.Equals("HSBS", val, StringComparison.OrdinalIgnoreCase)) - { - video.Video3DFormat = Video3DFormat.HalfSideBySide; - } - else if (string.Equals("HTAB", val, StringComparison.OrdinalIgnoreCase)) - { - video.Video3DFormat = Video3DFormat.HalfTopAndBottom; - } - else if (string.Equals("FTAB", val, StringComparison.OrdinalIgnoreCase)) - { - video.Video3DFormat = Video3DFormat.FullTopAndBottom; - } - else if (string.Equals("FSBS", val, StringComparison.OrdinalIgnoreCase)) - { - video.Video3DFormat = Video3DFormat.FullSideBySide; - } - else if (string.Equals("MVC", val, StringComparison.OrdinalIgnoreCase)) - { - video.Video3DFormat = Video3DFormat.MVC; - } - } + var val = reader.ReadNormalizedString(); - break; - } + if (item is Video video) + { + if (string.Equals("HSBS", val, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.HalfSideBySide; + } + else if (string.Equals("HTAB", val, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.HalfTopAndBottom; + } + else if (string.Equals("FTAB", val, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.FullTopAndBottom; + } + else if (string.Equals("FSBS", val, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.FullSideBySide; + } + else if (string.Equals("MVC", val, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.MVC; + } + } + + break; + } default: - { - string readerName = reader.Name; - if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue)) { - var id = reader.ReadNormalizedString(); - item.TrySetProviderId(providerIdValue, id); - } - else - { - reader.Skip(); - } + string readerName = reader.Name; + if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue)) + { + var id = reader.ReadNormalizedString(); + item.TrySetProviderId(providerIdValue, id); + } + else + { + reader.Skip(); + } - break; - } + break; + } } } @@ -526,31 +526,31 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "Share": - { - if (reader.IsEmptyElement) { - reader.Read(); - continue; - } - - using (var subReader = reader.ReadSubtree()) - { - var child = GetShare(subReader); - - if (child is not null) + if (reader.IsEmptyElement) { - list.Add(child); + reader.Read(); + continue; } - } - break; - } + using (var subReader = reader.ReadSubtree()) + { + var child = GetShare(subReader); + + if (child is not null) + { + list.Add(child); + } + } + + break; + } default: - { - reader.Skip(); - break; - } + { + reader.Skip(); + break; + } } } else diff --git a/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs index 00634de5b5..324505d17c 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs @@ -64,32 +64,32 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "CollectionItem": - { - if (!reader.IsEmptyElement) { - using (var subReader = reader.ReadSubtree()) + if (!reader.IsEmptyElement) { - var child = GetLinkedChild(subReader); - - if (child is not null) + using (var subReader = reader.ReadSubtree()) { - list.Add(child); + var child = GetLinkedChild(subReader); + + if (child is not null) + { + list.Add(child); + } } } - } - else - { - reader.Read(); - } + else + { + reader.Read(); + } - break; - } + break; + } default: - { - reader.Skip(); - break; - } + { + reader.Skip(); + break; + } } } else diff --git a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs index e0277870d1..0bda9e300a 100644 --- a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs @@ -76,25 +76,25 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "PlaylistItem": - { - if (reader.IsEmptyElement) { - reader.Read(); - continue; - } - - using (var subReader = reader.ReadSubtree()) - { - var child = GetLinkedChild(subReader); - - if (child is not null) + if (reader.IsEmptyElement) { - list.Add(child); + reader.Read(); + continue; } - } - break; - } + using (var subReader = reader.ReadSubtree()) + { + var child = GetLinkedChild(subReader); + + if (child is not null) + { + list.Add(child); + } + } + + break; + } default: reader.Skip(); diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index 7f40f4fd3e..d9cb7a450f 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -163,19 +163,19 @@ namespace MediaBrowser.MediaEncoding.Attachments int exitCode; using (var process = new Process + { + StartInfo = new ProcessStartInfo { - StartInfo = new ProcessStartInfo - { - Arguments = processArgs, - FileName = _mediaEncoder.EncoderPath, - UseShellExecute = false, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - WorkingDirectory = outputFolder, - ErrorDialog = false - }, - EnableRaisingEvents = true - }) + Arguments = processArgs, + FileName = _mediaEncoder.EncoderPath, + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + WorkingDirectory = outputFolder, + ErrorDialog = false + }, + EnableRaisingEvents = true + }) { _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); @@ -295,18 +295,18 @@ namespace MediaBrowser.MediaEncoding.Attachments int exitCode; using (var process = new Process + { + StartInfo = new ProcessStartInfo { - StartInfo = new ProcessStartInfo - { - Arguments = processArgs, - FileName = _mediaEncoder.EncoderPath, - UseShellExecute = false, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false - }, - EnableRaisingEvents = true - }) + Arguments = processArgs, + FileName = _mediaEncoder.EncoderPath, + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }, + EnableRaisingEvents = true + }) { _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index f34e911a05..66bf6ebd24 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -85,6 +85,8 @@ namespace MediaBrowser.MediaEncoding.Encoder private bool _isVaapiDeviceSupportVulkanDrmModifier = false; private bool _isVaapiDeviceSupportVulkanDrmInterop = false; + private bool _canSetProcessPriority = true; + private bool _isVideoToolboxAv1DecodeAvailable = false; private static string[] _vulkanImageDrmFmtModifierExts = @@ -1123,13 +1125,17 @@ namespace MediaBrowser.MediaEncoding.Encoder { process.Process.Start(); - try + if (_canSetProcessPriority) { - process.Process.PriorityClass = ProcessPriorityClass.BelowNormal; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Unable to set process priority to BelowNormal for {ProcessFileName}", process.Process.StartInfo.FileName); + try + { + process.Process.PriorityClass = ProcessPriorityClass.BelowNormal; + } + catch (Exception ex) + { + _canSetProcessPriority = false; + _logger.LogWarning(ex, "Unable to set process priority to BelowNormal for {ProcessFileName}. Further attempts will be skipped.", process.Process.StartInfo.FileName); + } } lock (_runningProcessesLock) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 8ad66fce40..e0c5f3ad39 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -190,10 +190,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles await using (stream.ConfigureAwait(false)) { - using var reader = new StreamReader(stream, detected.Encoding); - var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + using var reader = new StreamReader(stream, detected.Encoding); + var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); - return new MemoryStream(Encoding.UTF8.GetBytes(text)); + return new MemoryStream(Encoding.UTF8.GetBytes(text)); } } } @@ -1027,20 +1027,20 @@ namespace MediaBrowser.MediaEncoding.Subtitles switch (protocol) { case MediaProtocol.Http: - { - using var stream = await _httpClientFactory - .CreateClient(NamedClient.Default) - .GetStreamAsync(new Uri(path), cancellationToken) - .ConfigureAwait(false); + { + using var stream = await _httpClientFactory + .CreateClient(NamedClient.Default) + .GetStreamAsync(new Uri(path), cancellationToken) + .ConfigureAwait(false); - return await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false); - } + return await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false); + } case MediaProtocol.File: - { - return await CharsetDetector.DetectFromFileAsync(path, cancellationToken) - .ConfigureAwait(false); - } + { + return await CharsetDetector.DetectFromFileAsync(path, cancellationToken) + .ConfigureAwait(false); + } default: throw new ArgumentOutOfRangeException(nameof(protocol), protocol, "Unsupported protocol"); diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 44697837ca..2ccd2a6c28 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1451,7 +1451,7 @@ namespace MediaBrowser.Model.Dlna string? outputContainer, MediaStreamProtocol? transcodingSubProtocol) { - if (!subtitleStream.IsExternal && (playMethod != PlayMethod.Transcode || transcodingSubProtocol != MediaStreamProtocol.hls)) + if (CanConsiderEmbedSubtitle(subtitleStream, playMethod, transcodingSubProtocol, outputContainer)) { // Look for supported embedded subs of the same format foreach (var profile in subtitleProfiles) @@ -1540,6 +1540,19 @@ namespace MediaBrowser.Model.Dlna return false; } + private static bool CanConsiderEmbedSubtitle(MediaStream subtitleStream, PlayMethod playMethod, MediaStreamProtocol? transcodingSubProtocol, string? outputContainer) + { + if (subtitleStream.IsExternal) + { + return playMethod == PlayMethod.Transcode + && transcodingSubProtocol != MediaStreamProtocol.hls + && IsSubtitleEmbedSupported(outputContainer); + } + + return playMethod != PlayMethod.Transcode + || transcodingSubProtocol != MediaStreamProtocol.hls; + } + private static SubtitleProfile? GetExternalSubtitleProfile(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, ITranscoderSupport transcoderSupport, bool allowConversion) { foreach (var profile in subtitleProfiles) diff --git a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs index 7c9ee18ca4..28c3c66af7 100644 --- a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs +++ b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs @@ -50,7 +50,7 @@ namespace MediaBrowser.Model.Extensions return 0; }) - .ThenByDescending(i => Math.Round(i.CommunityRating ?? 0, 1) ) + .ThenByDescending(i => Math.Round(i.CommunityRating ?? 0, 1)) .ThenByDescending(i => i.VoteCount ?? 0); } } diff --git a/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs b/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs index 53d0173750..9a21461d82 100644 --- a/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs +++ b/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Model.MediaSegments; diff --git a/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs b/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs index 4429623dd9..ded66652ce 100644 --- a/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs +++ b/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs @@ -50,7 +50,7 @@ namespace MediaBrowser.Model.SyncPlay /// Seek = 8, - /// + /// /// A user is signaling that playback is buffering. /// Buffer = 9, diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayGroupDoesNotExistUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayGroupDoesNotExistUpdate.cs index 7e2d10c8b8..ccf5fdb07e 100644 --- a/MediaBrowser.Model/SyncPlay/SyncPlayGroupDoesNotExistUpdate.cs +++ b/MediaBrowser.Model/SyncPlay/SyncPlayGroupDoesNotExistUpdate.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; namespace MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayGroupJoinedUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayGroupJoinedUpdate.cs index bfb49152a3..dcb039ee93 100644 --- a/MediaBrowser.Model/SyncPlay/SyncPlayGroupJoinedUpdate.cs +++ b/MediaBrowser.Model/SyncPlay/SyncPlayGroupJoinedUpdate.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; namespace MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayGroupLeftUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayGroupLeftUpdate.cs index 5ff60c5c27..f20e143e02 100644 --- a/MediaBrowser.Model/SyncPlay/SyncPlayGroupLeftUpdate.cs +++ b/MediaBrowser.Model/SyncPlay/SyncPlayGroupLeftUpdate.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; namespace MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayLibraryAccessDeniedUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayLibraryAccessDeniedUpdate.cs index 0d9a722f78..89e5706d86 100644 --- a/MediaBrowser.Model/SyncPlay/SyncPlayLibraryAccessDeniedUpdate.cs +++ b/MediaBrowser.Model/SyncPlay/SyncPlayLibraryAccessDeniedUpdate.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; namespace MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayNotInGroupUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayNotInGroupUpdate.cs index a3b610f619..4ba893be5b 100644 --- a/MediaBrowser.Model/SyncPlay/SyncPlayNotInGroupUpdate.cs +++ b/MediaBrowser.Model/SyncPlay/SyncPlayNotInGroupUpdate.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; namespace MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayPlayQueueUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayPlayQueueUpdate.cs index 83d9bd40bc..a39f20735b 100644 --- a/MediaBrowser.Model/SyncPlay/SyncPlayPlayQueueUpdate.cs +++ b/MediaBrowser.Model/SyncPlay/SyncPlayPlayQueueUpdate.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; namespace MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayStateUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayStateUpdate.cs index 744ca46a0b..61cb8adbaa 100644 --- a/MediaBrowser.Model/SyncPlay/SyncPlayStateUpdate.cs +++ b/MediaBrowser.Model/SyncPlay/SyncPlayStateUpdate.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; namespace MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayUserJoinedUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayUserJoinedUpdate.cs index e8c6b4df41..247e6a57b2 100644 --- a/MediaBrowser.Model/SyncPlay/SyncPlayUserJoinedUpdate.cs +++ b/MediaBrowser.Model/SyncPlay/SyncPlayUserJoinedUpdate.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; namespace MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayUserLeftUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayUserLeftUpdate.cs index 97be8e63a8..ba053747cc 100644 --- a/MediaBrowser.Model/SyncPlay/SyncPlayUserLeftUpdate.cs +++ b/MediaBrowser.Model/SyncPlay/SyncPlayUserLeftUpdate.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; namespace MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 2c393ca862..95e4d46c59 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -187,7 +187,7 @@ namespace MediaBrowser.Model.Users [Required(AllowEmptyStrings = false)] public string AuthenticationProviderId { get; set; } - [Required(AllowEmptyStrings= false)] + [Required(AllowEmptyStrings = false)] public string PasswordResetProviderId { get; set; } /// diff --git a/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalId.cs b/MediaBrowser.Providers/Books/ComicVine/ComicVineExternalId.cs similarity index 91% rename from MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalId.cs rename to MediaBrowser.Providers/Books/ComicVine/ComicVineExternalId.cs index 8cbd1f89a7..e2e785eaca 100644 --- a/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalId.cs +++ b/MediaBrowser.Providers/Books/ComicVine/ComicVineExternalId.cs @@ -3,7 +3,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -namespace MediaBrowser.Providers.Plugins.ComicVine +namespace MediaBrowser.Providers.Books.ComicVine { /// public class ComicVineExternalId : IExternalId diff --git a/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalUrlProvider.cs b/MediaBrowser.Providers/Books/ComicVine/ComicVineExternalUrlProvider.cs similarity index 93% rename from MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalUrlProvider.cs rename to MediaBrowser.Providers/Books/ComicVine/ComicVineExternalUrlProvider.cs index 9122399179..a8450ec599 100644 --- a/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Books/ComicVine/ComicVineExternalUrlProvider.cs @@ -3,7 +3,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -namespace MediaBrowser.Providers.Plugins.ComicVine; +namespace MediaBrowser.Providers.Books.ComicVine; /// public class ComicVineExternalUrlProvider : IExternalUrlProvider diff --git a/MediaBrowser.Providers/Plugins/ComicVine/ComicVinePersonExternalId.cs b/MediaBrowser.Providers/Books/ComicVine/ComicVinePersonExternalId.cs similarity index 92% rename from MediaBrowser.Providers/Plugins/ComicVine/ComicVinePersonExternalId.cs rename to MediaBrowser.Providers/Books/ComicVine/ComicVinePersonExternalId.cs index 26b8e11380..f625fb9649 100644 --- a/MediaBrowser.Providers/Plugins/ComicVine/ComicVinePersonExternalId.cs +++ b/MediaBrowser.Providers/Books/ComicVine/ComicVinePersonExternalId.cs @@ -3,7 +3,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -namespace MediaBrowser.Providers.Plugins.ComicVine +namespace MediaBrowser.Providers.Books.ComicVine { /// public class ComicVinePersonExternalId : IExternalId diff --git a/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalId.cs b/MediaBrowser.Providers/Books/GoogleBooks/GoogleBooksExternalId.cs similarity index 91% rename from MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalId.cs rename to MediaBrowser.Providers/Books/GoogleBooks/GoogleBooksExternalId.cs index 02d3b36974..aac8cdff65 100644 --- a/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalId.cs +++ b/MediaBrowser.Providers/Books/GoogleBooks/GoogleBooksExternalId.cs @@ -3,7 +3,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -namespace MediaBrowser.Providers.Plugins.GoogleBooks +namespace MediaBrowser.Providers.Books.GoogleBooks { /// public class GoogleBooksExternalId : IExternalId diff --git a/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalUrlProvider.cs b/MediaBrowser.Providers/Books/GoogleBooks/GoogleBooksExternalUrlProvider.cs similarity index 92% rename from MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalUrlProvider.cs rename to MediaBrowser.Providers/Books/GoogleBooks/GoogleBooksExternalUrlProvider.cs index 95047ee83e..0559db2e2b 100644 --- a/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Books/GoogleBooks/GoogleBooksExternalUrlProvider.cs @@ -3,7 +3,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -namespace MediaBrowser.Providers.Plugins.GoogleBooks; +namespace MediaBrowser.Providers.Books.GoogleBooks; /// public class GoogleBooksExternalUrlProvider : IExternalUrlProvider diff --git a/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs b/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs index 6e678802c1..d2331c6864 100644 --- a/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs +++ b/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Threading; using System.Threading.Tasks; using System.Xml; diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 8b727a8cac..1032582900 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -47,22 +47,53 @@ + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs index 6c2ad0573e..bb6b67286d 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs @@ -5,12 +5,13 @@ using System; using System.Collections.Generic; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; namespace MediaBrowser.Providers.Plugins.AudioDb { - public class Plugin : BasePlugin, IHasWebPages + public class Plugin : BasePlugin, IHasWebPages, IHasEmbeddedImage { public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) @@ -29,6 +30,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb // TODO remove when plugin removed from server. public override string ConfigurationFileName => "Jellyfin.Plugin.AudioDb.xml"; + public string ImageResourceName => GetType().Namespace + ".jellyfin-plugin-tadb.svg"; + public IEnumerable GetPages() { yield return new PluginPageInfo diff --git a/MediaBrowser.Providers/Plugins/AudioDb/jellyfin-plugin-tadb.svg b/MediaBrowser.Providers/Plugins/AudioDb/jellyfin-plugin-tadb.svg new file mode 100644 index 0000000000..94fa55cc9c --- /dev/null +++ b/MediaBrowser.Providers/Plugins/AudioDb/jellyfin-plugin-tadb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/MediaBrowser.Providers/Plugins/ListenBrainz/ListenBrainzPlugin.cs b/MediaBrowser.Providers/Plugins/ListenBrainz/ListenBrainzPlugin.cs index efac93f94e..1681f0334d 100644 --- a/MediaBrowser.Providers/Plugins/ListenBrainz/ListenBrainzPlugin.cs +++ b/MediaBrowser.Providers/Plugins/ListenBrainz/ListenBrainzPlugin.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; using MediaBrowser.Providers.Plugins.ListenBrainz.Configuration; @@ -11,7 +12,7 @@ namespace MediaBrowser.Providers.Plugins.ListenBrainz; /// /// ListenBrainz plugin instance. /// -public class ListenBrainzPlugin : BasePlugin, IHasWebPages +public class ListenBrainzPlugin : BasePlugin, IHasWebPages, IHasEmbeddedImage { /// /// Initializes a new instance of the class. @@ -41,6 +42,9 @@ public class ListenBrainzPlugin : BasePlugin, IHasWebPages /// public override string ConfigurationFileName => "Jellyfin.Plugin.ListenBrainz.xml"; + /// + public string ImageResourceName => GetType().Namespace + ".Configuration.ListenBrainz_logo.svg"; + /// public IEnumerable GetPages() { @@ -51,11 +55,6 @@ public class ListenBrainzPlugin : BasePlugin, IHasWebPages EmbeddedResourcePath = resourcePrefix + "config.html" }; yield return new PluginPageInfo - { - Name = Name + "Logo", - EmbeddedResourcePath = resourcePrefix + "ListenBrainz_logo.svg" - }; - yield return new PluginPageInfo { Name = Name + "Notice", EmbeddedResourcePath = resourcePrefix + "NOTICE.md" diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs index dd0a939f72..f7c570692d 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs @@ -19,7 +19,7 @@ public class MusicBrainzReleaseGroupExternalUrlProvider : IExternalUrlProvider { if (item is MusicAlbum) { - if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out var externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out var externalId)) { yield return Plugin.Instance!.Configuration.Server + $"/release-group/{externalId}"; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs index 59e6f42b19..c2bbd8ba86 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs @@ -19,7 +19,7 @@ public class MusicBrainzTrackExternalUrlProvider : IExternalUrlProvider { if (item is Audio) { - if (item.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out var externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out var externalId)) { yield return Plugin.Instance!.Configuration.Server + $"/track/{externalId}"; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs index 69225d0b95..f448e6b20c 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs @@ -6,6 +6,7 @@ using System.Threading; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration; @@ -17,7 +18,7 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz; /// /// Plugin instance. /// -public class Plugin : BasePlugin, IHasWebPages, IDisposable +public class Plugin : BasePlugin, IHasWebPages, IHasEmbeddedImage, IDisposable { private readonly ILogger _logger; private readonly Lock _queryLock = new(); @@ -66,6 +67,9 @@ public class Plugin : BasePlugin, IHasWebPages, IDisposable // TODO remove when plugin removed from server. public override string ConfigurationFileName => "Jellyfin.Plugin.MusicBrainz.xml"; + /// + public string ImageResourceName => GetType().Namespace + ".jellyfin-plugin-musicbrainz.svg"; + /// /// Gets the current MusicBrainz query client. /// diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/jellyfin-plugin-musicbrainz.svg b/MediaBrowser.Providers/Plugins/MusicBrainz/jellyfin-plugin-musicbrainz.svg new file mode 100644 index 0000000000..8074d59d7b --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/jellyfin-plugin-musicbrainz.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs index a0fba48f05..9066ff8523 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs @@ -5,12 +5,13 @@ using System; using System.Collections.Generic; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; namespace MediaBrowser.Providers.Plugins.Omdb { - public class Plugin : BasePlugin, IHasWebPages + public class Plugin : BasePlugin, IHasWebPages, IHasEmbeddedImage { public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) @@ -29,6 +30,8 @@ namespace MediaBrowser.Providers.Plugins.Omdb // TODO remove when plugin removed from server. public override string ConfigurationFileName => "Jellyfin.Plugin.Omdb.xml"; + public string ImageResourceName => GetType().Namespace + ".jellyfin-plugin-omdb.png"; + public IEnumerable GetPages() { yield return new PluginPageInfo diff --git a/MediaBrowser.Providers/Plugins/Omdb/jellyfin-plugin-omdb.png b/MediaBrowser.Providers/Plugins/Omdb/jellyfin-plugin-omdb.png new file mode 100644 index 0000000000..a4bb7f7d5f Binary files /dev/null and b/MediaBrowser.Providers/Plugins/Omdb/jellyfin-plugin-omdb.png differ diff --git a/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs index 28f8c0c617..167959967d 100644 --- a/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; using MediaBrowser.Providers.Plugins.StudioImages.Configuration; @@ -13,7 +14,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages /// /// Artwork Plugin class. /// - public class Plugin : BasePlugin, IHasWebPages + public class Plugin : BasePlugin, IHasWebPages, IHasEmbeddedImage { /// /// Artwork repository URL. @@ -50,6 +51,9 @@ namespace MediaBrowser.Providers.Plugins.StudioImages /// public override string ConfigurationFileName => "Jellyfin.Plugin.StudioImages.xml"; + /// + public string ImageResourceName => GetType().Namespace + ".jellyfin-plugin-studioimages.svg"; + /// public IEnumerable GetPages() { diff --git a/MediaBrowser.Providers/Plugins/StudioImages/jellyfin-plugin-studioimages.svg b/MediaBrowser.Providers/Plugins/StudioImages/jellyfin-plugin-studioimages.svg new file mode 100644 index 0000000000..e9da69c571 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/StudioImages/jellyfin-plugin-studioimages.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs b/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs index 4adde8366a..04f1fd04a3 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; @@ -12,7 +13,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// /// Plugin class for the TMDb library. /// - public class Plugin : BasePlugin, IHasWebPages + public class Plugin : BasePlugin, IHasWebPages, IHasEmbeddedImage { /// /// Initializes a new instance of the class. @@ -44,6 +45,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// public override string ConfigurationFileName => "Jellyfin.Plugin.Tmdb.xml"; + /// + public string ImageResourceName => GetType().Namespace + ".jellyfin-plugin-tmdb.svg"; + /// /// Return the plugin configuration page. /// diff --git a/MediaBrowser.Providers/Plugins/Tmdb/jellyfin-plugin-tmdb.svg b/MediaBrowser.Providers/Plugins/Tmdb/jellyfin-plugin-tmdb.svg new file mode 100644 index 0000000000..fbebb32b60 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/jellyfin-plugin-tmdb.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs b/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs index 52b0583e58..e01b6c78ed 100644 --- a/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs +++ b/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs @@ -19,6 +19,6 @@ public class Zap2ItExternalUrlProvider : IExternalUrlProvider if (item.TryGetProviderId(MetadataProvider.Zap2It, out var externalId)) { yield return $"http://tvlistings.zap2it.com/overview.html?programSeriesId={externalId}"; - } + } } } diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs index 137abff478..f013863336 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs @@ -96,7 +96,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers var artist = reader.ReadNormalizedString(); if (!string.IsNullOrEmpty(artist) && item is MusicVideo artistVideo) { - artistVideo.Artists = [..artistVideo.Artists, artist]; + artistVideo.Artists = [.. artistVideo.Artists, artist]; } break; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs index 55b90a54d7..ff10440e0c 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241020103111_LibraryDbMigration.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241020103111_LibraryDbMigration.cs index 8cc7fb452d..9c03bfed9d 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241020103111_LibraryDbMigration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241020103111_LibraryDbMigration.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111131257_AddedCustomDataKey.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111131257_AddedCustomDataKey.cs index ac78019eda..3fe61f91df 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111131257_AddedCustomDataKey.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111131257_AddedCustomDataKey.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111135439_AddedCustomDataKeyKey.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111135439_AddedCustomDataKeyKey.cs index 4558d7c49c..d6b351e2ab 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111135439_AddedCustomDataKeyKey.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111135439_AddedCustomDataKeyKey.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112152323_FixAncestorIdConfig.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112152323_FixAncestorIdConfig.cs index 70e81f3676..a7c9e6fb50 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112152323_FixAncestorIdConfig.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112152323_FixAncestorIdConfig.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112232041_fixMediaStreams.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112232041_fixMediaStreams.cs index d57ea81b3a..ab8b792a5f 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112232041_fixMediaStreams.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112232041_fixMediaStreams.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112234144_FixMediaStreams2.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112234144_FixMediaStreams2.cs index 78611b9e4c..1ed23e7c42 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112234144_FixMediaStreams2.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112234144_FixMediaStreams2.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241113133548_EnforceUniqueItemValue.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241113133548_EnforceUniqueItemValue.cs index d1b06ceaec..e3a3f3ac64 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241113133548_EnforceUniqueItemValue.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241113133548_EnforceUniqueItemValue.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250202021306_FixedCollation.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250202021306_FixedCollation.cs index e82575e418..3d4fd85af2 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250202021306_FixedCollation.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250202021306_FixedCollation.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.cs index 2c60dd7a62..1493df35d0 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.cs index 1e904e833e..713b5c0434 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs index 71f56a1492..7049ccc214 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.cs index c17b35b404..d84940b7e6 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.cs index 5766cd3825..63010679e5 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.cs index f921856a20..ceb3d32452 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.cs index e1220bfcf7..1f6012bbf2 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.cs index aa667bafd4..6032969cf3 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250609115616_DetachUserDataInsteadOfDelete.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250609115616_DetachUserDataInsteadOfDelete.cs index 2935a608d1..a3d8fe2c3a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250609115616_DetachUserDataInsteadOfDelete.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250609115616_DetachUserDataInsteadOfDelete.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.cs index bce6029d5b..44b44dd581 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250714044826_ResetJournalMode.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250714044826_ResetJournalMode.cs index 23cb0c8ba5..e88518d74a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250714044826_ResetJournalMode.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250714044826_ResetJournalMode.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs index 38033d07f0..a7f5e369ab 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250925203415_ExtendPeopleMapKey.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250925203415_ExtendPeopleMapKey.cs index 7c1bcdf445..097504aebb 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250925203415_ExtendPeopleMapKey.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250925203415_ExtendPeopleMapKey.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113102337_AddLinkedChildrenTable.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113102337_AddLinkedChildrenTable.cs index 198bc78cff..1ab6b4240a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113102337_AddLinkedChildrenTable.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113102337_AddLinkedChildrenTable.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113203012_ChangeOwnerIdToGuid.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113203012_ChangeOwnerIdToGuid.cs index 6334d8b5f1..4927b0e78d 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113203012_ChangeOwnerIdToGuid.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113203012_ChangeOwnerIdToGuid.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.Designer.cs index 0e28abc862..f9cb9aa736 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.Designer.cs @@ -270,6 +270,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations b.Property("OfficialRating") .HasColumnType("TEXT"); + b.Property("OriginalLanguage") + .HasColumnType("TEXT"); + b.Property("OriginalTitle") .HasColumnType("TEXT"); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.cs index c84086d992..39a0805d2a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -23,12 +23,40 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations name: "BaseItemEntityId", table: "BaseItems"); + migrationBuilder.Sql( + """ + UPDATE BaseItems + SET OwnerId = '00000000-0000-0000-0000-000000000001' + WHERE OwnerId IS NOT NULL + AND OwnerId NOT IN (SELECT Id FROM BaseItems); + """); + migrationBuilder.AddForeignKey( name: "FK_BaseItems_BaseItems_OwnerId", table: "BaseItems", column: "OwnerId", principalTable: "BaseItems", principalColumn: "Id"); + + migrationBuilder.AddColumn( + name: "IsOriginal", + table: "MediaStreamInfos", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "OriginalLanguage", + table: "BaseItems", + type: "TEXT", + nullable: true); + + migrationBuilder.UpdateData( + table: "BaseItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0000-000000000001"), + column: "OriginalLanguage", + value: null); } /// @@ -62,6 +90,14 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations column: "BaseItemEntityId", principalTable: "BaseItems", principalColumn: "Id"); + + migrationBuilder.DropColumn( + name: "IsOriginal", + table: "MediaStreamInfos"); + + migrationBuilder.DropColumn( + name: "OriginalLanguage", + table: "BaseItems"); } } } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.Designer.cs index 92ed0cf6bf..29874264af 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.Designer.cs @@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations b.Property("OfficialRating") .HasColumnType("TEXT"); + b.Property("OriginalLanguage") + .HasColumnType("TEXT"); + b.Property("OriginalTitle") .HasColumnType("TEXT"); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.cs index 5387d3351d..6440d0a395 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.Designer.cs index 89fb3ee815..8282a8a582 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.Designer.cs @@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations b.Property("OfficialRating") .HasColumnType("TEXT"); + b.Property("OriginalLanguage") + .HasColumnType("TEXT"); + b.Property("OriginalTitle") .HasColumnType("TEXT"); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.cs index ba1a131e9b..710ffc35b1 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.Designer.cs index 83a6a7baf3..5541a0191b 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.Designer.cs @@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations b.Property("OfficialRating") .HasColumnType("TEXT"); + b.Property("OriginalLanguage") + .HasColumnType("TEXT"); + b.Property("OriginalTitle") .HasColumnType("TEXT"); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.cs index 8c8768645b..7e1d619b8a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.Designer.cs index 1b396a707c..f6fd1db21e 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.Designer.cs @@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations b.Property("OfficialRating") .HasColumnType("TEXT"); + b.Property("OriginalLanguage") + .HasColumnType("TEXT"); + b.Property("OriginalTitle") .HasColumnType("TEXT"); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.cs index da57c71662..0b540d799b 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.Designer.cs index ca995decde..5f7131ff65 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.Designer.cs @@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations b.Property("OfficialRating") .HasColumnType("TEXT"); + b.Property("OriginalLanguage") + .HasColumnType("TEXT"); + b.Property("OriginalTitle") .HasColumnType("TEXT"); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.cs index 92836e753f..ef0c039ffe 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260215201634_ChangePrimaryVersionIdToGuid.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260215201634_ChangePrimaryVersionIdToGuid.Designer.cs index 0184154566..0499921fec 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260215201634_ChangePrimaryVersionIdToGuid.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260215201634_ChangePrimaryVersionIdToGuid.Designer.cs @@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations b.Property("OfficialRating") .HasColumnType("TEXT"); + b.Property("OriginalLanguage") + .HasColumnType("TEXT"); + b.Property("OriginalTitle") .HasColumnType("TEXT"); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.Designer.cs index 4c9ccc13bf..bf46ad9b39 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.Designer.cs @@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations b.Property("OfficialRating") .HasColumnType("TEXT"); + b.Property("OriginalLanguage") + .HasColumnType("TEXT"); + b.Property("OriginalTitle") .HasColumnType("TEXT"); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.cs index 3932e1c3e4..00d4f24403 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.Designer.cs index 23ab2a4674..fc5c7afa0e 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.Designer.cs @@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations b.Property("OfficialRating") .HasColumnType("TEXT"); + b.Property("OriginalLanguage") + .HasColumnType("TEXT"); + b.Property("OriginalTitle") .HasColumnType("TEXT"); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.cs index e1f62c12fb..ad51786581 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504180809_AddOriginalLanguage.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504180809_AddOriginalLanguage.Designer.cs deleted file mode 100644 index e0f5125da1..0000000000 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504180809_AddOriginalLanguage.Designer.cs +++ /dev/null @@ -1,1802 +0,0 @@ -// -using System; -using Jellyfin.Database.Implementations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Jellyfin.Database.Providers.Sqlite.Migrations -{ - [DbContext(typeof(JellyfinDbContext))] - [Migration("20260504180809_AddOriginalLanguage")] - partial class AddOriginalLanguage - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "10.0.7"); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DayOfWeek") - .HasColumnType("INTEGER"); - - b.Property("EndHour") - .HasColumnType("REAL"); - - b.Property("StartHour") - .HasColumnType("REAL"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AccessSchedules"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ActivityLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DateCreated") - .HasColumnType("TEXT"); - - b.Property("ItemId") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("LogSeverity") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property("Overview") - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ShortOverview") - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property("Type") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("DateCreated"); - - b.ToTable("ActivityLogs"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("ParentItemId") - .HasColumnType("TEXT"); - - b.HasKey("ItemId", "ParentItemId"); - - b.HasIndex("ParentItemId"); - - b.ToTable("AncestorIds"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("Index") - .HasColumnType("INTEGER"); - - b.Property("Codec") - .HasColumnType("TEXT"); - - b.Property("CodecTag") - .HasColumnType("TEXT"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Filename") - .HasColumnType("TEXT"); - - b.Property("MimeType") - .HasColumnType("TEXT"); - - b.HasKey("ItemId", "Index"); - - b.ToTable("AttachmentStreamInfos"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("Album") - .HasColumnType("TEXT"); - - b.Property("AlbumArtists") - .HasColumnType("TEXT"); - - b.Property("Artists") - .HasColumnType("TEXT"); - - b.Property("Audio") - .HasColumnType("INTEGER"); - - b.Property("ChannelId") - .HasColumnType("TEXT"); - - b.Property("CleanName") - .HasColumnType("TEXT"); - - b.Property("CommunityRating") - .HasColumnType("REAL"); - - b.Property("CriticRating") - .HasColumnType("REAL"); - - b.Property("CustomRating") - .HasColumnType("TEXT"); - - b.Property("Data") - .HasColumnType("TEXT"); - - b.Property("DateCreated") - .HasColumnType("TEXT"); - - b.Property("DateLastMediaAdded") - .HasColumnType("TEXT"); - - b.Property("DateLastRefreshed") - .HasColumnType("TEXT"); - - b.Property("DateLastSaved") - .HasColumnType("TEXT"); - - b.Property("DateModified") - .HasColumnType("TEXT"); - - b.Property("EndDate") - .HasColumnType("TEXT"); - - b.Property("EpisodeTitle") - .HasColumnType("TEXT"); - - b.Property("ExternalId") - .HasColumnType("TEXT"); - - b.Property("ExternalSeriesId") - .HasColumnType("TEXT"); - - b.Property("ExternalServiceId") - .HasColumnType("TEXT"); - - b.Property("ExtraType") - .HasColumnType("INTEGER"); - - b.Property("ForcedSortName") - .HasColumnType("TEXT"); - - b.Property("Genres") - .HasColumnType("TEXT"); - - b.Property("Height") - .HasColumnType("INTEGER"); - - b.Property("IndexNumber") - .HasColumnType("INTEGER"); - - b.Property("InheritedParentalRatingSubValue") - .HasColumnType("INTEGER"); - - b.Property("InheritedParentalRatingValue") - .HasColumnType("INTEGER"); - - b.Property("IsFolder") - .HasColumnType("INTEGER"); - - b.Property("IsInMixedFolder") - .HasColumnType("INTEGER"); - - b.Property("IsLocked") - .HasColumnType("INTEGER"); - - b.Property("IsMovie") - .HasColumnType("INTEGER"); - - b.Property("IsRepeat") - .HasColumnType("INTEGER"); - - b.Property("IsSeries") - .HasColumnType("INTEGER"); - - b.Property("IsVirtualItem") - .HasColumnType("INTEGER"); - - b.Property("LUFS") - .HasColumnType("REAL"); - - b.Property("MediaType") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizationGain") - .HasColumnType("REAL"); - - b.Property("OfficialRating") - .HasColumnType("TEXT"); - - b.Property("OriginalLanguage") - .HasColumnType("TEXT"); - - b.Property("OriginalTitle") - .HasColumnType("TEXT"); - - b.Property("Overview") - .HasColumnType("TEXT"); - - b.Property("OwnerId") - .HasColumnType("TEXT"); - - b.Property("ParentId") - .HasColumnType("TEXT"); - - b.Property("ParentIndexNumber") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.Property("PreferredMetadataCountryCode") - .HasColumnType("TEXT"); - - b.Property("PreferredMetadataLanguage") - .HasColumnType("TEXT"); - - b.Property("PremiereDate") - .HasColumnType("TEXT"); - - b.Property("PresentationUniqueKey") - .HasColumnType("TEXT"); - - b.Property("PrimaryVersionId") - .HasColumnType("TEXT"); - - b.Property("ProductionLocations") - .HasColumnType("TEXT"); - - b.Property("ProductionYear") - .HasColumnType("INTEGER"); - - b.Property("RunTimeTicks") - .HasColumnType("INTEGER"); - - b.Property("SeasonId") - .HasColumnType("TEXT"); - - b.Property("SeasonName") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("TEXT"); - - b.Property("SeriesName") - .HasColumnType("TEXT"); - - b.Property("SeriesPresentationUniqueKey") - .HasColumnType("TEXT"); - - b.Property("ShowId") - .HasColumnType("TEXT"); - - b.Property("Size") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("StartDate") - .HasColumnType("TEXT"); - - b.Property("Studios") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("Tags") - .HasColumnType("TEXT"); - - b.Property("TopParentId") - .HasColumnType("TEXT"); - - b.Property("TotalBitrate") - .HasColumnType("INTEGER"); - - b.Property("Type") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UnratedType") - .HasColumnType("TEXT"); - - b.Property("Width") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("Name"); - - b.HasIndex("OwnerId"); - - b.HasIndex("ParentId"); - - b.HasIndex("Path"); - - b.HasIndex("PresentationUniqueKey"); - - b.HasIndex("SeasonId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("SeriesName"); - - b.HasIndex("ExtraType", "OwnerId"); - - b.HasIndex("TopParentId", "Id"); - - b.HasIndex("Type", "CleanName"); - - b.HasIndex("TopParentId", "Type", "IsVirtualItem") - .HasFilter("\"PrimaryVersionId\" IS NULL AND (\"OwnerId\" IS NULL OR \"ExtraType\" IS NOT NULL)"); - - b.HasIndex("Type", "TopParentId", "Id"); - - b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); - - b.HasIndex("Type", "TopParentId", "SortName"); - - b.HasIndex("Type", "TopParentId", "StartDate"); - - b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); - - b.HasIndex("TopParentId", "IsFolder", "IsVirtualItem", "DateCreated"); - - b.HasIndex("TopParentId", "MediaType", "IsVirtualItem", "DateCreated"); - - b.HasIndex("TopParentId", "Type", "IsVirtualItem", "DateCreated"); - - b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); - - b.HasIndex("Type", "SeriesPresentationUniqueKey", "ParentIndexNumber", "IndexNumber"); - - b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); - - b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); - - b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); - - b.ToTable("BaseItems"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - - b.HasData( - new - { - Id = new Guid("00000000-0000-0000-0000-000000000001"), - IsFolder = false, - IsInMixedFolder = false, - IsLocked = false, - IsMovie = false, - IsRepeat = false, - IsSeries = false, - IsVirtualItem = false, - Name = "This is a placeholder item for UserData that has been detached from its original item", - Type = "PLACEHOLDER" - }); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("Blurhash") - .HasColumnType("BLOB"); - - b.Property("DateModified") - .HasColumnType("TEXT"); - - b.Property("Height") - .HasColumnType("INTEGER"); - - b.Property("ImageType") - .HasColumnType("INTEGER"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("Path") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Width") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ItemId", "ImageType"); - - b.ToTable("BaseItemImageInfos"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b => - { - b.Property("Id") - .HasColumnType("INTEGER"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.HasKey("Id", "ItemId"); - - b.HasIndex("ItemId"); - - b.ToTable("BaseItemMetadataFields"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("ProviderId") - .HasColumnType("TEXT"); - - b.Property("ProviderValue") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("ItemId", "ProviderId"); - - b.HasIndex("ProviderId", "ItemId", "ProviderValue"); - - b.ToTable("BaseItemProviders"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b => - { - b.Property("Id") - .HasColumnType("INTEGER"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.HasKey("Id", "ItemId"); - - b.HasIndex("ItemId"); - - b.ToTable("BaseItemTrailerTypes"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("ChapterIndex") - .HasColumnType("INTEGER"); - - b.Property("ImageDateModified") - .HasColumnType("TEXT"); - - b.Property("ImagePath") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("StartPositionTicks") - .HasColumnType("INTEGER"); - - b.HasKey("ItemId", "ChapterIndex"); - - b.ToTable("Chapters"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.CustomItemDisplayPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("Key") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "ItemId", "Client", "Key") - .IsUnique(); - - b.ToTable("CustomItemDisplayPreferences"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChromecastVersion") - .HasColumnType("INTEGER"); - - b.Property("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("DashboardTheme") - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("EnableNextVideoInfoOverlay") - .HasColumnType("INTEGER"); - - b.Property("IndexBy") - .HasColumnType("INTEGER"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("ScrollDirection") - .HasColumnType("INTEGER"); - - b.Property("ShowBackdrop") - .HasColumnType("INTEGER"); - - b.Property("ShowSidebar") - .HasColumnType("INTEGER"); - - b.Property("SkipBackwardLength") - .HasColumnType("INTEGER"); - - b.Property("SkipForwardLength") - .HasColumnType("INTEGER"); - - b.Property("TvHome") - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "ItemId", "Client") - .IsUnique(); - - b.ToTable("DisplayPreferences"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DisplayPreferencesId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("DisplayPreferencesId"); - - b.ToTable("HomeSection"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("Path") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("ImageInfos"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("IndexBy") - .HasColumnType("INTEGER"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("RememberIndexing") - .HasColumnType("INTEGER"); - - b.Property("RememberSorting") - .HasColumnType("INTEGER"); - - b.Property("SortBy") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("ViewType") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("ItemDisplayPreferences"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b => - { - b.Property("ItemValueId") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("CleanValue") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.Property("Value") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("ItemValueId"); - - b.HasIndex("Type", "CleanValue"); - - b.HasIndex("Type", "Value") - .IsUnique(); - - b.ToTable("ItemValues"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b => - { - b.Property("ItemValueId") - .HasColumnType("TEXT"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.HasKey("ItemValueId", "ItemId"); - - b.HasIndex("ItemId"); - - b.ToTable("ItemValuesMap"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("KeyframeTicks") - .HasColumnType("TEXT"); - - b.Property("TotalDuration") - .HasColumnType("INTEGER"); - - b.HasKey("ItemId"); - - b.ToTable("KeyframeData"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.LinkedChildEntity", b => - { - b.Property("ParentId") - .HasColumnType("TEXT"); - - b.Property("ChildId") - .HasColumnType("TEXT"); - - b.Property("ChildType") - .HasColumnType("INTEGER"); - - b.Property("SortOrder") - .HasColumnType("INTEGER"); - - b.HasKey("ParentId", "ChildId"); - - b.HasIndex("ChildId", "ChildType"); - - b.HasIndex("ParentId", "ChildType"); - - b.HasIndex("ParentId", "SortOrder"); - - b.ToTable("LinkedChildren", (string)null); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("EndTicks") - .HasColumnType("INTEGER"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("SegmentProviderId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("StartTicks") - .HasColumnType("INTEGER"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("MediaSegments"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("StreamIndex") - .HasColumnType("INTEGER"); - - b.Property("AspectRatio") - .HasColumnType("TEXT"); - - b.Property("AverageFrameRate") - .HasColumnType("REAL"); - - b.Property("BitDepth") - .HasColumnType("INTEGER"); - - b.Property("BitRate") - .HasColumnType("INTEGER"); - - b.Property("BlPresentFlag") - .HasColumnType("INTEGER"); - - b.Property("ChannelLayout") - .HasColumnType("TEXT"); - - b.Property("Channels") - .HasColumnType("INTEGER"); - - b.Property("Codec") - .HasColumnType("TEXT"); - - b.Property("CodecTag") - .HasColumnType("TEXT"); - - b.Property("CodecTimeBase") - .HasColumnType("TEXT"); - - b.Property("ColorPrimaries") - .HasColumnType("TEXT"); - - b.Property("ColorSpace") - .HasColumnType("TEXT"); - - b.Property("ColorTransfer") - .HasColumnType("TEXT"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("DvBlSignalCompatibilityId") - .HasColumnType("INTEGER"); - - b.Property("DvLevel") - .HasColumnType("INTEGER"); - - b.Property("DvProfile") - .HasColumnType("INTEGER"); - - b.Property("DvVersionMajor") - .HasColumnType("INTEGER"); - - b.Property("DvVersionMinor") - .HasColumnType("INTEGER"); - - b.Property("ElPresentFlag") - .HasColumnType("INTEGER"); - - b.Property("Hdr10PlusPresentFlag") - .HasColumnType("INTEGER"); - - b.Property("Height") - .HasColumnType("INTEGER"); - - b.Property("IsAnamorphic") - .HasColumnType("INTEGER"); - - b.Property("IsAvc") - .HasColumnType("INTEGER"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("IsExternal") - .HasColumnType("INTEGER"); - - b.Property("IsForced") - .HasColumnType("INTEGER"); - - b.Property("IsHearingImpaired") - .HasColumnType("INTEGER"); - - b.Property("IsInterlaced") - .HasColumnType("INTEGER"); - - b.Property("IsOriginal") - .HasColumnType("INTEGER"); - - b.Property("KeyFrames") - .HasColumnType("TEXT"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("Level") - .HasColumnType("REAL"); - - b.Property("NalLengthSize") - .HasColumnType("TEXT"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.Property("PixelFormat") - .HasColumnType("TEXT"); - - b.Property("Profile") - .HasColumnType("TEXT"); - - b.Property("RealFrameRate") - .HasColumnType("REAL"); - - b.Property("RefFrames") - .HasColumnType("INTEGER"); - - b.Property("Rotation") - .HasColumnType("INTEGER"); - - b.Property("RpuPresentFlag") - .HasColumnType("INTEGER"); - - b.Property("SampleRate") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .HasColumnType("INTEGER"); - - b.Property("TimeBase") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("Width") - .HasColumnType("INTEGER"); - - b.HasKey("ItemId", "StreamIndex"); - - b.ToTable("MediaStreamInfos"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PersonType") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Name"); - - b.ToTable("Peoples"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("PeopleId") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("TEXT"); - - b.Property("ListOrder") - .HasColumnType("INTEGER"); - - b.Property("SortOrder") - .HasColumnType("INTEGER"); - - b.HasKey("ItemId", "PeopleId", "Role"); - - b.HasIndex("PeopleId"); - - b.HasIndex("ItemId", "ListOrder"); - - b.HasIndex("ItemId", "SortOrder"); - - b.ToTable("PeopleBaseItemMap"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("Permission_Permissions_Guid") - .HasColumnType("TEXT"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "Kind") - .IsUnique() - .HasFilter("[UserId] IS NOT NULL"); - - b.ToTable("Permissions"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("Preference_Preferences_Guid") - .HasColumnType("TEXT"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(65535) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "Kind") - .IsUnique() - .HasFilter("[UserId] IS NOT NULL"); - - b.ToTable("Preferences"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.ApiKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessToken") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("DateCreated") - .HasColumnType("TEXT"); - - b.Property("DateLastActivity") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AccessToken") - .IsUnique(); - - b.ToTable("ApiKeys"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessToken") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("AppName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("TEXT"); - - b.Property("AppVersion") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("DateCreated") - .HasColumnType("TEXT"); - - b.Property("DateLastActivity") - .HasColumnType("TEXT"); - - b.Property("DateModified") - .HasColumnType("TEXT"); - - b.Property("DeviceId") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("DeviceName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("TEXT"); - - b.Property("IsActive") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AccessToken", "DateLastActivity"); - - b.HasIndex("DeviceId", "DateLastActivity"); - - b.HasIndex("UserId", "DeviceId"); - - b.ToTable("Devices"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.DeviceOptions", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CustomName") - .HasColumnType("TEXT"); - - b.Property("DeviceId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("DeviceId") - .IsUnique(); - - b.ToTable("DeviceOptions"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.TrickplayInfo", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("Width") - .HasColumnType("INTEGER"); - - b.Property("Bandwidth") - .HasColumnType("INTEGER"); - - b.Property("Height") - .HasColumnType("INTEGER"); - - b.Property("Interval") - .HasColumnType("INTEGER"); - - b.Property("ThumbnailCount") - .HasColumnType("INTEGER"); - - b.Property("TileHeight") - .HasColumnType("INTEGER"); - - b.Property("TileWidth") - .HasColumnType("INTEGER"); - - b.HasKey("ItemId", "Width"); - - b.ToTable("TrickplayInfos"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("AudioLanguagePreference") - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property("AuthenticationProviderId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property("CastReceiverId") - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("DisplayCollectionsView") - .HasColumnType("INTEGER"); - - b.Property("DisplayMissingEpisodes") - .HasColumnType("INTEGER"); - - b.Property("EnableAutoLogin") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalPassword") - .HasColumnType("INTEGER"); - - b.Property("EnableNextEpisodeAutoPlay") - .HasColumnType("INTEGER"); - - b.Property("EnableUserPreferenceAccess") - .HasColumnType("INTEGER"); - - b.Property("HidePlayedInLatest") - .HasColumnType("INTEGER"); - - b.Property("InternalId") - .HasColumnType("INTEGER"); - - b.Property("InvalidLoginAttemptCount") - .HasColumnType("INTEGER"); - - b.Property("LastActivityDate") - .HasColumnType("TEXT"); - - b.Property("LastLoginDate") - .HasColumnType("TEXT"); - - b.Property("LoginAttemptsBeforeLockout") - .HasColumnType("INTEGER"); - - b.Property("MaxActiveSessions") - .HasColumnType("INTEGER"); - - b.Property("MaxParentalRatingScore") - .HasColumnType("INTEGER"); - - b.Property("MaxParentalRatingSubScore") - .HasColumnType("INTEGER"); - - b.Property("MustUpdatePassword") - .HasColumnType("INTEGER"); - - b.Property("Password") - .HasMaxLength(65535) - .HasColumnType("TEXT"); - - b.Property("PasswordResetProviderId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property("PlayDefaultAudioTrack") - .HasColumnType("INTEGER"); - - b.Property("RememberAudioSelections") - .HasColumnType("INTEGER"); - - b.Property("RememberSubtitleSelections") - .HasColumnType("INTEGER"); - - b.Property("RemoteClientBitrateLimit") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SubtitleLanguagePreference") - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property("SubtitleMode") - .HasColumnType("INTEGER"); - - b.Property("SyncPlayAccess") - .HasColumnType("INTEGER"); - - b.Property("Username") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("Users"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("CustomDataKey") - .HasColumnType("TEXT"); - - b.Property("AudioStreamIndex") - .HasColumnType("INTEGER"); - - b.Property("IsFavorite") - .HasColumnType("INTEGER"); - - b.Property("LastPlayedDate") - .HasColumnType("TEXT"); - - b.Property("Likes") - .HasColumnType("INTEGER"); - - b.Property("PlayCount") - .HasColumnType("INTEGER"); - - b.Property("PlaybackPositionTicks") - .HasColumnType("INTEGER"); - - b.Property("Played") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("RetentionDate") - .HasColumnType("TEXT"); - - b.Property("SubtitleStreamIndex") - .HasColumnType("INTEGER"); - - b.HasKey("ItemId", "UserId", "CustomDataKey"); - - b.HasIndex("ItemId", "UserId", "IsFavorite"); - - b.HasIndex("ItemId", "UserId", "LastPlayedDate"); - - b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks"); - - b.HasIndex("ItemId", "UserId", "Played"); - - b.HasIndex("UserId", "IsFavorite", "ItemId"); - - b.HasIndex("UserId", "ItemId", "LastPlayedDate"); - - b.HasIndex("UserId", "Played", "ItemId"); - - b.ToTable("UserData"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) - .WithMany("AccessSchedules") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany("Parents") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem") - .WithMany("Children") - .HasForeignKey("ParentItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - - b.Navigation("ParentItem"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany() - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Owner") - .WithMany("Extras") - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.NoAction); - - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "DirectParent") - .WithMany("DirectChildren") - .HasForeignKey("ParentId") - .OnDelete(DeleteBehavior.Cascade); - - b.Navigation("DirectParent"); - - b.Navigation("Owner"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany("Images") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany("LockedFields") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany("Provider") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany("TrailerTypes") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany("Chapters") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) - .WithMany("DisplayPreferences") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null) - .WithMany("HomeSections") - .HasForeignKey("DisplayPreferencesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) - .WithOne("ProfileImage") - .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) - .WithMany("ItemDisplayPreferences") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany("ItemValues") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue") - .WithMany("BaseItemsMap") - .HasForeignKey("ItemValueId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - - b.Navigation("ItemValue"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany() - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.LinkedChildEntity", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Child") - .WithMany("LinkedChildOfEntities") - .HasForeignKey("ChildId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Parent") - .WithMany("LinkedChildEntities") - .HasForeignKey("ParentId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.Navigation("Child"); - - b.Navigation("Parent"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany("MediaStreams") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany("Peoples") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People") - .WithMany("BaseItems") - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - - b.Navigation("People"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) - .WithMany("Permissions") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) - .WithMany("Preferences") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany("UserData") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => - { - b.Navigation("Chapters"); - - b.Navigation("Children"); - - b.Navigation("DirectChildren"); - - b.Navigation("Extras"); - - b.Navigation("Images"); - - b.Navigation("ItemValues"); - - b.Navigation("LinkedChildEntities"); - - b.Navigation("LinkedChildOfEntities"); - - b.Navigation("LockedFields"); - - b.Navigation("MediaStreams"); - - b.Navigation("Parents"); - - b.Navigation("Peoples"); - - b.Navigation("Provider"); - - b.Navigation("TrailerTypes"); - - b.Navigation("UserData"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => - { - b.Navigation("HomeSections"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b => - { - b.Navigation("BaseItemsMap"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b => - { - b.Navigation("BaseItems"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b => - { - b.Navigation("AccessSchedules"); - - b.Navigation("DisplayPreferences"); - - b.Navigation("ItemDisplayPreferences"); - - b.Navigation("Permissions"); - - b.Navigation("Preferences"); - - b.Navigation("ProfileImage"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504180809_AddOriginalLanguage.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504180809_AddOriginalLanguage.cs deleted file mode 100644 index cda226309a..0000000000 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504180809_AddOriginalLanguage.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Jellyfin.Database.Providers.Sqlite.Migrations -{ - /// - public partial class AddOriginalLanguage : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "IsOriginal", - table: "MediaStreamInfos", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "OriginalLanguage", - table: "BaseItems", - type: "TEXT", - nullable: true); - - migrationBuilder.UpdateData( - table: "BaseItems", - keyColumn: "Id", - keyValue: new Guid("00000000-0000-0000-0000-000000000001"), - column: "OriginalLanguage", - value: null); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "IsOriginal", - table: "MediaStreamInfos"); - - migrationBuilder.DropColumn( - name: "OriginalLanguage", - table: "BaseItems"); - } - } -} diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 3f7ae4d2cd..b6d2914efa 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -234,20 +234,20 @@ public class SkiaEncoder : IImageEncoder return default; default: - { - var boundsInfo = SKBitmap.DecodeBounds(safePath); - if (boundsInfo.Width > 0 && boundsInfo.Height > 0) { - return new ImageDimensions(boundsInfo.Width, boundsInfo.Height); + var boundsInfo = SKBitmap.DecodeBounds(safePath); + if (boundsInfo.Width > 0 && boundsInfo.Height > 0) + { + return new ImageDimensions(boundsInfo.Width, boundsInfo.Height); + } + + _logger.LogWarning( + "Unable to determine image dimensions for {FilePath}: {SkCodecResult}", + path, + result); + + return default; } - - _logger.LogWarning( - "Unable to determine image dimensions for {FilePath}: {SkCodecResult}", - path, - result); - - return default; - } } } finally @@ -398,7 +398,7 @@ public class SkiaEncoder : IImageEncoder try { - // If we have to resize these they often end up distorted + // If we have to resize these they often end up distorted if (resultBitmap.ColorType == SKColorType.Gray8) { using (resultBitmap) diff --git a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs index 2b8e5a0a08..ed02fe6a1d 100644 --- a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs +++ b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs @@ -1129,7 +1129,7 @@ namespace Jellyfin.LiveTv.Channels { if (!item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase)) { - item.Tags = [..item.Tags, "livestream"]; + item.Tags = [.. item.Tags, "livestream"]; _logger.LogDebug("Forcing update due to Tags {0}", item.Name); forceUpdate = true; } diff --git a/src/Jellyfin.LiveTv/Listings/ListingsManager.cs b/src/Jellyfin.LiveTv/Listings/ListingsManager.cs index 58683deb30..15e20d6f64 100644 --- a/src/Jellyfin.LiveTv/Listings/ListingsManager.cs +++ b/src/Jellyfin.LiveTv/Listings/ListingsManager.cs @@ -67,7 +67,7 @@ public class ListingsManager : IListingsManager if (index == -1 || string.IsNullOrWhiteSpace(info.Id)) { info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - config.ListingProviders = [..list, info]; + config.ListingProviders = [.. list, info]; } else { @@ -255,7 +255,7 @@ public class ListingsManager : IListingsManager Name = tunerChannelNumber, Value = providerChannelNumber }; - listingsProviderInfo.ChannelMappings = [..listingsProviderInfo.ChannelMappings, newItem]; + listingsProviderInfo.ChannelMappings = [.. listingsProviderInfo.ChannelMappings, newItem]; } _config.SaveConfiguration("livetv", config); diff --git a/src/Jellyfin.LiveTv/LiveTvManager.cs b/src/Jellyfin.LiveTv/LiveTvManager.cs index 2abc8a8c09..173d3c3e8e 100644 --- a/src/Jellyfin.LiveTv/LiveTvManager.cs +++ b/src/Jellyfin.LiveTv/LiveTvManager.cs @@ -178,6 +178,11 @@ namespace Jellyfin.LiveTv { var program = _libraryManager.GetItemById(id); + if (program is null) + { + return null; + } + var dto = _dtoService.GetBaseItemDto(program, new DtoOptions(), user); var list = new List<(BaseItemDto ItemDto, string ExternalId, string ExternalSeriesId)> diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs b/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs index 3a2c463695..7e68dbb547 100644 --- a/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs +++ b/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs @@ -288,7 +288,7 @@ public class RecordingsMetadataManager null, "dateadded", null, - DateTime.Now.ToString(DateAddedFormat, CultureInfo.InvariantCulture)).ConfigureAwait(false); + DateTime.UtcNow.ToString(DateAddedFormat, CultureInfo.InvariantCulture)).ConfigureAwait(false); if (item.ProductionYear.HasValue) { diff --git a/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs b/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs index 6a68b8c25c..74fa1415c6 100644 --- a/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs +++ b/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs @@ -116,7 +116,7 @@ namespace Jellyfin.LiveTv.Timers throw new ArgumentException("item already exists", nameof(item)); } - _items = [.._items, item]; + _items = [.. _items, item]; SaveList(); } @@ -131,7 +131,7 @@ namespace Jellyfin.LiveTv.Timers int index = Array.FindIndex(_items, i => EqualityComparer(i, item)); if (index == -1) { - _items = [.._items, item]; + _items = [.. _items, item]; } else { diff --git a/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs b/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs index cfd763b6fd..7c16d2b363 100644 --- a/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs @@ -83,7 +83,7 @@ public class TunerHostManager : ITunerHostManager if (index == -1 || string.IsNullOrWhiteSpace(info.Id)) { info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - config.TunerHosts = [..list, info]; + config.TunerHosts = [.. list, info]; } else { diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 0fe2fc43ad..4559f68ce8 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -356,7 +356,7 @@ public class NetworkManager : INetworkManager, IDisposable { lock (_initLock) { - _interfaces = FilterBindSettings(config, _interfaces, IsIPv4Enabled, IsIPv6Enabled).ToList(); + _interfaces = FilterBindSettings(config, _interfaces, IsIPv4Enabled, IsIPv6Enabled).ToList(); } } diff --git a/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs b/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs new file mode 100644 index 0000000000..d7ae6a8a18 --- /dev/null +++ b/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Jellyfin.Data.Enums; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Streaming; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using Moq; +using Xunit; + +using IConfiguration = Microsoft.Extensions.Configuration.IConfiguration; + +namespace Jellyfin.Controller.Tests.MediaEncoding; + +public class EncodingHelperTests +{ + [Fact] + public void GetMapArgs_NoSubtitle_ExcludesAllSubs() + { + var state = BuildState(subtitle: null, deliveryMethod: null); + var args = CreateHelper().GetMapArgs(state); + + Assert.Contains("-map -0:s", args, StringComparison.Ordinal); + Assert.DoesNotContain("-map 1:", args, StringComparison.Ordinal); + } + + [Fact] + public void GetMapArgs_InternalSrt_MapsFromPrimaryInput() + { + var sub = new MediaStream { Index = 2, Type = MediaStreamType.Subtitle, Codec = "srt" }; + var state = BuildState(sub, SubtitleDeliveryMethod.Embed); + var args = CreateHelper().GetMapArgs(state); + + Assert.Contains("-map 0:2", args, StringComparison.Ordinal); + Assert.DoesNotContain("-map 1:", args, StringComparison.Ordinal); + } + + [Fact] + public void GetMapArgs_InternalSubAtHigherIndex_MapsCorrectIndex() + { + var sub0 = new MediaStream { Index = 2, Type = MediaStreamType.Subtitle, Codec = "srt" }; + var sub1 = new MediaStream { Index = 3, Type = MediaStreamType.Subtitle, Codec = "ass" }; + var state = BuildState(sub1, SubtitleDeliveryMethod.Embed, additionalStreams: [sub0, sub1]); + var args = CreateHelper().GetMapArgs(state); + + Assert.Contains("-map 0:3", args, StringComparison.Ordinal); + } + + [Fact] + public void GetMapArgs_ExternalSrt_MapsFirstStreamFromInput1() + { + var sub = new MediaStream + { + Index = 2, + Type = MediaStreamType.Subtitle, + Codec = "srt", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.en.srt" + }; + var state = BuildState(sub, SubtitleDeliveryMethod.Embed); + var args = CreateHelper().GetMapArgs(state); + + Assert.Contains("-map 1:0", args, StringComparison.Ordinal); + } + + [Fact] + public void GetMapArgs_SecondExternalSrt_StillMaps1Colon0() + { + // Two separate .srt files — selecting the second one still maps 1:0 + // because Jellyfin feeds only the selected file as ffmpeg input 1. + var ext1 = new MediaStream + { + Index = 2, + Type = MediaStreamType.Subtitle, + Codec = "srt", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.en.srt" + }; + var ext2 = new MediaStream + { + Index = 3, + Type = MediaStreamType.Subtitle, + Codec = "srt", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.fr.srt" + }; + var state = BuildState(ext2, SubtitleDeliveryMethod.Embed, additionalStreams: [ext1, ext2]); + var args = CreateHelper().GetMapArgs(state); + + Assert.Contains("-map 1:0", args, StringComparison.Ordinal); + } + + [Fact] + public void GetMapArgs_MksFirstTrack_MapsInFileIndex0() + { + var mks0 = new MediaStream + { + Index = 2, + Type = MediaStreamType.Subtitle, + Codec = "subrip", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.mks" + }; + var mks1 = new MediaStream + { + Index = 3, + Type = MediaStreamType.Subtitle, + Codec = "ass", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.mks" + }; + var state = BuildState(mks0, SubtitleDeliveryMethod.Embed, additionalStreams: [mks0, mks1]); + var args = CreateHelper().GetMapArgs(state); + + Assert.Contains("-map 1:0", args, StringComparison.Ordinal); + } + + [Fact] + public void GetMapArgs_MksSecondTrack_MapsInFileIndex1() + { + var mks0 = new MediaStream + { + Index = 2, + Type = MediaStreamType.Subtitle, + Codec = "subrip", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.mks" + }; + var mks1 = new MediaStream + { + Index = 3, + Type = MediaStreamType.Subtitle, + Codec = "ass", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.mks" + }; + var mks2 = new MediaStream + { + Index = 4, + Type = MediaStreamType.Subtitle, + Codec = "subrip", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.mks" + }; + var state = BuildState(mks1, SubtitleDeliveryMethod.Embed, additionalStreams: [mks0, mks1, mks2]); + var args = CreateHelper().GetMapArgs(state); + + Assert.Contains("-map 1:1", args, StringComparison.Ordinal); + } + + [Theory] + [InlineData(SubtitleDeliveryMethod.Embed, true, "movie.idx")] + [InlineData(SubtitleDeliveryMethod.Encode, true, "movie.idx")] + [InlineData(SubtitleDeliveryMethod.Embed, false, "movie.sub")] + [InlineData(SubtitleDeliveryMethod.Encode, false, "movie.sub")] + public void GetInputArgument_VobSub_UsesCorrectPath( + SubtitleDeliveryMethod deliveryMethod, + bool createIdxFile, + string expectedFilename) + { + var tempDir = Directory.CreateTempSubdirectory("jellyfin-test-"); + try + { + var subFile = Path.Combine(tempDir.FullName, "movie.sub"); + File.WriteAllText(subFile, "dummy"); + + if (createIdxFile) + { + File.WriteAllText(Path.Combine(tempDir.FullName, "movie.idx"), "dummy"); + } + + var sub = new MediaStream + { + Index = 2, + Type = MediaStreamType.Subtitle, + Codec = "dvdsub", + IsExternal = true, + SupportsExternalStream = true, + Path = subFile + }; + var state = BuildState(sub, deliveryMethod); + var inputArgs = CreateHelper().GetInputArgument(state, new EncodingOptions(), null); + + Assert.Contains(expectedFilename, inputArgs, StringComparison.Ordinal); + } + finally + { + tempDir.Delete(true); + } + } + + private static EncodingJobInfo BuildState( + MediaStream? subtitle, + SubtitleDeliveryMethod? deliveryMethod, + MediaStream[]? additionalStreams = null) + { + var video = new MediaStream { Index = 0, Type = MediaStreamType.Video, Codec = "h264" }; + var audio = new MediaStream { Index = 1, Type = MediaStreamType.Audio, Codec = "aac" }; + var streams = new List { video, audio }; + + if (additionalStreams is not null) + { + streams.AddRange(additionalStreams); + } + else if (subtitle is not null) + { + streams.Add(subtitle); + } + + return new EncodingJobInfo(TranscodingJobType.Progressive) + { + MediaSource = new MediaSourceInfo + { + Container = "mkv", + MediaStreams = streams, + }, + VideoStream = video, + AudioStream = audio, + SubtitleStream = subtitle, + SubtitleDeliveryMethod = deliveryMethod ?? SubtitleDeliveryMethod.Drop, + BaseRequest = new VideoRequestDto(), + IsVideoRequest = true, + IsInputVideo = true, + }; + } + + private static EncodingHelper CreateHelper() + { + var appPaths = Mock.Of(); + var mediaEncoder = new Mock(); + var subtitleEncoder = new Mock(); + var config = new Mock(); + var configurationManager = new Mock(); + var pathManager = new Mock(); + + return new EncodingHelper( + appPaths, + mediaEncoder.Object, + subtitleEncoder.Object, + config.Object, + configurationManager.Object, + pathManager.Object); + } +} diff --git a/tests/Jellyfin.LiveTv.Tests/Recordings/RecordingsMetadataManagerTests.cs b/tests/Jellyfin.LiveTv.Tests/Recordings/RecordingsMetadataManagerTests.cs new file mode 100644 index 0000000000..14ce470fb4 --- /dev/null +++ b/tests/Jellyfin.LiveTv.Tests/Recordings/RecordingsMetadataManagerTests.cs @@ -0,0 +1,64 @@ +using System; +using System.Globalization; +using System.IO; +using System.Threading.Tasks; +using System.Xml; +using Jellyfin.Extensions; +using Jellyfin.LiveTv.Recordings; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.LiveTv; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; + +namespace Jellyfin.LiveTv.Tests.Recordings; + +public sealed class RecordingsMetadataManagerTests +{ + private readonly string _tempDir = + Path.Combine(Path.GetTempPath(), "jellyfin-test-" + Guid.NewGuid()); + + [Fact] + public async Task SaveRecordingMetadata_DateAddedIsUtc() + { + Directory.CreateDirectory(_tempDir); + var recordingPath = Path.Combine(_tempDir, "test-recording.ts"); + FileHelper.CreateEmpty(recordingPath); + + var config = new Mock(); + config.Setup(c => c.GetConfiguration("livetv")) + .Returns(new LiveTvOptions { SaveRecordingNFO = true, SaveRecordingImages = false }); + config.Setup(c => c.GetConfiguration("xbmcmetadata")) + .Returns(new XbmcMetadataOptions()); + + var libraryManager = new Mock(); + libraryManager + .Setup(l => l.GetItemList(It.IsAny())) + .Returns(Array.Empty()); + + var manager = new RecordingsMetadataManager( + NullLogger.Instance, + config.Object, + libraryManager.Object); + + var timer = new TimerInfo { Name = "Test Recording", ProgramId = null }; + + var beforeUtc = DateTime.UtcNow.AddSeconds(-2); + await manager.SaveRecordingMetadata(timer, recordingPath, null); + var afterUtc = DateTime.UtcNow.AddSeconds(2); + + var doc = new XmlDocument(); + doc.Load(Path.ChangeExtension(recordingPath, ".nfo")); + var dateAddedText = doc.SelectSingleNode("//dateadded")?.InnerText ?? string.Empty; + var parsed = DateTime.ParseExact( + dateAddedText, + "yyyy-MM-dd HH:mm:ss", + CultureInfo.InvariantCulture); + + Assert.InRange(parsed, beforeUtc, afterUtc); + } +} diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 0b103debad..16c586bcda 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -675,5 +675,59 @@ namespace Jellyfin.Model.Tests Assert.Equal(expectedMethod, result.Method); } + + [Theory] + // External text subs embedded into MKV when transcoding (#16403) + [InlineData("srt", true, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)] + [InlineData("ass", true, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)] + // External graphical subs embedded into MKV when transcoding + [InlineData("pgssub", true, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)] + [InlineData("dvdsub", true, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)] + // External subs remain external when transcoding to non-MKV containers + [InlineData("srt", true, PlayMethod.Transcode, "mp4", MediaStreamProtocol.hls, SubtitleDeliveryMethod.External)] + [InlineData("srt", true, PlayMethod.Transcode, "ts", MediaStreamProtocol.hls, SubtitleDeliveryMethod.External)] + // External subs remain external during DirectPlay even with MKV + [InlineData("srt", true, PlayMethod.DirectPlay, "mkv", null, SubtitleDeliveryMethod.External)] + // Internal subs still embedded into MKV when transcoding (existing behavior) + [InlineData("srt", false, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)] + [InlineData("pgssub", false, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)] + public void GetSubtitleProfile_ReturnsExpectedDeliveryMethod( + string codec, + bool isExternal, + PlayMethod playMethod, + string outputContainer, + MediaStreamProtocol? transcodingSubProtocol, + SubtitleDeliveryMethod expectedMethod) + { + var mediaSource = new MediaSourceInfo(); + var subtitleStream = new MediaStream + { + Codec = codec, + Language = "eng", + IsExternal = isExternal, + Type = MediaStreamType.Subtitle, + SupportsExternalStream = true + }; + + var subtitleProfiles = new[] + { + new SubtitleProfile { Format = codec, Method = SubtitleDeliveryMethod.Embed }, + new SubtitleProfile { Format = codec, Method = SubtitleDeliveryMethod.External } + }; + + var transcoderSupport = new Mock(); + transcoderSupport.Setup(x => x.CanExtractSubtitles(It.IsAny())).Returns(true); + + var result = StreamBuilder.GetSubtitleProfile( + mediaSource, + subtitleStream, + subtitleProfiles, + playMethod, + transcoderSupport.Object, + outputContainer, + transcodingSubProtocol); + + Assert.Equal(expectedMethod, result.Method); + } } } diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 66eec077dc..1f523f7f21 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -135,6 +135,65 @@ namespace Jellyfin.Networking.Tests Times.Once); } + /// + /// Verifies that IPv4 entries whose '!' polarity doesn't match the requested pass are skipped silently, + /// not logged as invalid. Callers parse the same list twice (LAN and excluded) so the off-polarity + /// entries are expected, not erroneous. + /// + [Fact] + public static void TryParseToSubnets_PolarityMismatchIPv4_DoesNotWarn() + { + var logger = new Mock(); + var values = new[] { "127.0.0.0/8", "192.168.178.0/24", "!10.0.0.0/8" }; + + // Non-negated pass picks up the two non-'!' entries and ignores '!10.0.0.0/8' silently. + Assert.True(NetworkUtils.TryParseToSubnets(values, out var lanResult, false, logger.Object)); + Assert.NotNull(lanResult); + Assert.Equal(2, lanResult.Count); + + // Negated pass picks up the single '!' entry and ignores the others silently. + Assert.True(NetworkUtils.TryParseToSubnets(values, out var excludedResult, true, logger.Object)); + Assert.NotNull(excludedResult); + Assert.Single(excludedResult); + + logger.Verify( + l => l.Log( + LogLevel.Warning, + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>()), + Times.Never); + } + + /// + /// Same as the IPv4 case but for IPv6 entries — makes sure the polarity pre-check works + /// for IPv6 CIDR notation (with '::') as well. + /// + [Fact] + public static void TryParseToSubnets_PolarityMismatchIPv6_DoesNotWarn() + { + var logger = new Mock(); + var values = new[] { "fd00::/8", "fe80::/10", "!fd12:3456:789a::/48" }; + + Assert.True(NetworkUtils.TryParseToSubnets(values, out var lanResult, false, logger.Object)); + Assert.NotNull(lanResult); + Assert.Equal(2, lanResult.Count); + + Assert.True(NetworkUtils.TryParseToSubnets(values, out var excludedResult, true, logger.Object)); + Assert.NotNull(excludedResult); + Assert.Single(excludedResult); + + logger.Verify( + l => l.Log( + LogLevel.Warning, + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>()), + Times.Never); + } + /// /// Checks if IPv4 address is within a defined subnet. /// diff --git a/tests/Jellyfin.Providers.Tests/ExternalId/ComicVineExternalUrlProviderTests.cs b/tests/Jellyfin.Providers.Tests/ExternalId/ComicVineExternalUrlProviderTests.cs index 99604e0933..aaa500b762 100644 --- a/tests/Jellyfin.Providers.Tests/ExternalId/ComicVineExternalUrlProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/ExternalId/ComicVineExternalUrlProviderTests.cs @@ -1,7 +1,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Model.Entities; -using MediaBrowser.Providers.Plugins.ComicVine; +using MediaBrowser.Providers.Books.ComicVine; using Xunit; namespace Jellyfin.Providers.Tests.ExternalId diff --git a/tests/Jellyfin.Providers.Tests/ExternalId/GoogleBooksExternalUrlProviderTests.cs b/tests/Jellyfin.Providers.Tests/ExternalId/GoogleBooksExternalUrlProviderTests.cs index eec64ac53f..b9ce895dbc 100644 --- a/tests/Jellyfin.Providers.Tests/ExternalId/GoogleBooksExternalUrlProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/ExternalId/GoogleBooksExternalUrlProviderTests.cs @@ -1,7 +1,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Model.Entities; -using MediaBrowser.Providers.Plugins.GoogleBooks; +using MediaBrowser.Providers.Books.GoogleBooks; using Xunit; namespace Jellyfin.Providers.Tests.ExternalId diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs index a7491f42e9..2438ef06d1 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs @@ -37,9 +37,9 @@ public class FFProbeVideoInfoTests { Assert.Throws( () => _fFProbeVideoInfo.CreateDummyChapters(new Video() - { - RunTimeTicks = runtime - })); + { + RunTimeTicks = runtime + })); } [Theory] @@ -53,9 +53,9 @@ public class FFProbeVideoInfoTests public void CreateDummyChapters_ValidRuntime_CorrectChaptersCount(long? runtime, int chaptersCount) { var chapters = _fFProbeVideoInfo.CreateDummyChapters(new Video() - { - RunTimeTicks = runtime - }); + { + RunTimeTicks = runtime + }); Assert.Equal(chaptersCount, chapters.Length); } @@ -69,9 +69,9 @@ public class FFProbeVideoInfoTests public void CreateDummyChapters_PositiveRuntime_NoChapterBeyondRuntime(long runtime) { var chapters = _fFProbeVideoInfo.CreateDummyChapters(new Video() - { - RunTimeTicks = runtime - }); + { + RunTimeTicks = runtime + }); Assert.All(chapters, chapter => Assert.True(chapter.StartPositionTicks < runtime)); }