aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTORS.md3
-rw-r--r--Directory.Packages.props6
-rw-r--r--Emby.Server.Implementations/Localization/Core/fo.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/he.json9
-rw-r--r--Emby.Server.Implementations/Localization/Core/hy.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-PT.json97
-rw-r--r--Emby.Server.Implementations/Localization/Core/ta.json17
-rw-r--r--Emby.Server.Implementations/Localization/Core/tr.json2
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs2
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs2
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs18
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/SessionManager/IdlePlaybackTests.cs80
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionInfoTests.cs92
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerProfileImageTests.cs23
14 files changed, 303 insertions, 59 deletions
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index f6a725853d..99f24e3a14 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -238,6 +238,7 @@
- [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
@@ -249,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)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 1638546f29..ac9830fc7e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -29,9 +29,9 @@
<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.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" />
diff --git a/Emby.Server.Implementations/Localization/Core/fo.json b/Emby.Server.Implementations/Localization/Core/fo.json
index b02d688e5e..d1e9065d97 100644
--- a/Emby.Server.Implementations/Localization/Core/fo.json
+++ b/Emby.Server.Implementations/Localization/Core/fo.json
@@ -55,8 +55,8 @@
"TaskUpdatePluginsDescription": "Niðurtekur og innleggur dagføringar til ískoytisforrit ið eru stillaði til at dagførast sjálvvirkandi.",
"TaskCleanTranscodeDescription": "Strikar umkotaðar fílar ið eru eldri enn 1 dag.",
"TaskOptimizeDatabase": "Albøt dátugrunn",
- "NameSeasonNumber": "Sesong {0}",
- "NameSeasonUnknown": "Ókend sesong",
+ "NameSeasonNumber": "Umfar {0}",
+ "NameSeasonUnknown": "Ókent umfar",
"ScheduledTaskFailedWithName": "{0} miseydnaðist",
"Undefined": "Óskilmarkað",
"TasksMaintenanceCategory": "Viðlíkahald",
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index 48056b0bb8..0ca7c6aa08 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -108,5 +108,12 @@
"CleanupUserDataTaskDescription": "ניקוי כל המידע של המשתמש (מצב צפייה, מועדפים וכו) ממדיה שאינה קיימת מעל 90 יום.",
"CleanupUserDataTask": "משימת ניקוי מידע משתמש",
"LyricDownloadFailureFromForItem": "הורדת המילים מ-{0} עבור {1} נכשלה",
- "Original": "מקור"
+ "Original": "מקור",
+ "NameExtraBehindTheScenes": "מאחורי הקלעים",
+ "NameExtraClip": "קליפ",
+ "NameExtraDeletedScene": "סצנה שנמחקה",
+ "NameExtraFeaturette": "סרט קצר",
+ "NameExtraInterview": "ריאיון",
+ "NameExtraSample": "דגימה",
+ "NameExtraScene": "סצנה"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hy.json b/Emby.Server.Implementations/Localization/Core/hy.json
index b79b540bf4..80be71ed70 100644
--- a/Emby.Server.Implementations/Localization/Core/hy.json
+++ b/Emby.Server.Implementations/Localization/Core/hy.json
@@ -24,5 +24,10 @@
"TaskDownloadMissingSubtitles": "Ներբեռնել պակասող ենթագրերը",
"AppDeviceValues": "Հավելված` {0}, Սարք `{1}",
"ChapterNameValue": "Գլուխ {0}",
- "Collections": "Հավաքածուներ"
+ "Collections": "Հավաքածուներ",
+ "Artists": "Երաժիշտներ",
+ "Default": "Լռելյայն",
+ "Favorites": "Ընտրյալ",
+ "Forced": "Ստիպուած",
+ "Genres": "Ոճ"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index b0fdd8c311..e71475afd8 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -23,92 +23,92 @@
"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",
"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 de 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",
@@ -117,8 +117,9 @@
"NameExtraScene": "Cena",
"NameExtraClip": "Excerto",
"NameExtraFeaturette": "Minidocumentário",
- "NameExtraThemeSong": "Tema principal",
+ "NameExtraThemeSong": "Tema musical",
"NameExtraThemeVideo": "Vídeo temático",
"NameExtraShort": "Curta-metragem",
- "NameExtraNumbered": "{0} {1}"
+ "NameExtraNumbered": "{0} {1}",
+ "NameExtraTrailer": "Trailer"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
index f613b973db..256f349926 100644
--- a/Emby.Server.Implementations/Localization/Core/ta.json
+++ b/Emby.Server.Implementations/Localization/Core/ta.json
@@ -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}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index 1aa4b6a4b6..67f4ecc4d5 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -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",
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 5d62332552..f4aa0ad03a 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -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)
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 81408d9aa8..932ced547a 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -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)
{
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index fb68bfb770..d9bc79dec5 100644
--- a/MediaBrowser.Controller/Session/SessionInfo.cs
+++ b/MediaBrowser.Controller/Session/SessionInfo.cs
@@ -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;
@@ -125,6 +126,22 @@ namespace MediaBrowser.Controller.Session
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>
/// <value>The last paused date.</value>
@@ -372,6 +389,7 @@ namespace MediaBrowser.Controller.Session
lock (_progressLock)
{
+ _lastPlaybackCheckInPositionTicks = progressInfo.PositionTicks;
_lastProgressInfo = progressInfo;
if (_progressTimer is null)
diff --git a/tests/Jellyfin.Server.Implementations.Tests/SessionManager/IdlePlaybackTests.cs b/tests/Jellyfin.Server.Implementations.Tests/SessionManager/IdlePlaybackTests.cs
new file mode 100644
index 0000000000..7722707cbe
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/SessionManager/IdlePlaybackTests.cs
@@ -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);
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionInfoTests.cs b/tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionInfoTests.cs
new file mode 100644
index 0000000000..c5b8f661b5
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionInfoTests.cs
@@ -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);
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerProfileImageTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerProfileImageTests.cs
index cb714a4014..778b888735 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerProfileImageTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerProfileImageTests.cs
@@ -1,5 +1,6 @@
using System;
using System.IO;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
@@ -92,6 +93,28 @@ namespace Jellyfin.Server.Implementations.Tests.Users
}
[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()
{
var user = await _userManager.CreateUserAsync("profileimageuser");