Merge remote-tracking branch 'upstream/master' into safeguard-invalid-provider-ids
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "10.0.10",
|
||||
"version": "10.0.11",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
||||
@@ -32,13 +32,13 @@ jobs:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
||||
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
||||
uses: github/codeql-action/autobuild@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
||||
uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
||||
|
||||
+4
-1
@@ -143,6 +143,7 @@
|
||||
- [Smith00101010](https://github.com/Smith00101010)
|
||||
- [sorinyo2004](https://github.com/sorinyo2004)
|
||||
- [Soumyadip Auddy](https://github.com/SoumyadipAuddy)
|
||||
- [st7105](https://github.com/st7105)
|
||||
- [sparky8251](https://github.com/sparky8251)
|
||||
- [spookbits](https://github.com/spookbits)
|
||||
- [ssenart](https://github.com/ssenart)
|
||||
@@ -237,6 +238,8 @@
|
||||
- [elio42](https://github.com/elio42)
|
||||
- [rwebster85](https://github.com/rwebster85)
|
||||
- [Florin-Popescu](https://github.com/Florin-Popescu)
|
||||
- [m0g3r](https://github.com/m0g3r)
|
||||
- [martin-77](https://github.com/martin-77)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
@@ -247,7 +250,7 @@
|
||||
- [Mark2xv](https://github.com/Mark2xv)
|
||||
- [ScottRapsey](https://github.com/ScottRapsey)
|
||||
- [skynet600](https://github.com/skynet600)
|
||||
- [Cheesegeezer](https://githum.com/Cheesegeezer)
|
||||
- [Cheesegeezer](https://github.com/Cheesegeezer)
|
||||
- [Radeon](https://github.com/radeonorama)
|
||||
- [gcw07](https://github.com/gcw07)
|
||||
- [SivaramAdhiappan](https://github.com/shivaram1190)
|
||||
|
||||
+23
-23
@@ -26,28 +26,28 @@
|
||||
<PackageVersion Include="libse" Version="4.0.12" />
|
||||
<PackageVersion Include="LrcParser" Version="2025.623.0" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="5.6.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.6.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.6.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.9.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.9.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.9.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />
|
||||
<PackageVersion Include="MimeTypes" Version="2.5.2" />
|
||||
<PackageVersion Include="Morestachio" Version="5.0.1.670" />
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
@@ -78,9 +78,9 @@
|
||||
<PackageVersion Include="Svg.Skia" Version="3.7.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.2.3" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.2.3" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.10" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.11" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="7.15.3" />
|
||||
<PackageVersion Include="z440.atl.core" Version="7.16.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="3.0.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||
|
||||
@@ -151,7 +151,7 @@ namespace Emby.Naming.Common
|
||||
|
||||
CleanStrings =
|
||||
[
|
||||
@"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS)(?=[ _\,\.\(\)\[\]\-]|$)",
|
||||
@"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|web-dl|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS)(?=[ _\,\.\(\)\[\]\-]|$)",
|
||||
@"^\s*(?<cleaned>.+?)((\s*\[[^\]]+\]\s*)+)(\.[^\s]+)?$",
|
||||
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
|
||||
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
|
||||
|
||||
@@ -412,6 +412,13 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
_persistenceService.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]);
|
||||
|
||||
// Evict the deleted items from the cache and announce each removal.
|
||||
foreach (var (item, _, _) in pathMaps)
|
||||
{
|
||||
_cache.TryRemove(item.Id, out _);
|
||||
ReportItemRemoved(item, item.GetOwner() ?? item.GetParent());
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
|
||||
@@ -611,6 +618,12 @@ namespace Emby.Server.Implementations.Library
|
||||
folder.UserData = null;
|
||||
}
|
||||
|
||||
// Announce the descendants before the item itself.
|
||||
foreach (var child in children)
|
||||
{
|
||||
ReportItemRemoved(child, item);
|
||||
}
|
||||
|
||||
ReportItemRemoved(item, parent);
|
||||
}
|
||||
|
||||
|
||||
@@ -92,37 +92,33 @@ public class SearchManager : ISearchManager
|
||||
await Task.WhenAll(externalTask, internalTask).ConfigureAwait(false);
|
||||
|
||||
var externalResults = await externalTask.ConfigureAwait(false);
|
||||
var fromExternal = externalResults.Count > 0;
|
||||
IReadOnlyList<SearchResult> results;
|
||||
if (fromExternal)
|
||||
{
|
||||
results = externalResults;
|
||||
}
|
||||
else
|
||||
{
|
||||
results = await internalTask.ConfigureAwait(false);
|
||||
if (_internalProviders.Length > 0)
|
||||
{
|
||||
_logger.LogDebug("No results from external providers, using internal provider results");
|
||||
}
|
||||
}
|
||||
|
||||
// Internal providers apply user-access filtering inline in their queries. External
|
||||
// providers don't know about user permissions, so they may return IDs from hidden
|
||||
// libraries or items the user is otherwise blocked from. Run the post-filter only
|
||||
// when results came from externals to close that gap. The Items controller's second
|
||||
// roundtrip via folder.GetItems applies most of these again, but it does not restrict
|
||||
// by TopParentIds when ItemIds is set.
|
||||
if (fromExternal && results.Count > 0 && query.UserId.HasValue && !query.UserId.Value.IsEmpty())
|
||||
// libraries or items the user is otherwise blocked from. Filter them here to close
|
||||
// that gap. The Items controller's second roundtrip via folder.GetItems applies most
|
||||
// of these again, but it does not restrict by TopParentIds when ItemIds is set.
|
||||
if (externalResults.Count > 0 && query.UserId.HasValue && !query.UserId.Value.IsEmpty())
|
||||
{
|
||||
var user = _userManager.GetUserById(query.UserId.Value);
|
||||
if (user is not null)
|
||||
{
|
||||
results = await FilterByUserAccessAsync(results, user, query, cancellationToken).ConfigureAwait(false);
|
||||
externalResults = await FilterByUserAccessAsync(externalResults, user, query, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
if (externalResults.Count > 0)
|
||||
{
|
||||
return externalResults;
|
||||
}
|
||||
|
||||
var internalResults = await internalTask.ConfigureAwait(false);
|
||||
if (_internalProviders.Length > 0)
|
||||
{
|
||||
_logger.LogDebug("No results from external providers, using internal provider results");
|
||||
}
|
||||
|
||||
return internalResults;
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<SearchResult>> FilterByUserAccessAsync(
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"Shows": "Serier",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin er i gang med at starte. Prøv igen om et øjeblik.",
|
||||
"SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke hentes fra {0} til {1}",
|
||||
"TvShows": "TV-serier",
|
||||
"TvShows": "Serier",
|
||||
"UserCreatedWithName": "Bruger {0} er blevet oprettet",
|
||||
"UserDeletedWithName": "Brugeren {0} er nu slettet",
|
||||
"UserDownloadingItemWithValues": "{0} henter {1}",
|
||||
@@ -80,8 +80,8 @@
|
||||
"TaskRefreshChapterImagesDescription": "Laver miniaturebilleder for videoer, der har kapitler.",
|
||||
"TaskRefreshChannelsDescription": "Opdaterer information for internetkanaler.",
|
||||
"TaskRefreshChannels": "Opdatér kanaler",
|
||||
"TaskCleanTranscodeDescription": "Fjerner omkodningsfiler, som er mere end 1 dag gamle.",
|
||||
"TaskCleanTranscode": "Tøm omkodningsmappen",
|
||||
"TaskCleanTranscodeDescription": "Fjerner transkodningsfiler, som er mere end 1 dag gamle.",
|
||||
"TaskCleanTranscode": "Tøm transkodningsmappen",
|
||||
"TaskRefreshPeople": "Opdatér personer",
|
||||
"TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.",
|
||||
"TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.",
|
||||
@@ -109,17 +109,17 @@
|
||||
"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",
|
||||
"NameExtraBehindTheScenes": "Bag Scenerne",
|
||||
"NameExtraBehindTheScenes": "Bag scenerne",
|
||||
"NameExtraClip": "Klip",
|
||||
"NameExtraDeletedScene": "Slettet Scene",
|
||||
"NameExtraDeletedScene": "Slettet scene",
|
||||
"NameExtraFeaturette": "Featurette",
|
||||
"NameExtraInterview": "Interview",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Smagsprøve",
|
||||
"NameExtraScene": "Scene",
|
||||
"NameExtraShort": "Kort",
|
||||
"NameExtraThemeSong": "Tema Sang",
|
||||
"NameExtraThemeVideo": "Tema Video",
|
||||
"NameExtraThemeSong": "Temasang",
|
||||
"NameExtraThemeVideo": "Temavideo",
|
||||
"NameExtraTrailer": "Trailer",
|
||||
"NameExtraUnknown": "Ekstra"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"AppDeviceValues": "App: {0}, Gerät: {1}",
|
||||
"Artists": "Interpreten",
|
||||
"Artists": "Künstler",
|
||||
"AuthenticationSucceededWithUserName": "{0} erfolgreich authentifiziert",
|
||||
"Books": "Bücher",
|
||||
"ChapterNameValue": "Kapitel {0}",
|
||||
|
||||
@@ -97,5 +97,9 @@
|
||||
"TaskAudioNormalizationDescription": "Skanas dosierojn por sonnivelaj normaligaj datumoj.",
|
||||
"TaskRefreshTrickplayImages": "Generi la bildojn por TrickPlay (Antaŭrigardo rapida antaŭen)",
|
||||
"TaskAudioNormalization": "Normaligo Sonnivela",
|
||||
"HearingImpaired": "Surda"
|
||||
"HearingImpaired": "Surda",
|
||||
"NameExtraDeletedScene": "Forigita sceno",
|
||||
"NameExtraScene": "Sceno",
|
||||
"NameExtraThemeSong": "Tema Kanto",
|
||||
"Original": "Originala"
|
||||
}
|
||||
|
||||
@@ -114,5 +114,12 @@
|
||||
"NameExtraScene": "Eszena",
|
||||
"NameExtraShort": "Laburra",
|
||||
"NameExtraThemeSong": "Gai-abestia",
|
||||
"NameExtraThemeVideo": "Gai-bideoa"
|
||||
"NameExtraThemeVideo": "Gai-bideoa",
|
||||
"NameExtraBehindTheScenes": "Eszenen atzean",
|
||||
"NameExtraClip": "Klipa",
|
||||
"NameExtraDeletedScene": "Ezabatutako eszena",
|
||||
"NameExtraFeaturette": "Erreportajea",
|
||||
"NameExtraInterview": "Elkarrizketa",
|
||||
"NameExtraTrailer": "Trailerra",
|
||||
"NameExtraUnknown": "Extra"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,10 @@
|
||||
"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.",
|
||||
"LyricDownloadFailureFromForItem": "Sanoitusten lataus kohteesta {0} kappaleelle {1} epäonnistui",
|
||||
"Original": "Alkuperäinen"
|
||||
"Original": "Alkuperäinen",
|
||||
"NameExtraBehindTheScenes": "Kulissien Takana",
|
||||
"NameExtraClip": "Klippi",
|
||||
"NameExtraDeletedScene": "Poistettu Kohtaus",
|
||||
"NameExtraFeaturette": "Lyhytelokuva",
|
||||
"NameExtraInterview": "Haastattelu"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"Artists": "Tónlistafólk",
|
||||
"Collections": "Søvn",
|
||||
"Collections": "Samlingar",
|
||||
"Default": "Forsett",
|
||||
"External": "Ytri",
|
||||
"Genres": "Greinar",
|
||||
|
||||
@@ -107,5 +107,19 @@
|
||||
"TaskAudioNormalizationDescription": "Escanea ficheiros á procura de datos de normalización de volume.",
|
||||
"CleanupUserDataTask": "Tarefa de limpeza de datos dos usuarios",
|
||||
"CleanupUserDataTaskDescription": "Limpa todos os datos do usuario (estado de visualización, de favorito etc.) dos medios ausentes polo menos 90 días.",
|
||||
"Original": "Orixinal"
|
||||
"Original": "Orixinal",
|
||||
"LyricDownloadFailureFromForItem": "Non se puideron descargar as letras desde {0} para {1}",
|
||||
"NameExtraBehindTheScenes": "Detrás das Cámaras",
|
||||
"NameExtraClip": "Clip",
|
||||
"NameExtraDeletedScene": "Escena Eliminada",
|
||||
"NameExtraFeaturette": "Reportaxe especial",
|
||||
"NameExtraInterview": "Entrevista",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Mostra",
|
||||
"NameExtraScene": "Escena",
|
||||
"NameExtraShort": "Curtametraxe",
|
||||
"NameExtraThemeSong": "Canción principal",
|
||||
"NameExtraThemeVideo": "Vídeo da canción principal",
|
||||
"NameExtraTrailer": "Tráiler",
|
||||
"NameExtraUnknown": "Extra"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,12 @@
|
||||
"CleanupUserDataTaskDescription": "ניקוי כל המידע של המשתמש (מצב צפייה, מועדפים וכו) ממדיה שאינה קיימת מעל 90 יום.",
|
||||
"CleanupUserDataTask": "משימת ניקוי מידע משתמש",
|
||||
"LyricDownloadFailureFromForItem": "הורדת המילים מ-{0} עבור {1} נכשלה",
|
||||
"Original": "מקור"
|
||||
"Original": "מקור",
|
||||
"NameExtraBehindTheScenes": "מאחורי הקלעים",
|
||||
"NameExtraClip": "קליפ",
|
||||
"NameExtraDeletedScene": "סצנה שנמחקה",
|
||||
"NameExtraFeaturette": "סרט קצר",
|
||||
"NameExtraInterview": "ריאיון",
|
||||
"NameExtraSample": "דגימה",
|
||||
"NameExtraScene": "סצנה"
|
||||
}
|
||||
|
||||
@@ -120,5 +120,6 @@
|
||||
"NameExtraThemeSong": "Főcímdal",
|
||||
"NameExtraThemeVideo": "Főcímvideó",
|
||||
"NameExtraTrailer": "Előzetes",
|
||||
"NameExtraUnknown": "Extra"
|
||||
"NameExtraUnknown": "Extra",
|
||||
"NameExtraNumbered": "{0} {1}"
|
||||
}
|
||||
|
||||
@@ -24,5 +24,10 @@
|
||||
"TaskDownloadMissingSubtitles": "Ներբեռնել պակասող ենթագրերը",
|
||||
"AppDeviceValues": "Հավելված` {0}, Սարք `{1}",
|
||||
"ChapterNameValue": "Գլուխ {0}",
|
||||
"Collections": "Հավաքածուներ"
|
||||
"Collections": "Հավաքածուներ",
|
||||
"Artists": "Երաժիշտներ",
|
||||
"Default": "Լռելյայն",
|
||||
"Favorites": "Ընտրյալ",
|
||||
"Forced": "Ստիպուած",
|
||||
"Genres": "Ոճ"
|
||||
}
|
||||
|
||||
@@ -118,5 +118,8 @@
|
||||
"NameExtraSample": "Campione",
|
||||
"NameExtraShort": "Corto",
|
||||
"NameExtraThemeSong": "Sigla musicale",
|
||||
"NameExtraTrailer": "Trailer"
|
||||
"NameExtraTrailer": "Trailer",
|
||||
"NameExtraFeaturette": "Caratteristica",
|
||||
"NameExtraThemeVideo": "Video tematico",
|
||||
"NameExtraUnknown": "Extra"
|
||||
}
|
||||
|
||||
@@ -59,8 +59,8 @@
|
||||
"UserOfflineFromDevice": "{0} buvo atjungtas nuo {1}",
|
||||
"UserOnlineFromDevice": "{0} prisijungęs iš {1}",
|
||||
"UserPasswordChangedWithName": "Slaptažodis pakeistas naudotojui {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} leidžia {1} į {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} baigė leisti {1} į {2}",
|
||||
"UserStartedPlayingItemWithValues": "{0} atkuriama {1} į {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} baigė atkūrimą {1} į {2}",
|
||||
"VersionNumber": "Versija {0}",
|
||||
"TaskUpdatePluginsDescription": "Atsisiunčia ir įdiegia įskiepių, kurie sukonfigūruoti atnaujinti automatiškai, naujinius.",
|
||||
"TaskUpdatePlugins": "Atnaujinti įskieius",
|
||||
|
||||
@@ -106,5 +106,11 @@
|
||||
"TaskAudioNormalization": "Normalisasi Audio",
|
||||
"TaskAudioNormalizationDescription": "Mengimbas fail-fail untuk data normalisasi audio.",
|
||||
"CleanupUserDataTaskDescription": "Membersihkan semua data pengguna (keadaan tontonan, status kegemaran, dan sebagainya) daripada media yang tidak lagi wujud sekurang-kurangnya selama 90 hari.",
|
||||
"CleanupUserDataTask": "Tugas pembersihan data pengguna"
|
||||
"CleanupUserDataTask": "Tugas pembersihan data pengguna",
|
||||
"LyricDownloadFailureFromForItem": "Lirik gagal dimuat turun dari {0} untuk {1}",
|
||||
"NameExtraBehindTheScenes": "Di Sebalik Takbir",
|
||||
"NameExtraClip": "Klip",
|
||||
"NameExtraInterview": "Temu bual",
|
||||
"NameExtraThemeSong": "Lagu Tema",
|
||||
"NameExtraThemeVideo": "Video Tema"
|
||||
}
|
||||
|
||||
@@ -5,108 +5,121 @@
|
||||
"Books": "Livros",
|
||||
"ChapterNameValue": "Capítulo {0}",
|
||||
"Collections": "Coleções",
|
||||
"FailedLoginAttemptWithUserName": "Tentativa de login falhada a partir de {0}",
|
||||
"FailedLoginAttemptWithUserName": "Tentativa falhada de início de sessão do utilizador {0}",
|
||||
"Favorites": "Favoritos",
|
||||
"Folders": "Pastas",
|
||||
"Genres": "Géneros",
|
||||
"HeaderContinueWatching": "Continuar a ver",
|
||||
"HeaderFavoriteEpisodes": "Episódios Favoritos",
|
||||
"HeaderFavoriteShows": "Séries Favoritas",
|
||||
"HeaderLiveTV": "TV em Direto",
|
||||
"HeaderNextUp": "A Seguir",
|
||||
"HomeVideos": "Vídeos Caseiros",
|
||||
"HeaderFavoriteEpisodes": "Episódios favoritos",
|
||||
"HeaderFavoriteShows": "Séries favoritas",
|
||||
"HeaderLiveTV": "TV em direto",
|
||||
"HeaderNextUp": "A seguir",
|
||||
"HomeVideos": "Vídeos caseiros",
|
||||
"Inherit": "Herdar",
|
||||
"LabelIpAddressValue": "Endereço IP: {0}",
|
||||
"LabelRunningTimeValue": "Duração: {0}",
|
||||
"Latest": "Mais Recente",
|
||||
"MixedContent": "Conteúdo Misto",
|
||||
"Latest": "Mais recente",
|
||||
"MixedContent": "Conteúdo misto",
|
||||
"Movies": "Filmes",
|
||||
"Music": "Música",
|
||||
"MusicVideos": "Videoclipes",
|
||||
"NameInstallFailed": "{0} falha na instalação",
|
||||
"NameInstallFailed": "Falha na instalação de {0}",
|
||||
"NameSeasonNumber": "Temporada {0}",
|
||||
"NameSeasonUnknown": "Temporada Desconhecida",
|
||||
"NameSeasonUnknown": "Temporada desconhecida",
|
||||
"NewVersionIsAvailable": "Está disponível para transferência uma nova versão do servidor Jellyfin.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Atualização de aplicação disponível",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Atualização de aplicação instalada",
|
||||
"NotificationOptionAudioPlayback": "Reprodução Iniciada",
|
||||
"NotificationOptionAudioPlaybackStopped": "Reprodução Parada",
|
||||
"NotificationOptionAudioPlayback": "Reprodução de áudio iniciada",
|
||||
"NotificationOptionAudioPlaybackStopped": "Reprodução de áudio interrompida",
|
||||
"NotificationOptionCameraImageUploaded": "Imagem da câmara enviada",
|
||||
"NotificationOptionInstallationFailed": "Falha na instalação",
|
||||
"NotificationOptionNewLibraryContent": "Novo conteúdo adicionado",
|
||||
"NotificationOptionPluginError": "Falha na extensão",
|
||||
"NotificationOptionPluginInstalled": "Extensão instalada",
|
||||
"NotificationOptionPluginUninstalled": "Extensão desinstalada",
|
||||
"NotificationOptionPluginUpdateInstalled": "Extensão atualizada",
|
||||
"NotificationOptionPluginError": "Falha no plugin",
|
||||
"NotificationOptionPluginInstalled": "Plugin instalado",
|
||||
"NotificationOptionPluginUninstalled": "Plugin desinstalado",
|
||||
"NotificationOptionPluginUpdateInstalled": "Atualização de plugin instalada",
|
||||
"NotificationOptionServerRestartRequired": "Necessário reiniciar o servidor",
|
||||
"NotificationOptionTaskFailed": "Falha em tarefa agendada",
|
||||
"NotificationOptionUserLockedOut": "Utilizador bloqueado",
|
||||
"NotificationOptionVideoPlayback": "Reprodução do vídeo iniciada",
|
||||
"NotificationOptionVideoPlaybackStopped": "Reprodução do vídeo parada",
|
||||
"NotificationOptionVideoPlayback": "Reprodução de vídeo iniciada",
|
||||
"NotificationOptionVideoPlaybackStopped": "Reprodução de vídeo interrompida",
|
||||
"Photos": "Fotografias",
|
||||
"PluginInstalledWithName": "{0} foi instalado",
|
||||
"PluginUninstalledWithName": "{0} foi desinstalado",
|
||||
"PluginUpdatedWithName": "{0} foi atualizado",
|
||||
"ScheduledTaskFailedWithName": "{0} falhou",
|
||||
"Shows": "Séries",
|
||||
"StartupEmbyServerIsLoading": "O servidor Jellyfin está a iniciar. Tente novamente mais tarde.",
|
||||
"SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas a partir de {0} para {1}",
|
||||
"TvShows": "Séries",
|
||||
"StartupEmbyServerIsLoading": "O servidor Jellyfin está a iniciar. Tenta novamente dentro de instantes.",
|
||||
"SubtitleDownloadFailureFromForItem": "Falha ao transferir legendas de {0} para {1}",
|
||||
"TvShows": "Séries de TV",
|
||||
"UserCreatedWithName": "Utilizador {0} criado",
|
||||
"UserDeletedWithName": "Utilizador {0} apagado",
|
||||
"UserDeletedWithName": "Utilizador {0} eliminado",
|
||||
"UserDownloadingItemWithValues": "{0} está a transferir {1}",
|
||||
"UserLockedOutWithName": "Utilizador {0} bloqueado",
|
||||
"UserOfflineFromDevice": "{0} desligou-se a partir de {1}",
|
||||
"UserOnlineFromDevice": "{0} ligou-se a partir de {1}",
|
||||
"UserOfflineFromDevice": "{0} desligou-se de {1}",
|
||||
"UserOnlineFromDevice": "{0} está online em {1}",
|
||||
"UserPasswordChangedWithName": "Palavra-passe alterada para o utilizador {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} está a reproduzir {1} em {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} terminou a reprodução de {1} em {2}",
|
||||
"VersionNumber": "Versão {0}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Procurar na internet por legendas em falta baseado na configuração de metadados.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Procura na Internet legendas em falta com base na configuração dos metadados.",
|
||||
"TaskDownloadMissingSubtitles": "Transferir legendas em falta",
|
||||
"TaskRefreshChannelsDescription": "Atualizar informação sobre canais da Internet.",
|
||||
"TaskRefreshChannels": "Atualizar Canais",
|
||||
"TaskCleanTranscodeDescription": "Apagar ficheiros de transcode com mais de um dia.",
|
||||
"TaskCleanTranscode": "Limpar a Diretoria de Transcode",
|
||||
"TaskUpdatePluginsDescription": "Faz o download e instala updates para os plugins que estão configurados para atualizar automaticamente.",
|
||||
"TaskUpdatePlugins": "Atualizar Plugins",
|
||||
"TaskRefreshPeopleDescription": "Atualizar metadados para elenco e equipa técnica da tua mediateca.",
|
||||
"TaskRefreshPeople": "Atualizar Pessoas",
|
||||
"TaskCleanLogsDescription": "Apagar ficheiros de log que têm mais de {0} dias.",
|
||||
"TaskCleanLogs": "Limpar a Diretoria de Logs",
|
||||
"TaskRefreshLibraryDescription": "Analisar a mediateca para novos ficheiros e atualizar os metadados.",
|
||||
"TaskRefreshLibrary": "Analisar mediateca",
|
||||
"TaskRefreshChapterImagesDescription": "Criar thumbnails para os vídeos que têm capítulos.",
|
||||
"TaskRefreshChapterImages": "Extrair Imagens dos Capítulos",
|
||||
"TaskCleanCacheDescription": "Apagar ficheiros em cache que já não são necessários.",
|
||||
"TaskCleanCache": "Limpar Cache",
|
||||
"TaskRefreshChannelsDescription": "Atualiza as informações dos canais da Internet.",
|
||||
"TaskRefreshChannels": "Atualizar canais",
|
||||
"TaskCleanTranscodeDescription": "Elimina ficheiros de transcodificação com mais de um dia.",
|
||||
"TaskCleanTranscode": "Limpar pasta de transcodificação",
|
||||
"TaskUpdatePluginsDescription": "Transfere e instala atualizações dos plugins configurados para atualização automática.",
|
||||
"TaskUpdatePlugins": "Atualizar plugins",
|
||||
"TaskRefreshPeopleDescription": "Atualiza os metadados de atores e realizadores na tua biblioteca multimédia.",
|
||||
"TaskRefreshPeople": "Atualizar pessoas",
|
||||
"TaskCleanLogsDescription": "Elimina ficheiros de registo com mais de {0} dias.",
|
||||
"TaskCleanLogs": "Limpar pasta de registos",
|
||||
"TaskRefreshLibraryDescription": "Analisa a biblioteca multimédia à procura de novos ficheiros e atualiza os metadados.",
|
||||
"TaskRefreshLibrary": "Analisar biblioteca multimédia",
|
||||
"TaskRefreshChapterImagesDescription": "Cria miniaturas para vídeos que têm capítulos.",
|
||||
"TaskRefreshChapterImages": "Extrair imagens dos capítulos",
|
||||
"TaskCleanCacheDescription": "Elimina ficheiros de cache que já não são necessários ao sistema.",
|
||||
"TaskCleanCache": "Limpar pasta de cache",
|
||||
"TasksChannelsCategory": "Canais da Internet",
|
||||
"TasksApplicationCategory": "Aplicação",
|
||||
"TasksLibraryCategory": "Mediateca",
|
||||
"TasksLibraryCategory": "Biblioteca",
|
||||
"TasksMaintenanceCategory": "Manutenção",
|
||||
"TaskCleanActivityLogDescription": "Apaga as entradas do registo de atividade anteriores à data configurada.",
|
||||
"TaskCleanActivityLogDescription": "Elimina as entradas do registo de atividade mais antigas do que o período configurado.",
|
||||
"TaskCleanActivityLog": "Limpar registo de atividade",
|
||||
"Undefined": "Indefinido",
|
||||
"Forced": "Forçado",
|
||||
"Default": "Predefinição",
|
||||
"TaskOptimizeDatabaseDescription": "Otimiza e liberta espaço livre na base de dados. A execução desta tarefa depois de analisar a mediateca ou efetuar outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.",
|
||||
"TaskOptimizeDatabaseDescription": "Compacta a base de dados e liberta espaço não utilizado. A execução desta tarefa depois de analisar a biblioteca ou de outras alterações que modifiquem a base de dados pode melhorar o desempenho.",
|
||||
"TaskOptimizeDatabase": "Otimizar base de dados",
|
||||
"TaskKeyframeExtractorDescription": "Extrai quadros-chave de ficheiros de video para criar listas de reprodução HLS mais precisas. Esta tarefa pode demorar algum tempo.",
|
||||
"TaskKeyframeExtractor": "Extrator de Quadros-chave",
|
||||
"TaskKeyframeExtractorDescription": "Extrai fotogramas-chave de ficheiros de vídeo para criar playlists HLS mais precisas. Esta tarefa pode demorar algum tempo.",
|
||||
"TaskKeyframeExtractor": "Extrator de fotogramas-chave",
|
||||
"External": "Externo",
|
||||
"HearingImpaired": "Surdo",
|
||||
"HearingImpaired": "Deficiência auditiva",
|
||||
"TaskRefreshTrickplayImages": "Gerar imagens de trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.",
|
||||
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
|
||||
"TaskAudioNormalization": "Normalização de áudio",
|
||||
"TaskExtractMediaSegments": "Analisar segmentos de multimédia",
|
||||
"TaskDownloadMissingLyrics": "Transferir letra em falta",
|
||||
"TaskMoveTrickplayImages": "Migrar a localização da imagem do Trickplay",
|
||||
"TaskDownloadMissingLyricsDescription": "Transferir letra para músicas",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de multimédia a partir de plugins com suporte para MediaSegment.",
|
||||
"TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca.",
|
||||
"CleanupUserDataTaskDescription": "Apaga todos os dados de utilizador (estados de reprodução, favoritos, etc) de arquivos média não presentes há 90 dias ou mais.",
|
||||
"TaskExtractMediaSegments": "Analisar segmentos multimédia",
|
||||
"TaskDownloadMissingLyrics": "Transferir letras em falta",
|
||||
"TaskMoveTrickplayImages": "Migrar localização das imagens de trickplay",
|
||||
"TaskDownloadMissingLyricsDescription": "Transfere letras para músicas.",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos multimédia de plugins com MediaSegment ativado.",
|
||||
"TaskMoveTrickplayImagesDescription": "Move os ficheiros de trickplay existentes de acordo com as definições da biblioteca.",
|
||||
"CleanupUserDataTaskDescription": "Remove todos os dados de utilizador (estado de reprodução, estado de favorito, etc.) de conteúdos multimédia que já não estejam presentes há pelo menos 90 dias.",
|
||||
"CleanupUserDataTask": "Limpeza de dados de utilizador",
|
||||
"Original": "Original",
|
||||
"LyricDownloadFailureFromForItem": "Erro ao descarregar letras de {0} para {1}"
|
||||
"LyricDownloadFailureFromForItem": "Falha ao transferir letras de {0} para {1}",
|
||||
"NameExtraDeletedScene": "Cena eliminada",
|
||||
"NameExtraInterview": "Entrevista",
|
||||
"NameExtraUnknown": "Extra",
|
||||
"NameExtraBehindTheScenes": "Bastidores",
|
||||
"NameExtraSample": "Amostra",
|
||||
"NameExtraScene": "Cena",
|
||||
"NameExtraClip": "Excerto",
|
||||
"NameExtraFeaturette": "Minidocumentário",
|
||||
"NameExtraThemeSong": "Tema musical",
|
||||
"NameExtraThemeVideo": "Vídeo temático",
|
||||
"NameExtraShort": "Curta-metragem",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraTrailer": "Trailer"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"HeaderLiveTV": "TV Em Direto",
|
||||
"HeaderLiveTV": "TV em direto",
|
||||
"Collections": "Coleções",
|
||||
"Books": "Livros",
|
||||
"Artists": "Artistas",
|
||||
"HeaderNextUp": "A Seguir",
|
||||
"HeaderFavoriteEpisodes": "Episódios Favoritos",
|
||||
"HeaderFavoriteShows": "Séries Favoritas",
|
||||
"HeaderNextUp": "A seguir",
|
||||
"HeaderFavoriteEpisodes": "Episódios favoritos",
|
||||
"HeaderFavoriteShows": "Séries favoritas",
|
||||
"HeaderContinueWatching": "Continuar a ver",
|
||||
"Genres": "Géneros",
|
||||
"Folders": "Pastas",
|
||||
@@ -44,21 +44,21 @@
|
||||
"NotificationOptionApplicationUpdateInstalled": "A atualização do aplicativo foi instalada",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Uma atualização do aplicativo está disponível",
|
||||
"NewVersionIsAvailable": "Uma nova versão do servidor Jellyfin está disponível para download.",
|
||||
"NameSeasonUnknown": "Temporada Desconhecida",
|
||||
"NameSeasonUnknown": "Temporada desconhecida",
|
||||
"NameSeasonNumber": "Temporada {0}",
|
||||
"NameInstallFailed": "Falha na instalação de {0}",
|
||||
"MusicVideos": "Videoclipes",
|
||||
"Music": "Música",
|
||||
"MixedContent": "Conteúdo diverso",
|
||||
"MixedContent": "Conteúdo misto",
|
||||
"Latest": "Mais Recente",
|
||||
"LabelRunningTimeValue": "Duração: {0}",
|
||||
"LabelIpAddressValue": "Endereço de IP: {0}",
|
||||
"Inherit": "Herdar",
|
||||
"HomeVideos": "Vídeos Caseiros",
|
||||
"HomeVideos": "Vídeos caseiros",
|
||||
"Shows": "Séries",
|
||||
"Photos": "Fotografias",
|
||||
"Movies": "Filmes",
|
||||
"FailedLoginAttemptWithUserName": "Tentativa de início de sessão falhada a partir de {0}",
|
||||
"FailedLoginAttemptWithUserName": "Tentativa falhada de início de sessão do utilizador {0}",
|
||||
"ChapterNameValue": "Capítulo {0}",
|
||||
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
|
||||
"AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}",
|
||||
@@ -110,15 +110,16 @@
|
||||
"Original": "Original",
|
||||
"LyricDownloadFailureFromForItem": "Erro ao descarregar letras de {0} para {1}",
|
||||
"NameExtraBehindTheScenes": "Bastidores",
|
||||
"NameExtraClip": "Clip",
|
||||
"NameExtraDeletedScene": "Cena Eliminada",
|
||||
"NameExtraFeaturette": "Média-metragem",
|
||||
"NameExtraClip": "Excerto",
|
||||
"NameExtraDeletedScene": "Cena eliminada",
|
||||
"NameExtraFeaturette": "Minidocumentário",
|
||||
"NameExtraInterview": "Entrevista",
|
||||
"NameExtraSample": "Amostra",
|
||||
"NameExtraShort": "Curta-metragem",
|
||||
"NameExtraThemeSong": "Tema Principal",
|
||||
"NameExtraThemeVideo": "Vídeo de Abertura",
|
||||
"NameExtraThemeSong": "Tema principal",
|
||||
"NameExtraThemeVideo": "Vídeo temático",
|
||||
"NameExtraScene": "Cena",
|
||||
"NameExtraUnknown": "Extra",
|
||||
"NameExtraTrailer": "Trailer"
|
||||
"NameExtraTrailer": "Trailer",
|
||||
"NameExtraNumbered": "{0} {1}"
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
"CleanupUserDataTaskDescription": "Очищает все пользовательские данные (состояние просмотра, статус избранного и т.д.) с медиа, отсутствующих по меньшей мере в течение 90 дней.",
|
||||
"Original": "Оригинальный",
|
||||
"LyricDownloadFailureFromForItem": "Не получилось скачать текст песни с {0} для {1}",
|
||||
"NameExtraBehindTheScenes": "За кулисами",
|
||||
"NameExtraBehindTheScenes": "За кадром",
|
||||
"NameExtraClip": "Отрывок",
|
||||
"NameExtraDeletedScene": "Удалённая сцена",
|
||||
"NameExtraFeaturette": "Короткометражка",
|
||||
@@ -119,5 +119,7 @@
|
||||
"NameExtraThemeSong": "Заглавная песня",
|
||||
"NameExtraThemeVideo": "Заглавное видео",
|
||||
"NameExtraTrailer": "Трейлер",
|
||||
"NameExtraUnknown": "Дополнительный материал"
|
||||
"NameExtraUnknown": "Дополнительный материал",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraShort": "Короткометражка"
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
"Default": "Predvolené",
|
||||
"TaskOptimizeDatabaseDescription": "Zmenší databázu a odstráni prázdne miesto. Spustenie tejto úlohy po skenovaní knižnice alebo po iných zmenách zahŕňajúcich úpravy databáze môže zlepšiť výkon.",
|
||||
"TaskOptimizeDatabase": "Optimalizovať databázu",
|
||||
"TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.",
|
||||
"TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS zoznamov prehrávania. Táto úloha môže trvať dlhšiu dobu.",
|
||||
"TaskKeyframeExtractor": "Extraktor kľúčových snímkov",
|
||||
"External": "Externé",
|
||||
"HearingImpaired": "Sluchovo postihnutí",
|
||||
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTaskDescription": "Pastron të gjitha të dhënat e përdorueseve (gjendja e shikimit, statusi i të preferuarave etj.) nga mediat që nuk janë më të pranishme për të paktën 90 ditë.",
|
||||
"CleanupUserDataTask": "Veprim për pastrimin të dhënave të përdorueseve",
|
||||
"LyricDownloadFailureFromForItem": "Teksti i këngës nuk arriti të shkarkohej nga {0} për {1}",
|
||||
"Original": "Origjinal"
|
||||
"Original": "Origjinal",
|
||||
"NameExtraBehindTheScenes": "Pamje nga prapaskenat",
|
||||
"NameExtraClip": "Pjesë",
|
||||
"NameExtraDeletedScene": "Skenë e fshirë",
|
||||
"NameExtraFeaturette": "Film i shkurtër",
|
||||
"NameExtraInterview": "Intervistë",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Shembull",
|
||||
"NameExtraScene": "Skenë",
|
||||
"NameExtraShort": "Film i shkurtër",
|
||||
"NameExtraThemeSong": "Kënga e temës",
|
||||
"NameExtraThemeVideo": "Videoja e temës",
|
||||
"NameExtraTrailer": "Parapamje",
|
||||
"NameExtraUnknown": "Shtesë"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,18 @@
|
||||
"TaskMoveTrickplayImages": "Промени локацију сличица за визуелно премотавање",
|
||||
"TaskDownloadMissingLyricsDescription": "Преузми стихове песама",
|
||||
"LyricDownloadFailureFromForItem": "Није успело преузимање стихова са {0} за {1}",
|
||||
"Original": "Изворно"
|
||||
"Original": "Изворно",
|
||||
"NameExtraBehindTheScenes": "Иза кулиса",
|
||||
"NameExtraClip": "Исечак",
|
||||
"NameExtraDeletedScene": "Обрисана сцена",
|
||||
"NameExtraFeaturette": "Кратки прилог",
|
||||
"NameExtraInterview": "Интервју",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Узорак",
|
||||
"NameExtraScene": "Сцена",
|
||||
"NameExtraShort": "Кратки филм",
|
||||
"NameExtraThemeSong": "Музичка тема",
|
||||
"NameExtraThemeVideo": "Спот музичке теме",
|
||||
"NameExtraTrailer": "Реклама",
|
||||
"NameExtraUnknown": "Додатак"
|
||||
}
|
||||
|
||||
@@ -106,5 +106,20 @@
|
||||
"TaskExtractMediaSegments": "மீடியா பிரிவு ஸ்கேன்",
|
||||
"TaskExtractMediaSegmentsDescription": "மீடியாசெக்மென்ட் இயக்கப்பட்ட செருகுநிரல்களிலிருந்து மீடியா பிரிவுகளைப் பிரித்தெடுக்கிறது அல்லது பெறுகிறது.",
|
||||
"CleanupUserDataTaskDescription": "குறைந்தது 90 நாட்களுக்கு இல்லாத மீடியாவிலிருந்து அனைத்து பயனர் தரவையும் (கண்காணிப்பு நிலை, பிடித்த நிலை போன்றவை) சுத்தம் செய்கிறது.",
|
||||
"CleanupUserDataTask": "பயனர் தரவை சுத்தம் செய்யும் பணி"
|
||||
"CleanupUserDataTask": "பயனர் தரவை சுத்தம் செய்யும் பணி",
|
||||
"LyricDownloadFailureFromForItem": "{0} இலிருந்து {1} க்கு பாடல் வரிகளைப் பதிவிறக்க முடியவில்லை",
|
||||
"NameExtraBehindTheScenes": "திரைக்குப் பின்னால்",
|
||||
"NameExtraClip": "துண்டுக்காட்சி",
|
||||
"NameExtraDeletedScene": "நீக்கப்பட்ட காட்சிகள்",
|
||||
"NameExtraFeaturette": "திரைப்பின்னணித் தொகுப்பு",
|
||||
"NameExtraInterview": "நேர்காணல்",
|
||||
"NameExtraSample": "மாதிரி",
|
||||
"NameExtraScene": "காட்சி",
|
||||
"NameExtraShort": "குறுகிய",
|
||||
"NameExtraThemeSong": "மையப் பாடல்",
|
||||
"NameExtraThemeVideo": "மையக் காணொளி",
|
||||
"NameExtraTrailer": "முன்னோட்டம்",
|
||||
"NameExtraUnknown": "கூடுதல்",
|
||||
"Original": "அசல்",
|
||||
"NameExtraNumbered": "{0} {1}"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"AppDeviceValues": "Uygulama: {0}, Aygıt: {1}",
|
||||
"AppDeviceValues": "Uygulama: {0}, Cihaz: {1}",
|
||||
"Artists": "Sanatçılar",
|
||||
"AuthenticationSucceededWithUserName": "{0} kimliği başarıyla doğrulandı",
|
||||
"Books": "Kitaplar",
|
||||
|
||||
@@ -655,7 +655,7 @@ namespace Emby.Server.Implementations.Session
|
||||
ItemId = session.NowPlayingItem is null ? Guid.Empty : session.NowPlayingItem.Id,
|
||||
SessionId = session.Id,
|
||||
MediaSourceId = session.PlayState?.MediaSourceId,
|
||||
PositionTicks = session.PlayState?.PositionTicks
|
||||
PositionTicks = session.LastPlaybackCheckInPositionTicks
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -318,13 +318,14 @@ public class ItemCountService : IItemCountService
|
||||
var parentIdsArray = parentIds.ToArray();
|
||||
|
||||
var hierarchicalCounts = dbContext.BaseItems
|
||||
.Where(b => b.ParentId.HasValue && parentIdsArray.Contains(b.ParentId.Value))
|
||||
.Where(b => b.ParentId.HasValue)
|
||||
.WhereOneOrMany(parentIdsArray, b => b.ParentId!.Value)
|
||||
.GroupBy(b => b.ParentId!.Value)
|
||||
.Select(g => new { ParentId = g.Key, Count = g.Count() })
|
||||
.ToDictionary(x => x.ParentId, x => x.Count);
|
||||
|
||||
var linkedCounts = dbContext.LinkedChildren
|
||||
.Where(lc => parentIdsArray.Contains(lc.ParentId))
|
||||
.WhereOneOrMany(parentIdsArray, lc => lc.ParentId)
|
||||
.GroupBy(lc => lc.ParentId)
|
||||
.Select(g => new { ParentId = g.Key, Count = g.Count() })
|
||||
.ToDictionary(x => x.ParentId, x => x.Count);
|
||||
|
||||
@@ -538,7 +538,7 @@ public class ItemPersistenceService : IItemPersistenceService
|
||||
var childIdsToCheck = resolvedChildren.Select(c => c.ChildId).Distinct().ToList();
|
||||
var existingChildIds = childIdsToCheck.Count > 0
|
||||
? context.BaseItems
|
||||
.Where(e => childIdsToCheck.Contains(e.Id))
|
||||
.WhereOneOrMany(childIdsToCheck, e => e.Id)
|
||||
.Select(e => e.Id)
|
||||
.ToHashSet()
|
||||
: [];
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -83,26 +84,27 @@ public class LinkedChildrenService : ILinkedChildrenService
|
||||
{
|
||||
using var dbContext = _dbProvider.CreateDbContext();
|
||||
|
||||
var lowerNames = artistNames.Select(n => n.ToLowerInvariant()).ToArray();
|
||||
var cleanNames = artistNames.Select(n => (Original: n, Clean: n.GetCleanValue())).ToArray();
|
||||
var cleanValues = cleanNames.Select(x => x.Clean).ToArray();
|
||||
|
||||
var artists = dbContext.BaseItems
|
||||
.AsNoTracking()
|
||||
.Where(e => e.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!)
|
||||
.Where(e => lowerNames.Contains(e.Name!.ToLower()))
|
||||
.Where(e => cleanValues.Contains(e.CleanName))
|
||||
.ToArray();
|
||||
|
||||
var lookup = artists
|
||||
.GroupBy(e => e.Name!, StringComparer.OrdinalIgnoreCase)
|
||||
.GroupBy(e => e.CleanName!)
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
g => g.Select(f => _queryHelpers.DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray(),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
g => g.Select(f => _queryHelpers.DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray());
|
||||
|
||||
var result = new Dictionary<string, MusicArtist[]>(artistNames.Count);
|
||||
foreach (var name in artistNames)
|
||||
var result = new Dictionary<string, MusicArtist[]>(cleanNames.Length);
|
||||
foreach (var (original, clean) in cleanNames)
|
||||
{
|
||||
if (lookup.TryGetValue(name, out var artistArray))
|
||||
if (lookup.TryGetValue(clean, out var artistArray))
|
||||
{
|
||||
result[name] = artistArray;
|
||||
result[original] = artistArray;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -225,12 +225,14 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
?? throw new ResourceNotFoundException(nameof(user.Id));
|
||||
|
||||
dbContext.Entry(dbUser).CurrentValues.SetValues(user);
|
||||
dbContext.Permissions.RemoveRange(dbUser.Permissions);
|
||||
dbUser.Permissions.Clear();
|
||||
foreach (var permission in user.Permissions)
|
||||
{
|
||||
dbUser.Permissions.Add(new Permission(permission.Kind, permission.Value));
|
||||
}
|
||||
|
||||
dbContext.Preferences.RemoveRange(dbUser.Preferences);
|
||||
dbUser.Preferences.Clear();
|
||||
foreach (var preference in user.Preferences)
|
||||
{
|
||||
|
||||
@@ -19,7 +19,9 @@ public class SearchProviderQuery
|
||||
public Guid? UserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item types to include in the search.
|
||||
/// Gets the item types to include in the search. An empty array means every type is eligible.
|
||||
/// When this is non-empty it is the authoritative type filter and <see cref="ExcludeItemTypes"/>
|
||||
/// does not apply; excludes only take effect when no include types were requested.
|
||||
/// </summary>
|
||||
public BaseItemKind[] IncludeItemTypes { get; init; } = [];
|
||||
|
||||
@@ -29,7 +31,9 @@ public class SearchProviderQuery
|
||||
public BaseItemKind[] ExcludeItemTypes { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the media types to include in the search.
|
||||
/// Gets the media types to include in the search. This is an additional constraint rather than
|
||||
/// an alternative one: a provider must return only items that match both the requested media
|
||||
/// types and the requested item types, not the union of the two.
|
||||
/// </summary>
|
||||
public MediaType[] MediaTypes { get; init; } = [];
|
||||
|
||||
@@ -39,7 +43,9 @@ public class SearchProviderQuery
|
||||
public int? Limit { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parent ID to scope the search.
|
||||
/// Gets the parent ID to scope the search. This scopes to the whole subtree, not just direct
|
||||
/// children - callers routinely pass a library folder id and expect items nested arbitrarily
|
||||
/// deep beneath it (an episode under a season under a series) to match.
|
||||
/// </summary>
|
||||
public Guid? ParentId { get; init; }
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace MediaBrowser.Controller.Session
|
||||
private readonly Lock _progressLock = new();
|
||||
private Timer _progressTimer;
|
||||
private PlaybackProgressInfo _lastProgressInfo;
|
||||
private long? _lastPlaybackCheckInPositionTicks;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
@@ -124,6 +125,22 @@ namespace MediaBrowser.Controller.Session
|
||||
/// <value>The last playback check in.</value>
|
||||
public DateTime LastPlaybackCheckIn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the position reported by the client at the last playback check-in.
|
||||
/// </summary>
|
||||
/// <value>The position ticks, or <see langword="null"/> if the client did not report a position.</value>
|
||||
[JsonIgnore]
|
||||
public long? LastPlaybackCheckInPositionTicks
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_progressLock)
|
||||
{
|
||||
return _lastPlaybackCheckInPositionTicks;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last paused date.
|
||||
/// </summary>
|
||||
@@ -372,6 +389,7 @@ namespace MediaBrowser.Controller.Session
|
||||
|
||||
lock (_progressLock)
|
||||
{
|
||||
_lastPlaybackCheckInPositionTicks = progressInfo.PositionTicks;
|
||||
_lastProgressInfo = progressInfo;
|
||||
|
||||
if (_progressTimer is null)
|
||||
|
||||
@@ -14,6 +14,7 @@ using MediaBrowser.Providers.Music;
|
||||
using MetaBrainz.MusicBrainz;
|
||||
using MetaBrainz.MusicBrainz.Interfaces.Entities;
|
||||
using MetaBrainz.MusicBrainz.Interfaces.Searches;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.MusicBrainz;
|
||||
|
||||
@@ -22,6 +23,17 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz;
|
||||
/// </summary>
|
||||
public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder
|
||||
{
|
||||
private readonly ILogger<MusicBrainzAlbumProvider> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MusicBrainzAlbumProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public MusicBrainzAlbumProvider(ILogger<MusicBrainzAlbumProvider> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "MusicBrainz";
|
||||
|
||||
@@ -32,21 +44,26 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
|
||||
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = MusicBrainz.Plugin.Instance!.MusicBrainzQuery;
|
||||
var releaseId = searchInfo.GetReleaseId();
|
||||
var releaseGroupId = searchInfo.GetReleaseGroupId();
|
||||
var releaseId = MusicBrainzQueryExtensions.ParseMusicBrainzId(searchInfo.GetReleaseId(), "release", _logger);
|
||||
var releaseGroupId = MusicBrainzQueryExtensions.ParseMusicBrainzId(searchInfo.GetReleaseGroupId(), "release group", _logger);
|
||||
|
||||
if (!string.IsNullOrEmpty(releaseId))
|
||||
if (releaseId is not null)
|
||||
{
|
||||
var releaseResult = await query.LookupReleaseAsync(new Guid(releaseId), Include.Artists | Include.ReleaseGroups, cancellationToken).ConfigureAwait(false);
|
||||
return GetReleaseResult(releaseResult).SingleItemAsEnumerable();
|
||||
var releaseResult = await query.LookupReleaseOrNullAsync(releaseId.Value, Include.Artists | Include.ReleaseGroups, _logger, cancellationToken).ConfigureAwait(false);
|
||||
if (releaseResult is not null)
|
||||
{
|
||||
return GetReleaseResult(releaseResult).SingleItemAsEnumerable();
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(releaseGroupId))
|
||||
if (releaseGroupId is not null)
|
||||
{
|
||||
var releaseGroupResult = await query.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.Releases, null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// No need to pass the cancellation token to GetReleaseGroupResultAsync as we're already passing it to ToBlockingEnumerable
|
||||
return GetReleaseGroupResultAsync(releaseGroupResult.Releases, CancellationToken.None).ToBlockingEnumerable(cancellationToken);
|
||||
var releaseGroupResult = await query.LookupReleaseGroupOrNullAsync(releaseGroupId.Value, Include.Releases, _logger, cancellationToken).ConfigureAwait(false);
|
||||
if (releaseGroupResult is not null)
|
||||
{
|
||||
// No need to pass the cancellation token to GetReleaseGroupResultAsync as we're already passing it to ToBlockingEnumerable
|
||||
return GetReleaseGroupResultAsync(releaseGroupResult.Releases, CancellationToken.None).ToBlockingEnumerable(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
var artistMusicBrainzId = searchInfo.GetMusicBrainzArtistId();
|
||||
@@ -102,8 +119,11 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
|
||||
foreach (var result in releaseSearchResults)
|
||||
{
|
||||
// Fetch full release info, otherwise artists are missing
|
||||
var fullResult = await query.LookupReleaseAsync(result.Id, Include.Artists | Include.ReleaseGroups, cancellationToken).ConfigureAwait(false);
|
||||
yield return GetReleaseResult(fullResult);
|
||||
var fullResult = await query.LookupReleaseOrNullAsync(result.Id, Include.Artists | Include.ReleaseGroups, _logger, cancellationToken).ConfigureAwait(false);
|
||||
if (fullResult is not null)
|
||||
{
|
||||
yield return GetReleaseResult(fullResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,8 +176,8 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
|
||||
public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = MusicBrainz.Plugin.Instance!.MusicBrainzQuery;
|
||||
var releaseId = info.GetReleaseId();
|
||||
var releaseGroupId = info.GetReleaseGroupId();
|
||||
var releaseId = MusicBrainzQueryExtensions.ParseMusicBrainzId(info.GetReleaseId(), "release", _logger);
|
||||
var releaseGroupId = MusicBrainzQueryExtensions.ParseMusicBrainzId(info.GetReleaseGroupId(), "release group", _logger);
|
||||
|
||||
var result = new MetadataResult<MusicAlbum>
|
||||
{
|
||||
@@ -165,15 +185,15 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
|
||||
};
|
||||
|
||||
// If there is a release group, but no release ID, try to match the release
|
||||
if (string.IsNullOrWhiteSpace(releaseId) && !string.IsNullOrWhiteSpace(releaseGroupId))
|
||||
if (releaseId is null && releaseGroupId is not null)
|
||||
{
|
||||
// TODO: Actually try to match the release. Simply taking the first result is stupid.
|
||||
var releaseGroupLookup = await query.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.None, null, cancellationToken).ConfigureAwait(false);
|
||||
releaseId = releaseGroupLookup.Releases?.Count > 0 ? releaseGroupLookup.Releases[0].Id.ToString() : null;
|
||||
var releaseGroupLookup = await query.LookupReleaseGroupOrNullAsync(releaseGroupId.Value, Include.None, _logger, cancellationToken).ConfigureAwait(false);
|
||||
releaseId = releaseGroupLookup?.Releases?.Count > 0 ? releaseGroupLookup.Releases[0].Id : null;
|
||||
}
|
||||
|
||||
// If there is no release ID, lookup a release with the info we have
|
||||
if (string.IsNullOrWhiteSpace(releaseId))
|
||||
if (releaseId is null)
|
||||
{
|
||||
var artistMusicBrainzId = info.GetMusicBrainzArtistId();
|
||||
IRelease? releaseResult = null;
|
||||
@@ -193,55 +213,61 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
|
||||
|
||||
if (releaseResult is not null)
|
||||
{
|
||||
releaseId = releaseResult.Id.ToString();
|
||||
releaseId = releaseResult.Id;
|
||||
|
||||
if (releaseResult.ReleaseGroup?.Id is not null)
|
||||
{
|
||||
releaseGroupId = releaseResult.ReleaseGroup.Id.ToString();
|
||||
releaseGroupId = releaseResult.ReleaseGroup.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId))
|
||||
if (releaseId is null && releaseGroupId is null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
// Fetch the full release (and its release group) so we can populate everything MusicBrainz returns.
|
||||
IRelease? release = null;
|
||||
if (!string.IsNullOrWhiteSpace(releaseId))
|
||||
if (releaseId is not null)
|
||||
{
|
||||
release = await query.LookupReleaseAsync(
|
||||
new Guid(releaseId),
|
||||
release = await query.LookupReleaseOrNullAsync(
|
||||
releaseId.Value,
|
||||
Include.Artists | Include.ReleaseGroups | Include.Labels | Include.Genres | Include.Tags,
|
||||
_logger,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(releaseGroupId) && release?.ReleaseGroup?.Id is not null)
|
||||
if (releaseGroupId is null && release?.ReleaseGroup?.Id is not null)
|
||||
{
|
||||
releaseGroupId = release.ReleaseGroup.Id.ToString();
|
||||
releaseGroupId = release.ReleaseGroup.Id;
|
||||
}
|
||||
}
|
||||
|
||||
IReleaseGroup? releaseGroup = null;
|
||||
if (!string.IsNullOrWhiteSpace(releaseGroupId))
|
||||
if (releaseGroupId is not null)
|
||||
{
|
||||
releaseGroup = await query.LookupReleaseGroupAsync(
|
||||
new Guid(releaseGroupId),
|
||||
releaseGroup = await query.LookupReleaseGroupOrNullAsync(
|
||||
releaseGroupId.Value,
|
||||
Include.Artists | Include.Genres | Include.Tags,
|
||||
null,
|
||||
_logger,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (release is null && releaseGroup is null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result.HasMetadata = true;
|
||||
|
||||
if (!string.IsNullOrEmpty(releaseId))
|
||||
if (releaseId is not null)
|
||||
{
|
||||
result.Item.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseId);
|
||||
result.Item.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseId.Value.ToString());
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(releaseGroupId))
|
||||
if (releaseGroupId is not null)
|
||||
{
|
||||
result.Item.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, releaseGroupId);
|
||||
result.Item.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, releaseGroupId.Value.ToString());
|
||||
}
|
||||
|
||||
Populate(result.Item, release, releaseGroup);
|
||||
|
||||
@@ -13,6 +13,7 @@ using MediaBrowser.Providers.Music;
|
||||
using MetaBrainz.MusicBrainz;
|
||||
using MetaBrainz.MusicBrainz.Interfaces.Entities;
|
||||
using MetaBrainz.MusicBrainz.Interfaces.Searches;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.MusicBrainz;
|
||||
|
||||
@@ -21,6 +22,17 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz;
|
||||
/// </summary>
|
||||
public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>, IHasOrder
|
||||
{
|
||||
private readonly ILogger<MusicBrainzArtistProvider> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MusicBrainzArtistProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public MusicBrainzArtistProvider(ILogger<MusicBrainzArtistProvider> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "MusicBrainz";
|
||||
|
||||
@@ -32,12 +44,15 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, Ar
|
||||
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = MusicBrainz.Plugin.Instance!.MusicBrainzQuery;
|
||||
var artistId = searchInfo.GetMusicBrainzArtistId();
|
||||
var artistId = MusicBrainzQueryExtensions.ParseMusicBrainzId(searchInfo.GetMusicBrainzArtistId(), "artist", _logger);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(artistId))
|
||||
if (artistId is not null)
|
||||
{
|
||||
var artistResult = await query.LookupArtistAsync(new Guid(artistId), Include.Aliases, null, null, cancellationToken).ConfigureAwait(false);
|
||||
return GetResultFromResponse(artistResult).SingleItemAsEnumerable();
|
||||
var artistResult = await query.LookupArtistOrNullAsync(artistId.Value, Include.Aliases, _logger, cancellationToken).ConfigureAwait(false);
|
||||
if (artistResult is not null)
|
||||
{
|
||||
return GetResultFromResponse(artistResult).SingleItemAsEnumerable();
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(searchInfo.Name))
|
||||
@@ -99,22 +114,22 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, Ar
|
||||
{
|
||||
var result = new MetadataResult<MusicArtist> { Item = new MusicArtist() };
|
||||
|
||||
var musicBrainzId = info.GetMusicBrainzArtistId();
|
||||
var musicBrainzId = MusicBrainzQueryExtensions.ParseMusicBrainzId(info.GetMusicBrainzArtistId(), "artist", _logger);
|
||||
|
||||
// If we don't have an id yet, resolve one by name so we can look the artist up.
|
||||
if (string.IsNullOrWhiteSpace(musicBrainzId))
|
||||
if (musicBrainzId is null)
|
||||
{
|
||||
var searchResults = await GetSearchResults(info, cancellationToken).ConfigureAwait(false);
|
||||
musicBrainzId = searchResults.FirstOrDefault()?.GetProviderId(MetadataProvider.MusicBrainzArtist);
|
||||
musicBrainzId = MusicBrainzQueryExtensions.ParseMusicBrainzId(searchResults.FirstOrDefault()?.GetProviderId(MetadataProvider.MusicBrainzArtist), "artist", _logger);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(musicBrainzId))
|
||||
if (musicBrainzId is null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var query = Plugin.Instance!.MusicBrainzQuery;
|
||||
var artist = await query.LookupArtistAsync(new Guid(musicBrainzId), Include.Genres | Include.Tags, null, null, cancellationToken).ConfigureAwait(false);
|
||||
var artist = await query.LookupArtistOrNullAsync(musicBrainzId.Value, Include.Genres | Include.Tags, _logger, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (artist is null)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MetaBrainz.Common;
|
||||
using MetaBrainz.MusicBrainz;
|
||||
using MetaBrainz.MusicBrainz.Interfaces.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.MusicBrainz;
|
||||
|
||||
/// <summary>
|
||||
/// Helpers for talking to MusicBrainz with identifiers that are not guaranteed to be valid.
|
||||
/// </summary>
|
||||
internal static class MusicBrainzQueryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a MusicBrainz identifier, which may come from user-supplied tags or NFO files and is therefore not
|
||||
/// guaranteed to be a valid GUID.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier to parse.</param>
|
||||
/// <param name="entityType">The type of entity the identifier refers to, used for logging.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <returns>The parsed identifier, or <see langword="null"/> if it is missing or malformed.</returns>
|
||||
public static Guid? ParseMusicBrainzId(string? id, string entityType, ILogger logger)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!Guid.TryParse(id, out var parsedId))
|
||||
{
|
||||
logger.LogDebug("Ignoring malformed MusicBrainz {EntityType} id {Id}", entityType, id);
|
||||
return null;
|
||||
}
|
||||
|
||||
return parsedId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a release, treating an unknown identifier as missing data rather than an error.
|
||||
/// </summary>
|
||||
/// <param name="query">The MusicBrainz query client.</param>
|
||||
/// <param name="releaseId">The release identifier.</param>
|
||||
/// <param name="include">The additional data to include in the lookup.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The release, or <see langword="null"/> if MusicBrainz does not have it.</returns>
|
||||
public static Task<IRelease?> LookupReleaseOrNullAsync(this Query query, Guid releaseId, Include include, ILogger logger, CancellationToken cancellationToken)
|
||||
=> NotFoundAsNullAsync(
|
||||
() => query.LookupReleaseAsync(releaseId, include, cancellationToken),
|
||||
"release",
|
||||
releaseId,
|
||||
logger);
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a release group, treating an unknown identifier as missing data rather than an error.
|
||||
/// </summary>
|
||||
/// <param name="query">The MusicBrainz query client.</param>
|
||||
/// <param name="releaseGroupId">The release group identifier.</param>
|
||||
/// <param name="include">The additional data to include in the lookup.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The release group, or <see langword="null"/> if MusicBrainz does not have it.</returns>
|
||||
public static Task<IReleaseGroup?> LookupReleaseGroupOrNullAsync(this Query query, Guid releaseGroupId, Include include, ILogger logger, CancellationToken cancellationToken)
|
||||
=> NotFoundAsNullAsync(
|
||||
() => query.LookupReleaseGroupAsync(releaseGroupId, include, null, cancellationToken),
|
||||
"release group",
|
||||
releaseGroupId,
|
||||
logger);
|
||||
|
||||
/// <summary>
|
||||
/// Looks up an artist, treating an unknown identifier as missing data rather than an error.
|
||||
/// </summary>
|
||||
/// <param name="query">The MusicBrainz query client.</param>
|
||||
/// <param name="artistId">The artist identifier.</param>
|
||||
/// <param name="include">The additional data to include in the lookup.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The artist, or <see langword="null"/> if MusicBrainz does not have it.</returns>
|
||||
public static Task<IArtist?> LookupArtistOrNullAsync(this Query query, Guid artistId, Include include, ILogger logger, CancellationToken cancellationToken)
|
||||
=> NotFoundAsNullAsync(
|
||||
() => query.LookupArtistAsync(artistId, include, null, null, cancellationToken),
|
||||
"artist",
|
||||
artistId,
|
||||
logger);
|
||||
|
||||
/// <summary>
|
||||
/// Runs a lookup, mapping a "not found" response to <see langword="null"/>. Identifiers stored on a library item
|
||||
/// can refer to entities that no longer exist in MusicBrainz, which is not an error worth failing a refresh over.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of entity being looked up.</typeparam>
|
||||
/// <param name="lookup">The lookup to run.</param>
|
||||
/// <param name="entityType">The type of entity being looked up, used for logging.</param>
|
||||
/// <param name="id">The identifier being looked up, used for logging.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <returns>The entity, or <see langword="null"/> if MusicBrainz does not have it.</returns>
|
||||
private static async Task<T?> NotFoundAsNullAsync<T>(Func<Task<T>> lookup, string entityType, Guid id, ILogger logger)
|
||||
where T : class
|
||||
{
|
||||
try
|
||||
{
|
||||
return await lookup().ConfigureAwait(false);
|
||||
}
|
||||
catch (HttpError ex) when (ex.Status == HttpStatusCode.NotFound)
|
||||
{
|
||||
logger.LogDebug("MusicBrainz has no {EntityType} with id {Id}", entityType, id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
-10
@@ -117,8 +117,6 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("UserId", "ItemId", "Client", "Key")
|
||||
.IsUnique();
|
||||
|
||||
@@ -176,8 +174,6 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("UserId", "ItemId", "Client")
|
||||
.IsUnique();
|
||||
|
||||
@@ -291,12 +287,17 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Value")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Permission_Permissions_Guid");
|
||||
b.HasIndex("UserId", "Kind")
|
||||
.IsUnique()
|
||||
.HasFilter("[UserId] IS NOT NULL");
|
||||
|
||||
b.ToTable("Permissions");
|
||||
});
|
||||
@@ -317,6 +318,9 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(65535)
|
||||
@@ -324,7 +328,9 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Preference_Preferences_Guid");
|
||||
b.HasIndex("UserId", "Kind")
|
||||
.IsUnique()
|
||||
.HasFilter("[UserId] IS NOT NULL");
|
||||
|
||||
b.ToTable("Preferences");
|
||||
});
|
||||
@@ -431,10 +437,14 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
.HasColumnType("TEXT")
|
||||
.UseCollation("NOCASE");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
@@ -469,7 +479,8 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithOne("ProfileImage")
|
||||
.HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
|
||||
.HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
|
||||
@@ -485,14 +496,16 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithMany("Permissions")
|
||||
.HasForeignKey("Permission_Permissions_Guid");
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithMany("Preferences")
|
||||
.HasForeignKey("Preference_Preferences_Guid");
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
||||
|
||||
+31
@@ -445,6 +445,37 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.ToTable("DeviceOptions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
|
||||
{
|
||||
b.Property<Guid>("ItemId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Width")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Bandwidth")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Height")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Interval")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ThumbnailCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("TileHeight")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("TileWidth")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ItemId", "Width");
|
||||
|
||||
b.ToTable("TrickplayInfos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
||||
+3
@@ -285,6 +285,9 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.Property<Guid>("ItemId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SegmentProviderId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("StartTicks")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
||||
+1
-2
@@ -1247,8 +1247,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT")
|
||||
.UseCollation("NOCASE");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
|
||||
+1
-2
@@ -1247,8 +1247,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT")
|
||||
.UseCollation("NOCASE");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
|
||||
+7
-1
@@ -229,6 +229,9 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.Property<int?>("IndexNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("InheritedParentalRatingSubValue")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("InheritedParentalRatingValue")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -1275,7 +1278,10 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.Property<int>("MaxActiveSessions")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("MaxParentalAgeRating")
|
||||
b.Property<int?>("MaxParentalRatingScore")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("MaxParentalRatingSubScore")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("MustUpdatePassword")
|
||||
|
||||
+36
-1
@@ -229,6 +229,9 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.Property<int?>("IndexNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("InheritedParentalRatingSubValue")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("InheritedParentalRatingValue")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -748,6 +751,24 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
|
||||
{
|
||||
b.Property<Guid>("ItemId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.PrimitiveCollection<string>("KeyframeTicks")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("TotalDuration")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ItemId");
|
||||
|
||||
b.ToTable("KeyframeData");
|
||||
|
||||
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -1260,7 +1281,10 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.Property<int>("MaxActiveSessions")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("MaxParentalAgeRating")
|
||||
b.Property<int?>("MaxParentalRatingScore")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("MaxParentalRatingSubScore")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("MustUpdatePassword")
|
||||
@@ -1519,6 +1543,17 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
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.MediaStreamInfo", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
|
||||
|
||||
+32
@@ -750,6 +750,24 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
|
||||
{
|
||||
b.Property<Guid>("ItemId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.PrimitiveCollection<string>("KeyframeTicks")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("TotalDuration")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ItemId");
|
||||
|
||||
b.ToTable("KeyframeData");
|
||||
|
||||
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -847,6 +865,9 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.Property<int?>("ElPresentFlag")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool?>("Hdr10PlusPresentFlag")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("Height")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -1521,6 +1542,17 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
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.MediaStreamInfo", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
|
||||
|
||||
+32
-1
@@ -123,7 +123,6 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Codec")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CodecTag")
|
||||
@@ -751,6 +750,24 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
|
||||
{
|
||||
b.Property<Guid>("ItemId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.PrimitiveCollection<string>("KeyframeTicks")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("TotalDuration")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ItemId");
|
||||
|
||||
b.ToTable("KeyframeData");
|
||||
|
||||
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -848,6 +865,9 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.Property<int?>("ElPresentFlag")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool?>("Hdr10PlusPresentFlag")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("Height")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -1522,6 +1542,17 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
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.MediaStreamInfo", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
|
||||
|
||||
-3
@@ -183,9 +183,6 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.Property<DateTime?>("DateCreated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("DateCreatedFilesystem")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("DateLastMediaAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
+3
@@ -955,6 +955,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
|
||||
b.Property<bool?>("IsInterlaced")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsOriginal")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("KeyFrames")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
+3
@@ -952,6 +952,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
|
||||
b.Property<bool?>("IsInterlaced")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsOriginal")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("KeyFrames")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
+3
@@ -952,6 +952,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
|
||||
b.Property<bool?>("IsInterlaced")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsOriginal")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("KeyFrames")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
+3
@@ -954,6 +954,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
|
||||
b.Property<bool?>("IsInterlaced")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsOriginal")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("KeyFrames")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
+3
@@ -956,6 +956,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
|
||||
b.Property<bool?>("IsInterlaced")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsOriginal")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("KeyFrames")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
+3
@@ -956,6 +956,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
|
||||
b.Property<bool?>("IsInterlaced")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsOriginal")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("KeyFrames")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
+3
@@ -956,6 +956,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
|
||||
b.Property<bool?>("IsInterlaced")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsOriginal")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("KeyFrames")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
+3
@@ -958,6 +958,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
|
||||
b.Property<bool?>("IsInterlaced")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsOriginal")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("KeyFrames")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
+3
@@ -961,6 +961,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
|
||||
b.Property<bool?>("IsInterlaced")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsOriginal")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("KeyFrames")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
+3
@@ -961,6 +961,9 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.Property<bool?>("IsInterlaced")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsOriginal")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("KeyFrames")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
+3
@@ -961,6 +961,9 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.Property<bool?>("IsInterlaced")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsOriginal")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("KeyFrames")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
+4
-6
@@ -818,23 +818,21 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.Property<Guid>("ParentId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("ChildId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ChildType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("SortOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ParentId", "ChildId");
|
||||
b.HasKey("ParentId", "SortOrder");
|
||||
|
||||
b.HasIndex("ChildId", "ChildType");
|
||||
|
||||
b.HasIndex("ParentId", "ChildType");
|
||||
|
||||
b.HasIndex("ParentId", "SortOrder");
|
||||
|
||||
b.ToTable("LinkedChildren", (string)null);
|
||||
|
||||
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
|
||||
|
||||
+4
-228
@@ -818,23 +818,21 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.Property<Guid>("ParentId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("ChildId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ChildType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("SortOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ParentId", "ChildId");
|
||||
b.HasKey("ParentId", "SortOrder");
|
||||
|
||||
b.HasIndex("ChildId", "ChildType");
|
||||
|
||||
b.HasIndex("ParentId", "ChildType");
|
||||
|
||||
b.HasIndex("ParentId", "SortOrder");
|
||||
|
||||
b.ToTable("LinkedChildren", (string)null);
|
||||
|
||||
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
|
||||
@@ -1107,58 +1105,6 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PlaybackItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("ItemId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MediaType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ItemId");
|
||||
|
||||
b.ToTable("PlaybackItems");
|
||||
|
||||
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PlaybackItemKey", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("PlaybackItemId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Key")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("PlaybackItemId");
|
||||
|
||||
b.ToTable("PlaybackItemKeys");
|
||||
|
||||
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -1526,131 +1472,6 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserPlaybackHistory", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("ActualBytesTransferred")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("Bitrate")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClientName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateStarted")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateStopped")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MediaSourceId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PlaySessionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("PlaybackItemId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("PlayedDurationTicks")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("PlayedToCompletion")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long?>("RunTimeTicks")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("StartPositionTicks")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("StopPositionTicks")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Transcoded")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlaybackItemId", "PlayedToCompletion");
|
||||
|
||||
b.HasIndex("UserId", "DateStopped");
|
||||
|
||||
b.HasIndex("UserId", "PlaybackItemId", "DateStopped");
|
||||
|
||||
b.ToTable("UserPlaybackHistory");
|
||||
|
||||
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserPlaybackHistoryStream", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Bitrate")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("Channels")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Codec")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Height")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("HistoryId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool?>("IsForced")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool?>("IsHearingImpaired")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Origin")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("StreamType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("VideoRange")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Width")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("HistoryId");
|
||||
|
||||
b.HasIndex("StreamType", "Origin", "Language");
|
||||
|
||||
b.HasIndex("StreamType", "Origin", "VideoRange");
|
||||
|
||||
b.ToTable("UserPlaybackHistoryStreams");
|
||||
|
||||
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
|
||||
@@ -1884,17 +1705,6 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PlaybackItemKey", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Database.Implementations.Entities.PlaybackItem", "PlaybackItem")
|
||||
.WithMany("Keys")
|
||||
.HasForeignKey("PlaybackItemId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("PlaybackItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
|
||||
@@ -1933,28 +1743,6 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserPlaybackHistory", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Database.Implementations.Entities.PlaybackItem", "PlaybackItem")
|
||||
.WithMany("History")
|
||||
.HasForeignKey("PlaybackItemId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("PlaybackItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserPlaybackHistoryStream", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Database.Implementations.Entities.UserPlaybackHistory", "History")
|
||||
.WithMany("Streams")
|
||||
.HasForeignKey("HistoryId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("History");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
|
||||
{
|
||||
b.Navigation("Chapters");
|
||||
@@ -2003,13 +1791,6 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.Navigation("BaseItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PlaybackItem", b =>
|
||||
{
|
||||
b.Navigation("History");
|
||||
|
||||
b.Navigation("Keys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b =>
|
||||
{
|
||||
b.Navigation("AccessSchedules");
|
||||
@@ -2024,11 +1805,6 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
|
||||
b.Navigation("ProfileImage");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserPlaybackHistory", b =>
|
||||
{
|
||||
b.Navigation("Streams");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
|
||||
+4
-6
@@ -818,23 +818,21 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
|
||||
b.Property<Guid>("ParentId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("ChildId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ChildType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("SortOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ParentId", "ChildId");
|
||||
b.HasKey("ParentId", "SortOrder");
|
||||
|
||||
b.HasIndex("ChildId", "ChildType");
|
||||
|
||||
b.HasIndex("ParentId", "ChildType");
|
||||
|
||||
b.HasIndex("ParentId", "SortOrder");
|
||||
|
||||
b.ToTable("LinkedChildren", (string)null);
|
||||
|
||||
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
[InlineData("Crouching.Tiger.Hidden.Dragon.BDrip.mkv", "Crouching.Tiger.Hidden.Dragon")]
|
||||
[InlineData("Crouching.Tiger.Hidden.Dragon.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon")]
|
||||
[InlineData("Crouching.Tiger.Hidden.Dragon.4K.UltraHD.HDR.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon")]
|
||||
[InlineData("Last.Call.for.Nowhere.WEB-DL.1080p", "Last.Call.for.Nowhere")]
|
||||
[InlineData("[HorribleSubs] Made in Abyss - 13 [720p].mkv", "Made in Abyss")]
|
||||
[InlineData("[Tsundere] Kore wa Zombie Desu ka of the Dead [BDRip h264 1920x1080 FLAC]", "Kore wa Zombie Desu ka of the Dead")]
|
||||
[InlineData("[Erai-raws] Jujutsu Kaisen - 03 [720p][Multiple Subtitle].mkv", "Jujutsu Kaisen")]
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Database.Implementations.Locking;
|
||||
using Jellyfin.Database.Providers.Sqlite;
|
||||
using Jellyfin.Server.Implementations.Item;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Tests.Item;
|
||||
|
||||
public sealed class ItemCountServiceTests : IDisposable
|
||||
{
|
||||
private readonly SqliteConnection _connection;
|
||||
private readonly DbContextOptions<JellyfinDbContext> _dbOptions;
|
||||
private readonly IApplicationPaths _applicationPaths;
|
||||
private readonly ItemCountService _service;
|
||||
|
||||
public ItemCountServiceTests()
|
||||
{
|
||||
_applicationPaths = new Mock<IApplicationPaths>().Object;
|
||||
|
||||
_connection = new SqliteConnection("Data Source=:memory:");
|
||||
_connection.Open();
|
||||
|
||||
_dbOptions = new DbContextOptionsBuilder<JellyfinDbContext>()
|
||||
.UseSqlite(_connection)
|
||||
.Options;
|
||||
|
||||
using (var context = CreateDbContext())
|
||||
{
|
||||
context.Database.EnsureCreated();
|
||||
}
|
||||
|
||||
var factory = new Mock<IDbContextFactory<JellyfinDbContext>>();
|
||||
factory.Setup(f => f.CreateDbContext()).Returns(CreateDbContext);
|
||||
|
||||
_service = new ItemCountService(
|
||||
factory.Object,
|
||||
new Mock<IItemTypeLookup>().Object,
|
||||
new Mock<IItemQueryHelpers>().Object);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_connection.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetChildCountBatch_LargeParentIdSet_DoesNotExceedSqliteVariableLimit()
|
||||
{
|
||||
var hierarchicalParentId = Guid.NewGuid();
|
||||
var linkedParentId = Guid.NewGuid();
|
||||
|
||||
var hierarchicalChildId = Guid.NewGuid();
|
||||
var linkedChildId1 = Guid.NewGuid();
|
||||
var linkedChildId2 = Guid.NewGuid();
|
||||
|
||||
using (var context = CreateDbContext())
|
||||
{
|
||||
context.BaseItems.AddRange(
|
||||
CreateItem(hierarchicalParentId),
|
||||
CreateItem(linkedParentId),
|
||||
CreateItem(hierarchicalChildId, hierarchicalParentId),
|
||||
CreateItem(linkedChildId1),
|
||||
CreateItem(linkedChildId2));
|
||||
|
||||
context.LinkedChildren.AddRange(
|
||||
new LinkedChildEntity
|
||||
{
|
||||
ParentId = linkedParentId,
|
||||
ChildId = linkedChildId1,
|
||||
ChildType = LinkedChildType.Manual,
|
||||
SortOrder = 0
|
||||
},
|
||||
new LinkedChildEntity
|
||||
{
|
||||
ParentId = linkedParentId,
|
||||
ChildId = linkedChildId2,
|
||||
ChildType = LinkedChildType.Manual,
|
||||
SortOrder = 1
|
||||
});
|
||||
|
||||
context.SaveChanges();
|
||||
}
|
||||
|
||||
var parentIds = Enumerable.Range(0, 40_000)
|
||||
.Select(_ => Guid.NewGuid())
|
||||
.ToList();
|
||||
|
||||
parentIds.Add(hierarchicalParentId);
|
||||
parentIds.Add(linkedParentId);
|
||||
|
||||
var result = _service.GetChildCountBatch(parentIds, null);
|
||||
|
||||
Assert.Equal(1, result[hierarchicalParentId]);
|
||||
Assert.Equal(2, result[linkedParentId]);
|
||||
Assert.Equal(parentIds.Count, result.Count);
|
||||
}
|
||||
|
||||
private static BaseItemEntity CreateItem(Guid id, Guid? parentId = null)
|
||||
{
|
||||
return new BaseItemEntity
|
||||
{
|
||||
Id = id,
|
||||
Type = "Folder",
|
||||
ParentId = parentId,
|
||||
IsFolder = true
|
||||
};
|
||||
}
|
||||
|
||||
private JellyfinDbContext CreateDbContext()
|
||||
{
|
||||
return new JellyfinDbContext(
|
||||
_dbOptions,
|
||||
NullLogger<JellyfinDbContext>.Instance,
|
||||
new SqliteDatabaseProvider(
|
||||
_applicationPaths,
|
||||
NullLogger<SqliteDatabaseProvider>.Instance),
|
||||
new NoLockBehavior(NullLogger<NoLockBehavior>.Instance));
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -349,7 +349,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
|
||||
});
|
||||
|
||||
var translated = localizationManager.GetLocalizedString("Artists", "de");
|
||||
Assert.Equal("Interpreten", translated);
|
||||
Assert.Equal("Künstler", translated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -406,7 +406,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
|
||||
{
|
||||
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("fr");
|
||||
var translated = localizationManager.GetServerLocalizedString("Artists");
|
||||
Assert.Equal("Interpreten", translated);
|
||||
Assert.Equal("Künstler", translated);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -427,7 +427,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
|
||||
{
|
||||
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("de");
|
||||
var translated = localizationManager.GetLocalizedString("Artists");
|
||||
Assert.Equal("Interpreten", translated);
|
||||
Assert.Equal("Künstler", translated);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Events;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Tests.SessionManager;
|
||||
|
||||
public class IdlePlaybackTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(null, null)]
|
||||
[InlineData(123456789L, 123456789L)]
|
||||
public async Task CheckForIdlePlayback_StopsAtLastClientReportedPosition(long? clientPositionTicks, long? expectedPositionTicks)
|
||||
{
|
||||
var playbackStopped = new TaskCompletionSource<long?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var eventManager = new Mock<IEventManager>();
|
||||
eventManager
|
||||
.Setup(manager => manager.PublishAsync(It.IsAny<PlaybackStopEventArgs>()))
|
||||
.Callback<PlaybackStopEventArgs>(eventArgs => playbackStopped.TrySetResult(eventArgs.PlaybackPositionTicks))
|
||||
.Returns(Task.CompletedTask);
|
||||
await using var sessionManager = new Emby.Server.Implementations.Session.SessionManager(
|
||||
NullLogger<Emby.Server.Implementations.Session.SessionManager>.Instance,
|
||||
eventManager.Object,
|
||||
Mock.Of<IUserDataManager>(),
|
||||
Mock.Of<IServerConfigurationManager>(),
|
||||
Mock.Of<ILibraryManager>(),
|
||||
Mock.Of<IUserManager>(),
|
||||
Mock.Of<IMusicManager>(),
|
||||
Mock.Of<IDtoService>(),
|
||||
Mock.Of<IImageProcessor>(),
|
||||
Mock.Of<IServerApplicationHost>(),
|
||||
Mock.Of<IDeviceManager>(),
|
||||
Mock.Of<IMediaSourceManager>(),
|
||||
Mock.Of<IHostApplicationLifetime>());
|
||||
var session = await sessionManager.LogSessionActivity(
|
||||
"Test Client",
|
||||
"1.0.0",
|
||||
"test-device",
|
||||
"Test Device",
|
||||
"127.0.0.1",
|
||||
null);
|
||||
session.NowPlayingItem = new BaseItemDto
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Test Item"
|
||||
};
|
||||
session.PlayState.PositionTicks = 987654321;
|
||||
|
||||
if (clientPositionTicks.HasValue)
|
||||
{
|
||||
session.StartAutomaticProgress(new PlaybackProgressInfo
|
||||
{
|
||||
IsPaused = true,
|
||||
PositionTicks = clientPositionTicks
|
||||
});
|
||||
session.StopAutomaticProgress();
|
||||
}
|
||||
|
||||
var idlePlaybackCallback = typeof(Emby.Server.Implementations.Session.SessionManager)
|
||||
.GetMethod("CheckForIdlePlayback", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
idlePlaybackCallback.Invoke(sessionManager, new object?[] { null });
|
||||
|
||||
var stoppedPositionTicks = await playbackStopped.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(expectedPositionTicks, stoppedPositionTicks);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Tests.SessionManager;
|
||||
|
||||
public class SessionInfoTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task StartAutomaticProgress_SnapshotsClientReportedPosition()
|
||||
{
|
||||
await using var session = new SessionInfo(Mock.Of<ISessionManager>(), NullLogger.Instance);
|
||||
var progressInfo = new PlaybackProgressInfo
|
||||
{
|
||||
IsPaused = true,
|
||||
PositionTicks = 123456789
|
||||
};
|
||||
|
||||
session.StartAutomaticProgress(progressInfo);
|
||||
|
||||
Assert.Equal(progressInfo.PositionTicks, session.LastPlaybackCheckInPositionTicks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutomaticProgress_AdvancesEstimatedPositionWithoutAdvancingSnapshot()
|
||||
{
|
||||
var sessionManager = new Mock<ISessionManager>();
|
||||
await using var session = new SessionInfo(sessionManager.Object, NullLogger.Instance);
|
||||
var automaticProgress = new TaskCompletionSource<long?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
const long reportedPositionTicks = 123456789;
|
||||
|
||||
sessionManager
|
||||
.Setup(manager => manager.OnPlaybackProgress(It.IsAny<PlaybackProgressInfo>(), true))
|
||||
.Callback<PlaybackProgressInfo, bool>((info, _) =>
|
||||
{
|
||||
session.PlayState.PositionTicks = info.PositionTicks;
|
||||
automaticProgress.TrySetResult(info.PositionTicks);
|
||||
})
|
||||
.Returns(Task.CompletedTask);
|
||||
session.PlayState.PositionTicks = reportedPositionTicks;
|
||||
|
||||
session.StartAutomaticProgress(new PlaybackProgressInfo
|
||||
{
|
||||
PositionTicks = reportedPositionTicks
|
||||
});
|
||||
|
||||
var estimatedPositionTicks = await automaticProgress.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
|
||||
session.StopAutomaticProgress();
|
||||
|
||||
Assert.Equal(reportedPositionTicks + TimeSpan.TicksPerSecond, estimatedPositionTicks);
|
||||
Assert.Equal(estimatedPositionTicks, session.PlayState.PositionTicks);
|
||||
Assert.Equal(reportedPositionTicks, session.LastPlaybackCheckInPositionTicks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAutomaticProgress_ReplacesSnapshotOnLaterClientReport()
|
||||
{
|
||||
await using var session = new SessionInfo(Mock.Of<ISessionManager>(), NullLogger.Instance);
|
||||
session.StartAutomaticProgress(new PlaybackProgressInfo
|
||||
{
|
||||
IsPaused = true,
|
||||
PositionTicks = 123456789
|
||||
});
|
||||
|
||||
session.StartAutomaticProgress(new PlaybackProgressInfo
|
||||
{
|
||||
IsPaused = true,
|
||||
PositionTicks = 987654321
|
||||
});
|
||||
|
||||
Assert.Equal(987654321, session.LastPlaybackCheckInPositionTicks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAutomaticProgress_PreservesExactPausedPosition()
|
||||
{
|
||||
await using var session = new SessionInfo(Mock.Of<ISessionManager>(), NullLogger.Instance);
|
||||
var pausedProgress = new PlaybackProgressInfo
|
||||
{
|
||||
IsPaused = true,
|
||||
PositionTicks = 314159265
|
||||
};
|
||||
|
||||
session.StartAutomaticProgress(pausedProgress);
|
||||
|
||||
Assert.Equal(pausedProgress.PositionTicks, session.LastPlaybackCheckInPositionTicks);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations;
|
||||
@@ -91,6 +92,28 @@ namespace Jellyfin.Server.Implementations.Tests.Users
|
||||
new NoLockBehavior(NullLogger<NoLockBehavior>.Instance));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateUserAsync_DoesNotLeaveOrphanedPermissionsOrPreferences()
|
||||
{
|
||||
var user = await _userManager.CreateUserAsync("updateduser");
|
||||
var permissionCount = user.Permissions.Count;
|
||||
var preferenceCount = user.Preferences.Count;
|
||||
|
||||
user.LastActivityDate = DateTime.UtcNow;
|
||||
await _userManager.UpdateUserAsync(user);
|
||||
await _userManager.UpdateUserAsync(user);
|
||||
|
||||
await using var context = CreateDbContext();
|
||||
Assert.Empty(await context.Permissions
|
||||
.Where(permission => !permission.UserId.HasValue)
|
||||
.ToListAsync(TestContext.Current.CancellationToken));
|
||||
Assert.Empty(await context.Preferences
|
||||
.Where(preference => !preference.UserId.HasValue)
|
||||
.ToListAsync(TestContext.Current.CancellationToken));
|
||||
Assert.Equal(permissionCount, await context.Permissions.CountAsync(TestContext.Current.CancellationToken));
|
||||
Assert.Equal(preferenceCount, await context.Preferences.CountAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClearProfileImageAsync_WhenInMemoryImageHasTemporaryKey_RemovesPersistedImage()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user