mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-09-06 05:47:14 -04:00
Merge branch 'master' of github.com:jellyfin/jellyfin into socket-binding
This commit is contained in:
commit
0e1bf316b5
@ -50,6 +50,13 @@ jobs:
|
|||||||
targetPath: '$(Build.SourcesDirectory)/deployment/dist'
|
targetPath: '$(Build.SourcesDirectory)/deployment/dist'
|
||||||
artifactName: 'jellyfin-server-$(BuildConfiguration)'
|
artifactName: 'jellyfin-server-$(BuildConfiguration)'
|
||||||
|
|
||||||
|
- task: SSH@0
|
||||||
|
displayName: 'Create target directory on repository server'
|
||||||
|
inputs:
|
||||||
|
sshEndpoint: repository
|
||||||
|
runOptions: 'inline'
|
||||||
|
inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
|
||||||
|
|
||||||
- task: CopyFilesOverSSH@0
|
- task: CopyFilesOverSSH@0
|
||||||
displayName: 'Upload artifacts to repository server'
|
displayName: 'Upload artifacts to repository server'
|
||||||
inputs:
|
inputs:
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
using System.Net.Sockets;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Server.Implementations.Udp;
|
using Emby.Server.Implementations.Udp;
|
||||||
@ -48,8 +49,16 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task RunAsync()
|
public Task RunAsync()
|
||||||
{
|
{
|
||||||
_udpServer = new UdpServer(_logger, _appHost, _config);
|
try
|
||||||
_udpServer.Start(PortNumber, _cancellationTokenSource.Token);
|
{
|
||||||
|
_udpServer = new UdpServer(_logger, _appHost, _config);
|
||||||
|
_udpServer.Start(PortNumber, _cancellationTokenSource.Token);
|
||||||
|
}
|
||||||
|
catch (SocketException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Unable to start AutoDiscovery listener on UDP port {PortNumber}", PortNumber);
|
||||||
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -341,7 +341,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
if (item is LiveTvProgram)
|
if (item is LiveTvProgram)
|
||||||
{
|
{
|
||||||
_logger.LogDebug(
|
_logger.LogDebug(
|
||||||
"Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
|
"Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
|
||||||
item.GetType().Name,
|
item.GetType().Name,
|
||||||
item.Name ?? "Unknown name",
|
item.Name ?? "Unknown name",
|
||||||
item.Path ?? string.Empty,
|
item.Path ?? string.Empty,
|
||||||
@ -350,7 +350,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
|
"Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
|
||||||
item.GetType().Name,
|
item.GetType().Name,
|
||||||
item.Name ?? "Unknown name",
|
item.Name ?? "Unknown name",
|
||||||
item.Path ?? string.Empty,
|
item.Path ?? string.Empty,
|
||||||
@ -368,7 +368,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("Deleting path {MetadataPath}", metadataPath);
|
_logger.LogDebug(
|
||||||
|
"Deleting metadata path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
|
||||||
|
item.GetType().Name,
|
||||||
|
item.Name ?? "Unknown name",
|
||||||
|
metadataPath,
|
||||||
|
item.Id);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -392,7 +397,13 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Deleting path {path}", fileSystemInfo.FullName);
|
_logger.LogInformation(
|
||||||
|
"Deleting item path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
|
||||||
|
item.GetType().Name,
|
||||||
|
item.Name ?? "Unknown name",
|
||||||
|
fileSystemInfo.FullName,
|
||||||
|
item.Id);
|
||||||
|
|
||||||
if (fileSystemInfo.IsDirectory)
|
if (fileSystemInfo.IsDirectory)
|
||||||
{
|
{
|
||||||
Directory.Delete(fileSystemInfo.FullName, true);
|
Directory.Delete(fileSystemInfo.FullName, true);
|
||||||
|
@ -19,8 +19,8 @@
|
|||||||
"Sync": "Sinkroniseer",
|
"Sync": "Sinkroniseer",
|
||||||
"HeaderFavoriteSongs": "Gunsteling Liedjies",
|
"HeaderFavoriteSongs": "Gunsteling Liedjies",
|
||||||
"Songs": "Liedjies",
|
"Songs": "Liedjies",
|
||||||
"DeviceOnlineWithName": "{0} is verbind",
|
"DeviceOnlineWithName": "{0} gekoppel is",
|
||||||
"DeviceOfflineWithName": "{0} het afgesluit",
|
"DeviceOfflineWithName": "{0} is ontkoppel",
|
||||||
"Collections": "Versamelings",
|
"Collections": "Versamelings",
|
||||||
"Inherit": "Ontvang",
|
"Inherit": "Ontvang",
|
||||||
"HeaderLiveTV": "Live TV",
|
"HeaderLiveTV": "Live TV",
|
||||||
@ -91,5 +91,9 @@
|
|||||||
"ChapterNameValue": "Hoofstuk",
|
"ChapterNameValue": "Hoofstuk",
|
||||||
"CameraImageUploadedFrom": "'n Nuwe kamera photo opgelaai van {0}",
|
"CameraImageUploadedFrom": "'n Nuwe kamera photo opgelaai van {0}",
|
||||||
"AuthenticationSucceededWithUserName": "{0} suksesvol geverifieer",
|
"AuthenticationSucceededWithUserName": "{0} suksesvol geverifieer",
|
||||||
"Albums": "Albums"
|
"Albums": "Albums",
|
||||||
|
"TasksChannelsCategory": "Internet kanale",
|
||||||
|
"TasksApplicationCategory": "aansoek",
|
||||||
|
"TasksLibraryCategory": "biblioteek",
|
||||||
|
"TasksMaintenanceCategory": "onderhoud"
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"AppDeviceValues": "App: {0}, Gerät: {1}",
|
"AppDeviceValues": "App: {0}, Gerät: {1}",
|
||||||
"Application": "Anwendung",
|
"Application": "Anwendung",
|
||||||
"Artists": "Interpreten",
|
"Artists": "Interpreten",
|
||||||
"AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifiziert",
|
"AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich angemeldet",
|
||||||
"Books": "Bücher",
|
"Books": "Bücher",
|
||||||
"CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
|
"CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
|
||||||
"Channels": "Kanäle",
|
"Channels": "Kanäle",
|
||||||
|
@ -5,47 +5,47 @@
|
|||||||
"Artists": "Artis",
|
"Artists": "Artis",
|
||||||
"AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
|
"AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
|
||||||
"Books": "Buku-buku",
|
"Books": "Buku-buku",
|
||||||
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
|
"CameraImageUploadedFrom": "Ada gambar dari kamera yang baru dimuat naik melalui {0}",
|
||||||
"Channels": "Saluran",
|
"Channels": "Saluran",
|
||||||
"ChapterNameValue": "Chapter {0}",
|
"ChapterNameValue": "Bab {0}",
|
||||||
"Collections": "Koleksi",
|
"Collections": "Koleksi",
|
||||||
"DeviceOfflineWithName": "{0} has disconnected",
|
"DeviceOfflineWithName": "{0} telah diputuskan sambungan",
|
||||||
"DeviceOnlineWithName": "{0} is connected",
|
"DeviceOnlineWithName": "{0} telah disambung",
|
||||||
"FailedLoginAttemptWithUserName": "Cubaan log masuk gagal dari {0}",
|
"FailedLoginAttemptWithUserName": "Cubaan log masuk gagal dari {0}",
|
||||||
"Favorites": "Favorites",
|
"Favorites": "Kegemaran",
|
||||||
"Folders": "Folders",
|
"Folders": "Fail-fail",
|
||||||
"Genres": "Genre-genre",
|
"Genres": "Genre-genre",
|
||||||
"HeaderAlbumArtists": "Album Artists",
|
"HeaderAlbumArtists": "Album Artis-artis",
|
||||||
"HeaderCameraUploads": "Muatnaik Kamera",
|
"HeaderCameraUploads": "Muatnaik Kamera",
|
||||||
"HeaderContinueWatching": "Terus Menonton",
|
"HeaderContinueWatching": "Terus Menonton",
|
||||||
"HeaderFavoriteAlbums": "Favorite Albums",
|
"HeaderFavoriteAlbums": "Album-album Kegemaran",
|
||||||
"HeaderFavoriteArtists": "Favorite Artists",
|
"HeaderFavoriteArtists": "Artis-artis Kegemaran",
|
||||||
"HeaderFavoriteEpisodes": "Favorite Episodes",
|
"HeaderFavoriteEpisodes": "Episod-episod Kegemaran",
|
||||||
"HeaderFavoriteShows": "Favorite Shows",
|
"HeaderFavoriteShows": "Rancangan-rancangan Kegemaran",
|
||||||
"HeaderFavoriteSongs": "Favorite Songs",
|
"HeaderFavoriteSongs": "Lagu-lagu Kegemaran",
|
||||||
"HeaderLiveTV": "Live TV",
|
"HeaderLiveTV": "TV Siaran Langsung",
|
||||||
"HeaderNextUp": "Next Up",
|
"HeaderNextUp": "Seterusnya",
|
||||||
"HeaderRecordingGroups": "Recording Groups",
|
"HeaderRecordingGroups": "Kumpulan-kumpulan Rakaman",
|
||||||
"HomeVideos": "Home videos",
|
"HomeVideos": "Video Personal",
|
||||||
"Inherit": "Inherit",
|
"Inherit": "Mewarisi",
|
||||||
"ItemAddedWithName": "{0} was added to the library",
|
"ItemAddedWithName": "{0} telah ditambahkan ke dalam pustaka",
|
||||||
"ItemRemovedWithName": "{0} was removed from the library",
|
"ItemRemovedWithName": "{0} telah dibuang daripada pustaka",
|
||||||
"LabelIpAddressValue": "Alamat IP: {0}",
|
"LabelIpAddressValue": "Alamat IP: {0}",
|
||||||
"LabelRunningTimeValue": "Running time: {0}",
|
"LabelRunningTimeValue": "Masa berjalan: {0}",
|
||||||
"Latest": "Latest",
|
"Latest": "Terbaru",
|
||||||
"MessageApplicationUpdated": "Jellyfin Server has been updated",
|
"MessageApplicationUpdated": "Jellyfin Server telah dikemas kini",
|
||||||
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
|
"MessageApplicationUpdatedTo": "Jellyfin Server telah dikemas kini ke {0}",
|
||||||
"MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
|
"MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan di bahagian {0} telah dikemas kini",
|
||||||
"MessageServerConfigurationUpdated": "Server configuration has been updated",
|
"MessageServerConfigurationUpdated": "Konfigurasi pelayan telah dikemas kini",
|
||||||
"MixedContent": "Mixed content",
|
"MixedContent": "Kandungan campuran",
|
||||||
"Movies": "Movies",
|
"Movies": "Filem",
|
||||||
"Music": "Muzik",
|
"Music": "Muzik",
|
||||||
"MusicVideos": "Video muzik",
|
"MusicVideos": "Video muzik",
|
||||||
"NameInstallFailed": "{0} installation failed",
|
"NameInstallFailed": "{0} pemasangan gagal",
|
||||||
"NameSeasonNumber": "Season {0}",
|
"NameSeasonNumber": "Musim {0}",
|
||||||
"NameSeasonUnknown": "Season Unknown",
|
"NameSeasonUnknown": "Musim Tidak Diketahui",
|
||||||
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
|
"NewVersionIsAvailable": "Versi terbaru Jellyfin Server bersedia untuk dimuat turunkan.",
|
||||||
"NotificationOptionApplicationUpdateAvailable": "Application update available",
|
"NotificationOptionApplicationUpdateAvailable": "Kemas kini aplikasi telah sedia",
|
||||||
"NotificationOptionApplicationUpdateInstalled": "Application update installed",
|
"NotificationOptionApplicationUpdateInstalled": "Application update installed",
|
||||||
"NotificationOptionAudioPlayback": "Audio playback started",
|
"NotificationOptionAudioPlayback": "Audio playback started",
|
||||||
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
|
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
|
||||||
|
@ -104,5 +104,14 @@
|
|||||||
"TaskRefreshChapterImagesDescription": "Cria miniaturas para vídeos que têm capítulos.",
|
"TaskRefreshChapterImagesDescription": "Cria miniaturas para vídeos que têm capítulos.",
|
||||||
"TaskCleanCacheDescription": "Apaga ficheiros em cache que já não são usados pelo sistema.",
|
"TaskCleanCacheDescription": "Apaga ficheiros em cache que já não são usados pelo sistema.",
|
||||||
"TasksChannelsCategory": "Canais de Internet",
|
"TasksChannelsCategory": "Canais de Internet",
|
||||||
"TaskRefreshChapterImages": "Extrair Imagens do Capítulo"
|
"TaskRefreshChapterImages": "Extrair Imagens do Capítulo",
|
||||||
|
"TaskDownloadMissingSubtitlesDescription": "Pesquisa na Internet as legendas em falta com base na configuração de metadados.",
|
||||||
|
"TaskDownloadMissingSubtitles": "Download das legendas em falta",
|
||||||
|
"TaskRefreshChannelsDescription": "Atualiza as informações do canal da Internet.",
|
||||||
|
"TaskCleanTranscodeDescription": "Apagar os ficheiros com mais de um dia, de Transcode.",
|
||||||
|
"TaskCleanTranscode": "Limpar o diretório de Transcode",
|
||||||
|
"TaskUpdatePluginsDescription": "Download e instala as atualizações para plug-ins configurados para atualização automática.",
|
||||||
|
"TaskRefreshPeopleDescription": "Atualiza os metadados para atores e diretores na tua biblioteca de media.",
|
||||||
|
"TaskRefreshPeople": "Atualizar pessoas",
|
||||||
|
"TaskRefreshLibraryDescription": "Pesquisa a tua biblioteca de media por novos ficheiros e atualiza os metadados."
|
||||||
}
|
}
|
||||||
|
@ -67,5 +67,7 @@
|
|||||||
"Artists": "นักแสดง",
|
"Artists": "นักแสดง",
|
||||||
"Application": "แอปพลิเคชั่น",
|
"Application": "แอปพลิเคชั่น",
|
||||||
"AppDeviceValues": "App: {0}, อุปกรณ์: {1}",
|
"AppDeviceValues": "App: {0}, อุปกรณ์: {1}",
|
||||||
"Albums": "อัลบั้ม"
|
"Albums": "อัลบั้ม",
|
||||||
|
"ScheduledTaskStartedWithName": "{0} เริ่มต้น",
|
||||||
|
"ScheduledTaskFailedWithName": "{0} ล้มเหลว"
|
||||||
}
|
}
|
||||||
|
@ -296,7 +296,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
}
|
}
|
||||||
catch (DbUpdateConcurrencyException e)
|
catch (DbUpdateConcurrencyException e)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(e, "Error updating user's last activity date.");
|
_logger.LogDebug(e, "Error updating user's last activity date.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -194,26 +194,24 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void InitGroup(SessionInfo session, CancellationToken cancellationToken)
|
public void CreateGroup(SessionInfo session, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_group.AddSession(session);
|
_group.AddSession(session);
|
||||||
_syncPlayManager.AddSessionToGroup(session, this);
|
_syncPlayManager.AddSessionToGroup(session, this);
|
||||||
|
|
||||||
_group.PlayingItem = session.FullNowPlayingItem;
|
_group.PlayingItem = session.FullNowPlayingItem;
|
||||||
_group.IsPaused = true;
|
_group.IsPaused = session.PlayState.IsPaused;
|
||||||
_group.PositionTicks = session.PlayState.PositionTicks ?? 0;
|
_group.PositionTicks = session.PlayState.PositionTicks ?? 0;
|
||||||
_group.LastActivity = DateTime.UtcNow;
|
_group.LastActivity = DateTime.UtcNow;
|
||||||
|
|
||||||
var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow));
|
var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow));
|
||||||
SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken);
|
SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken);
|
||||||
var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause);
|
|
||||||
SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken)
|
public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (session.NowPlayingItem?.Id == _group.PlayingItem.Id && request.PlayingItemId == _group.PlayingItem.Id)
|
if (session.NowPlayingItem?.Id == _group.PlayingItem.Id)
|
||||||
{
|
{
|
||||||
_group.AddSession(session);
|
_group.AddSession(session);
|
||||||
_syncPlayManager.AddSessionToGroup(session, this);
|
_syncPlayManager.AddSessionToGroup(session, this);
|
||||||
@ -224,7 +222,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName);
|
var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName);
|
||||||
SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
|
SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
|
||||||
|
|
||||||
// Client join and play, syncing will happen client side
|
// Syncing will happen client-side
|
||||||
if (!_group.IsPaused)
|
if (!_group.IsPaused)
|
||||||
{
|
{
|
||||||
var playCommand = NewSyncPlayCommand(SendCommandType.Play);
|
var playCommand = NewSyncPlayCommand(SendCommandType.Play);
|
||||||
@ -262,10 +260,9 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
|
public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// The server's job is to mantain a consistent state to which clients refer to,
|
// The server's job is to maintain a consistent state for clients to reference
|
||||||
// as also to notify clients of state changes.
|
// and notify clients of state changes. The actual syncing of media playback
|
||||||
// The actual syncing of media playback happens client side.
|
// happens client side. Clients are aware of the server's time and use it to sync.
|
||||||
// Clients are aware of the server's time and use it to sync.
|
|
||||||
switch (request.Type)
|
switch (request.Type)
|
||||||
{
|
{
|
||||||
case PlaybackRequestType.Play:
|
case PlaybackRequestType.Play:
|
||||||
@ -277,13 +274,13 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
case PlaybackRequestType.Seek:
|
case PlaybackRequestType.Seek:
|
||||||
HandleSeekRequest(session, request, cancellationToken);
|
HandleSeekRequest(session, request, cancellationToken);
|
||||||
break;
|
break;
|
||||||
case PlaybackRequestType.Buffering:
|
case PlaybackRequestType.Buffer:
|
||||||
HandleBufferingRequest(session, request, cancellationToken);
|
HandleBufferingRequest(session, request, cancellationToken);
|
||||||
break;
|
break;
|
||||||
case PlaybackRequestType.BufferingDone:
|
case PlaybackRequestType.Ready:
|
||||||
HandleBufferingDoneRequest(session, request, cancellationToken);
|
HandleBufferingDoneRequest(session, request, cancellationToken);
|
||||||
break;
|
break;
|
||||||
case PlaybackRequestType.UpdatePing:
|
case PlaybackRequestType.Ping:
|
||||||
HandlePingUpdateRequest(session, request);
|
HandlePingUpdateRequest(session, request);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -301,7 +298,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
{
|
{
|
||||||
// Pick a suitable time that accounts for latency
|
// Pick a suitable time that accounts for latency
|
||||||
var delay = _group.GetHighestPing() * 2;
|
var delay = _group.GetHighestPing() * 2;
|
||||||
delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
|
delay = delay < _group.DefaultPing ? _group.DefaultPing : delay;
|
||||||
|
|
||||||
// Unpause group and set starting point in future
|
// Unpause group and set starting point in future
|
||||||
// Clients will start playback at LastActivity (datetime) from PositionTicks (playback position)
|
// Clients will start playback at LastActivity (datetime) from PositionTicks (playback position)
|
||||||
@ -337,8 +334,9 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
var currentTime = DateTime.UtcNow;
|
var currentTime = DateTime.UtcNow;
|
||||||
var elapsedTime = currentTime - _group.LastActivity;
|
var elapsedTime = currentTime - _group.LastActivity;
|
||||||
_group.LastActivity = currentTime;
|
_group.LastActivity = currentTime;
|
||||||
|
|
||||||
// Seek only if playback actually started
|
// Seek only if playback actually started
|
||||||
// (a pause request may be issued during the delay added to account for latency)
|
// Pause request may be issued during the delay added to account for latency
|
||||||
_group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
|
_group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
|
||||||
|
|
||||||
var command = NewSyncPlayCommand(SendCommandType.Pause);
|
var command = NewSyncPlayCommand(SendCommandType.Pause);
|
||||||
@ -451,7 +449,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
{
|
{
|
||||||
// Client, that was buffering, resumed playback but did not update others in time
|
// Client, that was buffering, resumed playback but did not update others in time
|
||||||
delay = _group.GetHighestPing() * 2;
|
delay = _group.GetHighestPing() * 2;
|
||||||
delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
|
delay = delay < _group.DefaultPing ? _group.DefaultPing : delay;
|
||||||
|
|
||||||
_group.LastActivity = currentTime.AddMilliseconds(
|
_group.LastActivity = currentTime.AddMilliseconds(
|
||||||
delay);
|
delay);
|
||||||
@ -495,7 +493,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request)
|
private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request)
|
||||||
{
|
{
|
||||||
// Collected pings are used to account for network latency when unpausing playback
|
// Collected pings are used to account for network latency when unpausing playback
|
||||||
_group.UpdatePing(session, request.Ping ?? _group.DefaulPing);
|
_group.UpdatePing(session, request.Ping ?? _group.DefaultPing);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -170,10 +170,11 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
{
|
{
|
||||||
_logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id);
|
_logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id);
|
||||||
|
|
||||||
var error = new GroupUpdate<string>()
|
var error = new GroupUpdate<string>
|
||||||
{
|
{
|
||||||
Type = GroupUpdateType.CreateGroupDenied
|
Type = GroupUpdateType.CreateGroupDenied
|
||||||
};
|
};
|
||||||
|
|
||||||
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
|
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -188,7 +189,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
var group = new SyncPlayController(_sessionManager, this);
|
var group = new SyncPlayController(_sessionManager, this);
|
||||||
_groups[group.GetGroupId()] = group;
|
_groups[group.GetGroupId()] = group;
|
||||||
|
|
||||||
group.InitGroup(session, cancellationToken);
|
group.CreateGroup(session, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,6 +206,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
{
|
{
|
||||||
Type = GroupUpdateType.JoinGroupDenied
|
Type = GroupUpdateType.JoinGroupDenied
|
||||||
};
|
};
|
||||||
|
|
||||||
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
|
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -300,9 +302,9 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId())).Select(
|
group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId())).Select(
|
||||||
group => group.GetInfo()).ToList();
|
group => group.GetInfo()).ToList();
|
||||||
}
|
}
|
||||||
// Otherwise show all available groups
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// Otherwise show all available groups
|
||||||
return _groups.Values.Where(
|
return _groups.Values.Where(
|
||||||
group => HasAccessToItem(user, group.GetPlayingItemId())).Select(
|
group => HasAccessToItem(user, group.GetPlayingItemId())).Select(
|
||||||
group => group.GetInfo()).ToList();
|
group => group.GetInfo()).ToList();
|
||||||
@ -322,6 +324,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
{
|
{
|
||||||
Type = GroupUpdateType.JoinGroupDenied
|
Type = GroupUpdateType.JoinGroupDenied
|
||||||
};
|
};
|
||||||
|
|
||||||
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
|
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -366,7 +369,6 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
}
|
}
|
||||||
|
|
||||||
_sessionToGroupMap.Remove(session.Id, out var tempGroup);
|
_sessionToGroupMap.Remove(session.Id, out var tempGroup);
|
||||||
|
|
||||||
if (!tempGroup.GetGroupId().Equals(group.GetGroupId()))
|
if (!tempGroup.GetGroupId().Equals(group.GetGroupId()))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Session was in wrong group!");
|
throw new InvalidOperationException("Session was in wrong group!");
|
||||||
|
@ -117,23 +117,20 @@ namespace Emby.Server.Implementations.TV
|
|||||||
limit = limit.Value + 10;
|
limit = limit.Value + 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
var items = _libraryManager
|
||||||
{
|
.GetItemList(
|
||||||
IncludeItemTypes = new[] { typeof(Episode).Name },
|
new InternalItemsQuery(user)
|
||||||
OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) },
|
|
||||||
SeriesPresentationUniqueKey = presentationUniqueKey,
|
|
||||||
Limit = limit,
|
|
||||||
DtoOptions = new DtoOptions
|
|
||||||
{
|
|
||||||
Fields = new ItemFields[]
|
|
||||||
{
|
{
|
||||||
ItemFields.SeriesPresentationUniqueKey
|
IncludeItemTypes = new[] { typeof(Episode).Name },
|
||||||
},
|
OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) },
|
||||||
EnableImages = false
|
SeriesPresentationUniqueKey = presentationUniqueKey,
|
||||||
},
|
Limit = limit,
|
||||||
GroupBySeriesPresentationUniqueKey = true
|
DtoOptions = new DtoOptions { Fields = new[] { ItemFields.SeriesPresentationUniqueKey }, EnableImages = false },
|
||||||
|
GroupBySeriesPresentationUniqueKey = true
|
||||||
}, parentsFolders.ToList()).Cast<Episode>().Select(GetUniqueSeriesKey);
|
}, parentsFolders.ToList())
|
||||||
|
.Cast<Episode>()
|
||||||
|
.Where(episode => !string.IsNullOrEmpty(episode.SeriesPresentationUniqueKey))
|
||||||
|
.Select(GetUniqueSeriesKey);
|
||||||
|
|
||||||
// Avoid implicitly captured closure
|
// Avoid implicitly captured closure
|
||||||
var episodes = GetNextUpEpisodes(request, user, items, dtoOptions);
|
var episodes = GetNextUpEpisodes(request, user, items, dtoOptions);
|
||||||
|
@ -148,6 +148,11 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
_logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
|
_logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
|
||||||
return Array.Empty<PackageInfo>();
|
return Array.Empty<PackageInfo>();
|
||||||
}
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
|
||||||
|
return Array.Empty<PackageInfo>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -21,7 +21,6 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.5" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.5" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.5" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.5" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="3.1.5" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -23,6 +23,7 @@ using MediaBrowser.Model.Cryptography;
|
|||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Events;
|
using MediaBrowser.Model.Events;
|
||||||
using MediaBrowser.Model.Users;
|
using MediaBrowser.Model.Users;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Jellyfin.Server.Implementations.Users
|
namespace Jellyfin.Server.Implementations.Users
|
||||||
@ -86,7 +87,19 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
public event EventHandler<GenericEventArgs<User>>? OnUserLockedOut;
|
public event EventHandler<GenericEventArgs<User>>? OnUserLockedOut;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IEnumerable<User> Users => _dbProvider.CreateContext().Users;
|
public IEnumerable<User> Users
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
using var dbContext = _dbProvider.CreateContext();
|
||||||
|
return dbContext.Users
|
||||||
|
.Include(user => user.Permissions)
|
||||||
|
.Include(user => user.Preferences)
|
||||||
|
.Include(user => user.AccessSchedules)
|
||||||
|
.Include(user => user.ProfileImage)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IEnumerable<Guid> UsersIds => _dbProvider.CreateContext().Users.Select(u => u.Id);
|
public IEnumerable<Guid> UsersIds => _dbProvider.CreateContext().Users.Select(u => u.Id);
|
||||||
@ -99,7 +112,13 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
throw new ArgumentException("Guid can't be empty", nameof(id));
|
throw new ArgumentException("Guid can't be empty", nameof(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
return _dbProvider.CreateContext().Users.Find(id);
|
using var dbContext = _dbProvider.CreateContext();
|
||||||
|
return dbContext.Users
|
||||||
|
.Include(user => user.Permissions)
|
||||||
|
.Include(user => user.Preferences)
|
||||||
|
.Include(user => user.AccessSchedules)
|
||||||
|
.Include(user => user.ProfileImage)
|
||||||
|
.FirstOrDefault(user => user.Id == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@ -110,9 +129,14 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
throw new ArgumentException("Invalid username", nameof(name));
|
throw new ArgumentException("Invalid username", nameof(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
// This can't use an overload with StringComparer because that would cause the query to
|
using var dbContext = _dbProvider.CreateContext();
|
||||||
// have to be evaluated client-side.
|
return dbContext.Users
|
||||||
return _dbProvider.CreateContext().Users.FirstOrDefault(u => string.Equals(u.Username, name));
|
.Include(user => user.Permissions)
|
||||||
|
.Include(user => user.Preferences)
|
||||||
|
.Include(user => user.AccessSchedules)
|
||||||
|
.Include(user => user.ProfileImage)
|
||||||
|
.AsEnumerable()
|
||||||
|
.FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@ -128,7 +152,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
throw new ArgumentException("Invalid username", nameof(newName));
|
throw new ArgumentException("Invalid username", nameof(newName));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.Username.Equals(newName, StringComparison.Ordinal))
|
if (user.Username.Equals(newName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("The new and old names must be different.");
|
throw new ArgumentException("The new and old names must be different.");
|
||||||
}
|
}
|
||||||
@ -150,7 +174,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void UpdateUser(User user)
|
public void UpdateUser(User user)
|
||||||
{
|
{
|
||||||
var dbContext = _dbProvider.CreateContext();
|
using var dbContext = _dbProvider.CreateContext();
|
||||||
dbContext.Users.Update(user);
|
dbContext.Users.Update(user);
|
||||||
dbContext.SaveChanges();
|
dbContext.SaveChanges();
|
||||||
}
|
}
|
||||||
@ -158,7 +182,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task UpdateUserAsync(User user)
|
public async Task UpdateUserAsync(User user)
|
||||||
{
|
{
|
||||||
var dbContext = _dbProvider.CreateContext();
|
await using var dbContext = _dbProvider.CreateContext();
|
||||||
dbContext.Users.Update(user);
|
dbContext.Users.Update(user);
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
@ -172,7 +196,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
|
throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
|
||||||
}
|
}
|
||||||
|
|
||||||
var dbContext = _dbProvider.CreateContext();
|
using var dbContext = _dbProvider.CreateContext();
|
||||||
|
|
||||||
// TODO: Remove after user item data is migrated.
|
// TODO: Remove after user item data is migrated.
|
||||||
var max = dbContext.Users.Any() ? dbContext.Users.Select(u => u.InternalId).Max() : 0;
|
var max = dbContext.Users.Any() ? dbContext.Users.Select(u => u.InternalId).Max() : 0;
|
||||||
@ -195,8 +219,13 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void DeleteUser(Guid userId)
|
public void DeleteUser(Guid userId)
|
||||||
{
|
{
|
||||||
var dbContext = _dbProvider.CreateContext();
|
using var dbContext = _dbProvider.CreateContext();
|
||||||
var user = dbContext.Users.Find(userId);
|
var user = dbContext.Users
|
||||||
|
.Include(u => u.Permissions)
|
||||||
|
.Include(u => u.Preferences)
|
||||||
|
.Include(u => u.AccessSchedules)
|
||||||
|
.Include(u => u.ProfileImage)
|
||||||
|
.FirstOrDefault(u => u.Id == userId);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
throw new ResourceNotFoundException(nameof(userId));
|
throw new ResourceNotFoundException(nameof(userId));
|
||||||
@ -381,7 +410,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
throw new ArgumentNullException(nameof(username));
|
throw new ArgumentNullException(nameof(username));
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = Users.ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
|
var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
|
||||||
bool success;
|
bool success;
|
||||||
IAuthenticationProvider? authenticationProvider;
|
IAuthenticationProvider? authenticationProvider;
|
||||||
|
|
||||||
@ -409,8 +438,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
|
|
||||||
// Search the database for the user again
|
// Search the database for the user again
|
||||||
// the authentication provider might have created it
|
// the authentication provider might have created it
|
||||||
user = Users
|
user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
|
||||||
.ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy)
|
if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy)
|
||||||
{
|
{
|
||||||
@ -547,7 +575,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
public void Initialize()
|
public void Initialize()
|
||||||
{
|
{
|
||||||
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
|
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
|
||||||
var dbContext = _dbProvider.CreateContext();
|
using var dbContext = _dbProvider.CreateContext();
|
||||||
|
|
||||||
if (dbContext.Users.Any())
|
if (dbContext.Users.Any())
|
||||||
{
|
{
|
||||||
@ -610,7 +638,14 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
public void UpdateConfiguration(Guid userId, UserConfiguration config)
|
public void UpdateConfiguration(Guid userId, UserConfiguration config)
|
||||||
{
|
{
|
||||||
var dbContext = _dbProvider.CreateContext();
|
var dbContext = _dbProvider.CreateContext();
|
||||||
var user = dbContext.Users.Find(userId) ?? throw new ArgumentException("No user exists with given Id!");
|
var user = dbContext.Users
|
||||||
|
.Include(u => u.Permissions)
|
||||||
|
.Include(u => u.Preferences)
|
||||||
|
.Include(u => u.AccessSchedules)
|
||||||
|
.Include(u => u.ProfileImage)
|
||||||
|
.FirstOrDefault(u => u.Id == userId)
|
||||||
|
?? throw new ArgumentException("No user exists with given Id!");
|
||||||
|
|
||||||
user.SubtitleMode = config.SubtitleMode;
|
user.SubtitleMode = config.SubtitleMode;
|
||||||
user.HidePlayedInLatest = config.HidePlayedInLatest;
|
user.HidePlayedInLatest = config.HidePlayedInLatest;
|
||||||
user.EnableLocalPassword = config.EnableLocalPassword;
|
user.EnableLocalPassword = config.EnableLocalPassword;
|
||||||
@ -699,7 +734,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void ClearProfileImage(User user)
|
public void ClearProfileImage(User user)
|
||||||
{
|
{
|
||||||
var dbContext = _dbProvider.CreateContext();
|
using var dbContext = _dbProvider.CreateContext();
|
||||||
dbContext.Remove(user.ProfileImage);
|
dbContext.Remove(user.ProfileImage);
|
||||||
dbContext.SaveChanges();
|
dbContext.SaveChanges();
|
||||||
}
|
}
|
||||||
|
@ -66,8 +66,7 @@ namespace Jellyfin.Server
|
|||||||
// TODO: Set up scoping and use AddDbContextPool
|
// TODO: Set up scoping and use AddDbContextPool
|
||||||
serviceCollection.AddDbContext<JellyfinDb>(
|
serviceCollection.AddDbContext<JellyfinDb>(
|
||||||
options => options
|
options => options
|
||||||
.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}")
|
.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
|
||||||
.UseLazyLoadingProxies(),
|
|
||||||
ServiceLifetime.Transient);
|
ServiceLifetime.Transient);
|
||||||
|
|
||||||
serviceCollection.AddSingleton<JellyfinDbProvider>();
|
serviceCollection.AddSingleton<JellyfinDbProvider>();
|
||||||
|
@ -27,13 +27,6 @@ namespace MediaBrowser.Api.SyncPlay
|
|||||||
/// <value>The Group id to join.</value>
|
/// <value>The Group id to join.</value>
|
||||||
[ApiMember(Name = "GroupId", Description = "Group Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
[ApiMember(Name = "GroupId", Description = "Group Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||||
public string GroupId { get; set; }
|
public string GroupId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the playing item id.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The client's currently playing item id.</value>
|
|
||||||
[ApiMember(Name = "PlayingItemId", Description = "Client's playing item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
|
||||||
public string PlayingItemId { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route("/SyncPlay/Leave", "POST", Summary = "Leave joined SyncPlay group")]
|
[Route("/SyncPlay/Leave", "POST", Summary = "Leave joined SyncPlay group")]
|
||||||
@ -89,7 +82,7 @@ namespace MediaBrowser.Api.SyncPlay
|
|||||||
public long PositionTicks { get; set; }
|
public long PositionTicks { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets whether this is a buffering or a buffering-done request.
|
/// Gets or sets whether this is a buffering or a ready request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value><c>true</c> if buffering is complete; <c>false</c> otherwise.</value>
|
/// <value><c>true</c> if buffering is complete; <c>false</c> otherwise.</value>
|
||||||
[ApiMember(Name = "BufferingDone", IsRequired = true, DataType = "bool", ParameterType = "query", Verb = "POST")]
|
[ApiMember(Name = "BufferingDone", IsRequired = true, DataType = "bool", ParameterType = "query", Verb = "POST")]
|
||||||
@ -150,25 +143,15 @@ namespace MediaBrowser.Api.SyncPlay
|
|||||||
var currentSession = GetSession(_sessionContext);
|
var currentSession = GetSession(_sessionContext);
|
||||||
|
|
||||||
Guid groupId;
|
Guid groupId;
|
||||||
Guid playingItemId = Guid.Empty;
|
|
||||||
|
|
||||||
if (!Guid.TryParse(request.GroupId, out groupId))
|
if (!Guid.TryParse(request.GroupId, out groupId))
|
||||||
{
|
{
|
||||||
Logger.LogError("JoinGroup: {0} is not a valid format for GroupId. Ignoring request.", request.GroupId);
|
Logger.LogError("JoinGroup: {0} is not a valid format for GroupId. Ignoring request.", request.GroupId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Both null and empty strings mean that client isn't playing anything
|
|
||||||
if (!string.IsNullOrEmpty(request.PlayingItemId) && !Guid.TryParse(request.PlayingItemId, out playingItemId))
|
|
||||||
{
|
|
||||||
Logger.LogError("JoinGroup: {0} is not a valid format for PlayingItemId. Ignoring request.", request.PlayingItemId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var joinRequest = new JoinGroupRequest()
|
var joinRequest = new JoinGroupRequest()
|
||||||
{
|
{
|
||||||
GroupId = groupId,
|
GroupId = groupId
|
||||||
PlayingItemId = playingItemId
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_syncPlayManager.JoinGroup(currentSession, groupId, joinRequest, CancellationToken.None);
|
_syncPlayManager.JoinGroup(currentSession, groupId, joinRequest, CancellationToken.None);
|
||||||
@ -189,7 +172,7 @@ namespace MediaBrowser.Api.SyncPlay
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">The request.</param>
|
/// <param name="request">The request.</param>
|
||||||
/// <value>The requested list of groups.</value>
|
/// <value>The requested list of groups.</value>
|
||||||
public List<GroupInfoView> Post(SyncPlayList request)
|
public List<GroupInfoView> Get(SyncPlayList request)
|
||||||
{
|
{
|
||||||
var currentSession = GetSession(_sessionContext);
|
var currentSession = GetSession(_sessionContext);
|
||||||
var filterItemId = Guid.Empty;
|
var filterItemId = Guid.Empty;
|
||||||
@ -254,7 +237,7 @@ namespace MediaBrowser.Api.SyncPlay
|
|||||||
var currentSession = GetSession(_sessionContext);
|
var currentSession = GetSession(_sessionContext);
|
||||||
var syncPlayRequest = new PlaybackRequest()
|
var syncPlayRequest = new PlaybackRequest()
|
||||||
{
|
{
|
||||||
Type = request.BufferingDone ? PlaybackRequestType.BufferingDone : PlaybackRequestType.Buffering,
|
Type = request.BufferingDone ? PlaybackRequestType.Ready : PlaybackRequestType.Buffer,
|
||||||
When = DateTime.Parse(request.When),
|
When = DateTime.Parse(request.When),
|
||||||
PositionTicks = request.PositionTicks
|
PositionTicks = request.PositionTicks
|
||||||
};
|
};
|
||||||
@ -270,7 +253,7 @@ namespace MediaBrowser.Api.SyncPlay
|
|||||||
var currentSession = GetSession(_sessionContext);
|
var currentSession = GetSession(_sessionContext);
|
||||||
var syncPlayRequest = new PlaybackRequest()
|
var syncPlayRequest = new PlaybackRequest()
|
||||||
{
|
{
|
||||||
Type = PlaybackRequestType.UpdatePing,
|
Type = PlaybackRequestType.Ping,
|
||||||
Ping = Convert.ToInt64(request.Ping)
|
Ping = Convert.ToInt64(request.Ping)
|
||||||
};
|
};
|
||||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||||
|
@ -483,7 +483,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
{
|
{
|
||||||
if (isQsvDecoder)
|
if (isQsvDecoder)
|
||||||
{
|
{
|
||||||
arg.Append("-hwaccel qsv ");
|
arg.Append("-hwaccel qsv -init_hw_device qsv=hw ");
|
||||||
}
|
}
|
||||||
// While using SW decoder
|
// While using SW decoder
|
||||||
else
|
else
|
||||||
@ -1351,7 +1351,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
transcoderChannelLimit = 6;
|
transcoderChannelLimit = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
var isTranscodingAudio = !EncodingHelper.IsCopyCodec(codec);
|
var isTranscodingAudio = !IsCopyCodec(codec);
|
||||||
|
|
||||||
int? resultChannels = state.GetRequestedAudioChannels(codec);
|
int? resultChannels = state.GetRequestedAudioChannels(codec);
|
||||||
if (isTranscodingAudio)
|
if (isTranscodingAudio)
|
||||||
@ -1757,7 +1757,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
// output dimensions. Output dimensions are guaranteed to be even.
|
// output dimensions. Output dimensions are guaranteed to be even.
|
||||||
var outputWidth = width.Value;
|
var outputWidth = width.Value;
|
||||||
var outputHeight = height.Value;
|
var outputHeight = height.Value;
|
||||||
var vaapi_or_qsv = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) ? "qsv" : "vaapi";
|
var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
if (!videoWidth.HasValue
|
if (!videoWidth.HasValue
|
||||||
|| outputWidth != videoWidth.Value
|
|| outputWidth != videoWidth.Value
|
||||||
@ -1765,17 +1765,19 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|| outputHeight != videoHeight.Value)
|
|| outputHeight != videoHeight.Value)
|
||||||
{
|
{
|
||||||
// Force nv12 pixel format to enable 10-bit to 8-bit colour conversion.
|
// Force nv12 pixel format to enable 10-bit to 8-bit colour conversion.
|
||||||
|
// use vpp_qsv filter to avoid green bar when the fixed output size is requested.
|
||||||
filters.Add(
|
filters.Add(
|
||||||
string.Format(
|
string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
"scale_{0}=w={1}:h={2}:format=nv12",
|
"{0}=w={1}:h={2}:format=nv12",
|
||||||
vaapi_or_qsv,
|
qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi",
|
||||||
outputWidth,
|
outputWidth,
|
||||||
outputHeight));
|
outputHeight));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
filters.Add(string.Format(CultureInfo.InvariantCulture, "scale_{0}=format=nv12", vaapi_or_qsv));
|
// set w=0:h=0 for vpp_qsv to keep the original dimensions, otherwise it will fail.
|
||||||
|
filters.Add(string.Format(CultureInfo.InvariantCulture, "{0}format=nv12", qsv_or_vaapi ? "vpp_qsv=w=0:h=0:" : "scale_vaapi="));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1
|
else if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1
|
||||||
@ -2262,7 +2264,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
flags.Add("+ignidx");
|
flags.Add("+ignidx");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.GenPtsInput || EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
|
if (state.GenPtsInput || IsCopyCodec(state.OutputVideoCodec))
|
||||||
{
|
{
|
||||||
flags.Add("+genpts");
|
flags.Add("+genpts");
|
||||||
}
|
}
|
||||||
@ -2521,21 +2523,21 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the name of the output video codec.
|
/// Gets the ffmpeg option string for the hardware accelerated video decoder.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="state">The encoding job info.</param>
|
||||||
|
/// <param name="encodingOptions">The encoding options.</param>
|
||||||
|
/// <returns>The option string or null if none available.</returns>
|
||||||
protected string GetHardwareAcceleratedVideoDecoder(EncodingJobInfo state, EncodingOptions encodingOptions)
|
protected string GetHardwareAcceleratedVideoDecoder(EncodingJobInfo state, EncodingOptions encodingOptions)
|
||||||
{
|
{
|
||||||
var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile;
|
|
||||||
var videoStream = state.VideoStream;
|
var videoStream = state.VideoStream;
|
||||||
var isColorDepth10 = !string.IsNullOrEmpty(videoStream.Profile) && (videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
|
if (videoStream == null)
|
||||||
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
|
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile;
|
||||||
// Only use alternative encoders for video files.
|
// Only use alternative encoders for video files.
|
||||||
// When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
|
// When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
|
||||||
// Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
|
// Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
|
||||||
@ -2544,10 +2546,16 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (videoStream != null
|
if (IsCopyCodec(state.OutputVideoCodec))
|
||||||
&& !string.IsNullOrEmpty(videoStream.Codec)
|
|
||||||
&& !string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
|
|
||||||
{
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(videoStream.Codec) && !string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
|
||||||
|
{
|
||||||
|
var isColorDepth10 = !string.IsNullOrEmpty(videoStream.Profile)
|
||||||
|
&& (videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase) || videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
// Only hevc and vp9 formats have 10-bit hardware decoder support now.
|
// Only hevc and vp9 formats have 10-bit hardware decoder support now.
|
||||||
if (isColorDepth10 && !(string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
|
if (isColorDepth10 && !(string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||||
@ -3000,7 +3008,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
args += " -mpegts_m2ts_mode 1";
|
args += " -mpegts_m2ts_mode 1";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (EncodingHelper.IsCopyCodec(videoCodec))
|
if (IsCopyCodec(videoCodec))
|
||||||
{
|
{
|
||||||
if (state.VideoStream != null
|
if (state.VideoStream != null
|
||||||
&& string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase)
|
&& string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase)
|
||||||
@ -3102,7 +3110,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
var args = "-codec:a:0 " + codec;
|
var args = "-codec:a:0 " + codec;
|
||||||
|
|
||||||
if (EncodingHelper.IsCopyCodec(codec))
|
if (IsCopyCodec(codec))
|
||||||
{
|
{
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ namespace MediaBrowser.Controller.SyncPlay
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the default ping value used for sessions.
|
/// Gets the default ping value used for sessions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long DefaulPing { get; } = 500;
|
public long DefaultPing { get; } = 500;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the group identifier.
|
/// Gets or sets the group identifier.
|
||||||
@ -70,16 +70,16 @@ namespace MediaBrowser.Controller.SyncPlay
|
|||||||
/// <param name="session">The session.</param>
|
/// <param name="session">The session.</param>
|
||||||
public void AddSession(SessionInfo session)
|
public void AddSession(SessionInfo session)
|
||||||
{
|
{
|
||||||
if (ContainsSession(session.Id.ToString()))
|
if (ContainsSession(session.Id))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var member = new GroupMember();
|
var member = new GroupMember();
|
||||||
member.Session = session;
|
member.Session = session;
|
||||||
member.Ping = DefaulPing;
|
member.Ping = DefaultPing;
|
||||||
member.IsBuffering = false;
|
member.IsBuffering = false;
|
||||||
Participants[session.Id.ToString()] = member;
|
Participants[session.Id] = member;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -88,12 +88,12 @@ namespace MediaBrowser.Controller.SyncPlay
|
|||||||
/// <param name="session">The session.</param>
|
/// <param name="session">The session.</param>
|
||||||
public void RemoveSession(SessionInfo session)
|
public void RemoveSession(SessionInfo session)
|
||||||
{
|
{
|
||||||
if (!ContainsSession(session.Id.ToString()))
|
if (!ContainsSession(session.Id))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Participants.Remove(session.Id.ToString(), out _);
|
Participants.Remove(session.Id, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -103,12 +103,12 @@ namespace MediaBrowser.Controller.SyncPlay
|
|||||||
/// <param name="ping">The ping.</param>
|
/// <param name="ping">The ping.</param>
|
||||||
public void UpdatePing(SessionInfo session, long ping)
|
public void UpdatePing(SessionInfo session, long ping)
|
||||||
{
|
{
|
||||||
if (!ContainsSession(session.Id.ToString()))
|
if (!ContainsSession(session.Id))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Participants[session.Id.ToString()].Ping = ping;
|
Participants[session.Id].Ping = ping;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -117,7 +117,7 @@ namespace MediaBrowser.Controller.SyncPlay
|
|||||||
/// <value name="session">The highest ping in the group.</value>
|
/// <value name="session">The highest ping in the group.</value>
|
||||||
public long GetHighestPing()
|
public long GetHighestPing()
|
||||||
{
|
{
|
||||||
long max = Int64.MinValue;
|
long max = long.MinValue;
|
||||||
foreach (var session in Participants.Values)
|
foreach (var session in Participants.Values)
|
||||||
{
|
{
|
||||||
max = Math.Max(max, session.Ping);
|
max = Math.Max(max, session.Ping);
|
||||||
@ -133,12 +133,12 @@ namespace MediaBrowser.Controller.SyncPlay
|
|||||||
/// <param name="isBuffering">The state.</param>
|
/// <param name="isBuffering">The state.</param>
|
||||||
public void SetBuffering(SessionInfo session, bool isBuffering)
|
public void SetBuffering(SessionInfo session, bool isBuffering)
|
||||||
{
|
{
|
||||||
if (!ContainsSession(session.Id.ToString()))
|
if (!ContainsSession(session.Id))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Participants[session.Id.ToString()].IsBuffering = isBuffering;
|
Participants[session.Id].IsBuffering = isBuffering;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.SyncPlay
|
|||||||
public class GroupMember
|
public class GroupMember
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets whether this member is buffering.
|
/// Gets or sets a value indicating whether this member is buffering.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value><c>true</c> if member is buffering; <c>false</c> otherwise.</value>
|
/// <value><c>true</c> if member is buffering; <c>false</c> otherwise.</value>
|
||||||
public bool IsBuffering { get; set; }
|
public bool IsBuffering { get; set; }
|
||||||
|
@ -33,7 +33,7 @@ namespace MediaBrowser.Controller.SyncPlay
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="session">The session.</param>
|
/// <param name="session">The session.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
void InitGroup(SessionInfo session, CancellationToken cancellationToken);
|
void CreateGroup(SessionInfo session, CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds the session to the group.
|
/// Adds the session to the group.
|
||||||
|
@ -90,6 +90,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
// $ ffmpeg -version | perl -ne ' print "$1=$2.$3," if /^(lib\w+)\s+(\d+)\.\s*(\d+)/'
|
// $ ffmpeg -version | perl -ne ' print "$1=$2.$3," if /^(lib\w+)\s+(\d+)\.\s*(\d+)/'
|
||||||
private static readonly IReadOnlyDictionary<string, Version> _ffmpegVersionMap = new Dictionary<string, Version>
|
private static readonly IReadOnlyDictionary<string, Version> _ffmpegVersionMap = new Dictionary<string, Version>
|
||||||
{
|
{
|
||||||
|
{ "libavutil=56.51,libavcodec=58.91,libavformat=58.45,libavdevice=58.10,libavfilter=7.85,libswscale=5.7,libswresample=3.7,libpostproc=55.7,", new Version(4, 3) },
|
||||||
{ "libavutil=56.31,libavcodec=58.54,libavformat=58.29,libavdevice=58.8,libavfilter=7.57,libswscale=5.5,libswresample=3.5,libpostproc=55.5,", new Version(4, 2) },
|
{ "libavutil=56.31,libavcodec=58.54,libavformat=58.29,libavdevice=58.8,libavfilter=7.57,libswscale=5.5,libswresample=3.5,libpostproc=55.5,", new Version(4, 2) },
|
||||||
{ "libavutil=56.22,libavcodec=58.35,libavformat=58.20,libavdevice=58.5,libavfilter=7.40,libswscale=5.3,libswresample=3.3,libpostproc=55.3,", new Version(4, 1) },
|
{ "libavutil=56.22,libavcodec=58.35,libavformat=58.20,libavdevice=58.5,libavfilter=7.40,libswscale=5.3,libswresample=3.3,libpostproc=55.3,", new Version(4, 1) },
|
||||||
{ "libavutil=56.14,libavcodec=58.18,libavformat=58.12,libavdevice=58.3,libavfilter=7.16,libswscale=5.1,libswresample=3.1,libpostproc=55.1,", new Version(4, 0) },
|
{ "libavutil=56.14,libavcodec=58.18,libavformat=58.12,libavdevice=58.3,libavfilter=7.16,libswscale=5.1,libswresample=3.1,libpostproc=55.1,", new Version(4, 0) },
|
||||||
|
@ -12,11 +12,5 @@ namespace MediaBrowser.Model.SyncPlay
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The Group id to join.</value>
|
/// <value>The Group id to join.</value>
|
||||||
public Guid GroupId { get; set; }
|
public Guid GroupId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the playing item id.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The client's currently playing item id.</value>
|
|
||||||
public Guid PlayingItemId { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,16 +23,16 @@ namespace MediaBrowser.Model.SyncPlay
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A user is signaling that playback is buffering.
|
/// A user is signaling that playback is buffering.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Buffering = 3,
|
Buffer = 3,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A user is signaling that playback resumed.
|
/// A user is signaling that playback resumed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
BufferingDone = 4,
|
Ready = 4,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A user is reporting its ping.
|
/// A user is reporting its ping.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
UpdatePing = 5
|
Ping = 5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
@ -229,6 +230,45 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
|
|||||||
return GetEpisodesPageAsync(tvdbId, 1, episodeQuery, language, cancellationToken);
|
return GetEpisodesPageAsync(tvdbId, 1, episodeQuery, language, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeriesAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId);
|
||||||
|
var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (imagesSummary.Data.Fanart > 0)
|
||||||
|
{
|
||||||
|
yield return KeyType.Fanart;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imagesSummary.Data.Series > 0)
|
||||||
|
{
|
||||||
|
yield return KeyType.Series;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imagesSummary.Data.Poster > 0)
|
||||||
|
{
|
||||||
|
yield return KeyType.Poster;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeasonAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId);
|
||||||
|
var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (imagesSummary.Data.Season > 0)
|
||||||
|
{
|
||||||
|
yield return KeyType.Season;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imagesSummary.Data.Fanart > 0)
|
||||||
|
{
|
||||||
|
yield return KeyType.Fanart;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO seasonwide is not supported in TvDbSharper
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<T> TryGetValue<T>(string key, string language, Func<Task<T>> resultFactory)
|
private async Task<T> TryGetValue<T>(string key, string language, Func<Task<T>> resultFactory)
|
||||||
{
|
{
|
||||||
if (_cache.TryGetValue(key, out T cachedValue))
|
if (_cache.TryGetValue(key, out T cachedValue))
|
||||||
|
@ -65,8 +65,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
|
|||||||
var language = item.GetPreferredMetadataLanguage();
|
var language = item.GetPreferredMetadataLanguage();
|
||||||
var remoteImages = new List<RemoteImageInfo>();
|
var remoteImages = new List<RemoteImageInfo>();
|
||||||
|
|
||||||
var keyTypes = new[] { KeyType.Season, KeyType.Seasonwide, KeyType.Fanart };
|
var keyTypes = _tvdbClientManager.GetImageKeyTypesForSeasonAsync(tvdbId, language, cancellationToken).ConfigureAwait(false);
|
||||||
foreach (var keyType in keyTypes)
|
await foreach (var keyType in keyTypes)
|
||||||
{
|
{
|
||||||
var imageQuery = new ImagesQuery
|
var imageQuery = new ImagesQuery
|
||||||
{
|
{
|
||||||
|
@ -59,9 +59,10 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
|
|||||||
|
|
||||||
var language = item.GetPreferredMetadataLanguage();
|
var language = item.GetPreferredMetadataLanguage();
|
||||||
var remoteImages = new List<RemoteImageInfo>();
|
var remoteImages = new List<RemoteImageInfo>();
|
||||||
var keyTypes = new[] { KeyType.Poster, KeyType.Series, KeyType.Fanart };
|
|
||||||
var tvdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tvdb));
|
var tvdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tvdb));
|
||||||
foreach (KeyType keyType in keyTypes)
|
var allowedKeyTypes = _tvdbClientManager.GetImageKeyTypesForSeriesAsync(tvdbId, language, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
await foreach (KeyType keyType in allowedKeyTypes)
|
||||||
{
|
{
|
||||||
var imageQuery = new ImagesQuery
|
var imageQuery = new ImagesQuery
|
||||||
{
|
{
|
||||||
|
@ -247,10 +247,15 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
|
|||||||
{
|
{
|
||||||
Name = tvdbTitles.FirstOrDefault(),
|
Name = tvdbTitles.FirstOrDefault(),
|
||||||
ProductionYear = firstAired.Year,
|
ProductionYear = firstAired.Year,
|
||||||
SearchProviderName = Name,
|
SearchProviderName = Name
|
||||||
ImageUrl = TvdbUtils.BannerUrl + seriesSearchResult.Banner
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(seriesSearchResult.Banner))
|
||||||
|
{
|
||||||
|
// Results from their Search endpoints already include the /banners/ part in the url, because reasons...
|
||||||
|
remoteSearchResult.ImageUrl = TvdbUtils.TvdbImageBaseUrl + seriesSearchResult.Banner;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var seriesSesult =
|
var seriesSesult =
|
||||||
@ -365,10 +370,14 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
|
|||||||
Type = PersonType.Actor,
|
Type = PersonType.Actor,
|
||||||
Name = (actor.Name ?? string.Empty).Trim(),
|
Name = (actor.Name ?? string.Empty).Trim(),
|
||||||
Role = actor.Role,
|
Role = actor.Role,
|
||||||
ImageUrl = TvdbUtils.BannerUrl + actor.Image,
|
|
||||||
SortOrder = actor.SortOrder
|
SortOrder = actor.SortOrder
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(actor.Image))
|
||||||
|
{
|
||||||
|
personInfo.ImageUrl = TvdbUtils.TvdbImageBaseUrl + actor.Image;
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(personInfo.Name))
|
if (!string.IsNullOrWhiteSpace(personInfo.Name))
|
||||||
{
|
{
|
||||||
result.AddPerson(personInfo);
|
result.AddPerson(personInfo);
|
||||||
|
@ -9,7 +9,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
|
|||||||
{
|
{
|
||||||
public const string TvdbApiKey = "OG4V3YJ3FAP7FP2K";
|
public const string TvdbApiKey = "OG4V3YJ3FAP7FP2K";
|
||||||
public const string TvdbBaseUrl = "https://www.thetvdb.com/";
|
public const string TvdbBaseUrl = "https://www.thetvdb.com/";
|
||||||
public const string BannerUrl = TvdbBaseUrl + "banners/";
|
public const string TvdbImageBaseUrl = "https://www.thetvdb.com";
|
||||||
|
public const string BannerUrl = TvdbImageBaseUrl + "/banners/";
|
||||||
|
|
||||||
public static ImageType GetImageTypeFromKeyType(string keyType)
|
public static ImageType GetImageTypeFromKeyType(string keyType)
|
||||||
{
|
{
|
||||||
|
@ -8,7 +8,7 @@ set -o xtrace
|
|||||||
# Version variables
|
# Version variables
|
||||||
NSSM_VERSION="nssm-2.24-101-g897c7ad"
|
NSSM_VERSION="nssm-2.24-101-g897c7ad"
|
||||||
NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip"
|
NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip"
|
||||||
FFMPEG_VERSION="ffmpeg-4.2.1-win64-static"
|
FFMPEG_VERSION="ffmpeg-4.3-win64-static"
|
||||||
FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win64/static/${FFMPEG_VERSION}.zip"
|
FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win64/static/${FFMPEG_VERSION}.zip"
|
||||||
|
|
||||||
# Move to source directory
|
# Move to source directory
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AutoFixture" Version="4.12.0" />
|
<PackageReference Include="AutoFixture" Version="4.13.0" />
|
||||||
<PackageReference Include="AutoFixture.AutoMoq" Version="4.12.0" />
|
<PackageReference Include="AutoFixture.AutoMoq" Version="4.12.0" />
|
||||||
<PackageReference Include="AutoFixture.Xunit2" Version="4.12.0" />
|
<PackageReference Include="AutoFixture.Xunit2" Version="4.12.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.5" />
|
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.5" />
|
||||||
|
@ -17,6 +17,7 @@ namespace Jellyfin.MediaEncoding.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
[InlineData(EncoderValidatorTestsData.FFmpegV43Output, true)]
|
||||||
[InlineData(EncoderValidatorTestsData.FFmpegV421Output, true)]
|
[InlineData(EncoderValidatorTestsData.FFmpegV421Output, true)]
|
||||||
[InlineData(EncoderValidatorTestsData.FFmpegV42Output, true)]
|
[InlineData(EncoderValidatorTestsData.FFmpegV42Output, true)]
|
||||||
[InlineData(EncoderValidatorTestsData.FFmpegV414Output, true)]
|
[InlineData(EncoderValidatorTestsData.FFmpegV414Output, true)]
|
||||||
@ -32,6 +33,7 @@ namespace Jellyfin.MediaEncoding.Tests
|
|||||||
{
|
{
|
||||||
public IEnumerator<object?[]> GetEnumerator()
|
public IEnumerator<object?[]> GetEnumerator()
|
||||||
{
|
{
|
||||||
|
yield return new object?[] { EncoderValidatorTestsData.FFmpegV43Output, new Version(4, 3) };
|
||||||
yield return new object?[] { EncoderValidatorTestsData.FFmpegV421Output, new Version(4, 2, 1) };
|
yield return new object?[] { EncoderValidatorTestsData.FFmpegV421Output, new Version(4, 2, 1) };
|
||||||
yield return new object?[] { EncoderValidatorTestsData.FFmpegV42Output, new Version(4, 2) };
|
yield return new object?[] { EncoderValidatorTestsData.FFmpegV42Output, new Version(4, 2) };
|
||||||
yield return new object?[] { EncoderValidatorTestsData.FFmpegV414Output, new Version(4, 1, 4) };
|
yield return new object?[] { EncoderValidatorTestsData.FFmpegV414Output, new Version(4, 1, 4) };
|
||||||
|
@ -2,6 +2,18 @@ namespace Jellyfin.MediaEncoding.Tests
|
|||||||
{
|
{
|
||||||
internal static class EncoderValidatorTestsData
|
internal static class EncoderValidatorTestsData
|
||||||
{
|
{
|
||||||
|
public const string FFmpegV43Output = @"ffmpeg version 4.3 Copyright (c) 2000-2020 the FFmpeg developers
|
||||||
|
built with gcc 7 (Ubuntu 7.5.0-3ubuntu1~18.04)
|
||||||
|
configuration: --prefix=/usr/lib/jellyfin-ffmpeg --target-os=linux --disable-doc --disable-ffplay --disable-shared --disable-libxcb --disable-vdpau --disable-sdl2 --disable-xlib --enable-gpl --enable-version3 --enable-static --enable-libfontconfig --enable-fontconfig --enable-gmp --enable-gnutls --enable-libass --enable-libbluray --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libx264 --enable-libx265 --enable-libzvbi --arch=amd64 --enable-amf --enable-nvenc --enable-nvdec --enable-vaapi --enable-opencl
|
||||||
|
libavutil 56. 51.100 / 56. 51.100
|
||||||
|
libavcodec 58. 91.100 / 58. 91.100
|
||||||
|
libavformat 58. 45.100 / 58. 45.100
|
||||||
|
libavdevice 58. 10.100 / 58. 10.100
|
||||||
|
libavfilter 7. 85.100 / 7. 85.100
|
||||||
|
libswscale 5. 7.100 / 5. 7.100
|
||||||
|
libswresample 3. 7.100 / 3. 7.100
|
||||||
|
libpostproc 55. 7.100 / 55. 7.100";
|
||||||
|
|
||||||
public const string FFmpegV421Output = @"ffmpeg version 4.2.1 Copyright (c) 2000-2019 the FFmpeg developers
|
public const string FFmpegV421Output = @"ffmpeg version 4.2.1 Copyright (c) 2000-2019 the FFmpeg developers
|
||||||
built with gcc 9.1.1 (GCC) 20190807
|
built with gcc 9.1.1 (GCC) 20190807
|
||||||
configuration: --enable-gpl --enable-version3 --enable-sdl2 --enable-fontconfig --enable-gnutls --enable-iconv --enable-libass --enable-libdav1d --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopus --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libtheora --enable-libtwolame --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libzimg --enable-lzma --enable-zlib --enable-gmp --enable-libvidstab --enable-libvorbis --enable-libvo-amrwbenc --enable-libmysofa --enable-libspeex --enable-libxvid --enable-libaom --enable-libmfx --enable-amf --enable-ffnvcodec --enable-cuvid --enable-d3d11va --enable-nvenc --enable-nvdec --enable-dxva2 --enable-avisynth --enable-libopenmpt
|
configuration: --enable-gpl --enable-version3 --enable-sdl2 --enable-fontconfig --enable-gnutls --enable-iconv --enable-libass --enable-libdav1d --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopus --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libtheora --enable-libtwolame --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libzimg --enable-lzma --enable-zlib --enable-gmp --enable-libvidstab --enable-libvorbis --enable-libvo-amrwbenc --enable-libmysofa --enable-libspeex --enable-libxvid --enable-libaom --enable-libmfx --enable-amf --enable-ffnvcodec --enable-cuvid --enable-d3d11va --enable-nvenc --enable-nvdec --enable-dxva2 --enable-avisynth --enable-libopenmpt
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AutoFixture" Version="4.12.0" />
|
<PackageReference Include="AutoFixture" Version="4.13.0" />
|
||||||
<PackageReference Include="AutoFixture.AutoMoq" Version="4.12.0" />
|
<PackageReference Include="AutoFixture.AutoMoq" Version="4.12.0" />
|
||||||
<PackageReference Include="Moq" Version="4.14.5" />
|
<PackageReference Include="Moq" Version="4.14.5" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
|
Loading…
Reference in New Issue
Block a user