├── Emby.ApiClient ├── Model │ ├── ConnectionMode.cs │ ├── RemoteLogoutReason.cs │ ├── ConnectionState.cs │ ├── SeasonQuery.cs │ ├── ConnectionResult.cs │ ├── ConnectionOptions.cs │ ├── NetworkStatus.cs │ ├── IDevice.cs │ ├── IClientWebSocket.cs │ ├── ServerInfo.cs │ ├── ServerCredentials.cs │ ├── IServerEvents.cs │ ├── IConnectionManager.cs │ └── ItemQuery.cs ├── Cryptography │ ├── ICryptographyProvider.cs │ └── CryptographyProvider.cs ├── Sync │ ├── IServerSync.cs │ ├── IMediaSync.cs │ ├── IMultiServerSync.cs │ ├── IFileTransferManager.cs │ ├── FileTransferManager.cs │ ├── DoubleProgress.cs │ ├── ContentUploader.cs │ ├── MultiServerSync.cs │ ├── ServerSync.cs │ └── MediaSync.cs ├── IServerLocator.cs ├── Net │ ├── IServerLocator.cs │ ├── HttpHeaders.cs │ ├── HttpRequest.cs │ ├── IAsyncHttpClient.cs │ ├── INetworkConnection.cs │ ├── HttpWebRequestFactory.cs │ ├── NetworkConnection.cs │ ├── ServerLocator.cs │ └── HttpWebRequestClient.cs ├── ICredentialProvider.cs ├── Data │ ├── IUserActionRepository.cs │ ├── IUserRepository.cs │ ├── IImageRepository.cs │ ├── IFileRepository.cs │ ├── IItemRepository.cs │ └── ILocalAssetManager.cs ├── Device.cs ├── Emby.ApiClient.csproj ├── WebSocket │ ├── ClientWebSocketFactory.cs │ ├── WebSocket4NetClientWebSocket.cs │ └── NativeClientWebSocket.cs ├── PortableHttpWebRequestFactory.cs ├── NewtonsoftJsonSerializer.cs ├── Playback │ ├── IPlaybackManager.cs │ └── PlaybackManager.cs ├── ServerLocator.cs ├── QueryStringDictionary.cs └── ConnectService.cs ├── LICENSE.md ├── Emby.ApiClient.sln ├── .gitignore └── README.md /Emby.ApiClient/Model/ConnectionMode.cs: -------------------------------------------------------------------------------- 1 | namespace Emby.ApiClient.Model 2 | { 3 | public enum ConnectionMode 4 | { 5 | Local = 1, 6 | Remote = 2, 7 | Manual = 3 8 | } 9 | } -------------------------------------------------------------------------------- /Emby.ApiClient/Model/RemoteLogoutReason.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Emby.ApiClient.Model 3 | { 4 | public enum RemoteLogoutReason 5 | { 6 | GeneralAccesError = 0, 7 | ParentalControlRestriction = 1 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Emby.ApiClient/Cryptography/ICryptographyProvider.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Emby.ApiClient.Cryptography 3 | { 4 | public interface ICryptographyProvider 5 | { 6 | byte[] CreateSha1(byte[] value); 7 | byte[] CreateMD5(byte[] value); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Emby.ApiClient/Model/ConnectionState.cs: -------------------------------------------------------------------------------- 1 | namespace Emby.ApiClient.Model 2 | { 3 | public enum ConnectionState 4 | { 5 | Unavailable = 1, 6 | ServerSignIn = 2, 7 | SignedIn = 3, 8 | ServerSelection = 4, 9 | ConnectSignIn = 5, 10 | OfflineSignIn = 6, 11 | OfflineSignedIn = 7 12 | } 13 | } -------------------------------------------------------------------------------- /Emby.ApiClient/Sync/IServerSync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Emby.ApiClient.Model; 5 | using MediaBrowser.Model.ApiClient; 6 | 7 | namespace Emby.ApiClient.Sync 8 | { 9 | public interface IServerSync 10 | { 11 | Task Sync(ServerInfo server, bool enableCameraUpload, IProgress progress, CancellationToken cancellationToken = default(CancellationToken)); 12 | } 13 | } -------------------------------------------------------------------------------- /Emby.ApiClient/Sync/IMediaSync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Emby.ApiClient.Model; 5 | using MediaBrowser.Model.ApiClient; 6 | 7 | namespace Emby.ApiClient.Sync 8 | { 9 | public interface IMediaSync 10 | { 11 | Task Sync(IApiClient apiClient, 12 | ServerInfo serverInfo, 13 | IProgress progress, 14 | CancellationToken cancellationToken = default(CancellationToken)); 15 | } 16 | } -------------------------------------------------------------------------------- /Emby.ApiClient/Sync/IMultiServerSync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Emby.ApiClient.Sync 7 | { 8 | public interface IMultiServerSync 9 | { 10 | Task Sync(IProgress progress, 11 | List cameraUploadServers, 12 | bool syncOnlyOnLocalNetwork, 13 | CancellationToken cancellationToken = default(CancellationToken)); 14 | } 15 | } -------------------------------------------------------------------------------- /Emby.ApiClient/Sync/IFileTransferManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using Emby.ApiClient.Model; 4 | using MediaBrowser.Model.Sync; 5 | 6 | namespace Emby.ApiClient.Sync 7 | { 8 | public interface IFileTransferManager 9 | { 10 | System.Threading.Tasks.Task GetItemFileAsync(IApiClient apiClient, ServerInfo server, LocalItem item, string syncJobItemId, IProgress transferProgress, System.Threading.CancellationToken cancellationToken = default(CancellationToken)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Emby.ApiClient/IServerLocator.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.ApiClient; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Emby.ApiClient 7 | { 8 | public interface IServerLocator 9 | { 10 | /// 11 | /// Attemps to discover the server within a local network 12 | /// 13 | Task> FindServers(int timeoutMs, CancellationToken cancellationToken = default(CancellationToken)); 14 | } 15 | } -------------------------------------------------------------------------------- /Emby.ApiClient/Net/IServerLocator.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.ApiClient; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Emby.ApiClient.Net 7 | { 8 | public interface IServerLocator 9 | { 10 | /// 11 | /// Attemps to discover the server within a local network 12 | /// 13 | Task> FindServers(int timeoutMs, CancellationToken cancellationToken = default(CancellationToken)); 14 | } 15 | } -------------------------------------------------------------------------------- /Emby.ApiClient/Model/SeasonQuery.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Querying; 2 | 3 | namespace Emby.ApiClient.Model 4 | { 5 | public class SeasonQuery 6 | { 7 | public string UserId { get; set; } 8 | 9 | public string SeriesId { get; set; } 10 | 11 | public bool? IsMissing { get; set; } 12 | 13 | public bool? IsVirtualUnaired { get; set; } 14 | 15 | public ItemFields[] Fields { get; set; } 16 | 17 | public bool? IsSpecialSeason { get; set; } 18 | 19 | public SeasonQuery() 20 | { 21 | Fields = new ItemFields[] { }; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /Emby.ApiClient/Cryptography/CryptographyProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | 3 | namespace Emby.ApiClient.Cryptography 4 | { 5 | public class CryptographyProvider : ICryptographyProvider 6 | { 7 | public byte[] CreateSha1(byte[] value) 8 | { 9 | using (var provider = SHA1.Create()) 10 | { 11 | return provider.ComputeHash(value); 12 | } 13 | } 14 | 15 | public byte[] CreateMD5(byte[] value) 16 | { 17 | using (var provider = MD5.Create()) 18 | { 19 | return provider.ComputeHash(value); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Emby.ApiClient/Model/ConnectionResult.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Connect; 2 | using MediaBrowser.Model.Dto; 3 | using System.Collections.Generic; 4 | 5 | namespace Emby.ApiClient.Model 6 | { 7 | public class ConnectionResult 8 | { 9 | public ConnectionState State { get; set; } 10 | public List Servers { get; set; } 11 | public IApiClient ApiClient { get; set; } 12 | public ConnectUser ConnectUser { get; set; } 13 | public UserDto OfflineUser { get; set; } 14 | 15 | public ConnectionResult() 16 | { 17 | State = ConnectionState.Unavailable; 18 | Servers = new List(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Emby.ApiClient/ICredentialProvider.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.ApiClient; 2 | using System.Threading.Tasks; 3 | using Emby.ApiClient.Model; 4 | 5 | namespace Emby.ApiClient 6 | { 7 | public interface ICredentialProvider 8 | { 9 | /// 10 | /// Gets the server credentials. 11 | /// 12 | /// ServerCredentialConfiguration. 13 | Task GetServerCredentials(); 14 | 15 | /// 16 | /// Saves the server credentials. 17 | /// 18 | /// The configuration. 19 | Task SaveServerCredentials(ServerCredentials configuration); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Emby.ApiClient/Data/IUserActionRepository.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Users; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace Emby.ApiClient.Data 6 | { 7 | public interface IUserActionRepository 8 | { 9 | /// 10 | /// Creates the specified action. 11 | /// 12 | /// The action. 13 | /// Task. 14 | Task Create(UserAction action); 15 | 16 | /// 17 | /// Deletes the specified action. 18 | /// 19 | /// The action. 20 | /// Task. 21 | Task Delete(UserAction action); 22 | 23 | /// 24 | /// Gets all user actions by serverId 25 | /// 26 | /// 27 | /// 28 | Task> Get(string serverId); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Emby.ApiClient/Device.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.ApiClient; 2 | using MediaBrowser.Model.Devices; 3 | using Microsoft.Win32; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Emby.ApiClient.Model; 10 | 11 | namespace Emby.ApiClient 12 | { 13 | public class Device : IDevice 14 | { 15 | public string DeviceName { get; set; } 16 | public string DeviceId { get; set; } 17 | 18 | public virtual async Task> GetLocalPhotos() 19 | { 20 | return new List(); 21 | } 22 | 23 | public virtual async Task> GetLocalVideos() 24 | { 25 | return new List(); 26 | } 27 | 28 | public Task UploadFile(LocalFileInfo file, IApiClient apiClient, CancellationToken cancellationToken = default(CancellationToken)) 29 | { 30 | return apiClient.UploadFile(File.OpenRead(file.Id), file, cancellationToken); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Emby.ApiClient/Emby.ApiClient.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net46 5 | 1.1.1.0 6 | 1.1.1.0 7 | true 8 | MediaBrowser.ApiClient 9 | 3.1.1 10 | Emby 11 | Emby 12 | Api libraries for connecting to Emby Server. 13 | http://www.mb3admin.com/images/mb3icons1-1.png 14 | https://github.com/MediaBrowser/Emby.ApiClient 15 | https://github.com/MediaBrowser/Emby.ApiClient 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Emby.ApiClient/Net/HttpHeaders.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Emby.ApiClient.Net 4 | { 5 | public class HttpHeaders : Dictionary 6 | { 7 | /// 8 | /// Gets or sets the authorization scheme. 9 | /// 10 | /// The authorization scheme. 11 | public string AuthorizationScheme { get; set; } 12 | /// 13 | /// Gets or sets the authorization parameter. 14 | /// 15 | /// The authorization parameter. 16 | public string AuthorizationParameter { get; set; } 17 | 18 | /// 19 | /// Sets the access token. 20 | /// 21 | /// The token. 22 | public void SetAccessToken(string token) 23 | { 24 | if (string.IsNullOrEmpty(token)) 25 | { 26 | Remove("X-MediaBrowser-Token"); 27 | } 28 | else 29 | { 30 | this["X-MediaBrowser-Token"] = token; 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) Media Browser http://mediabrowser.tv 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Emby.ApiClient/Model/ConnectionOptions.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Emby.ApiClient.Model 3 | { 4 | public class ConnectionOptions 5 | { 6 | /// 7 | /// Gets or sets a value indicating whether [enable web socket]. 8 | /// 9 | /// true if [enable web socket]; otherwise, false. 10 | public bool EnableWebSocket { get; set; } 11 | /// 12 | /// Gets or sets a value indicating whether [report capabilities]. 13 | /// 14 | /// true if [report capabilities]; otherwise, false. 15 | public bool ReportCapabilities { get; set; } 16 | /// 17 | /// Gets or sets a value indicating whether [update date last accessed]. 18 | /// 19 | /// true if [update date last accessed]; otherwise, false. 20 | public bool UpdateDateLastAccessed { get; set; } 21 | 22 | public ConnectionOptions() 23 | { 24 | EnableWebSocket = true; 25 | ReportCapabilities = true; 26 | UpdateDateLastAccessed = true; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Emby.ApiClient.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Emby.ApiClient", "Emby.ApiClient\Emby.ApiClient.csproj", "{78198B43-83EF-4B5B-ADBC-443E54FF7E8E}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {78198B43-83EF-4B5B-ADBC-443E54FF7E8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {78198B43-83EF-4B5B-ADBC-443E54FF7E8E}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {78198B43-83EF-4B5B-ADBC-443E54FF7E8E}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {78198B43-83EF-4B5B-ADBC-443E54FF7E8E}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {3B6AEE46-C88D-448D-9332-8C6218BC0233} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Emby.ApiClient/Data/IUserRepository.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Dto; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace Emby.ApiClient.Data 6 | { 7 | public interface IUserRepository 8 | { 9 | /// 10 | /// Adds the or update. 11 | /// 12 | /// The identifier. 13 | /// The user. 14 | /// Task. 15 | Task AddOrUpdate(string id, UserDto user); 16 | /// 17 | /// Deletes the specified identifier. 18 | /// 19 | /// The identifier. 20 | /// Task. 21 | Task Delete(string id); 22 | /// 23 | /// Gets the specified identifier. 24 | /// 25 | /// The identifier. 26 | /// Task<UserDto>. 27 | Task Get(string id); 28 | /// 29 | /// Gets all. 30 | /// 31 | /// Task<List<UserDto>>. 32 | Task> GetAll(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Emby.ApiClient/Net/HttpRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading; 5 | 6 | namespace Emby.ApiClient.Net 7 | { 8 | public class HttpRequest 9 | { 10 | public string Method { get; set; } 11 | public CancellationToken CancellationToken { get; set; } 12 | public string RequestContent { get; set; } 13 | public string RequestContentType { get; set; } 14 | public HttpHeaders RequestHeaders { get; set; } 15 | public string Url { get; set; } 16 | public Stream RequestStream { get; set; } 17 | 18 | public int Timeout { get; set; } 19 | 20 | public HttpRequest() 21 | { 22 | RequestHeaders = new HttpHeaders(); 23 | Timeout = 30000; 24 | } 25 | 26 | public void SetPostData(IDictionary values) 27 | { 28 | var strings = values.Keys.Select(key => string.Format("{0}={1}", key, values[key])); 29 | var postContent = string.Join("&", strings.ToArray()); 30 | 31 | RequestContent = postContent; 32 | RequestContentType = "application/x-www-form-urlencoded"; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Emby.ApiClient/Model/NetworkStatus.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Emby.ApiClient.Model 3 | { 4 | public class NetworkStatus 5 | { 6 | /// 7 | /// Gets or sets a value indicating whether this instance is network available. 8 | /// 9 | /// true if this instance is network available; otherwise, false. 10 | public bool IsNetworkAvailable { get; set; } 11 | /// 12 | /// Gets or sets a value indicating whether this instance is local network available. 13 | /// 14 | /// null if [is local network available] contains no value, true if [is local network available]; otherwise, false. 15 | public bool? IsLocalNetworkAvailable { get; set; } 16 | /// 17 | /// Gets the is any local network available. 18 | /// 19 | /// true if XXXX, false otherwise. 20 | public bool GetIsAnyLocalNetworkAvailable() 21 | { 22 | if (!IsLocalNetworkAvailable.HasValue) 23 | { 24 | return IsNetworkAvailable; 25 | } 26 | 27 | return IsLocalNetworkAvailable.Value; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Emby.ApiClient/Net/IAsyncHttpClient.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.ApiClient; 2 | using MediaBrowser.Model.Net; 3 | using System; 4 | using System.IO; 5 | using System.Threading.Tasks; 6 | 7 | namespace Emby.ApiClient.Net 8 | { 9 | /// 10 | /// Interface IHttpClient 11 | /// 12 | public interface IAsyncHttpClient : IDisposable 13 | { 14 | /// 15 | /// Occurs when [HTTP response received]. 16 | /// 17 | event EventHandler HttpResponseReceived; 18 | 19 | /// 20 | /// Sends the asynchronous. 21 | /// 22 | /// The options. 23 | /// Task<Stream>. 24 | Task SendAsync(HttpRequest options); 25 | 26 | /// 27 | /// Gets the response. 28 | /// 29 | /// The options. 30 | /// if set to true [send failure response]. 31 | /// 32 | /// Task<HttpResponse>. 33 | /// 34 | Task GetResponse(HttpRequest options, bool sendFailureResponse = false); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Emby.ApiClient/Model/IDevice.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Devices; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Emby.ApiClient.Model 8 | { 9 | public interface IDevice 10 | { 11 | /// 12 | /// Gets the name of the device. 13 | /// 14 | /// The name of the device. 15 | string DeviceName { get; } 16 | /// 17 | /// Gets the device identifier. 18 | /// 19 | /// The device identifier. 20 | string DeviceId { get; } 21 | /// 22 | /// Gets the local images. 23 | /// 24 | /// IEnumerable<LocalFileInfo>. 25 | Task> GetLocalPhotos(); 26 | /// 27 | /// Gets the local videos. 28 | /// 29 | /// IEnumerable<LocalFileInfo>. 30 | Task> GetLocalVideos(); 31 | /// 32 | /// Uploads the file. 33 | /// 34 | /// The file. 35 | /// The API client. 36 | /// The cancellation token. 37 | /// Task. 38 | Task UploadFile(LocalFileInfo file, IApiClient apiClient, CancellationToken cancellationToken = default(CancellationToken)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Emby.ApiClient/Sync/FileTransferManager.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.ApiClient; 2 | using MediaBrowser.Model.Logging; 3 | using MediaBrowser.Model.Sync; 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Emby.ApiClient.Data; 8 | using Emby.ApiClient.Model; 9 | 10 | namespace Emby.ApiClient.Sync 11 | { 12 | class FileTransferManager : IFileTransferManager 13 | { 14 | private readonly ILocalAssetManager _localAssetManager; 15 | private readonly ILogger _logger; 16 | 17 | internal FileTransferManager(ILocalAssetManager localAssetManager, ILogger logger) 18 | { 19 | _localAssetManager = localAssetManager; 20 | _logger = logger; 21 | } 22 | 23 | public async Task GetItemFileAsync(IApiClient apiClient, 24 | ServerInfo server, 25 | LocalItem item, 26 | string syncJobItemId, 27 | IProgress transferProgress, 28 | CancellationToken cancellationToken = default(CancellationToken)) 29 | { 30 | _logger.Debug("Downloading media with Id {0} to local repository", item.Item.Id); 31 | 32 | using (var stream = await apiClient.GetSyncJobItemFile(syncJobItemId, cancellationToken).ConfigureAwait(false)) 33 | { 34 | await _localAssetManager.SaveMedia(stream, item, server).ConfigureAwait(false); 35 | } 36 | transferProgress.Report(100); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Emby.ApiClient/WebSocket/ClientWebSocketFactory.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Logging; 2 | using System; 3 | using Emby.ApiClient.Model; 4 | 5 | namespace Emby.ApiClient.WebSocket 6 | { 7 | /// 8 | /// Class ClientWebSocketFactory 9 | /// 10 | public static class ClientWebSocketFactory 11 | { 12 | /// 13 | /// Creates the web socket. 14 | /// 15 | /// The logger. 16 | /// IClientWebSocket. 17 | public static IClientWebSocket CreateWebSocket(ILogger logger) 18 | { 19 | try 20 | { 21 | // This is preferred but only supported on windows 8 or server 2012 22 | // Comment NativeClientWebSocket out for now due to message parsing errors 23 | // return new NativeClientWebSocket(logger); 24 | return new NativeClientWebSocket(logger); 25 | } 26 | catch (NotSupportedException) 27 | { 28 | return new WebSocket4NetClientWebSocket(logger); 29 | } 30 | } 31 | 32 | /// 33 | /// Creates the web socket. 34 | /// 35 | /// IClientWebSocket. 36 | public static IClientWebSocket CreateWebSocket() 37 | { 38 | return CreateWebSocket(new NullLogger()); 39 | } 40 | } 41 | 42 | public static class SocketExtensions 43 | { 44 | public static void OpenWebSocket(this ApiClient client) 45 | { 46 | client.OpenWebSocket(() => ClientWebSocketFactory.CreateWebSocket(new NullLogger())); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Emby.ApiClient/Net/INetworkConnection.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.ApiClient; 2 | using System; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Emby.ApiClient.Model; 6 | 7 | namespace Emby.ApiClient.Net 8 | { 9 | public interface INetworkConnection 10 | { 11 | /// 12 | /// Occurs when [network changed]. 13 | /// 14 | event EventHandler NetworkChanged; 15 | 16 | /// 17 | /// Sends the wake on lan. 18 | /// 19 | /// The mac address. 20 | /// The ip address. 21 | /// The port. 22 | /// The cancellation token. 23 | /// Task. 24 | Task SendWakeOnLan(string macAddress, string ipAddress, int port, CancellationToken cancellationToken = default(CancellationToken)); 25 | 26 | /// 27 | /// Sends the wake on lan. 28 | /// 29 | /// The mac address. 30 | /// The port. 31 | /// The cancellation token. 32 | /// Task. 33 | Task SendWakeOnLan(string macAddress, int port, CancellationToken cancellationToken = default(CancellationToken)); 34 | 35 | /// 36 | /// Gets the network status. 37 | /// 38 | /// NetworkStatus. 39 | NetworkStatus GetNetworkStatus(); 40 | 41 | #if WINDOWS_UWP 42 | bool HasUnmeteredConnection(); 43 | #endif 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Emby.ApiClient/Data/IImageRepository.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | 4 | namespace Emby.ApiClient.Data 5 | { 6 | public interface IImageRepository 7 | { 8 | /// 9 | /// Saves the image. 10 | /// 11 | /// The item identifier. 12 | /// The image identifier. 13 | /// The stream. 14 | /// Task. 15 | Task SaveImage(string itemId, string imageId, Stream stream); 16 | /// 17 | /// Gets the image. 18 | /// 19 | /// The item identifier. 20 | /// The image identifier. 21 | /// Task<Stream>. 22 | Task GetImage(string itemId, string imageId); 23 | /// 24 | /// Deletes the image. 25 | /// 26 | /// The item identifier. 27 | /// The image identifier. 28 | /// Task. 29 | Task DeleteImage(string itemId, string imageId); 30 | /// 31 | /// Determines whether the specified item identifier has image. 32 | /// 33 | /// The item identifier. 34 | /// The image identifier. 35 | Task HasImage(string itemId, string imageId); 36 | /// 37 | /// Deletes the images. 38 | /// 39 | /// The item identifier. 40 | /// Task. 41 | Task DeleteImages(string itemId); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Emby.ApiClient/Model/IClientWebSocket.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Net; 2 | using System; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Emby.ApiClient.Model 7 | { 8 | /// 9 | /// Interface IClientWebSocket 10 | /// 11 | public interface IClientWebSocket : IDisposable 12 | { 13 | /// 14 | /// Occurs when [closed]. 15 | /// 16 | event EventHandler Closed; 17 | 18 | /// 19 | /// Gets or sets the state. 20 | /// 21 | /// The state. 22 | WebSocketState State { get; } 23 | 24 | /// 25 | /// Connects the async. 26 | /// 27 | /// The URL. 28 | /// The cancellation token. 29 | /// Task. 30 | Task ConnectAsync(string url, CancellationToken cancellationToken); 31 | 32 | /// 33 | /// Gets or sets the receive action. 34 | /// 35 | /// The receive action. 36 | Action OnReceiveBytes { get; set; } 37 | 38 | /// 39 | /// Gets or sets the on receive. 40 | /// 41 | /// The on receive. 42 | Action OnReceive { get; set; } 43 | 44 | /// 45 | /// Sends the async. 46 | /// 47 | /// The bytes. 48 | /// The type. 49 | /// if set to true [end of message]. 50 | /// The cancellation token. 51 | /// Task. 52 | Task SendAsync(byte[] bytes, WebSocketMessageType type, bool endOfMessage, CancellationToken cancellationToken); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Emby.ApiClient/Sync/DoubleProgress.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Emby.ApiClient.Sync 5 | { 6 | public class DoubleProgress : Progress, IDisposable 7 | { 8 | /// 9 | /// The _actions 10 | /// 11 | private readonly List> _actions = new List>(); 12 | 13 | /// 14 | /// Registers the action. 15 | /// 16 | /// The action. 17 | public void RegisterAction(Action action) 18 | { 19 | _actions.Add(action); 20 | 21 | ProgressChanged -= ActionableProgress_ProgressChanged; 22 | ProgressChanged += ActionableProgress_ProgressChanged; 23 | } 24 | 25 | /// 26 | /// Actionables the progress_ progress changed. 27 | /// 28 | /// The sender. 29 | /// The e. 30 | void ActionableProgress_ProgressChanged(object sender, double e) 31 | { 32 | foreach (var action in _actions) 33 | { 34 | action(e); 35 | } 36 | } 37 | 38 | /// 39 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 40 | /// 41 | public void Dispose() 42 | { 43 | Dispose(true); 44 | } 45 | 46 | /// 47 | /// Releases unmanaged and - optionally - managed resources. 48 | /// 49 | /// true to release both managed and unmanaged resources; false to release only unmanaged resources. 50 | protected virtual void Dispose(bool disposing) 51 | { 52 | if (disposing) 53 | { 54 | ProgressChanged -= ActionableProgress_ProgressChanged; 55 | _actions.Clear(); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Emby.ApiClient/Sync/ContentUploader.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.ApiClient; 2 | using MediaBrowser.Model.Logging; 3 | using System; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Emby.ApiClient.Model; 8 | 9 | namespace Emby.ApiClient.Sync 10 | { 11 | public class ContentUploader 12 | { 13 | private readonly IApiClient _apiClient; 14 | private readonly ILogger _logger; 15 | 16 | public ContentUploader(IApiClient apiClient, ILogger logger) 17 | { 18 | _apiClient = apiClient; 19 | _logger = logger; 20 | } 21 | 22 | public async Task UploadImages(IProgress progress, CancellationToken cancellationToken = default(CancellationToken)) 23 | { 24 | var device = _apiClient.Device; 25 | 26 | var deviceId = device.DeviceId; 27 | 28 | var history = await _apiClient.GetContentUploadHistory(deviceId).ConfigureAwait(false); 29 | 30 | var files = (await device.GetLocalPhotos().ConfigureAwait(false)) 31 | .ToList(); 32 | 33 | files.AddRange((await device.GetLocalVideos().ConfigureAwait(false))); 34 | 35 | files = files 36 | .Where(i => !history.FilesUploaded.Any(f => string.Equals(f.Id, i.Id, StringComparison.OrdinalIgnoreCase))) 37 | .ToList(); 38 | 39 | var numComplete = 0; 40 | 41 | foreach (var file in files) 42 | { 43 | cancellationToken.ThrowIfCancellationRequested(); 44 | 45 | _logger.Debug("Uploading {0}", file.Id); 46 | 47 | try 48 | { 49 | await device.UploadFile(file, _apiClient, cancellationToken); 50 | } 51 | catch (Exception ex) 52 | { 53 | _logger.ErrorException("Error uploading file", ex); 54 | } 55 | 56 | numComplete++; 57 | double percent = numComplete; 58 | percent /= files.Count; 59 | 60 | progress.Report(100 * percent); 61 | } 62 | 63 | progress.Report(100); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Emby.ApiClient/Model/ServerInfo.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Connect; 2 | using MediaBrowser.Model.Extensions; 3 | using MediaBrowser.Model.System; 4 | using System; 5 | using System.Collections.Generic; 6 | using MediaBrowser.Model.ApiClient; 7 | 8 | namespace Emby.ApiClient.Model 9 | { 10 | public class ServerInfo 11 | { 12 | public String Name { get; set; } 13 | public String Id { get; set; } 14 | public String ConnectServerId { get; set; } 15 | public String LocalAddress { get; set; } 16 | public String RemoteAddress { get; set; } 17 | public String ManualAddress { get; set; } 18 | public String UserId { get; set; } 19 | public String AccessToken { get; set; } 20 | public List WakeOnLanInfos { get; set; } 21 | public DateTime DateLastAccessed { get; set; } 22 | public String ExchangeToken { get; set; } 23 | public UserLinkType? UserLinkType { get; set; } 24 | public ConnectionMode? LastConnectionMode { get; set; } 25 | 26 | public ServerInfo() 27 | { 28 | WakeOnLanInfos = new List(); 29 | } 30 | 31 | public void ImportInfo(PublicSystemInfo systemInfo) 32 | { 33 | Name = systemInfo.ServerName; 34 | Id = systemInfo.Id; 35 | 36 | if (!string.IsNullOrEmpty(systemInfo.LocalAddress)) 37 | { 38 | LocalAddress = systemInfo.LocalAddress; 39 | } 40 | 41 | if (!string.IsNullOrEmpty(systemInfo.WanAddress)) 42 | { 43 | RemoteAddress = systemInfo.WanAddress; 44 | } 45 | 46 | var fullSystemInfo = systemInfo as SystemInfo; 47 | 48 | if (fullSystemInfo != null) 49 | { 50 | WakeOnLanInfos = new List(); 51 | 52 | if (!string.IsNullOrEmpty(fullSystemInfo.MacAddress)) 53 | { 54 | WakeOnLanInfos.Add(new WakeOnLanInfo 55 | { 56 | MacAddress = fullSystemInfo.MacAddress 57 | }); 58 | } 59 | } 60 | } 61 | 62 | public string GetAddress(ConnectionMode mode) 63 | { 64 | switch (mode) 65 | { 66 | case ConnectionMode.Local: 67 | return LocalAddress; 68 | case ConnectionMode.Manual: 69 | return ManualAddress; 70 | case ConnectionMode.Remote: 71 | return RemoteAddress; 72 | default: 73 | throw new ArgumentException("Unexpected ConnectionMode"); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Emby.ApiClient/Data/IFileRepository.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Sync; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | 6 | namespace Emby.ApiClient.Data 7 | { 8 | public interface IFileRepository 9 | { 10 | /// 11 | /// Gets the file system entries. 12 | /// 13 | /// The path. 14 | /// Task<List<DeviceFileInfo>>. 15 | Task> GetFileSystemEntries(string path); 16 | 17 | /// 18 | /// Saves the file. 19 | /// 20 | /// The stream. 21 | /// The path. 22 | /// Task. 23 | Task SaveFile(Stream stream, string path); 24 | 25 | #if WINDOWS_UWP 26 | /// 27 | /// Saves the file. 28 | /// 29 | /// The file. 30 | /// The path. 31 | /// Task. 32 | Task SaveFile(Windows.Storage.IStorageFile file, string path); 33 | #endif 34 | 35 | /// 36 | /// Deletes the file. 37 | /// 38 | /// The path. 39 | /// Task. 40 | Task DeleteFile(string path); 41 | 42 | /// 43 | /// Deletes the directory. 44 | /// 45 | /// The path. 46 | /// Task. 47 | Task DeleteFolder(string path); 48 | 49 | /// 50 | /// Strips invalid characters from a given name 51 | /// 52 | /// The name. 53 | /// System.String. 54 | string GetValidFileName(string name); 55 | 56 | /// 57 | /// Files the exists. 58 | /// 59 | /// The path. 60 | /// Task<System.Boolean>. 61 | Task FileExists(string path); 62 | 63 | /// 64 | /// Gets the full local path. 65 | /// 66 | /// The path. 67 | /// System.String. 68 | string GetFullLocalPath(IEnumerable path); 69 | 70 | /// 71 | /// Gets the parent directory path. 72 | /// 73 | /// The path. 74 | /// System.String. 75 | string GetParentDirectoryPath(string path); 76 | 77 | /// 78 | /// Gets the file stream. 79 | /// 80 | /// The path. 81 | /// Task<Stream>. 82 | Task GetFileStream(string path); 83 | #if WINDOWS_UWP 84 | /// 85 | /// Gets the file. 86 | /// 87 | /// The path. 88 | /// Task<File>. 89 | Task GetFile(string path); 90 | #endif 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Emby.ApiClient/PortableHttpWebRequestFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net; 4 | using System.Threading.Tasks; 5 | using Emby.ApiClient.Net; 6 | 7 | namespace Emby.ApiClient 8 | { 9 | public class PortableHttpWebRequestFactory : IHttpWebRequestFactory 10 | { 11 | public HttpWebRequest Create(HttpRequest options) 12 | { 13 | var request = HttpWebRequest.CreateHttp(options.Url); 14 | 15 | request.Method = options.Method; 16 | 17 | return request; 18 | } 19 | 20 | public void SetContentLength(HttpWebRequest request, long length) 21 | { 22 | //request.Headers["Content-Length"] = length.ToString(CultureInfo.InvariantCulture); 23 | } 24 | 25 | public Task GetResponseAsync(HttpWebRequest request, int timeoutMs) 26 | { 27 | if (timeoutMs > 0) 28 | { 29 | return GetResponseAsync(request, TimeSpan.FromMilliseconds(timeoutMs)); 30 | } 31 | 32 | var tcs = new TaskCompletionSource(); 33 | 34 | try 35 | { 36 | request.BeginGetResponse(iar => 37 | { 38 | try 39 | { 40 | var response = (HttpWebResponse)request.EndGetResponse(iar); 41 | tcs.SetResult(response); 42 | } 43 | catch (Exception exc) 44 | { 45 | tcs.SetException(exc); 46 | } 47 | }, null); 48 | } 49 | catch (Exception exc) 50 | { 51 | tcs.SetException(exc); 52 | } 53 | 54 | return tcs.Task; 55 | } 56 | 57 | private Task GetResponseAsync(WebRequest request, TimeSpan timeout) 58 | { 59 | return Task.Factory.StartNew(() => 60 | { 61 | var t = Task.Factory.FromAsync( 62 | request.BeginGetResponse, 63 | request.EndGetResponse, 64 | null); 65 | 66 | if (!t.Wait(timeout)) throw new TimeoutException(); 67 | 68 | return t.Result; 69 | }); 70 | } 71 | 72 | public Task GetRequestStreamAsync(HttpWebRequest request) 73 | { 74 | var tcs = new TaskCompletionSource(); 75 | 76 | try 77 | { 78 | request.BeginGetRequestStream(iar => 79 | { 80 | try 81 | { 82 | var response = request.EndGetRequestStream(iar); 83 | tcs.SetResult(response); 84 | } 85 | catch (Exception exc) 86 | { 87 | tcs.SetException(exc); 88 | } 89 | }, null); 90 | } 91 | catch (Exception exc) 92 | { 93 | tcs.SetException(exc); 94 | } 95 | 96 | return tcs.Task; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Emby.ApiClient/Data/IItemRepository.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Sync; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace Emby.ApiClient.Data 6 | { 7 | /// 8 | /// Interface IItemRepository 9 | /// 10 | public interface IItemRepository 11 | { 12 | /// 13 | /// Adds the or update. 14 | /// 15 | /// The item. 16 | /// Task. 17 | Task AddOrUpdate(LocalItem item); 18 | 19 | /// 20 | /// Gets the item. 21 | /// 22 | /// The identifier. 23 | /// Task<BaseItemDto>. 24 | Task Get(string id); 25 | 26 | /// 27 | /// Deletes the specified identifier. 28 | /// 29 | /// The identifier. 30 | /// Task. 31 | Task Delete(string id); 32 | 33 | /// 34 | /// Gets the server item ids. 35 | /// 36 | /// The server identifier. 37 | /// Task<List<System.String>>. 38 | Task> GetServerItemIds(string serverId); 39 | 40 | /// 41 | /// Queries all items for a server Id and returns a list of unique item types. 42 | /// 43 | /// The server identifier. 44 | /// The user identifier. 45 | /// Task<List<System.String>>. 46 | Task> GetItemTypes(string serverId, string userId); 47 | 48 | /// 49 | /// Gets the items. 50 | /// 51 | /// The query. 52 | /// Task<List<LocalItem>>. 53 | Task> GetItems(LocalItemQuery query); 54 | 55 | /// 56 | /// Gets a list of unique AlbumArtist values 57 | /// 58 | /// The server identifier. 59 | /// The user identifier. 60 | /// Task<List<System.String>>. 61 | Task> GetAlbumArtists(string serverId, string userId); 62 | 63 | /// 64 | /// Gets a list of unique series, by id 65 | /// Name = Album property 66 | /// Id = SeriesId property 67 | /// PrimaryImageTag = SeriesPrimaryImageTag 68 | /// 69 | /// The server identifier. 70 | /// The user identifier. 71 | /// Task<List<LocalItemInfo>>. 72 | Task> GetTvSeries(string serverId, string userId); 73 | 74 | /// 75 | /// Gets a list of unique photo albums, by Id 76 | /// Name = Album property 77 | /// Id = AlbumId property 78 | /// PrimaryImageTag = AlbumPrimaryImageTag 79 | /// 80 | /// The server identifier. 81 | /// The user identifier. 82 | /// Task<List<LocalItemInfo>>. 83 | Task> GetPhotoAlbums(string serverId, string userId); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | ################# 32 | ## Media Browser 33 | ################# 34 | ProgramData*/ 35 | CorePlugins*/ 36 | ProgramData-Server*/ 37 | ProgramData-UI*/ 38 | 39 | ################# 40 | ## Visual Studio 41 | ################# 42 | 43 | ## Ignore Visual Studio temporary files, build results, and 44 | ## files generated by popular Visual Studio add-ons. 45 | 46 | # User-specific files 47 | *.suo 48 | *.user 49 | *.sln.docstates 50 | 51 | # Build results 52 | 53 | [Dd]ebug/ 54 | [Rr]elease/ 55 | build/ 56 | [Bb]in/ 57 | [Oo]bj/ 58 | 59 | # MSTest test Results 60 | [Tt]est[Rr]esult*/ 61 | [Bb]uild[Ll]og.* 62 | 63 | *_i.c 64 | *_p.c 65 | *.ilk 66 | *.meta 67 | *.obj 68 | *.pch 69 | *.pdb 70 | *.pgc 71 | *.pgd 72 | *.rsp 73 | *.sbr 74 | *.tlb 75 | *.tli 76 | *.tlh 77 | *.tmp 78 | *.tmp_proj 79 | *.log 80 | *.vspscc 81 | *.vssscc 82 | .builds 83 | *.pidb 84 | *.log 85 | *.scc 86 | *.scc 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.orig 91 | *.rej 92 | *.sdf 93 | *.opensdf 94 | *.ipch 95 | 96 | # Visual C++ cache files 97 | ipch/ 98 | *.aps 99 | *.ncb 100 | *.opensdf 101 | *.sdf 102 | *.cachefile 103 | 104 | # Visual Studio profiler 105 | *.psess 106 | *.vsp 107 | *.vspx 108 | 109 | # Guidance Automation Toolkit 110 | *.gpState 111 | 112 | # ReSharper is a .NET coding add-in 113 | _ReSharper*/ 114 | *.[Rr]e[Ss]harper 115 | 116 | # TeamCity is a build add-in 117 | _TeamCity* 118 | 119 | # DotCover is a Code Coverage Tool 120 | *.dotCover 121 | 122 | # NCrunch 123 | *.ncrunch* 124 | .*crunch*.local.xml 125 | 126 | # Installshield output folder 127 | [Ee]xpress/ 128 | 129 | # DocProject is a documentation generator add-in 130 | DocProject/buildhelp/ 131 | DocProject/Help/*.HxT 132 | DocProject/Help/*.HxC 133 | DocProject/Help/*.hhc 134 | DocProject/Help/*.hhk 135 | DocProject/Help/*.hhp 136 | DocProject/Help/Html2 137 | DocProject/Help/html 138 | 139 | # Click-Once directory 140 | publish/ 141 | 142 | # Publish Web Output 143 | *.Publish.xml 144 | *.pubxml 145 | 146 | # NuGet Packages Directory 147 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 148 | packages/ 149 | dlls/ 150 | 151 | # Windows Azure Build Output 152 | csx 153 | *.build.csdef 154 | 155 | # Windows Store app package directory 156 | AppPackages/ 157 | 158 | # Others 159 | sql/ 160 | *.Cache 161 | ClientBin/ 162 | [Ss]tyle[Cc]op.* 163 | ~$* 164 | *~ 165 | *.dbmdl 166 | *.[Pp]ublish.xml 167 | *.publishsettings 168 | 169 | # RIA/Silverlight projects 170 | Generated_Code/ 171 | 172 | # Backup & report files from converting an old project file to a newer 173 | # Visual Studio version. Backup files are not needed, because we have git ;-) 174 | _UpgradeReport_Files/ 175 | Backup*/ 176 | UpgradeLog*.XML 177 | UpgradeLog*.htm 178 | 179 | # SQL Server files 180 | App_Data/*.mdf 181 | App_Data/*.ldf 182 | 183 | ############# 184 | ## Windows detritus 185 | ############# 186 | 187 | # Windows image file caches 188 | Thumbs.db 189 | ehthumbs.db 190 | 191 | # Folder config file 192 | Desktop.ini 193 | 194 | # Recycle Bin used on file shares 195 | $RECYCLE.BIN/ 196 | 197 | # Mac crap 198 | .DS_Store 199 | 200 | 201 | ############# 202 | ## Python 203 | ############# 204 | 205 | *.py[co] 206 | 207 | # Packages 208 | *.egg 209 | *.egg-info 210 | dist/ 211 | build/ 212 | eggs/ 213 | parts/ 214 | var/ 215 | sdist/ 216 | develop-eggs/ 217 | .installed.cfg 218 | 219 | # Installer logs 220 | pip-log.txt 221 | 222 | # Unit test / coverage reports 223 | .coverage 224 | .tox 225 | 226 | #Translations 227 | *.mo 228 | 229 | #Mr Developer 230 | .mr.developer.cfg 231 | /.vs 232 | -------------------------------------------------------------------------------- /Emby.ApiClient/Net/HttpWebRequestFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Net; 5 | using System.Reflection; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace Emby.ApiClient.Net 11 | { 12 | public class HttpWebRequestFactory : IHttpWebRequestFactory 13 | { 14 | private static PropertyInfo _httpBehaviorPropertyInfo; 15 | public HttpWebRequest Create(HttpRequest options) 16 | { 17 | var request = HttpWebRequest.CreateHttp(options.Url); 18 | 19 | request.AutomaticDecompression = DecompressionMethods.Deflate; 20 | request.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.Revalidate); 21 | request.KeepAlive = true; 22 | request.Method = options.Method; 23 | request.Pipelined = true; 24 | request.Timeout = options.Timeout; 25 | 26 | // This is a hack to prevent KeepAlive from getting disabled internally by the HttpWebRequest 27 | /*var sp = request.ServicePoint; 28 | if (_httpBehaviorPropertyInfo == null) 29 | { 30 | _httpBehaviorPropertyInfo = sp.GetType().GetProperty("HttpBehaviour", BindingFlags.Instance | BindingFlags.NonPublic); 31 | } 32 | _httpBehaviorPropertyInfo?.SetValue(sp, (byte)0, null);*/ 33 | 34 | if (!string.IsNullOrEmpty(options.RequestContent) || 35 | string.Equals(options.Method, "post", StringComparison.OrdinalIgnoreCase)) 36 | { 37 | var bytes = Encoding.UTF8.GetBytes(options.RequestContent ?? string.Empty); 38 | 39 | request.SendChunked = false; 40 | request.ContentLength = bytes.Length; 41 | } 42 | 43 | return request; 44 | } 45 | 46 | public void SetContentLength(HttpWebRequest request, long length) 47 | { 48 | request.ContentLength = length; 49 | } 50 | 51 | public Task GetResponseAsync(HttpWebRequest request, int timeoutMs) 52 | { 53 | if (timeoutMs > 0) 54 | { 55 | return GetResponseAsync(request, TimeSpan.FromMilliseconds(timeoutMs)); 56 | } 57 | 58 | return request.GetResponseAsync(); 59 | } 60 | 61 | public Task GetRequestStreamAsync(HttpWebRequest request) 62 | { 63 | return request.GetRequestStreamAsync(); 64 | } 65 | 66 | private Task GetResponseAsync(WebRequest request, TimeSpan timeout) 67 | { 68 | var taskCompletion = new TaskCompletionSource(); 69 | 70 | Task asyncTask = Task.Factory.FromAsync(request.BeginGetResponse, request.EndGetResponse, null); 71 | 72 | ThreadPool.RegisterWaitForSingleObject((asyncTask as IAsyncResult).AsyncWaitHandle, TimeoutCallback, request, timeout, true); 73 | asyncTask.ContinueWith(task => 74 | { 75 | taskCompletion.TrySetResult(task.Result); 76 | 77 | }, TaskContinuationOptions.NotOnFaulted); 78 | 79 | // Handle errors 80 | asyncTask.ContinueWith(task => 81 | { 82 | if (task.Exception != null) 83 | { 84 | taskCompletion.TrySetException(task.Exception); 85 | } 86 | else 87 | { 88 | taskCompletion.TrySetException(new List()); 89 | } 90 | 91 | }, TaskContinuationOptions.OnlyOnFaulted); 92 | 93 | return taskCompletion.Task; 94 | } 95 | 96 | private static void TimeoutCallback(object state, bool timedOut) 97 | { 98 | if (timedOut) 99 | { 100 | WebRequest request = (WebRequest)state; 101 | if (state != null) 102 | { 103 | request.Abort(); 104 | } 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Emby.ApiClient/Model/ServerCredentials.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Extensions; 2 | using System; 3 | using System.Collections.Generic; 4 | using MediaBrowser.Model.ApiClient; 5 | 6 | namespace Emby.ApiClient.Model 7 | { 8 | public class ServerCredentials 9 | { 10 | public List Servers { get; set; } 11 | 12 | public string ConnectUserId { get; set; } 13 | public string ConnectAccessToken { get; set; } 14 | 15 | public ServerCredentials() 16 | { 17 | Servers = new List(); 18 | } 19 | 20 | public void AddOrUpdateServer(ServerInfo server) 21 | { 22 | if (server == null) 23 | { 24 | throw new ArgumentNullException("server"); 25 | } 26 | 27 | // Clone the existing list of servers 28 | var list = new List(); 29 | foreach (ServerInfo serverInfo in Servers) 30 | { 31 | list.Add(serverInfo); 32 | } 33 | 34 | var index = FindIndex(list, server.Id); 35 | 36 | if (index != -1) 37 | { 38 | var existing = list[index]; 39 | 40 | // Take the most recent DateLastAccessed 41 | if (server.DateLastAccessed > existing.DateLastAccessed) 42 | { 43 | existing.DateLastAccessed = server.DateLastAccessed; 44 | } 45 | 46 | existing.UserLinkType = server.UserLinkType; 47 | 48 | if (!string.IsNullOrEmpty(server.AccessToken)) 49 | { 50 | existing.AccessToken = server.AccessToken; 51 | existing.UserId = server.UserId; 52 | } 53 | if (!string.IsNullOrEmpty(server.ExchangeToken)) 54 | { 55 | existing.ExchangeToken = server.ExchangeToken; 56 | } 57 | if (!string.IsNullOrEmpty(server.RemoteAddress)) 58 | { 59 | existing.RemoteAddress = server.RemoteAddress; 60 | } 61 | if (!string.IsNullOrEmpty(server.ConnectServerId)) 62 | { 63 | existing.ConnectServerId = server.ConnectServerId; 64 | } 65 | if (!string.IsNullOrEmpty(server.LocalAddress)) 66 | { 67 | existing.LocalAddress = server.LocalAddress; 68 | } 69 | if (!string.IsNullOrEmpty(server.ManualAddress)) 70 | { 71 | existing.ManualAddress = server.ManualAddress; 72 | } 73 | if (!string.IsNullOrEmpty(server.Name)) 74 | { 75 | existing.Name = server.Name; 76 | } 77 | if (server.WakeOnLanInfos != null && server.WakeOnLanInfos.Count > 0) 78 | { 79 | existing.WakeOnLanInfos = new List(); 80 | foreach (WakeOnLanInfo info in server.WakeOnLanInfos) 81 | { 82 | existing.WakeOnLanInfos.Add(info); 83 | } 84 | } 85 | if (server.LastConnectionMode.HasValue) 86 | { 87 | existing.LastConnectionMode = server.LastConnectionMode; 88 | } 89 | } 90 | else 91 | { 92 | list.Add(server); 93 | } 94 | 95 | Servers = list; 96 | } 97 | 98 | private int FindIndex(List servers, string id) 99 | { 100 | var index = 0; 101 | 102 | foreach (ServerInfo server in servers) 103 | { 104 | if (StringHelper.EqualsIgnoreCase(id, server.Id)) 105 | { 106 | return index; 107 | } 108 | 109 | index++; 110 | } 111 | 112 | return -1; 113 | } 114 | 115 | public ServerInfo GetServer(string id) 116 | { 117 | foreach (ServerInfo server in Servers) 118 | { 119 | if (StringHelper.EqualsIgnoreCase(id, server.Id)) 120 | { 121 | return server; 122 | } 123 | } 124 | 125 | return null; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Emby.ApiClient/Sync/MultiServerSync.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.ApiClient; 2 | using MediaBrowser.Model.Logging; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using System.Linq; 8 | using Emby.ApiClient.Data; 9 | using Emby.ApiClient.Model; 10 | 11 | namespace Emby.ApiClient.Sync 12 | { 13 | public class MultiServerSync : IMultiServerSync 14 | { 15 | private readonly IConnectionManager _connectionManager; 16 | private readonly ILogger _logger; 17 | private readonly ILocalAssetManager _localAssetManager; 18 | private readonly IFileTransferManager _fileTransferManager; 19 | 20 | public MultiServerSync(IConnectionManager connectionManager, ILogger logger, ILocalAssetManager userActionAssetManager, IFileTransferManager fileTransferManager) 21 | { 22 | _connectionManager = connectionManager; 23 | _logger = logger; 24 | _localAssetManager = userActionAssetManager; 25 | _fileTransferManager = fileTransferManager; 26 | } 27 | 28 | public async Task Sync(IProgress progress, 29 | List cameraUploadServers, 30 | bool syncOnlyOnLocalNetwork, 31 | CancellationToken cancellationToken = default(CancellationToken)) 32 | { 33 | var servers = await _connectionManager.GetAvailableServers(cancellationToken).ConfigureAwait(false); 34 | 35 | await Sync(servers, cameraUploadServers, syncOnlyOnLocalNetwork, progress, cancellationToken).ConfigureAwait(false); 36 | } 37 | 38 | private async Task Sync(List servers, List cameraUploadServers, bool syncOnlyOnLocalNetwork, IProgress progress, CancellationToken cancellationToken = default(CancellationToken)) 39 | { 40 | var numComplete = 0; 41 | double startingPercent = 0; 42 | double percentPerServer = 1; 43 | if (servers.Count > 0) 44 | { 45 | percentPerServer /= servers.Count; 46 | } 47 | 48 | _logger.Debug("Beginning MultiServerSync with {0} servers", servers.Count); 49 | 50 | foreach (var server in servers) 51 | { 52 | cancellationToken.ThrowIfCancellationRequested(); 53 | 54 | var currentPercent = startingPercent; 55 | var serverProgress = new DoubleProgress(); 56 | serverProgress.RegisterAction(pct => 57 | { 58 | var totalProgress = pct * percentPerServer; 59 | totalProgress += currentPercent; 60 | progress.Report(totalProgress); 61 | }); 62 | 63 | // Grab the latest info from the connection manager about that server 64 | var serverInfo = await _connectionManager.GetServerInfo(server.Id).ConfigureAwait(false); 65 | 66 | if (serverInfo == null) 67 | { 68 | serverInfo = server; 69 | } 70 | 71 | if (syncOnlyOnLocalNetwork) 72 | { 73 | var result = await _connectionManager.Connect(server, new ConnectionOptions 74 | { 75 | EnableWebSocket = false, 76 | ReportCapabilities = false, 77 | UpdateDateLastAccessed = false 78 | 79 | }, cancellationToken).ConfigureAwait(false); 80 | 81 | var apiClient = result.ApiClient; 82 | 83 | var endpointInfo = await apiClient.GetEndPointInfo(cancellationToken).ConfigureAwait(false); 84 | 85 | _logger.Debug("Server: {0}, Id: {1}, IsInNetwork:{2}", server.Name, server.Id, endpointInfo.IsInNetwork); 86 | 87 | if (!endpointInfo.IsInNetwork) 88 | { 89 | continue; 90 | } 91 | } 92 | 93 | var enableCameraUpload = cameraUploadServers.Contains(serverInfo.Id, StringComparer.OrdinalIgnoreCase); 94 | 95 | await new ServerSync(_connectionManager, _logger, _localAssetManager, _fileTransferManager, _connectionManager.ClientCapabilities) 96 | .Sync(serverInfo, enableCameraUpload, serverProgress, cancellationToken).ConfigureAwait(false); 97 | 98 | numComplete++; 99 | startingPercent = numComplete; 100 | startingPercent /= servers.Count; 101 | startingPercent *= 100; 102 | progress.Report(startingPercent); 103 | } 104 | 105 | progress.Report(100); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Emby.ApiClient/Sync/ServerSync.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.ApiClient; 2 | using MediaBrowser.Model.Logging; 3 | using MediaBrowser.Model.Session; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Emby.ApiClient.Data; 9 | using Emby.ApiClient.Model; 10 | 11 | namespace Emby.ApiClient.Sync 12 | { 13 | public class ServerSync : IServerSync 14 | { 15 | private readonly IConnectionManager _connectionManager; 16 | private readonly IFileTransferManager _fileTransferManager; 17 | private readonly ILogger _logger; 18 | private readonly ILocalAssetManager _localAssetManager; 19 | 20 | private readonly ClientCapabilities _clientCapabilities; 21 | private static readonly Dictionary SemaphoreLocks = new Dictionary(StringComparer.OrdinalIgnoreCase); 22 | 23 | public ServerSync(IConnectionManager connectionManager, ILogger logger, ILocalAssetManager localAssetManager, IFileTransferManager fileTransferManager, ClientCapabilities clientCapabilities) 24 | { 25 | _connectionManager = connectionManager; 26 | _fileTransferManager = fileTransferManager; 27 | _clientCapabilities = clientCapabilities; 28 | _logger = logger; 29 | _localAssetManager = localAssetManager; 30 | } 31 | 32 | public async Task Sync(ServerInfo server, bool enableCameraUpload, IProgress progress, CancellationToken cancellationToken = default(CancellationToken)) 33 | { 34 | var semaphore = GetLock(server.Id); 35 | 36 | await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); 37 | 38 | try 39 | { 40 | await SyncInternal(server, enableCameraUpload, progress, cancellationToken).ConfigureAwait(false); 41 | } 42 | finally 43 | { 44 | semaphore.Release(); 45 | } 46 | } 47 | 48 | private static SemaphoreSlim GetLock(string serverId) 49 | { 50 | SemaphoreSlim semaphore; 51 | 52 | if (SemaphoreLocks.TryGetValue(serverId, out semaphore)) 53 | { 54 | return semaphore; 55 | } 56 | 57 | semaphore = new SemaphoreSlim(1, 1); 58 | SemaphoreLocks[serverId] = semaphore; 59 | 60 | return semaphore; 61 | } 62 | 63 | private async Task SyncInternal(ServerInfo server, bool enableCameraUpload, IProgress progress, CancellationToken cancellationToken) 64 | { 65 | _logger.Debug("Beginning ServerSync with server {0}, Id {1}", server.Name, server.Id); 66 | 67 | if (string.IsNullOrWhiteSpace(server.AccessToken) && string.IsNullOrWhiteSpace(server.ExchangeToken)) 68 | { 69 | _logger.Info("Skipping sync process for server " + server.Name + ". No server authentication information available."); 70 | progress.Report(100); 71 | return; 72 | } 73 | 74 | // Don't need these here 75 | var result = await _connectionManager.Connect(server, new ConnectionOptions 76 | { 77 | EnableWebSocket = false, 78 | ReportCapabilities = false, 79 | UpdateDateLastAccessed = false 80 | 81 | }, cancellationToken).ConfigureAwait(false); 82 | 83 | if (result.State == ConnectionState.SignedIn) 84 | { 85 | await SyncInternal(server, result.ApiClient, enableCameraUpload, progress, cancellationToken).ConfigureAwait(false); 86 | progress.Report(100); 87 | } 88 | else 89 | { 90 | _logger.Info("Skipping sync process for server " + server.Name + ". ConnectionManager returned a state of {0}", result.State.ToString()); 91 | progress.Report(100); 92 | } 93 | } 94 | 95 | private async Task SyncInternal(ServerInfo server, IApiClient apiClient, bool enableCameraUpload, IProgress progress, CancellationToken cancellationToken) 96 | { 97 | const double cameraUploadTotalPercentage = .25; 98 | 99 | var uploadProgress = new DoubleProgress(); 100 | uploadProgress.RegisterAction(p => progress.Report(p * cameraUploadTotalPercentage)); 101 | 102 | if (enableCameraUpload) 103 | { 104 | await new ContentUploader(apiClient, _logger) 105 | .UploadImages(uploadProgress, cancellationToken).ConfigureAwait(false); 106 | } 107 | 108 | var syncProgress = new DoubleProgress(); 109 | syncProgress.RegisterAction(p => progress.Report((cameraUploadTotalPercentage * 100) + (p * (1 - cameraUploadTotalPercentage)))); 110 | 111 | await new MediaSync(_localAssetManager, _logger, _fileTransferManager) 112 | .Sync(apiClient, server, uploadProgress, cancellationToken).ConfigureAwait(false); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Emby.ApiClient/NewtonsoftJsonSerializer.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Serialization; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.IO; 5 | 6 | namespace Emby.ApiClient 7 | { 8 | /// 9 | /// Class NewtonsoftJsonSerializer 10 | /// 11 | public class NewtonsoftJsonSerializer : IJsonSerializer 12 | { 13 | /// 14 | /// Serializes to stream. 15 | /// 16 | /// The obj. 17 | /// The stream. 18 | /// obj 19 | public void SerializeToStream(object obj, Stream stream) 20 | { 21 | if (obj == null) 22 | throw new ArgumentNullException("obj"); 23 | using (var jsonWriter = new JsonTextWriter(new StreamWriter(stream))) 24 | { 25 | JsonSerializer.Create(new JsonSerializerSettings()).Serialize(jsonWriter, obj); 26 | } 27 | } 28 | 29 | /// 30 | /// Deserializes from stream. 31 | /// 32 | /// The stream. 33 | /// The type. 34 | /// System.Object. 35 | public object DeserializeFromStream(Stream stream, Type type) 36 | { 37 | using (var jsonReader = new JsonTextReader(new StreamReader(stream))) 38 | { 39 | return JsonSerializer.Create(new JsonSerializerSettings()).Deserialize(jsonReader, type); 40 | } 41 | } 42 | 43 | /// 44 | /// Deserializes from stream. 45 | /// 46 | /// 47 | /// The stream. 48 | /// ``0. 49 | public T DeserializeFromStream(Stream stream) 50 | { 51 | return (T)DeserializeFromStream(stream, typeof(T)); 52 | } 53 | 54 | /// 55 | /// Deserializes from string. 56 | /// 57 | /// 58 | /// The text. 59 | /// ``0. 60 | /// 61 | public T DeserializeFromString(string text) 62 | { 63 | return JsonConvert.DeserializeObject(text); 64 | } 65 | 66 | /// 67 | /// Deserializes from string. 68 | /// 69 | /// The json. 70 | /// The type. 71 | /// System.Object. 72 | /// 73 | public object DeserializeFromString(string json, Type type) 74 | { 75 | return JsonConvert.DeserializeObject(json, type); 76 | } 77 | 78 | /// 79 | /// Serializes to string. 80 | /// 81 | /// The obj. 82 | /// System.String. 83 | public string SerializeToString(object obj) 84 | { 85 | return JsonConvert.SerializeObject(obj); 86 | } 87 | 88 | /// 89 | /// Serializes to bytes. 90 | /// 91 | /// The obj. 92 | /// 93 | public byte[] SerializeToBytes(object obj) 94 | { 95 | string serialized = SerializeToString(obj); 96 | return System.Text.Encoding.UTF8.GetBytes(serialized); 97 | } 98 | 99 | /// 100 | /// Serializes to file. 101 | /// 102 | /// The obj. 103 | /// The file. 104 | /// 105 | /// obj 106 | public void SerializeToFile(object obj, string file) 107 | { 108 | throw new NotImplementedException(); 109 | } 110 | 111 | /// 112 | /// Deserializes from file. 113 | /// 114 | /// The type. 115 | /// The file. 116 | /// System.Object. 117 | /// 118 | public object DeserializeFromFile(Type type, string file) 119 | { 120 | throw new NotImplementedException(); 121 | } 122 | 123 | /// 124 | /// Deserializes from file. 125 | /// 126 | /// 127 | /// The file. 128 | /// ``0. 129 | /// 130 | public T DeserializeFromFile(string file) where T : class 131 | { 132 | throw new NotImplementedException(); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Emby.ApiClient/Playback/IPlaybackManager.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.ApiClient; 2 | using MediaBrowser.Model.Dlna; 3 | using MediaBrowser.Model.Entities; 4 | using MediaBrowser.Model.Session; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | using Emby.ApiClient.Model; 8 | 9 | namespace Emby.ApiClient.Playback 10 | { 11 | public interface IPlaybackManager 12 | { 13 | /// 14 | /// Gets the pre playback selectable audio streams. 15 | /// 16 | /// The server identifier. 17 | /// The options. 18 | /// Task<IEnumerable<MediaStream>>. 19 | Task> GetPrePlaybackSelectableAudioStreams(string serverId, VideoOptions options); 20 | 21 | /// 22 | /// Gets the pre playback selectable subtitle streams. 23 | /// 24 | /// The server identifier. 25 | /// The options. 26 | /// Task<IEnumerable<MediaStream>>. 27 | Task> GetPrePlaybackSelectableSubtitleStreams(string serverId, VideoOptions options); 28 | 29 | /// 30 | /// Gets the in playback selectable audio streams. 31 | /// 32 | /// The information. 33 | /// IEnumerable<MediaStream>. 34 | IEnumerable GetInPlaybackSelectableAudioStreams(StreamInfo info); 35 | 36 | /// 37 | /// Gets the in playback selectable subtitle streams. 38 | /// 39 | /// The information. 40 | /// IEnumerable<MediaStream>. 41 | IEnumerable GetInPlaybackSelectableSubtitleStreams(StreamInfo info); 42 | 43 | /// 44 | /// Gets the audio stream information. 45 | /// 46 | /// The server identifier. 47 | /// The options. 48 | /// if set to true [is offline]. 49 | /// The API client. 50 | /// Task<StreamInfo>. 51 | Task GetAudioStreamInfo(string serverId, AudioOptions options, bool isOffline, IApiClient apiClient); 52 | 53 | /// 54 | /// Gets the video stream information. 55 | /// 56 | /// The server identifier. 57 | /// The options. 58 | /// if set to true [is offline]. 59 | /// The API client. 60 | /// Task<StreamInfo>. 61 | Task GetVideoStreamInfo(string serverId, VideoOptions options, bool isOffline, IApiClient apiClient); 62 | 63 | /// 64 | /// Changes the video stream. 65 | /// 66 | /// The current information. 67 | /// The server identifier. 68 | /// The options. 69 | /// The API client. 70 | /// Task<StreamInfo>. 71 | Task ChangeVideoStream(StreamInfo currentInfo, string serverId, VideoOptions options, IApiClient apiClient); 72 | 73 | /// 74 | /// Reports playback start 75 | /// 76 | /// The information. 77 | /// if set to true [is offline]. 78 | /// The current apiClient. It can be null if offline 79 | /// Task. 80 | Task ReportPlaybackStart(PlaybackStartInfo info, bool isOffline, IApiClient apiClient); 81 | 82 | /// 83 | /// Reports playback progress 84 | /// 85 | /// The information. 86 | /// The stream information. 87 | /// if set to true [is offline]. 88 | /// The current apiClient. It can be null if offline 89 | /// Task. 90 | Task ReportPlaybackProgress(PlaybackProgressInfo info, StreamInfo streamInfo, bool isOffline, IApiClient apiClient); 91 | 92 | /// 93 | /// Reports playback progress 94 | /// 95 | /// The information. 96 | /// The stream information. 97 | /// The server identifier. 98 | /// The user identifier. 99 | /// if set to true [is offline]. 100 | /// The current apiClient. It can be null if offline 101 | /// Task. 102 | Task ReportPlaybackStopped(PlaybackStopInfo info, StreamInfo streamInfo, string serverId, string userId, bool isOffline, IApiClient apiClient); 103 | } 104 | } -------------------------------------------------------------------------------- /Emby.ApiClient/Net/NetworkConnection.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.ApiClient; 2 | using MediaBrowser.Model.Logging; 3 | using System; 4 | using System.Net; 5 | using System.Net.NetworkInformation; 6 | using System.Net.Sockets; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Emby.ApiClient.Model; 10 | 11 | namespace Emby.ApiClient.Net 12 | { 13 | public class NetworkConnection : INetworkConnection 14 | { 15 | private readonly ILogger _logger; 16 | 17 | public NetworkConnection(ILogger logger) 18 | { 19 | _logger = logger; 20 | NetworkChange.NetworkAvailabilityChanged += NetworkChange_NetworkAvailabilityChanged; 21 | NetworkChange.NetworkAddressChanged += NetworkChange_NetworkAddressChanged; 22 | } 23 | 24 | private void OnNetworkChange() 25 | { 26 | NetworkChanged?.Invoke(this, EventArgs.Empty); 27 | } 28 | 29 | void NetworkChange_NetworkAddressChanged(object sender, EventArgs e) 30 | { 31 | OnNetworkChange(); 32 | } 33 | 34 | void NetworkChange_NetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e) 35 | { 36 | OnNetworkChange(); 37 | } 38 | 39 | public event EventHandler NetworkChanged; 40 | 41 | public Task SendWakeOnLan(string macAddress, string ipAddress, int port, CancellationToken cancellationToken) 42 | { 43 | return SendWakeOnLan(macAddress, new IPEndPoint(IPAddress.Parse(ipAddress), port), cancellationToken); 44 | } 45 | 46 | public Task SendWakeOnLan(string macAddress, int port, CancellationToken cancellationToken) 47 | { 48 | return SendWakeOnLan(macAddress, new IPEndPoint(IPAddress.Broadcast, port), cancellationToken); 49 | } 50 | 51 | private async Task SendWakeOnLan(string macAddress, IPEndPoint endPoint, CancellationToken cancellationToken) 52 | { 53 | const int payloadSize = 102; 54 | 55 | var macBytes = PhysicalAddress.Parse(macAddress).GetAddressBytes(); 56 | _logger.Debug(string.Format("Sending magic packet to {0}", macAddress)); 57 | 58 | // Construct magic packet 59 | var payload = new byte[payloadSize]; 60 | Buffer.BlockCopy(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, 0, payload, 0, 6); 61 | 62 | for (var i = 1; i < 17; i++) 63 | { 64 | Buffer.BlockCopy(macBytes, 0, payload, 6 * i, 6); 65 | } 66 | 67 | // Send packet LAN 68 | using (var udp = new UdpClient()) 69 | { 70 | udp.Connect(endPoint); 71 | 72 | cancellationToken.ThrowIfCancellationRequested(); 73 | 74 | await udp.SendAsync(payload, payloadSize).ConfigureAwait(false); 75 | } 76 | } 77 | 78 | public NetworkStatus GetNetworkStatus() 79 | { 80 | return new NetworkStatus 81 | { 82 | IsNetworkAvailable = IsNetworkAvailable() 83 | }; 84 | } 85 | 86 | /// 87 | /// Indicates whether any network connection is available 88 | /// Filter connections below a specified speed, as well as virtual network cards. 89 | /// 90 | /// 91 | /// true if a network connection is available; otherwise, false. 92 | /// 93 | private bool IsNetworkAvailable() 94 | { 95 | return true; 96 | //return IsNetworkAvailable(0); 97 | } 98 | 99 | /// 100 | /// Indicates whether any network connection is available. 101 | /// Filter connections below a specified speed, as well as virtual network cards. 102 | /// 103 | /// The minimum speed required. Passing 0 will not filter connection using speed. 104 | /// 105 | /// true if a network connection is available; otherwise, false. 106 | /// 107 | private bool IsNetworkAvailable(long minimumSpeed) 108 | { 109 | if (!NetworkInterface.GetIsNetworkAvailable()) 110 | return false; 111 | 112 | foreach (NetworkInterface ni in NetworkInterface.GetAllNetworkInterfaces()) 113 | { 114 | // discard because of standard reasons 115 | if ((ni.OperationalStatus != OperationalStatus.Up) || 116 | (ni.NetworkInterfaceType == NetworkInterfaceType.Loopback) || 117 | (ni.NetworkInterfaceType == NetworkInterfaceType.Tunnel)) 118 | continue; 119 | 120 | // this allow to filter modems, serial, etc. 121 | // I use 10000000 as a minimum speed for most cases 122 | if (ni.Speed < minimumSpeed) 123 | continue; 124 | 125 | // discard virtual cards (virtual box, virtual pc, etc.) 126 | if ((ni.Description.IndexOf("virtual", StringComparison.OrdinalIgnoreCase) >= 0) || 127 | (ni.Name.IndexOf("virtual", StringComparison.OrdinalIgnoreCase) >= 0)) 128 | continue; 129 | 130 | // discard "Microsoft Loopback Adapter", it will not show as NetworkInterfaceType.Loopback but as Ethernet Card. 131 | if (string.Equals(ni.Description, "Microsoft Loopback Adapter", StringComparison.OrdinalIgnoreCase)) 132 | continue; 133 | 134 | return true; 135 | } 136 | return false; 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Emby.ApiClient/Net/ServerLocator.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.ApiClient; 2 | using MediaBrowser.Model.Logging; 3 | using MediaBrowser.Model.Serialization; 4 | using System; 5 | using System.Collections.Concurrent; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Net; 9 | using System.Net.Sockets; 10 | using System.Text; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | 14 | namespace Emby.ApiClient.Net 15 | { 16 | public class ServerLocator : IServerLocator 17 | { 18 | private readonly IJsonSerializer _jsonSerializer = new NewtonsoftJsonSerializer(); 19 | private readonly ILogger _logger; 20 | 21 | public ServerLocator() 22 | : this(new NullLogger()) 23 | { 24 | } 25 | 26 | public ServerLocator(ILogger logger) 27 | { 28 | _logger = logger; 29 | } 30 | 31 | public Task> FindServers(int timeoutMs, CancellationToken cancellationToken = default(CancellationToken)) 32 | { 33 | var taskCompletionSource = new TaskCompletionSource>(); 34 | var serversFound = new ConcurrentBag(); 35 | 36 | _logger.Debug("Searching for servers with timeout of {0} ms", timeoutMs); 37 | 38 | var innerCancellationSource = new CancellationTokenSource(); 39 | var linkedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource( 40 | innerCancellationSource.Token, cancellationToken); 41 | 42 | BeginFindServers(serversFound, taskCompletionSource, innerCancellationSource); 43 | 44 | Task.Run(async () => 45 | { 46 | try 47 | { 48 | await Task.Delay(timeoutMs, linkedCancellationSource.Token).ConfigureAwait(false); 49 | taskCompletionSource.TrySetResult(serversFound.ToList()); 50 | } 51 | catch (OperationCanceledException) 52 | { 53 | 54 | } 55 | }); 56 | 57 | return taskCompletionSource.Task; 58 | } 59 | 60 | private void BeginFindServers(ConcurrentBag serversFound, TaskCompletionSource> taskCompletionSource, CancellationTokenSource cancellationTokenSource) 61 | { 62 | FindServers(serversFound.Add, exception => 63 | { 64 | taskCompletionSource.TrySetException(exception); 65 | cancellationTokenSource.Cancel(); 66 | 67 | }, cancellationTokenSource.Token); 68 | } 69 | 70 | private async void FindServers(Action serverFound, Action error, CancellationToken cancellationToken) 71 | { 72 | var serverIdsFound = new List(); 73 | 74 | // Create a udp client 75 | using (var client = new UdpClient(new IPEndPoint(IPAddress.Any, GetRandomUnusedPort()))) 76 | { 77 | // Construct the message the server is expecting 78 | var bytes = Encoding.UTF8.GetBytes("who is EmbyServer?"); 79 | 80 | // Send it - must be IPAddress.Broadcast, 7359 81 | var targetEndPoint = new IPEndPoint(IPAddress.Broadcast, 7359); 82 | 83 | try 84 | { 85 | // Send the broadcast 86 | await client.SendAsync(bytes, bytes.Length, targetEndPoint).ConfigureAwait(false); 87 | 88 | while (!cancellationToken.IsCancellationRequested) 89 | { 90 | // Get a result back 91 | var result = await client.ReceiveAsync().ConfigureAwait(false); 92 | 93 | if (result.RemoteEndPoint.Port == targetEndPoint.Port) 94 | { 95 | // Convert bytes to text 96 | var json = Encoding.UTF8.GetString(result.Buffer); 97 | 98 | _logger.Debug("Received response from endpoint: " + result.RemoteEndPoint + ". Response: " + json); 99 | 100 | if (!string.IsNullOrEmpty(json)) 101 | { 102 | try 103 | { 104 | var info = _jsonSerializer.DeserializeFromString(json); 105 | 106 | if (!serverIdsFound.Contains(info.Id)) 107 | { 108 | serverIdsFound.Add(info.Id); 109 | info.EndpointAddress = result.RemoteEndPoint.Address.ToString(); 110 | serverFound(info); 111 | } 112 | } 113 | catch (Exception ex) 114 | { 115 | _logger.ErrorException("Error parsing server discovery info", ex); 116 | } 117 | } 118 | } 119 | } 120 | } 121 | catch (Exception ex) 122 | { 123 | error(ex); 124 | } 125 | } 126 | } 127 | 128 | /// 129 | /// Gets a random port number that is currently available 130 | /// 131 | private static int GetRandomUnusedPort() 132 | { 133 | var listener = new TcpListener(IPAddress.Any, 0); 134 | listener.Start(); 135 | var port = ((IPEndPoint)listener.LocalEndpoint).Port; 136 | listener.Stop(); 137 | return port; 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Emby.ApiClient/ServerLocator.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.ApiClient; 2 | using MediaBrowser.Model.Logging; 3 | using MediaBrowser.Model.Serialization; 4 | using System; 5 | using System.Collections.Concurrent; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Net; 9 | using System.Net.Sockets; 10 | using System.Text; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | using Emby.ApiClient.Net; 14 | 15 | namespace Emby.ApiClient 16 | { 17 | public class ServerLocator : IServerLocator 18 | { 19 | private readonly IJsonSerializer _jsonSerializer = new NewtonsoftJsonSerializer(); 20 | private readonly ILogger _logger; 21 | 22 | public ServerLocator() 23 | : this(new NullLogger()) 24 | { 25 | } 26 | 27 | public ServerLocator(ILogger logger) 28 | { 29 | _logger = logger; 30 | } 31 | 32 | public Task> FindServers(int timeoutMs, CancellationToken cancellationToken = default(CancellationToken)) 33 | { 34 | var taskCompletionSource = new TaskCompletionSource>(); 35 | var serversFound = new ConcurrentBag(); 36 | 37 | _logger.Debug("Searching for servers with timeout of {0} ms", timeoutMs); 38 | 39 | var innerCancellationSource = new CancellationTokenSource(); 40 | var linkedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource( 41 | innerCancellationSource.Token, cancellationToken); 42 | 43 | BeginFindServers(serversFound, taskCompletionSource, innerCancellationSource); 44 | 45 | Task.Run(async () => 46 | { 47 | try 48 | { 49 | await Task.Delay(timeoutMs, linkedCancellationSource.Token).ConfigureAwait(false); 50 | taskCompletionSource.TrySetResult(serversFound.ToList()); 51 | } 52 | catch (OperationCanceledException) 53 | { 54 | 55 | } 56 | }); 57 | 58 | return taskCompletionSource.Task; 59 | } 60 | 61 | private void BeginFindServers(ConcurrentBag serversFound, TaskCompletionSource> taskCompletionSource, CancellationTokenSource cancellationTokenSource) 62 | { 63 | FindServers(serversFound.Add, exception => 64 | { 65 | taskCompletionSource.TrySetException(exception); 66 | cancellationTokenSource.Cancel(); 67 | 68 | }, cancellationTokenSource.Token); 69 | } 70 | 71 | private async void FindServers(Action serverFound, Action error, CancellationToken cancellationToken) 72 | { 73 | var serverIdsFound = new List(); 74 | 75 | // Create a udp client 76 | using (var client = new UdpClient(new IPEndPoint(IPAddress.Any, GetRandomUnusedPort()))) 77 | { 78 | // Construct the message the server is expecting 79 | var bytes = Encoding.UTF8.GetBytes("who is EmbyServer?"); 80 | 81 | // Send it - must be IPAddress.Broadcast, 7359 82 | var targetEndPoint = new IPEndPoint(IPAddress.Broadcast, 7359); 83 | 84 | try 85 | { 86 | // Send the broadcast 87 | await client.SendAsync(bytes, bytes.Length, targetEndPoint).ConfigureAwait(false); 88 | 89 | while (!cancellationToken.IsCancellationRequested) 90 | { 91 | // Get a result back 92 | var result = await client.ReceiveAsync().ConfigureAwait(false); 93 | 94 | if (result.RemoteEndPoint.Port == targetEndPoint.Port) 95 | { 96 | // Convert bytes to text 97 | var json = Encoding.UTF8.GetString(result.Buffer); 98 | 99 | _logger.Debug("Received response from endpoint: " + result.RemoteEndPoint + ". Response: " + json); 100 | 101 | if (!string.IsNullOrEmpty(json)) 102 | { 103 | try 104 | { 105 | var info = _jsonSerializer.DeserializeFromString(json); 106 | 107 | if (!serverIdsFound.Contains(info.Id)) 108 | { 109 | serverIdsFound.Add(info.Id); 110 | info.EndpointAddress = result.RemoteEndPoint.Address.ToString(); 111 | serverFound(info); 112 | } 113 | } 114 | catch (Exception ex) 115 | { 116 | _logger.ErrorException("Error parsing server discovery info", ex); 117 | } 118 | } 119 | } 120 | } 121 | } 122 | catch (Exception ex) 123 | { 124 | error(ex); 125 | } 126 | } 127 | } 128 | 129 | /// 130 | /// Gets a random port number that is currently available 131 | /// 132 | private static int GetRandomUnusedPort() 133 | { 134 | var listener = new TcpListener(IPAddress.Any, 0); 135 | listener.Start(); 136 | var port = ((IPEndPoint)listener.LocalEndpoint).Port; 137 | listener.Stop(); 138 | return port; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Emby.ApiClient/WebSocket/WebSocket4NetClientWebSocket.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.ApiClient; 2 | using MediaBrowser.Model.Logging; 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Emby.ApiClient.Model; 7 | using WebSocket4Net; 8 | 9 | namespace Emby.ApiClient.WebSocket 10 | { 11 | /// 12 | /// Class WebSocket4NetClientWebSocket 13 | /// 14 | public class WebSocket4NetClientWebSocket : IClientWebSocket 15 | { 16 | private readonly ILogger _logger; 17 | 18 | /// 19 | /// The _socket 20 | /// 21 | private WebSocket4Net.WebSocket _socket; 22 | 23 | public WebSocket4NetClientWebSocket(ILogger logger) 24 | { 25 | _logger = logger; 26 | } 27 | 28 | /// 29 | /// Gets or sets the state. 30 | /// 31 | /// The state. 32 | public MediaBrowser.Model.Net.WebSocketState State 33 | { 34 | get 35 | { 36 | 37 | switch (_socket.State) 38 | { 39 | case WebSocketState.Closed: 40 | return MediaBrowser.Model.Net.WebSocketState.Closed; 41 | case WebSocketState.Closing: 42 | return MediaBrowser.Model.Net.WebSocketState.Closed; 43 | case WebSocketState.Connecting: 44 | return MediaBrowser.Model.Net.WebSocketState.Connecting; 45 | case WebSocketState.None: 46 | return MediaBrowser.Model.Net.WebSocketState.None; 47 | case WebSocketState.Open: 48 | return MediaBrowser.Model.Net.WebSocketState.Open; 49 | default: 50 | return MediaBrowser.Model.Net.WebSocketState.None; 51 | } 52 | } 53 | } 54 | 55 | /// 56 | /// Connects the async. 57 | /// 58 | /// The URL. 59 | /// The cancellation token. 60 | /// Task. 61 | public Task ConnectAsync(string url, CancellationToken cancellationToken = default(CancellationToken)) 62 | { 63 | var taskCompletionSource = new TaskCompletionSource(); 64 | 65 | try 66 | { 67 | _socket = new WebSocket4Net.WebSocket(url); 68 | 69 | _socket.MessageReceived += websocket_MessageReceived; 70 | 71 | _socket.Open(); 72 | 73 | _socket.Opened += (sender, args) => taskCompletionSource.TrySetResult(true); 74 | _socket.Closed += _socket_Closed; 75 | } 76 | catch (Exception ex) 77 | { 78 | _socket = null; 79 | 80 | taskCompletionSource.TrySetException(ex); 81 | } 82 | 83 | return taskCompletionSource.Task; 84 | } 85 | 86 | /// 87 | /// Handles the WebSocketClosed event of the _socket control. 88 | /// 89 | /// The source of the event. 90 | /// The instance containing the event data. 91 | void _socket_Closed(object sender, EventArgs e) 92 | { 93 | if (Closed != null) 94 | { 95 | Closed(this, EventArgs.Empty); 96 | } 97 | } 98 | 99 | /// 100 | /// Handles the MessageReceived event of the websocket control. 101 | /// 102 | /// The source of the event. 103 | /// The instance containing the event data. 104 | void websocket_MessageReceived(object sender, MessageReceivedEventArgs e) 105 | { 106 | if (OnReceive != null) 107 | { 108 | OnReceive(e.Message); 109 | } 110 | } 111 | 112 | /// 113 | /// Gets or sets the receive action. 114 | /// 115 | /// The receive action. 116 | public Action OnReceiveBytes { get; set; } 117 | 118 | /// 119 | /// Gets or sets the on receive. 120 | /// 121 | /// The on receive. 122 | public Action OnReceive { get; set; } 123 | 124 | /// 125 | /// Sends the async. 126 | /// 127 | /// The bytes. 128 | /// The type. 129 | /// if set to true [end of message]. 130 | /// The cancellation token. 131 | /// Task. 132 | public Task SendAsync(byte[] bytes, MediaBrowser.Model.Net.WebSocketMessageType type, bool endOfMessage, CancellationToken cancellationToken = default(CancellationToken)) 133 | { 134 | return Task.Run(() => _socket.Send(bytes, 0, bytes.Length), cancellationToken); 135 | } 136 | 137 | /// 138 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 139 | /// 140 | public void Dispose() 141 | { 142 | if (_socket != null) 143 | { 144 | var state = State; 145 | 146 | if (state == MediaBrowser.Model.Net.WebSocketState.Open || state == MediaBrowser.Model.Net.WebSocketState.Connecting) 147 | { 148 | _logger.Info("Sending web socket close message"); 149 | 150 | _socket.Close(); 151 | } 152 | 153 | _socket = null; 154 | } 155 | } 156 | 157 | /// 158 | /// Occurs when [closed]. 159 | /// 160 | public event EventHandler Closed; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Emby.ApiClient/Model/IServerEvents.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Dto; 2 | using MediaBrowser.Model.Entities; 3 | using MediaBrowser.Model.Events; 4 | using MediaBrowser.Model.Plugins; 5 | using MediaBrowser.Model.Session; 6 | using MediaBrowser.Model.Sync; 7 | using MediaBrowser.Model.Tasks; 8 | using MediaBrowser.Model.Updates; 9 | using System; 10 | using System.Collections.Generic; 11 | using MediaBrowser.Model.ApiClient; 12 | 13 | namespace Emby.ApiClient.Model 14 | { 15 | /// 16 | /// Interface IServerEvents 17 | /// 18 | public interface IServerEvents 19 | { 20 | /// 21 | /// Occurs when [user deleted]. 22 | /// 23 | event EventHandler> UserDeleted; 24 | /// 25 | /// Occurs when [scheduled task ended]. 26 | /// 27 | event EventHandler> ScheduledTaskEnded; 28 | /// 29 | /// Occurs when [package installing]. 30 | /// 31 | event EventHandler> PackageInstalling; 32 | /// 33 | /// Occurs when [package installation failed]. 34 | /// 35 | event EventHandler> PackageInstallationFailed; 36 | /// 37 | /// Occurs when [package installation completed]. 38 | /// 39 | event EventHandler> PackageInstallationCompleted; 40 | /// 41 | /// Occurs when [package installation cancelled]. 42 | /// 43 | event EventHandler> PackageInstallationCancelled; 44 | /// 45 | /// Occurs when [user updated]. 46 | /// 47 | event EventHandler> UserUpdated; 48 | /// 49 | /// Occurs when [plugin uninstalled]. 50 | /// 51 | event EventHandler> PluginUninstalled; 52 | /// 53 | /// Occurs when [library changed]. 54 | /// 55 | event EventHandler> LibraryChanged; 56 | /// 57 | /// Occurs when [browse command]. 58 | /// 59 | event EventHandler> BrowseCommand; 60 | /// 61 | /// Occurs when [play command]. 62 | /// 63 | event EventHandler> PlayCommand; 64 | /// 65 | /// Occurs when [playstate command]. 66 | /// 67 | event EventHandler> PlaystateCommand; 68 | /// 69 | /// Occurs when [message command]. 70 | /// 71 | event EventHandler> MessageCommand; 72 | /// 73 | /// Occurs when [system command]. 74 | /// 75 | event EventHandler> GeneralCommand; 76 | /// 77 | /// Occurs when [notification added]. 78 | /// 79 | event EventHandler NotificationAdded; 80 | /// 81 | /// Occurs when [notification updated]. 82 | /// 83 | event EventHandler NotificationUpdated; 84 | /// 85 | /// Occurs when [notifications marked read]. 86 | /// 87 | event EventHandler NotificationsMarkedRead; 88 | /// 89 | /// Occurs when [server restarting]. 90 | /// 91 | event EventHandler ServerRestarting; 92 | /// 93 | /// Occurs when [server shutting down]. 94 | /// 95 | event EventHandler ServerShuttingDown; 96 | /// 97 | /// Occurs when [send text command]. 98 | /// 99 | event EventHandler> SendStringCommand; 100 | /// 101 | /// Occurs when [set volume command]. 102 | /// 103 | event EventHandler> SetVolumeCommand; 104 | /// 105 | /// Occurs when [set audio stream index command]. 106 | /// 107 | event EventHandler> SetAudioStreamIndexCommand; 108 | /// 109 | /// Occurs when [set video stream index command]. 110 | /// 111 | event EventHandler> SetSubtitleStreamIndexCommand; 112 | /// 113 | /// Occurs when [sessions updated]. 114 | /// 115 | event EventHandler> SessionsUpdated; 116 | /// 117 | /// Occurs when [restart required]. 118 | /// 119 | event EventHandler RestartRequired; 120 | /// 121 | /// Occurs when [user data changed]. 122 | /// 123 | event EventHandler> UserDataChanged; 124 | /// 125 | /// Occurs when [playback start]. 126 | /// 127 | event EventHandler> PlaybackStart; 128 | /// 129 | /// Occurs when [playback stopped]. 130 | /// 131 | event EventHandler> PlaybackStopped; 132 | /// 133 | /// Occurs when [session ended]. 134 | /// 135 | event EventHandler> SessionEnded; 136 | /// 137 | /// Occurs when [synchronize job created]. 138 | /// 139 | event EventHandler> SyncJobCreated; 140 | /// 141 | /// Occurs when [synchronize job cancelled]. 142 | /// 143 | event EventHandler> SyncJobCancelled; 144 | /// 145 | /// Occurs when [synchronize jobs updated]. 146 | /// 147 | event EventHandler>> SyncJobsUpdated; 148 | /// 149 | /// Occurs when [synchronize job updated]. 150 | /// 151 | event EventHandler> SyncJobUpdated; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Emby.ApiClient/Model/IConnectionManager.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Connect; 2 | using MediaBrowser.Model.Dto; 3 | using MediaBrowser.Model.Events; 4 | using MediaBrowser.Model.Session; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using MediaBrowser.Model.ApiClient; 10 | 11 | namespace Emby.ApiClient.Model 12 | { 13 | public interface IConnectionManager 14 | { 15 | /// 16 | /// Occurs when [connected]. 17 | /// 18 | event EventHandler> Connected; 19 | /// 20 | /// Occurs when [local user sign in]. 21 | /// 22 | event EventHandler> LocalUserSignIn; 23 | /// 24 | /// Occurs when [connect user sign in]. 25 | /// 26 | event EventHandler> ConnectUserSignIn; 27 | /// 28 | /// Occurs when [local user sign out]. 29 | /// 30 | event EventHandler> LocalUserSignOut; 31 | /// 32 | /// Occurs when [connect user sign out]. 33 | /// 34 | event EventHandler ConnectUserSignOut; 35 | /// 36 | /// Occurs when [remote logged out]. 37 | /// 38 | event EventHandler RemoteLoggedOut; 39 | 40 | /// 41 | /// Gets the device. 42 | /// 43 | /// The device. 44 | IDevice Device { get; } 45 | 46 | /// 47 | /// Gets the connect user. 48 | /// 49 | /// The connect user. 50 | ConnectUser ConnectUser { get; } 51 | 52 | /// 53 | /// Gets or sets a value indicating whether [save local credentials]. 54 | /// 55 | /// true if [save local credentials]; otherwise, false. 56 | bool SaveLocalCredentials { get; set; } 57 | 58 | /// 59 | /// Gets the client capabilities. 60 | /// 61 | /// The client capabilities. 62 | ClientCapabilities ClientCapabilities { get; } 63 | 64 | /// 65 | /// Gets the API client. 66 | /// 67 | /// The item. 68 | /// IApiClient. 69 | IApiClient GetApiClient(IHasServerId item); 70 | 71 | /// 72 | /// Gets the API client. 73 | /// 74 | /// The server identifier. 75 | /// IApiClient. 76 | IApiClient GetApiClient(string serverId); 77 | 78 | /// 79 | /// Connects the specified cancellation token. 80 | /// 81 | /// The cancellation token. 82 | /// Task<ConnectionResult>. 83 | Task Connect(CancellationToken cancellationToken = default(CancellationToken)); 84 | 85 | /// 86 | /// Connects the specified API client. 87 | /// 88 | /// The API client. 89 | /// The cancellation token. 90 | /// Task<ConnectionResult>. 91 | Task Connect(IApiClient apiClient, CancellationToken cancellationToken = default(CancellationToken)); 92 | 93 | /// 94 | /// Connects the specified server. 95 | /// 96 | /// The server. 97 | /// The cancellation token. 98 | /// Task<ConnectionResult>. 99 | Task Connect(ServerInfo server, CancellationToken cancellationToken = default(CancellationToken)); 100 | 101 | /// 102 | /// Connects the specified server. 103 | /// 104 | /// The server. 105 | /// The options. 106 | /// The cancellation token. 107 | /// Task<ConnectionResult>. 108 | Task Connect(ServerInfo server, ConnectionOptions options, CancellationToken cancellationToken = default(CancellationToken)); 109 | 110 | /// 111 | /// Connects the specified server. 112 | /// 113 | /// The address. 114 | /// The cancellation token. 115 | /// Task<ConnectionResult>. 116 | Task Connect(string address, CancellationToken cancellationToken = default(CancellationToken)); 117 | 118 | /// 119 | /// Logouts this instance. 120 | /// 121 | /// Task<ConnectionResult>. 122 | Task Logout(); 123 | 124 | /// 125 | /// Logins to connect. 126 | /// 127 | /// Task. 128 | Task LoginToConnect(string username, string password); 129 | 130 | /// 131 | /// Gets the active api client instance 132 | /// 133 | IApiClient CurrentApiClient { get; } 134 | 135 | /// 136 | /// Creates the pin. 137 | /// 138 | /// Task<PinCreationResult>. 139 | Task CreatePin(); 140 | 141 | /// 142 | /// Gets the pin status. 143 | /// 144 | /// The pin. 145 | /// Task<PinStatusResult>. 146 | Task GetPinStatus(PinCreationResult pin); 147 | 148 | /// 149 | /// Exchanges the pin. 150 | /// 151 | /// The pin. 152 | /// Task. 153 | Task ExchangePin(PinCreationResult pin); 154 | 155 | /// 156 | /// Gets the server information. 157 | /// 158 | /// The identifier. 159 | /// Task<ServerInfo>. 160 | Task GetServerInfo(string id); 161 | 162 | /// 163 | /// Gets the available servers. 164 | /// 165 | /// The cancellation token. 166 | Task> GetAvailableServers(CancellationToken cancellationToken = default(CancellationToken)); 167 | 168 | /// 169 | /// Signups for connect. 170 | /// 171 | /// The email. 172 | /// The username. 173 | /// The password. 174 | /// The cancellation token. 175 | /// Task. 176 | Task SignupForConnect(string email, string username, string password, CancellationToken cancellationToken = default(CancellationToken)); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Emby.ApiClient/WebSocket/NativeClientWebSocket.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.ApiClient; 2 | using MediaBrowser.Model.Logging; 3 | using System; 4 | using System.Net.WebSockets; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Emby.ApiClient.Model; 8 | 9 | namespace Emby.ApiClient.WebSocket 10 | { 11 | /// 12 | /// Class NativeClientWebSocket 13 | /// 14 | public class NativeClientWebSocket : IClientWebSocket 15 | { 16 | /// 17 | /// Occurs when [closed]. 18 | /// 19 | public event EventHandler Closed; 20 | 21 | /// 22 | /// The _client 23 | /// 24 | private readonly ClientWebSocket _client; 25 | /// 26 | /// The _logger 27 | /// 28 | private readonly ILogger _logger; 29 | 30 | /// 31 | /// The _send resource 32 | /// 33 | private readonly SemaphoreSlim _sendResource = new SemaphoreSlim(1, 1); 34 | 35 | private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); 36 | 37 | /// 38 | /// Initializes a new instance of the class. 39 | /// 40 | /// The logger. 41 | public NativeClientWebSocket(ILogger logger) 42 | { 43 | _logger = logger; 44 | _client = new ClientWebSocket(); 45 | } 46 | 47 | /// 48 | /// Gets or sets the receive action. 49 | /// 50 | /// The receive action. 51 | public Action OnReceiveBytes { get; set; } 52 | 53 | /// 54 | /// Gets or sets the on receive. 55 | /// 56 | /// The on receive. 57 | public Action OnReceive { get; set; } 58 | 59 | /// 60 | /// Connects the async. 61 | /// 62 | /// The URL. 63 | /// The cancellation token. 64 | /// Task. 65 | public async Task ConnectAsync(string url, CancellationToken cancellationToken = default(CancellationToken)) 66 | { 67 | try 68 | { 69 | await _client.ConnectAsync(new Uri(url), cancellationToken).ConfigureAwait(false); 70 | } 71 | catch (Exception ex) 72 | { 73 | _logger.ErrorException("Error connecting to {0}", ex, url); 74 | 75 | throw; 76 | } 77 | 78 | Receive(); 79 | } 80 | 81 | /// 82 | /// Receives this instance. 83 | /// 84 | private async void Receive() 85 | { 86 | while (true) 87 | { 88 | byte[] bytes; 89 | 90 | try 91 | { 92 | bytes = await ReceiveBytesAsync(_cancellationTokenSource.Token).ConfigureAwait(false); 93 | } 94 | catch (OperationCanceledException) 95 | { 96 | OnClosed(); 97 | break; 98 | } 99 | catch (Exception ex) 100 | { 101 | _logger.ErrorException("Error receiving web socket message", ex); 102 | 103 | break; 104 | } 105 | 106 | // Connection closed 107 | if (bytes == null) 108 | { 109 | break; 110 | } 111 | 112 | if (OnReceiveBytes != null) 113 | { 114 | OnReceiveBytes(bytes); 115 | } 116 | } 117 | } 118 | 119 | /// 120 | /// Receives the async. 121 | /// 122 | /// The cancellation token. 123 | /// Task{WebSocketMessageInfo}. 124 | /// Connection closed 125 | private async Task ReceiveBytesAsync(CancellationToken cancellationToken) 126 | { 127 | var bytes = new byte[4096]; 128 | var buffer = new ArraySegment(bytes); 129 | 130 | var result = await _client.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false); 131 | 132 | if (result.CloseStatus.HasValue) 133 | { 134 | OnClosed(); 135 | return null; 136 | } 137 | 138 | return buffer.Array; 139 | } 140 | 141 | /// 142 | /// Sends the async. 143 | /// 144 | /// The bytes. 145 | /// The type. 146 | /// if set to true [end of message]. 147 | /// The cancellation token. 148 | /// Task. 149 | public async Task SendAsync(byte[] bytes, MediaBrowser.Model.Net.WebSocketMessageType type, bool endOfMessage, CancellationToken cancellationToken = default(CancellationToken)) 150 | { 151 | await _sendResource.WaitAsync(cancellationToken).ConfigureAwait(false); 152 | 153 | try 154 | { 155 | WebSocketMessageType nativeType; 156 | 157 | if (!Enum.TryParse(type.ToString(), true, out nativeType)) 158 | { 159 | _logger.Warn("Unrecognized WebSocketMessageType: {0}", type.ToString()); 160 | } 161 | 162 | var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token); 163 | 164 | await _client.SendAsync(new ArraySegment(bytes), nativeType, true, linkedTokenSource.Token).ConfigureAwait(false); 165 | } 166 | finally 167 | { 168 | _sendResource.Release(); 169 | } 170 | } 171 | 172 | /// 173 | /// Gets or sets the state. 174 | /// 175 | /// The state. 176 | public MediaBrowser.Model.Net.WebSocketState State 177 | { 178 | get 179 | { 180 | if (_client == null) 181 | { 182 | return MediaBrowser.Model.Net.WebSocketState.None; 183 | } 184 | 185 | MediaBrowser.Model.Net.WebSocketState commonState; 186 | 187 | if (!Enum.TryParse(_client.State.ToString(), true, out commonState)) 188 | { 189 | _logger.Warn("Unrecognized WebSocketState: {0}", _client.State.ToString()); 190 | } 191 | 192 | return commonState; 193 | } 194 | } 195 | 196 | /// 197 | /// Called when [closed]. 198 | /// 199 | void OnClosed() 200 | { 201 | if (Closed != null) 202 | { 203 | Closed(this, EventArgs.Empty); 204 | } 205 | } 206 | 207 | /// 208 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 209 | /// 210 | public void Dispose() 211 | { 212 | Dispose(true); 213 | } 214 | 215 | /// 216 | /// Releases unmanaged and - optionally - managed resources. 217 | /// 218 | /// true to release both managed and unmanaged resources; false to release only unmanaged resources. 219 | protected virtual void Dispose(bool dispose) 220 | { 221 | if (dispose) 222 | { 223 | _cancellationTokenSource.Cancel(); 224 | 225 | if (_client != null) 226 | { 227 | if (_client.State == WebSocketState.Open) 228 | { 229 | _logger.Info("Sending web socket close message."); 230 | 231 | // Can't wait on this. Try to close gracefully though. 232 | _client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); 233 | } 234 | _client.Dispose(); 235 | } 236 | } 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /Emby.ApiClient/QueryStringDictionary.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | 6 | namespace Emby.ApiClient 7 | { 8 | /// 9 | /// Class QueryStringDictionary 10 | /// 11 | public class QueryStringDictionary : Dictionary 12 | { 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// 16 | public QueryStringDictionary() 17 | : base(StringComparer.OrdinalIgnoreCase) 18 | { 19 | } 20 | 21 | /// 22 | /// Adds the specified name. 23 | /// 24 | /// The name. 25 | /// The value. 26 | public void Add(string name, int value) 27 | { 28 | Add(name, value.ToString(CultureInfo.InvariantCulture)); 29 | } 30 | 31 | /// 32 | /// Adds the specified name. 33 | /// 34 | /// The name. 35 | /// The value. 36 | public void Add(string name, long value) 37 | { 38 | Add(name, value.ToString(CultureInfo.InvariantCulture)); 39 | } 40 | 41 | /// 42 | /// Adds the specified name. 43 | /// 44 | /// The name. 45 | /// The value. 46 | public void Add(string name, double value) 47 | { 48 | Add(name, value.ToString(CultureInfo.InvariantCulture)); 49 | } 50 | 51 | /// 52 | /// Adds if not null or empty. 53 | /// 54 | /// The name. 55 | /// The value. 56 | public void AddIfNotNullOrEmpty(string name, string value) 57 | { 58 | if (!string.IsNullOrEmpty(value)) 59 | { 60 | Add(name, value); 61 | } 62 | } 63 | 64 | /// 65 | /// Adds if not null. 66 | /// 67 | /// The name. 68 | /// The value. 69 | public void AddIfNotNull(string name, int? value) 70 | { 71 | if (value.HasValue) 72 | { 73 | Add(name, value.Value); 74 | } 75 | } 76 | 77 | /// 78 | /// Adds if not null. 79 | /// 80 | /// The name. 81 | /// The value. 82 | public void AddIfNotNull(string name, double? value) 83 | { 84 | if (value.HasValue) 85 | { 86 | Add(name, value.Value); 87 | } 88 | } 89 | 90 | /// 91 | /// Adds if not null. 92 | /// 93 | /// The name. 94 | /// The value. 95 | public void AddIfNotNull(string name, long? value) 96 | { 97 | if (value.HasValue) 98 | { 99 | Add(name, value.Value); 100 | } 101 | } 102 | 103 | /// 104 | /// Adds the specified name. 105 | /// 106 | /// The name. 107 | /// if set to true [value]. 108 | public void Add(string name, bool value) 109 | { 110 | Add(name, value.ToString()); 111 | } 112 | 113 | /// 114 | /// Adds if not null. 115 | /// 116 | /// The name. 117 | /// if set to true [value]. 118 | public void AddIfNotNull(string name, bool? value) 119 | { 120 | if (value.HasValue) 121 | { 122 | Add(name, value.Value); 123 | } 124 | } 125 | 126 | /// 127 | /// Adds the specified name. 128 | /// 129 | /// The name. 130 | /// The value. 131 | /// value 132 | public void Add(string name, IEnumerable value) 133 | { 134 | if (value == null) 135 | { 136 | throw new ArgumentNullException("value"); 137 | } 138 | 139 | Add(name, string.Join(",", value.Select(v => v.ToString(CultureInfo.InvariantCulture)).ToArray())); 140 | } 141 | 142 | /// 143 | /// Adds if not null. 144 | /// 145 | /// The name. 146 | /// The value. 147 | public void AddIfNotNull(string name, IEnumerable value) 148 | { 149 | if (value != null) 150 | { 151 | Add(name, value); 152 | } 153 | } 154 | 155 | /// 156 | /// Adds the specified name. 157 | /// 158 | /// The name. 159 | /// The value. 160 | /// value 161 | public void Add(string name, IEnumerable value) 162 | { 163 | if (value == null) 164 | { 165 | throw new ArgumentNullException("value"); 166 | } 167 | 168 | string paramValue = string.Join(",", value.ToArray()); 169 | 170 | Add(name, paramValue); 171 | } 172 | 173 | /// 174 | /// Adds if not null. 175 | /// 176 | /// The name. 177 | /// The value. 178 | public void AddIfNotNull(string name, IEnumerable value) 179 | { 180 | if (value != null) 181 | { 182 | Add(name, value); 183 | } 184 | } 185 | 186 | /// 187 | /// Adds the specified name. 188 | /// 189 | /// The name. 190 | /// The value. 191 | /// The delimiter. 192 | /// value 193 | public void Add(string name, IEnumerable value, string delimiter) 194 | { 195 | if (value == null) 196 | { 197 | throw new ArgumentNullException("value"); 198 | } 199 | 200 | string paramValue = string.Join(delimiter, value.ToArray()); 201 | 202 | Add(name, paramValue); 203 | } 204 | 205 | /// 206 | /// Adds if not null. 207 | /// 208 | /// The name. 209 | /// The value. 210 | /// The delimiter. 211 | public void AddIfNotNull(string name, IEnumerable value, string delimiter) 212 | { 213 | if (value != null) 214 | { 215 | Add(name, value, delimiter); 216 | } 217 | } 218 | 219 | /// 220 | /// Gets the query string. 221 | /// 222 | /// System.String. 223 | public string GetQueryString() 224 | { 225 | string[] queryParams = this.Select(i => string.Format("{0}={1}", i.Key, GetEncodedValue(i.Value))).ToArray(); 226 | 227 | return string.Join("&", queryParams); 228 | } 229 | 230 | /// 231 | /// Gets the encoded value. 232 | /// 233 | /// The value. 234 | /// System.String. 235 | private string GetEncodedValue(string value) 236 | { 237 | return value; 238 | } 239 | 240 | /// 241 | /// Gets the URL. 242 | /// 243 | /// The prefix. 244 | /// System.String. 245 | public string GetUrl(string prefix) 246 | { 247 | string query = GetQueryString(); 248 | 249 | if (string.IsNullOrEmpty(query)) 250 | { 251 | return prefix; 252 | } 253 | 254 | return prefix + "?" + query; 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > ### This repository is outdated and archived! 3 | > You can find up-to-date client libraries in the new [Emby.ApiClient Repository](https://github.com/MediaBrowser/Emby.ApiClients/tree/master) 4 | 5 | --- 6 | 7 | Emby.ApiClient 8 | ====================== 9 | 10 | This portable class library makes it very easy to harness the power of the Embyr API. This is available as a Nuget package: 11 | 12 | [Emby.ApiClient](https://www.nuget.org/packages/MediaBrowser.ApiClient/) 13 | 14 | # Single Server Example # 15 | 16 | This is an example of connecting to a single server using a fixed, predictable address, from an app that has user-specific features. 17 | 18 | ``` c# 19 | 20 | // Developers are encouraged to create their own ILogger implementation 21 | var logger = new NullLogger(); 22 | 23 | // This describes the device capabilities 24 | var capabilities = new ClientCapabilities(); 25 | 26 | // If using the portable class library you'll need to supply your own IDevice implementation. 27 | var device = new Device 28 | { 29 | DeviceName = "My Device Name", 30 | DeviceId = "My Device Id" 31 | }; 32 | 33 | // If using the portable class library you'll need to supply your own ICryptographyProvider implementation. 34 | var cryptoProvider = new CryptographyProvider(); 35 | 36 | var client = new ApiClient(logger, "http://localhost:8096", "My client name", device, cryptoProvider); 37 | 38 | var authResult = await AuthenticateUserAsync("username", passwordHash); 39 | 40 | // Report capabilities after authentication 41 | await ApiClient.ReportCapabilities(capabilities); 42 | 43 | // RemoteLoggedOut indicates the user was logged out remotely by the server 44 | ApiClient.RemoteLoggedOut += ApiClient_RemoteLoggedOut; 45 | 46 | // Get the ten most recently added items for the current user 47 | var items = await client.GetItemsAsync(new ItemQuery 48 | { 49 | UserId = client.UserId, 50 | 51 | SortBy = new[] { ItemSortBy.DateCreated }, 52 | SortOrder = SortOrder.Descending, 53 | 54 | // Get media only, don't return folder items 55 | Filters = new[] { ItemFilter.IsNotFolder }, 56 | 57 | Limit = 10, 58 | 59 | // Search recursively through the user's library 60 | Recursive = true 61 | }); 62 | 63 | await client.Logout(); 64 | ``` 65 | 66 | # Service Apps # 67 | 68 | If your app is some kind of service or utility (e.g. Sickbeard), you should construct ApiClient with your api key. 69 | 70 | ``` c# 71 | 72 | // Developers are encouraged to create their own ILogger implementation 73 | var logger = new NullLogger(); 74 | 75 | // If using the portable class library you'll need to supply your own ICryptographyProvider implementation. 76 | var cryptoProvider = new CryptographyProvider(); 77 | 78 | // This describes the device capabilities 79 | var capabilities = new ClientCapabilities(); 80 | 81 | var client = new ApiClient(logger, "http://localhost:8096", "0123456789", capabilities, cryptoProvider); 82 | 83 | // Report capabilities 84 | await ApiClient.ReportCapabilities(capabilities); 85 | 86 | // RemoteLoggedOut indicates the access token was revoked remotely by the server 87 | ApiClient.RemoteLoggedOut += ApiClient_RemoteLoggedOut; 88 | 89 | // Get the ten most recently added items for the current user 90 | var items = await client.GetItemsAsync(new ItemQuery 91 | { 92 | SortBy = new[] { ItemSortBy.DateCreated }, 93 | SortOrder = SortOrder.Descending, 94 | 95 | // Get media only, don't return folder items 96 | Filters = new[] { ItemFilter.IsNotFolder }, 97 | 98 | Limit = 10, 99 | 100 | // Search recursively through the user's library 101 | Recursive = true 102 | }); 103 | ``` 104 | 105 | # Web Socket # 106 | 107 | Once you have an ApiClient instance, you can easily connect to the server's web socket using: 108 | 109 | ``` c# 110 | 111 | ApiClient.OpenWebSocket(); 112 | ``` 113 | 114 | This will open a connection in a background thread, and periodically check to ensure it's still connected. The web socket provides various events that can be used to receive notifications from the server: 115 | 116 | 117 | ``` c# 118 | 119 | ApiClient.UserUpdated += webSocket_UserUpdated; 120 | ``` 121 | 122 | # Multi-Server Usage # 123 | 124 | 125 | The above examples are designed for cases when your app always connects to a single server, and you always know the address. An example is an app that will always run within a local network and only connect to one server at a time. If your app is designed to support multiple networks and/or multiple servers, then **IConnectionManager** should be used in place of the above example. 126 | 127 | 128 | ``` c# 129 | 130 | // Developers are encouraged to create their own ILogger implementation 131 | var logger = new NullLogger(); 132 | 133 | // This describes the device capabilities 134 | var capabilities = new ClientCapabilities(); 135 | 136 | // If using the portable class library you'll need to supply your own IDevice implementation. 137 | var device = new Device 138 | { 139 | DeviceName = "My Device Name", 140 | DeviceId = "My Device Id" 141 | }; 142 | 143 | // Developers will have to implement ICredentialProvider to provide storage for saving server information 144 | var credentialProvider = new CredentialProvider(); 145 | 146 | // If using the portable class library you'll need to supply your own INetworkConnection implementation. 147 | var networkConnection = new NetworkConnection(logger); 148 | 149 | // If using the portable class library you'll need to supply your own ICryptographyProvider implementation. 150 | var cryptoProvider = new CryptographyProvider(); 151 | 152 | // If using the portable class library you'll need to supply your own IServerLocator implementation. 153 | var serverLocator = new ServerLocator(logger); 154 | 155 | var connectionManager = new ConnectionManager(logger, 156 | credentialProvider, 157 | networkConnection, 158 | serverLocator, 159 | "My App Name", 160 | // Application version 161 | "1.0.0.0", 162 | device, 163 | capabilities, 164 | cryptoProvider, 165 | ClientWebSocketFactory.CreateWebSocket); 166 | ``` 167 | 168 | # Multi-Server Startup Workflow # 169 | 170 | After you've created your instance of IConnectionManager, simply call the Connect method. It will return a result object with three properties: 171 | 172 | - State 173 | - Servers 174 | - ApiClient 175 | 176 | ServerInfo and ApiClient will be null if State == Unavailable. If State==SignedIn or State==ServerSignIn, the Servers list will always have one single entry. Let's look at an example. 177 | 178 | 179 | ``` c# 180 | 181 | var result = await connectionManager.Connect(cancellationToken); 182 | 183 | switch (result.State) 184 | { 185 | case ConnectionState.ConnectSignIn: 186 | // Connect sign in screen should be presented 187 | // Authenticate using LoginToConnect, then call Connect again to start over 188 | 189 | case ConnectionState.ServerSignIn: 190 | // A server was found and the user needs to login. 191 | // Display a login screen and authenticate with the server using result.ApiClient 192 | 193 | case ConnectionState.ServerSelection: 194 | // Multiple servers available 195 | // Display a selection screen using result.Servers 196 | // When a server is chosen, call the Connect overload that accept either a ServerInfo object or a String url. 197 | 198 | case ConnectionState.SignedIn: 199 | // A server was found and the user has been signed in using previously saved credentials. 200 | // Ready to browse using result.ApiClient 201 | } 202 | 203 | ``` 204 | 205 | If the user wishes to connect to a new server, simply use the Connect overload that accepts an address. 206 | 207 | 208 | ``` c# 209 | 210 | var address = "http://localhost:8096"; 211 | 212 | var result = await connectionManager.Connect(address, cancellationToken); 213 | 214 | switch (result.State) 215 | { 216 | case ConnectionState.Unavailable: 217 | // Server unreachable 218 | 219 | case ConnectionState.ServerSignIn: 220 | // A server was found and the user needs to login. 221 | // Display a login screen and authenticate with the server using result.ApiClient 222 | 223 | case ConnectionState.SignedIn: 224 | // A server was found and the user has been signed in using previously saved credentials. 225 | // Ready to browse using result.ApiClient 226 | } 227 | 228 | ``` 229 | 230 | Similarly, if the user selects a server from the selection screen, use the overload that accepts a ServerInfo instance. When the user wishes to logout, use connectionManager.Logout instead of the individual apiClient.Logout. 231 | 232 | If at anytime the RemoteLoggedOut event is fired, simply start the workflow all over again by calling connectionManager.Connect(cancellationToken). 233 | 234 | ConnectionManager will handle opening and closing web socket connections at the appropriate times. All your app needs to do is use an ApiClient instance to subscribe to individual events. 235 | 236 | 237 | ``` c# 238 | 239 | ApiClient.UserUpdated += webSocket_UserUpdated; 240 | ``` 241 | 242 | With multi-server connectivity it is not recommended to keep a global ApiClient instance, or pass an ApiClient around the application. Instead keep a factory that will resolve the appropiate ApiClient instance depending on context. In order to help with this, ConnectionManager has a GetApiClient method that accepts a BaseItemDto and returns an ApiClient from the server it belongs to. 243 | -------------------------------------------------------------------------------- /Emby.ApiClient/Data/ILocalAssetManager.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.ApiClient; 2 | using MediaBrowser.Model.Dlna; 3 | using MediaBrowser.Model.Dto; 4 | using MediaBrowser.Model.Sync; 5 | using MediaBrowser.Model.Users; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Threading.Tasks; 9 | using Emby.ApiClient.Model; 10 | 11 | namespace Emby.ApiClient.Data 12 | { 13 | public interface ILocalAssetManager 14 | { 15 | /// 16 | /// Records the user action. 17 | /// 18 | /// The action. 19 | /// Task. 20 | Task RecordUserAction(UserAction action); 21 | 22 | /// 23 | /// Deletes the specified action. 24 | /// 25 | /// The action. 26 | /// Task. 27 | Task Delete(UserAction action); 28 | 29 | /// 30 | /// Deletes the specified item. 31 | /// 32 | /// The item. 33 | /// Task. 34 | Task Delete(LocalItem item); 35 | 36 | /// 37 | /// Gets all user actions by serverId 38 | /// 39 | /// 40 | /// 41 | Task> GetUserActions(string serverId); 42 | 43 | /// 44 | /// Adds the or update. 45 | /// 46 | /// The item. 47 | /// Task. 48 | Task AddOrUpdate(LocalItem item); 49 | 50 | /// 51 | /// Gets the files. 52 | /// 53 | /// The item. 54 | /// Task<List<ItemFileInfo>>. 55 | Task> GetFiles(LocalItem item); 56 | 57 | /// 58 | /// Deletes the specified file. 59 | /// 60 | /// The path. 61 | /// Task. 62 | Task DeleteFile(string path); 63 | 64 | /// 65 | /// Saves the subtitles. 66 | /// 67 | /// The stream. 68 | /// The format. 69 | /// The item. 70 | /// The language. 71 | /// if set to true [is forced]. 72 | /// Task<System.String>. 73 | Task SaveSubtitles(Stream stream, 74 | string format, 75 | LocalItem item, 76 | string language, 77 | bool isForced); 78 | 79 | /// 80 | /// Saves the media. 81 | /// 82 | /// The stream. 83 | /// The local item. 84 | /// The server. 85 | /// Task. 86 | Task SaveMedia(Stream stream, LocalItem localItem, ServerInfo server); 87 | #if WINDOWS_UWP 88 | /// 89 | /// Saves the media. 90 | /// 91 | /// The file. 92 | /// The local item. 93 | /// The server. 94 | /// Task. 95 | Task SaveMedia(Windows.Storage.IStorageFile file, LocalItem localItem, ServerInfo server); 96 | #endif 97 | /// 98 | /// Creates the local item. 99 | /// 100 | /// The library item. 101 | /// The server. 102 | /// The synchronize job item identifier. 103 | /// Name of the original file. 104 | /// LocalItem. 105 | LocalItem CreateLocalItem(BaseItemDto libraryItem, ServerInfo server, string syncJobItemId, string originalFileName); 106 | /// 107 | /// Gets the local item. 108 | /// 109 | /// The local identifier. 110 | /// Task<LocalItem>. 111 | Task GetLocalItem(string localId); 112 | /// 113 | /// Gets the local item. 114 | /// 115 | /// The server identifier. 116 | /// The item identifier. 117 | /// Task<LocalItem>. 118 | Task GetLocalItem(string serverId, string itemId); 119 | /// 120 | /// Files the exists. 121 | /// 122 | /// The path. 123 | /// Task<System.Boolean>. 124 | Task FileExists(string path); 125 | /// 126 | /// Gets the server item ids. 127 | /// 128 | /// The server identifier. 129 | /// Task<List<System.String>>. 130 | Task> GetServerItemIds(string serverId); 131 | /// 132 | /// Gets the file stream. 133 | /// 134 | /// The information. 135 | /// Task<Stream>. 136 | Task GetFileStream(StreamInfo info); 137 | /// 138 | /// Gets the file stream. 139 | /// 140 | /// The path. 141 | /// Task<Stream>. 142 | Task GetFileStream(string path); 143 | /// 144 | /// Saves the offline user. 145 | /// 146 | /// The user. 147 | /// Task. 148 | Task SaveOfflineUser(UserDto user); 149 | /// 150 | /// Deletes the offline user. 151 | /// 152 | /// The identifier. 153 | /// Task. 154 | Task DeleteOfflineUser(string id); 155 | /// 156 | /// Saves the user image. 157 | /// 158 | /// The user. 159 | /// The stream. 160 | /// Task. 161 | Task SaveImage(UserDto user, Stream stream); 162 | /// 163 | /// Gets the user image. 164 | /// 165 | /// The user. 166 | /// Task<Stream>. 167 | Task GetImage(UserDto user); 168 | /// 169 | /// Deletes the user image. 170 | /// 171 | /// The user. 172 | /// Task. 173 | Task DeleteImage(UserDto user); 174 | /// 175 | /// Determines whether the specified user has image. 176 | /// 177 | /// The user. 178 | Task HasImage(UserDto user); 179 | /// 180 | /// Saves the item image. 181 | /// 182 | /// The server identifier. 183 | /// The item identifier. 184 | /// The image identifier. 185 | /// The stream. 186 | /// Task. 187 | Task SaveImage(string serverId, string itemId, string imageId, Stream stream); 188 | /// 189 | /// Determines whether the specified server identifier has image. 190 | /// 191 | /// The server identifier. 192 | /// The item identifier. 193 | /// The image identifier. 194 | Task HasImage(string serverId, string itemId, string imageId); 195 | /// 196 | /// Gets the image. 197 | /// 198 | /// The server identifier. 199 | /// The item identifier. 200 | /// The image identifier. 201 | /// Task<Stream>. 202 | Task GetImage(string serverId, string itemId, string imageId); 203 | /// 204 | /// Determines whether the specified item has image. 205 | /// 206 | /// The item. 207 | /// The image identifier. 208 | Task HasImage(BaseItemDto item, string imageId); 209 | /// 210 | /// Gets the image. 211 | /// 212 | /// The item. 213 | /// The image identifier. 214 | /// Task<Stream>. 215 | Task GetImage(BaseItemDto item, string imageId); 216 | /// 217 | /// Gets the views. 218 | /// 219 | /// The server identifier. 220 | /// The user identifier. 221 | /// Task<List<BaseItemDto>>. 222 | Task> GetViews(string serverId, string userId); 223 | /// 224 | /// Gets the items. 225 | /// 226 | /// The user. 227 | /// The parent item. 228 | /// Task<List<BaseItemDto>>. 229 | Task> GetItems(UserDto user, BaseItemDto parentItem); 230 | /// 231 | /// Gets the user. 232 | /// 233 | /// The identifier. 234 | /// Task<UserDto>. 235 | Task GetUser(string id); 236 | } 237 | } -------------------------------------------------------------------------------- /Emby.ApiClient/ConnectService.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.ApiClient; 2 | using MediaBrowser.Model.Connect; 3 | using MediaBrowser.Model.Entities; 4 | using MediaBrowser.Model.Logging; 5 | using MediaBrowser.Model.Serialization; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Text; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Emby.ApiClient.Cryptography; 13 | using Emby.ApiClient.Net; 14 | 15 | namespace Emby.ApiClient 16 | { 17 | public class ConnectService 18 | { 19 | internal IJsonSerializer JsonSerializer { get; set; } 20 | private readonly ILogger _logger; 21 | private readonly IAsyncHttpClient _httpClient; 22 | private readonly ICryptographyProvider _cryptographyProvider; 23 | private readonly string _appName; 24 | private readonly string _appVersion; 25 | 26 | public ConnectService(IJsonSerializer jsonSerializer, ILogger logger, IAsyncHttpClient httpClient, ICryptographyProvider cryptographyProvider, string appName, string appVersion) 27 | { 28 | JsonSerializer = jsonSerializer; 29 | _logger = logger; 30 | _httpClient = httpClient; 31 | _cryptographyProvider = cryptographyProvider; 32 | _appName = appName; 33 | _appVersion = appVersion; 34 | } 35 | 36 | public static string GetConnectPasswordMd5(string password, ICryptographyProvider cryptographyProvider) 37 | { 38 | password = ConnectPassword.PerformPreHashFilter(password ?? string.Empty); 39 | 40 | var bytes = Encoding.UTF8.GetBytes(password); 41 | 42 | bytes = cryptographyProvider.CreateMD5(bytes); 43 | 44 | var hash = BitConverter.ToString(bytes, 0, bytes.Length).Replace("-", string.Empty); 45 | 46 | return hash; 47 | } 48 | 49 | public Task Authenticate(string username, string password) 50 | { 51 | var md5 = GetConnectPasswordMd5(password ?? string.Empty, _cryptographyProvider); 52 | 53 | var args = new Dictionary 54 | { 55 | {"nameOrEmail",username}, 56 | {"password",md5} 57 | }; 58 | 59 | return PostAsync(GetConnectUrl("user/authenticate"), args); 60 | } 61 | 62 | public Task Logout(string accessToken) 63 | { 64 | if (string.IsNullOrWhiteSpace(accessToken)) 65 | { 66 | throw new ArgumentNullException("accessToken"); 67 | } 68 | 69 | var args = new Dictionary 70 | { 71 | }; 72 | 73 | return PostAsync(GetConnectUrl("user/logout"), args, accessToken); 74 | } 75 | 76 | public Task CreatePin(string deviceId) 77 | { 78 | var args = new Dictionary 79 | { 80 | {"deviceId",deviceId} 81 | }; 82 | 83 | return PostAsync(GetConnectUrl("pin"), args); 84 | } 85 | 86 | public async Task GetPinStatus(PinCreationResult pin) 87 | { 88 | var dict = new QueryStringDictionary(); 89 | 90 | dict.Add("deviceId", pin.DeviceId); 91 | dict.Add("pin", pin.Pin); 92 | 93 | var url = GetConnectUrl("pin") + "?" + dict.GetQueryString(); 94 | 95 | var request = new HttpRequest 96 | { 97 | Method = "GET", 98 | Url = url 99 | 100 | }; 101 | 102 | AddAppInfo(request, _appName, _appVersion); 103 | 104 | using (var stream = await _httpClient.SendAsync(request).ConfigureAwait(false)) 105 | { 106 | return JsonSerializer.DeserializeFromStream(stream); 107 | } 108 | } 109 | 110 | public Task ExchangePin(PinCreationResult pin) 111 | { 112 | var args = new Dictionary 113 | { 114 | {"deviceId",pin.DeviceId}, 115 | {"pin",pin.Pin} 116 | }; 117 | 118 | return PostAsync(GetConnectUrl("pin/authenticate"), args); 119 | } 120 | 121 | private async Task PostAsync(string url, Dictionary args, string userAccessToken = null) 122 | where T : class 123 | { 124 | var request = new HttpRequest 125 | { 126 | Url = url, 127 | Method = "POST" 128 | }; 129 | 130 | request.SetPostData(args); 131 | 132 | if (!string.IsNullOrEmpty(userAccessToken)) 133 | { 134 | AddUserAccessToken(request, userAccessToken); 135 | } 136 | 137 | AddAppInfo(request, _appName, _appVersion); 138 | 139 | using (var stream = await _httpClient.SendAsync(request).ConfigureAwait(false)) 140 | { 141 | return JsonSerializer.DeserializeFromStream(stream); 142 | } 143 | } 144 | 145 | public async Task GetConnectUser(ConnectUserQuery query, string accessToken, CancellationToken cancellationToken = default(CancellationToken)) 146 | { 147 | if (string.IsNullOrWhiteSpace(accessToken)) 148 | { 149 | throw new ArgumentNullException("accessToken"); 150 | } 151 | 152 | var dict = new QueryStringDictionary(); 153 | 154 | if (!string.IsNullOrWhiteSpace(query.Id)) 155 | { 156 | dict.Add("id", query.Id); 157 | } 158 | else if (!string.IsNullOrWhiteSpace(query.NameOrEmail)) 159 | { 160 | dict.Add("nameOrEmail", query.NameOrEmail); 161 | } 162 | else if (!string.IsNullOrWhiteSpace(query.Name)) 163 | { 164 | dict.Add("name", query.Name); 165 | } 166 | else if (!string.IsNullOrWhiteSpace(query.Email)) 167 | { 168 | dict.Add("email", query.Email); 169 | } 170 | else 171 | { 172 | throw new ArgumentException("Empty ConnectUserQuery supplied"); 173 | } 174 | 175 | var url = GetConnectUrl("user") + "?" + dict.GetQueryString(); 176 | 177 | var request = new HttpRequest 178 | { 179 | Method = "GET", 180 | Url = url, 181 | CancellationToken = cancellationToken 182 | }; 183 | 184 | AddUserAccessToken(request, accessToken); 185 | AddAppInfo(request, _appName, _appVersion); 186 | 187 | using (var stream = await _httpClient.SendAsync(request).ConfigureAwait(false)) 188 | { 189 | return JsonSerializer.DeserializeFromStream(stream); 190 | } 191 | } 192 | 193 | public async Task GetServers(string userId, string accessToken, CancellationToken cancellationToken = default(CancellationToken)) 194 | { 195 | if (string.IsNullOrWhiteSpace(userId)) 196 | { 197 | throw new ArgumentNullException("userId"); 198 | } 199 | if (string.IsNullOrWhiteSpace(accessToken)) 200 | { 201 | throw new ArgumentNullException("accessToken"); 202 | } 203 | 204 | var dict = new QueryStringDictionary(); 205 | 206 | dict.Add("userId", userId); 207 | 208 | var url = GetConnectUrl("servers") + "?" + dict.GetQueryString(); 209 | 210 | var request = new HttpRequest 211 | { 212 | Method = "GET", 213 | Url = url, 214 | CancellationToken = cancellationToken 215 | }; 216 | 217 | AddUserAccessToken(request, accessToken); 218 | AddAppInfo(request, _appName, _appVersion); 219 | 220 | using (var stream = await _httpClient.SendAsync(request).ConfigureAwait(false)) 221 | { 222 | using (var reader = new StreamReader(stream)) 223 | { 224 | var json = await reader.ReadToEndAsync().ConfigureAwait(false); 225 | 226 | _logger.Debug("Connect servers response: {0}", json); 227 | 228 | return JsonSerializer.DeserializeFromString(json); 229 | } 230 | } 231 | } 232 | 233 | private void AddUserAccessToken(HttpRequest request, string accessToken) 234 | { 235 | if (string.IsNullOrWhiteSpace(accessToken)) 236 | { 237 | throw new ArgumentNullException("accessToken"); 238 | } 239 | request.RequestHeaders["X-Connect-UserToken"] = accessToken; 240 | } 241 | 242 | private void AddAppInfo(HttpRequest request, string appName, string appVersion) 243 | { 244 | if (string.IsNullOrWhiteSpace(appName)) 245 | { 246 | throw new ArgumentNullException("appName"); 247 | } 248 | if (string.IsNullOrWhiteSpace(appVersion)) 249 | { 250 | throw new ArgumentNullException("appVersion"); 251 | } 252 | request.RequestHeaders["X-Application"] = appName + "/" + appVersion; 253 | } 254 | 255 | private string GetConnectUrl(string handler) 256 | { 257 | if (string.IsNullOrWhiteSpace(handler)) 258 | { 259 | throw new ArgumentNullException("handler"); 260 | } 261 | return "https://connect.emby.media/service/" + handler; 262 | } 263 | 264 | public async Task SignupForConnect(string email, string username, string password) 265 | { 266 | if (string.IsNullOrWhiteSpace(email)) 267 | { 268 | throw new ArgumentNullException("email"); 269 | } 270 | if (string.IsNullOrWhiteSpace(username)) 271 | { 272 | throw new ArgumentNullException("username"); 273 | } 274 | if (string.IsNullOrWhiteSpace(password)) 275 | { 276 | throw new ArgumentNullException("password"); 277 | } 278 | if (password.Length < 8) 279 | { 280 | throw new ArgumentException("password must be at least 8 characters"); 281 | } 282 | var request = new HttpRequest 283 | { 284 | Url = GetConnectUrl("register"), 285 | Method = "POST" 286 | }; 287 | 288 | var dict = new QueryStringDictionary(); 289 | 290 | dict.Add("email", Uri.EscapeDataString(email)); 291 | dict.Add("userName", username); 292 | dict.Add("password", password); 293 | request.SetPostData(dict); 294 | 295 | request.RequestHeaders["X-Connect-Token"] = "CONNECT-REGISTER"; 296 | AddAppInfo(request, _appName, _appVersion); 297 | 298 | using (var response = await _httpClient.GetResponse(request, true).ConfigureAwait(false)) 299 | { 300 | var responseObject = JsonSerializer.DeserializeFromStream(response.Content); 301 | 302 | if (string.Equals(responseObject.Status, "SUCCESS", StringComparison.OrdinalIgnoreCase)) 303 | { 304 | return ConnectSignupResponse.Success; 305 | } 306 | if (string.Equals(responseObject.Status, "USERNAME_IN_USE", StringComparison.OrdinalIgnoreCase)) 307 | { 308 | return ConnectSignupResponse.UsernameInUser; 309 | } 310 | if (string.Equals(responseObject.Status, "EMAIL_IN_USE", StringComparison.OrdinalIgnoreCase)) 311 | { 312 | return ConnectSignupResponse.EmailInUse; 313 | } 314 | return ConnectSignupResponse.Failure; 315 | } 316 | } 317 | 318 | private class RawConnectResponse 319 | { 320 | public string Status { get; set; } 321 | public string Message { get; set; } 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /Emby.ApiClient/Model/ItemQuery.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Entities; 2 | using System; 3 | using MediaBrowser.Model.Querying; 4 | 5 | namespace Emby.ApiClient.Model 6 | { 7 | /// 8 | /// Contains all the possible parameters that can be used to query for items 9 | /// 10 | public class ItemQuery 11 | { 12 | /// 13 | /// The user to localize search results for 14 | /// 15 | /// The user id. 16 | public string UserId { get; set; } 17 | 18 | /// 19 | /// Specify this to localize the search to a specific item or folder. Omit to use the root. 20 | /// 21 | /// The parent id. 22 | public string ParentId { get; set; } 23 | 24 | /// 25 | /// Skips over a given number of items within the results. Use for paging. 26 | /// 27 | /// The start index. 28 | public int? StartIndex { get; set; } 29 | 30 | /// 31 | /// The maximum number of items to return 32 | /// 33 | /// The limit. 34 | public int? Limit { get; set; } 35 | 36 | /// 37 | /// What to sort the results by 38 | /// 39 | /// The sort by. 40 | public string[] SortBy { get; set; } 41 | 42 | /// 43 | /// Gets or sets the artist ids. 44 | /// 45 | /// The artist ids. 46 | public string[] ArtistIds { get; set; } 47 | 48 | /// 49 | /// The sort order to return results with 50 | /// 51 | /// The sort order. 52 | public SortOrder? SortOrder { get; set; } 53 | 54 | /// 55 | /// Filters to apply to the results 56 | /// 57 | /// The filters. 58 | public ItemFilter[] Filters { get; set; } 59 | 60 | /// 61 | /// Fields to return within the items, in addition to basic information 62 | /// 63 | /// The fields. 64 | public ItemFields[] Fields { get; set; } 65 | 66 | /// 67 | /// Gets or sets the media types. 68 | /// 69 | /// The media types. 70 | public string[] MediaTypes { get; set; } 71 | 72 | /// 73 | /// Gets or sets the video formats. 74 | /// 75 | /// The video formats. 76 | public bool? Is3D { get; set; } 77 | 78 | /// 79 | /// Gets or sets the video types. 80 | /// 81 | /// The video types. 82 | public VideoType[] VideoTypes { get; set; } 83 | 84 | /// 85 | /// Whether or not to perform the query recursively 86 | /// 87 | /// true if recursive; otherwise, false. 88 | public bool Recursive { get; set; } 89 | 90 | /// 91 | /// Limit results to items containing specific genres 92 | /// 93 | /// The genres. 94 | public string[] Genres { get; set; } 95 | 96 | /// 97 | /// Gets or sets the studio ids. 98 | /// 99 | /// The studio ids. 100 | public string[] StudioIds { get; set; } 101 | 102 | /// 103 | /// Gets or sets the exclude item types. 104 | /// 105 | /// The exclude item types. 106 | public string[] ExcludeItemTypes { get; set; } 107 | 108 | /// 109 | /// Gets or sets the include item types. 110 | /// 111 | /// The include item types. 112 | public string[] IncludeItemTypes { get; set; } 113 | 114 | /// 115 | /// Limit results to items containing specific years 116 | /// 117 | /// The years. 118 | public int[] Years { get; set; } 119 | 120 | /// 121 | /// Limit results to items containing a specific person 122 | /// 123 | /// The person. 124 | public string[] PersonIds { get; set; } 125 | 126 | /// 127 | /// If the Person filter is used, this can also be used to restrict to a specific person type 128 | /// 129 | /// The type of the person. 130 | public string[] PersonTypes { get; set; } 131 | 132 | /// 133 | /// Search characters used to find items 134 | /// 135 | /// The index by. 136 | public string SearchTerm { get; set; } 137 | 138 | /// 139 | /// Gets or sets the image types. 140 | /// 141 | /// The image types. 142 | public ImageType[] ImageTypes { get; set; } 143 | 144 | /// 145 | /// Gets or sets the air days. 146 | /// 147 | /// The air days. 148 | public DayOfWeek[] AirDays { get; set; } 149 | 150 | /// 151 | /// Gets or sets the series status. 152 | /// 153 | /// The series status. 154 | public SeriesStatus[] SeriesStatuses { get; set; } 155 | 156 | /// 157 | /// Gets or sets the ids, which are specific items to retrieve 158 | /// 159 | /// The ids. 160 | public string[] Ids { get; set; } 161 | 162 | /// 163 | /// Gets or sets the min official rating. 164 | /// 165 | /// The min official rating. 166 | public string MinOfficialRating { get; set; } 167 | 168 | /// 169 | /// Gets or sets the max official rating. 170 | /// 171 | /// The max official rating. 172 | public string MaxOfficialRating { get; set; } 173 | 174 | /// 175 | /// Gets or sets the min index number. 176 | /// 177 | /// The min index number. 178 | public int? MinIndexNumber { get; set; } 179 | 180 | /// 181 | /// Gets or sets a value indicating whether this instance has parental rating. 182 | /// 183 | /// null if [has parental rating] contains no value, true if [has parental rating]; otherwise, false. 184 | public bool? HasParentalRating { get; set; } 185 | 186 | /// 187 | /// Gets or sets a value indicating whether this instance is HD. 188 | /// 189 | /// null if [is HD] contains no value, true if [is HD]; otherwise, false. 190 | public bool? IsHD { get; set; } 191 | 192 | /// 193 | /// Gets or sets the parent index number. 194 | /// 195 | /// The parent index number. 196 | public int? ParentIndexNumber { get; set; } 197 | 198 | /// 199 | /// Gets or sets the min players. 200 | /// 201 | /// The min players. 202 | public int? MinPlayers { get; set; } 203 | 204 | /// 205 | /// Gets or sets the max players. 206 | /// 207 | /// The max players. 208 | public int? MaxPlayers { get; set; } 209 | 210 | /// 211 | /// Gets or sets the name starts with or greater. 212 | /// 213 | /// The name starts with or greater. 214 | public string NameStartsWithOrGreater { get; set; } 215 | 216 | /// 217 | /// Gets or sets the name starts with. 218 | /// 219 | /// The name starts with or greater. 220 | public string NameStartsWith { get; set; } 221 | 222 | /// 223 | /// Gets or sets the name starts with. 224 | /// 225 | /// The name lessthan. 226 | public string NameLessThan { get; set; } 227 | 228 | /// 229 | /// Gets or sets the album artist starts with or greater. 230 | /// 231 | /// The album artist starts with or greater. 232 | public string AlbumArtistStartsWithOrGreater { get; set; } 233 | 234 | /// 235 | /// Gets or sets a value indicating whether [include index containers]. 236 | /// 237 | /// true if [include index containers]; otherwise, false. 238 | public bool IncludeIndexContainers { get; set; } 239 | 240 | /// 241 | /// Gets or sets the location types. 242 | /// 243 | /// The location types. 244 | public LocationType[] LocationTypes { get; set; } 245 | 246 | /// 247 | /// Gets or sets a value indicating whether this instance is missing episode. 248 | /// 249 | /// null if [is missing episode] contains no value, true if [is missing episode]; otherwise, false. 250 | public bool? IsMissing { get; set; } 251 | 252 | /// 253 | /// Gets or sets a value indicating whether this instance is unaired episode. 254 | /// 255 | /// null if [is unaired episode] contains no value, true if [is unaired episode]; otherwise, false. 256 | public bool? IsUnaired { get; set; } 257 | 258 | public bool? IsVirtualUnaired { get; set; } 259 | 260 | public bool? IsInBoxSet { get; set; } 261 | 262 | public bool? CollapseBoxSetItems { get; set; } 263 | 264 | public bool? IsPlayed { get; set; } 265 | 266 | /// 267 | /// Gets or sets the exclude location types. 268 | /// 269 | /// The exclude location types. 270 | public LocationType[] ExcludeLocationTypes { get; set; } 271 | 272 | public double? MinCommunityRating { get; set; } 273 | public double? MinCriticRating { get; set; } 274 | 275 | public int? AiredDuringSeason { get; set; } 276 | 277 | public DateTime? MinPremiereDate { get; set; } 278 | 279 | public DateTime? MaxPremiereDate { get; set; } 280 | 281 | public bool? EnableImages { get; set; } 282 | public int? ImageTypeLimit { get; set; } 283 | public ImageType[] EnableImageTypes { get; set; } 284 | 285 | [Obsolete] 286 | public string[] Artists { get; set; } 287 | [Obsolete] 288 | public string[] Studios { get; set; } 289 | [Obsolete] 290 | public string Person { get; set; } 291 | 292 | public bool EnableTotalRecordCount { get; set; } 293 | 294 | /// 295 | /// Initializes a new instance of the class. 296 | /// 297 | public ItemQuery() 298 | { 299 | LocationTypes = new LocationType[] { }; 300 | ExcludeLocationTypes = new LocationType[] { }; 301 | 302 | SortBy = new string[] { }; 303 | 304 | Filters = new ItemFilter[] { }; 305 | 306 | Fields = new ItemFields[] { }; 307 | 308 | MediaTypes = new string[] { }; 309 | 310 | VideoTypes = new VideoType[] { }; 311 | 312 | EnableTotalRecordCount = true; 313 | 314 | Artists = new string[] { }; 315 | Studios = new string[] { }; 316 | 317 | Genres = new string[] { }; 318 | StudioIds = new string[] { }; 319 | IncludeItemTypes = new string[] { }; 320 | ExcludeItemTypes = new string[] { }; 321 | Years = new int[] { }; 322 | PersonTypes = new string[] { }; 323 | Ids = new string[] { }; 324 | ArtistIds = new string[] { }; 325 | PersonIds = new string[] { }; 326 | 327 | ImageTypes = new ImageType[] { }; 328 | AirDays = new DayOfWeek[] { }; 329 | SeriesStatuses = new SeriesStatus[] { }; 330 | EnableImageTypes = new ImageType[] { }; 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /Emby.ApiClient/Net/HttpWebRequestClient.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.ApiClient; 2 | using MediaBrowser.Model.Logging; 3 | using MediaBrowser.Model.Net; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Net; 9 | using System.Text; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace Emby.ApiClient.Net 14 | { 15 | /// 16 | /// Class HttpWebRequestClient 17 | /// 18 | public class HttpWebRequestClient : IAsyncHttpClient 19 | { 20 | public event EventHandler HttpResponseReceived; 21 | private readonly IHttpWebRequestFactory _requestFactory; 22 | 23 | /// 24 | /// Called when [response received]. 25 | /// 26 | /// The URL. 27 | /// The verb. 28 | /// The status code. 29 | /// The headers. 30 | /// The request time. 31 | private void OnResponseReceived(string url, 32 | string verb, 33 | HttpStatusCode statusCode, 34 | Dictionary headers, 35 | DateTime requestTime) 36 | { 37 | var duration = DateTime.Now - requestTime; 38 | 39 | Logger.Debug("Received {0} status code after {1} ms from {2}: {3}", (int)statusCode, duration.TotalMilliseconds, verb, url); 40 | 41 | if (HttpResponseReceived != null) 42 | { 43 | try 44 | { 45 | HttpResponseReceived(this, new HttpResponseEventArgs 46 | { 47 | Url = url, 48 | StatusCode = statusCode, 49 | Headers = headers 50 | }); 51 | } 52 | catch (Exception ex) 53 | { 54 | Logger.ErrorException("Error in HttpResponseReceived event handler", ex); 55 | } 56 | } 57 | } 58 | 59 | /// 60 | /// Gets or sets the logger. 61 | /// 62 | /// The logger. 63 | private ILogger Logger { get; set; } 64 | 65 | /// 66 | /// Initializes a new instance of the class. 67 | /// 68 | /// The logger. 69 | /// The request factory. 70 | public HttpWebRequestClient(ILogger logger, IHttpWebRequestFactory requestFactory) 71 | { 72 | Logger = logger; 73 | _requestFactory = requestFactory; 74 | } 75 | 76 | public async Task GetResponse(HttpRequest options, bool sendFailureResponse = false) 77 | { 78 | options.CancellationToken.ThrowIfCancellationRequested(); 79 | 80 | var httpWebRequest = _requestFactory.Create(options); 81 | 82 | ApplyHeaders(options.RequestHeaders, httpWebRequest); 83 | 84 | if (options.RequestStream != null) 85 | { 86 | httpWebRequest.ContentType = options.RequestContentType; 87 | _requestFactory.SetContentLength(httpWebRequest, options.RequestStream.Length); 88 | 89 | using (var requestStream = await _requestFactory.GetRequestStreamAsync(httpWebRequest).ConfigureAwait(false)) 90 | { 91 | await options.RequestStream.CopyToAsync(requestStream).ConfigureAwait(false); 92 | } 93 | } 94 | else if (!string.IsNullOrEmpty(options.RequestContent) || 95 | string.Equals(options.Method, "post", StringComparison.OrdinalIgnoreCase)) 96 | { 97 | var bytes = Encoding.UTF8.GetBytes(options.RequestContent ?? string.Empty); 98 | 99 | httpWebRequest.ContentType = options.RequestContentType ?? "application/x-www-form-urlencoded"; 100 | _requestFactory.SetContentLength(httpWebRequest, bytes.Length); 101 | 102 | using (var requestStream = await _requestFactory.GetRequestStreamAsync(httpWebRequest).ConfigureAwait(false)) 103 | { 104 | await requestStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); 105 | } 106 | } 107 | 108 | Logger.Debug(options.Method + " {0}", options.Url); 109 | 110 | var requestTime = DateTime.Now; 111 | 112 | try 113 | { 114 | options.CancellationToken.ThrowIfCancellationRequested(); 115 | 116 | var response = await _requestFactory.GetResponseAsync(httpWebRequest, options.Timeout).ConfigureAwait(false); 117 | 118 | var httpResponse = (HttpWebResponse)response; 119 | 120 | var headers = ConvertHeaders(response); 121 | OnResponseReceived(options.Url, options.Method, httpResponse.StatusCode, headers, requestTime); 122 | 123 | EnsureSuccessStatusCode(httpResponse); 124 | 125 | options.CancellationToken.ThrowIfCancellationRequested(); 126 | 127 | return GetResponse(httpResponse, headers); 128 | } 129 | catch (OperationCanceledException ex) 130 | { 131 | var exception = GetCancellationException(options.Url, options.CancellationToken, ex); 132 | 133 | throw exception; 134 | } 135 | catch (Exception ex) 136 | { 137 | if (sendFailureResponse) 138 | { 139 | var webException = ex as WebException ?? ex.InnerException as WebException; 140 | if (webException != null) 141 | { 142 | var response = webException.Response as HttpWebResponse; 143 | if (response != null) 144 | { 145 | var headers = ConvertHeaders(response); 146 | return GetResponse(response, headers); 147 | } 148 | } 149 | } 150 | 151 | throw GetExceptionToThrow(ex, options, requestTime); 152 | } 153 | } 154 | 155 | private HttpResponse GetResponse(HttpWebResponse httpResponse, Dictionary headers) 156 | { 157 | return new HttpResponse(httpResponse) 158 | { 159 | Content = httpResponse.GetResponseStream(), 160 | 161 | StatusCode = httpResponse.StatusCode, 162 | 163 | ContentType = httpResponse.ContentType, 164 | 165 | Headers = headers, 166 | 167 | ContentLength = GetContentLength(httpResponse), 168 | 169 | ResponseUrl = httpResponse.ResponseUri.ToString() 170 | }; 171 | } 172 | 173 | private long? GetContentLength(HttpWebResponse response) 174 | { 175 | var length = response.ContentLength; 176 | 177 | if (length == 0) 178 | { 179 | return null; 180 | } 181 | 182 | return length; 183 | } 184 | 185 | public async Task SendAsync(HttpRequest options) 186 | { 187 | var response = await GetResponse(options).ConfigureAwait(false); 188 | 189 | return response.Content; 190 | } 191 | 192 | /// 193 | /// Converts the headers. 194 | /// 195 | /// The response. 196 | /// Dictionary<System.String, System.String>. 197 | private Dictionary ConvertHeaders(WebResponse response) 198 | { 199 | var headers = response.Headers; 200 | 201 | return headers.Cast().ToDictionary(p => p, p => headers[p]); 202 | } 203 | 204 | private Exception GetExceptionToThrow(Exception ex, HttpRequest options, DateTime requestTime) 205 | { 206 | var webException = ex as WebException ?? ex.InnerException as WebException; 207 | 208 | if (webException != null) 209 | { 210 | Logger.ErrorException("Error getting response from " + options.Url, ex); 211 | 212 | var httpException = new HttpException(ex.Message, ex); 213 | 214 | var response = webException.Response as HttpWebResponse; 215 | if (response != null) 216 | { 217 | httpException.StatusCode = response.StatusCode; 218 | OnResponseReceived(options.Url, options.Method, response.StatusCode, ConvertHeaders(response), requestTime); 219 | } 220 | 221 | return httpException; 222 | } 223 | 224 | var timeoutException = ex as TimeoutException ?? ex.InnerException as TimeoutException; 225 | if (timeoutException != null) 226 | { 227 | Logger.ErrorException("Request timeout to " + options.Url, ex); 228 | 229 | var httpException = new HttpException(ex.Message, ex) 230 | { 231 | IsTimedOut = true 232 | }; 233 | 234 | return httpException; 235 | } 236 | 237 | Logger.ErrorException("Error getting response from " + options.Url, ex); 238 | return ex; 239 | } 240 | 241 | private void ApplyHeaders(HttpHeaders headers, HttpWebRequest request) 242 | { 243 | foreach (var header in headers) 244 | { 245 | request.Headers[header.Key] = header.Value; 246 | } 247 | 248 | if (!string.IsNullOrEmpty(headers.AuthorizationScheme)) 249 | { 250 | var val = string.Format("{0} {1}", headers.AuthorizationScheme, headers.AuthorizationParameter); 251 | request.Headers["X-Emby-Authorization"] = val; 252 | } 253 | } 254 | 255 | /// 256 | /// Throws the cancellation exception. 257 | /// 258 | /// The URL. 259 | /// The cancellation token. 260 | /// The exception. 261 | /// Exception. 262 | private Exception GetCancellationException(string url, CancellationToken cancellationToken, OperationCanceledException exception) 263 | { 264 | // If the HttpClient's timeout is reached, it will cancel the Task internally 265 | if (!cancellationToken.IsCancellationRequested) 266 | { 267 | var msg = string.Format("Connection to {0} timed out", url); 268 | 269 | Logger.Error(msg); 270 | 271 | // Throw an HttpException so that the caller doesn't think it was cancelled by user code 272 | return new HttpException(msg, exception) 273 | { 274 | IsTimedOut = true 275 | }; 276 | } 277 | 278 | return exception; 279 | } 280 | 281 | private void EnsureSuccessStatusCode(HttpWebResponse response) 282 | { 283 | var statusCode = response.StatusCode; 284 | var isSuccessful = statusCode >= HttpStatusCode.OK && statusCode <= (HttpStatusCode)299; 285 | 286 | if (!isSuccessful) 287 | { 288 | throw new HttpException(response.StatusDescription) { StatusCode = response.StatusCode }; 289 | } 290 | } 291 | 292 | /// 293 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 294 | /// 295 | public void Dispose() 296 | { 297 | } 298 | } 299 | 300 | public interface IHttpWebRequestFactory 301 | { 302 | HttpWebRequest Create(HttpRequest options); 303 | 304 | void SetContentLength(HttpWebRequest request, long length); 305 | 306 | Task GetResponseAsync(HttpWebRequest request, int timeoutMs); 307 | Task GetRequestStreamAsync(HttpWebRequest request); 308 | } 309 | 310 | public static class AsyncHttpClientFactory 311 | { 312 | public static IAsyncHttpClient Create(ILogger logger) 313 | { 314 | #if PORTABLE || WINDOWS_UWP 315 | return new HttpWebRequestClient(logger, new PortableHttpWebRequestFactory()); 316 | #else 317 | return new HttpWebRequestClient(logger, new HttpWebRequestFactory()); 318 | #endif 319 | } 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /Emby.ApiClient/Sync/MediaSync.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.ApiClient; 2 | using MediaBrowser.Model.Dto; 3 | using MediaBrowser.Model.Entities; 4 | using MediaBrowser.Model.Logging; 5 | using MediaBrowser.Model.Sync; 6 | using System; 7 | using System.Linq; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Emby.ApiClient.Data; 11 | using Emby.ApiClient.Model; 12 | 13 | namespace Emby.ApiClient.Sync 14 | { 15 | public class MediaSync : IMediaSync 16 | { 17 | private readonly IFileTransferManager _fileTransferManager; 18 | private readonly ILocalAssetManager _localAssetManager; 19 | private readonly ILogger _logger; 20 | 21 | public MediaSync(ILocalAssetManager localAssetManager, ILogger logger, IFileTransferManager fileTransferManager) 22 | { 23 | _localAssetManager = localAssetManager; 24 | _logger = logger; 25 | _fileTransferManager = fileTransferManager; 26 | } 27 | 28 | public async Task Sync(IApiClient apiClient, 29 | ServerInfo serverInfo, 30 | IProgress progress, 31 | CancellationToken cancellationToken = default(CancellationToken)) 32 | { 33 | _logger.Debug("Beginning media sync process with server Id: {0}", serverInfo.Id); 34 | 35 | // First report actions to the server that occurred while offline 36 | await ReportOfflineActions(apiClient, serverInfo, cancellationToken).ConfigureAwait(false); 37 | progress.Report(1); 38 | 39 | await SyncData(apiClient, serverInfo, cancellationToken).ConfigureAwait(false); 40 | progress.Report(3); 41 | 42 | var innerProgress = new DoubleProgress(); 43 | innerProgress.RegisterAction(pct => 44 | { 45 | var totalProgress = pct * .97; 46 | totalProgress += 1; 47 | progress.Report(totalProgress); 48 | }); 49 | 50 | await GetNewMedia(apiClient, serverInfo, innerProgress, cancellationToken); 51 | progress.Report(100); 52 | 53 | // Do the data sync twice so the server knows what was removed from the device 54 | await SyncData(apiClient, serverInfo, cancellationToken).ConfigureAwait(false); 55 | } 56 | 57 | private async Task GetNewMedia(IApiClient apiClient, 58 | ServerInfo server, 59 | IProgress progress, 60 | CancellationToken cancellationToken) 61 | { 62 | var jobItems = await apiClient.GetReadySyncItems(apiClient.DeviceId).ConfigureAwait(false); 63 | 64 | var numComplete = 0; 65 | double startingPercent = 0; 66 | double percentPerItem = 1; 67 | if (jobItems.Count > 0) 68 | { 69 | percentPerItem /= jobItems.Count; 70 | } 71 | 72 | foreach (var jobItem in jobItems) 73 | { 74 | cancellationToken.ThrowIfCancellationRequested(); 75 | 76 | var currentPercent = startingPercent; 77 | var innerProgress = new DoubleProgress(); 78 | innerProgress.RegisterAction(pct => 79 | { 80 | var totalProgress = pct * percentPerItem; 81 | totalProgress += currentPercent; 82 | progress.Report(totalProgress); 83 | }); 84 | 85 | try 86 | { 87 | await GetItem(apiClient, server, jobItem, innerProgress, cancellationToken).ConfigureAwait(false); 88 | } 89 | catch (Exception ex) 90 | { 91 | _logger.ErrorException("Error syncing new media", ex); 92 | } 93 | 94 | numComplete++; 95 | startingPercent = numComplete; 96 | startingPercent /= jobItems.Count; 97 | startingPercent *= 100; 98 | progress.Report(startingPercent); 99 | } 100 | } 101 | 102 | private async Task GetItem(IApiClient apiClient, 103 | ServerInfo server, 104 | SyncedItem jobItem, 105 | IProgress progress, 106 | CancellationToken cancellationToken) 107 | { 108 | var libraryItem = jobItem.Item; 109 | 110 | var localItem = _localAssetManager.CreateLocalItem(libraryItem, server, jobItem.SyncJobItemId, jobItem.OriginalFileName); 111 | 112 | // Create db record 113 | await _localAssetManager.AddOrUpdate(localItem).ConfigureAwait(false); 114 | 115 | var fileTransferProgress = new DoubleProgress(); 116 | fileTransferProgress.RegisterAction(pct => progress.Report(pct * .92)); 117 | 118 | // Download item file 119 | await _fileTransferManager.GetItemFileAsync(apiClient, server, localItem, jobItem.SyncJobItemId, fileTransferProgress, cancellationToken).ConfigureAwait(false); 120 | progress.Report(92); 121 | 122 | // Download images 123 | await GetItemImages(apiClient, localItem, cancellationToken).ConfigureAwait(false); 124 | progress.Report(95); 125 | 126 | // Download subtitles 127 | await GetItemSubtitles(apiClient, jobItem, localItem, cancellationToken).ConfigureAwait(false); 128 | progress.Report(99); 129 | 130 | // Let the server know it was successfully downloaded 131 | await apiClient.ReportSyncJobItemTransferred(jobItem.SyncJobItemId).ConfigureAwait(false); 132 | progress.Report(100); 133 | } 134 | 135 | public bool HasPrimaryImage(BaseItemDto item) 136 | { 137 | return item.ImageTags != null && item.ImageTags.ContainsKey(ImageType.Primary); 138 | } 139 | 140 | private async Task GetItemImages(IApiClient apiClient, 141 | LocalItem item, 142 | CancellationToken cancellationToken) 143 | { 144 | var libraryItem = item.Item; 145 | 146 | if (HasPrimaryImage(libraryItem)) 147 | { 148 | await DownloadImage(apiClient, item.ServerId, libraryItem.Id, libraryItem.ImageTags[ImageType.Primary], ImageType.Primary, cancellationToken) 149 | .ConfigureAwait(false); 150 | } 151 | 152 | // Container images 153 | 154 | // Series Primary 155 | if (!string.IsNullOrWhiteSpace(libraryItem.SeriesPrimaryImageTag)) 156 | { 157 | await DownloadImage(apiClient, item.ServerId, libraryItem.SeriesId, libraryItem.SeriesPrimaryImageTag, ImageType.Primary, cancellationToken) 158 | .ConfigureAwait(false); 159 | } 160 | 161 | // Series Thumb 162 | if (!string.IsNullOrWhiteSpace(libraryItem.SeriesThumbImageTag)) 163 | { 164 | await DownloadImage(apiClient, item.ServerId, libraryItem.SeriesId, libraryItem.SeriesThumbImageTag, ImageType.Thumb, cancellationToken) 165 | .ConfigureAwait(false); 166 | } 167 | 168 | // Album Primary 169 | if (!string.IsNullOrWhiteSpace(libraryItem.AlbumPrimaryImageTag)) 170 | { 171 | await DownloadImage(apiClient, item.ServerId, libraryItem.AlbumId, libraryItem.AlbumPrimaryImageTag, ImageType.Primary, cancellationToken) 172 | .ConfigureAwait(false); 173 | } 174 | } 175 | 176 | private async Task DownloadImage(IApiClient apiClient, 177 | string serverId, 178 | string itemId, 179 | string imageTag, 180 | ImageType imageType, 181 | CancellationToken cancellationToken) 182 | { 183 | var hasImage = await _localAssetManager.HasImage(serverId, itemId, imageTag).ConfigureAwait(false); 184 | 185 | if (hasImage) 186 | { 187 | return; 188 | } 189 | 190 | var url = apiClient.GetImageUrl(itemId, new ImageOptions 191 | { 192 | ImageType = imageType, 193 | Tag = imageTag 194 | }); 195 | 196 | using (var response = await apiClient.GetResponse(url, cancellationToken).ConfigureAwait(false)) 197 | { 198 | await _localAssetManager.SaveImage(serverId, itemId, imageTag, response.Content).ConfigureAwait(false); 199 | } 200 | } 201 | 202 | private async Task GetItemSubtitles(IApiClient apiClient, 203 | SyncedItem jobItem, 204 | LocalItem item, 205 | CancellationToken cancellationToken) 206 | { 207 | var hasDownloads = false; 208 | 209 | var mediaSource = jobItem.Item.MediaSources.FirstOrDefault(); 210 | 211 | if (mediaSource == null) 212 | { 213 | _logger.Error("Cannot download subtitles because video has no media source info."); 214 | return; 215 | } 216 | 217 | foreach (var file in jobItem.AdditionalFiles.Where(i => i.Type == ItemFileType.Subtitles)) 218 | { 219 | var subtitleStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Subtitle && i.Index == file.Index); 220 | 221 | if (subtitleStream != null) 222 | { 223 | using (var response = await apiClient.GetSyncJobItemAdditionalFile(jobItem.SyncJobItemId, file.Name, cancellationToken).ConfigureAwait(false)) 224 | { 225 | var path = await _localAssetManager.SaveSubtitles(response, subtitleStream.Codec, item, subtitleStream.Language, subtitleStream.IsForced).ConfigureAwait(false); 226 | 227 | subtitleStream.Path = path; 228 | 229 | var files = item.AdditionalFiles.ToList(); 230 | files.Add(path); 231 | item.AdditionalFiles = files.ToArray(); 232 | } 233 | 234 | hasDownloads = true; 235 | } 236 | else 237 | { 238 | _logger.Error("Cannot download subtitles because matching stream info wasn't found."); 239 | } 240 | } 241 | 242 | // Save the changes to the item 243 | if (hasDownloads) 244 | { 245 | await _localAssetManager.AddOrUpdate(item).ConfigureAwait(false); 246 | } 247 | } 248 | 249 | private async Task SyncData(IApiClient apiClient, 250 | ServerInfo serverInfo, 251 | CancellationToken cancellationToken) 252 | { 253 | _logger.Debug("Beginning SyncData with server {0}", serverInfo.Name); 254 | 255 | var localIds = await _localAssetManager.GetServerItemIds(serverInfo.Id).ConfigureAwait(false); 256 | 257 | var result = await apiClient.SyncData(new SyncDataRequest 258 | { 259 | TargetId = apiClient.DeviceId, 260 | LocalItemIds = localIds.ToArray() 261 | 262 | }).ConfigureAwait(false); 263 | 264 | cancellationToken.ThrowIfCancellationRequested(); 265 | 266 | foreach (var itemIdToRemove in result.ItemIdsToRemove) 267 | { 268 | try 269 | { 270 | await RemoveItem(serverInfo.Id, itemIdToRemove).ConfigureAwait(false); 271 | } 272 | catch (Exception ex) 273 | { 274 | _logger.ErrorException("Error deleting item from device. Id: {0}", ex, itemIdToRemove); 275 | } 276 | } 277 | } 278 | 279 | private async Task ReportOfflineActions(IApiClient apiClient, 280 | ServerInfo serverInfo, 281 | CancellationToken cancellationToken) 282 | { 283 | var actions = await _localAssetManager.GetUserActions(serverInfo.Id).ConfigureAwait(false); 284 | 285 | cancellationToken.ThrowIfCancellationRequested(); 286 | 287 | var actionList = actions 288 | .OrderBy(i => i.Date) 289 | .ToList(); 290 | 291 | _logger.Debug("Reporting {0} offline actions to server {1}", 292 | actionList.Count, 293 | serverInfo.Id); 294 | 295 | if (actionList.Count > 0) 296 | { 297 | await apiClient.ReportOfflineActions(actionList).ConfigureAwait(false); 298 | } 299 | 300 | foreach (var action in actionList) 301 | { 302 | await _localAssetManager.Delete(action).ConfigureAwait(false); 303 | } 304 | } 305 | 306 | private async Task RemoveItem(string serverId, string itemId) 307 | { 308 | _logger.Debug("Removing item. ServerId: {0}, ItemId: {1}", serverId, itemId); 309 | var localItem = await _localAssetManager.GetLocalItem(serverId, itemId); 310 | 311 | if (localItem == null) 312 | { 313 | return; 314 | } 315 | 316 | var additionalFiles = localItem.AdditionalFiles.ToList(); 317 | var localPath = localItem.LocalPath; 318 | 319 | await _localAssetManager.Delete(localItem).ConfigureAwait(false); 320 | 321 | foreach (var file in additionalFiles) 322 | { 323 | await _localAssetManager.DeleteFile(file).ConfigureAwait(false); 324 | } 325 | 326 | await _localAssetManager.DeleteFile(localPath).ConfigureAwait(false); 327 | } 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /Emby.ApiClient/Playback/PlaybackManager.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.ApiClient; 2 | using MediaBrowser.Model.Dlna; 3 | using MediaBrowser.Model.Dto; 4 | using MediaBrowser.Model.Entities; 5 | using MediaBrowser.Model.Logging; 6 | using MediaBrowser.Model.MediaInfo; 7 | using MediaBrowser.Model.Session; 8 | using MediaBrowser.Model.Users; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Linq; 12 | using System.Threading; 13 | using System.Threading.Tasks; 14 | using Emby.ApiClient.Data; 15 | using Emby.ApiClient.Model; 16 | using Emby.ApiClient.Net; 17 | 18 | namespace Emby.ApiClient.Playback 19 | { 20 | public class PlaybackManager : IPlaybackManager 21 | { 22 | private readonly ILocalAssetManager _localAssetManager; 23 | private readonly ILogger _logger; 24 | private readonly IDevice _device; 25 | 26 | /// 27 | /// Initializes a new instance of the class. 28 | /// 29 | /// The local asset manager. 30 | /// The device. 31 | /// The logger. 32 | public PlaybackManager(ILocalAssetManager localAssetManager, IDevice device, ILogger logger) 33 | { 34 | _localAssetManager = localAssetManager; 35 | _device = device; 36 | _logger = logger; 37 | } 38 | 39 | public PlaybackManager(ILocalAssetManager localAssetManager, IDevice device, ILogger logger, INetworkConnection network) 40 | : this(localAssetManager, device, logger) 41 | { 42 | } 43 | 44 | /// 45 | /// Gets the pre playback selectable audio streams. 46 | /// 47 | /// The server identifier. 48 | /// The options. 49 | /// Task<IEnumerable<MediaStream>>. 50 | public async Task> GetPrePlaybackSelectableAudioStreams(string serverId, VideoOptions options) 51 | { 52 | var info = await GetVideoStreamInfoInternal(serverId, options).ConfigureAwait(false); 53 | 54 | return info.GetSelectableAudioStreams(); 55 | } 56 | 57 | /// 58 | /// Gets the pre playback selectable subtitle streams. 59 | /// 60 | /// The server identifier. 61 | /// The options. 62 | /// Task<IEnumerable<MediaStream>>. 63 | public async Task> GetPrePlaybackSelectableSubtitleStreams(string serverId, VideoOptions options) 64 | { 65 | var info = await GetVideoStreamInfoInternal(serverId, options).ConfigureAwait(false); 66 | 67 | return info.GetSelectableSubtitleStreams(); 68 | } 69 | 70 | /// 71 | /// Gets the in playback selectable audio streams. 72 | /// 73 | /// The information. 74 | /// IEnumerable<MediaStream>. 75 | public IEnumerable GetInPlaybackSelectableAudioStreams(StreamInfo info) 76 | { 77 | return info.GetSelectableAudioStreams(); 78 | } 79 | 80 | /// 81 | /// Gets the in playback selectable subtitle streams. 82 | /// 83 | /// The information. 84 | /// IEnumerable<MediaStream>. 85 | public IEnumerable GetInPlaybackSelectableSubtitleStreams(StreamInfo info) 86 | { 87 | return info.GetSelectableSubtitleStreams(); 88 | } 89 | 90 | /// 91 | /// Gets the stream builder. 92 | /// 93 | /// StreamBuilder. 94 | private StreamBuilder GetStreamBuilder() 95 | { 96 | return new StreamBuilder(_logger); 97 | } 98 | 99 | /// 100 | /// Gets the audio stream information. 101 | /// 102 | /// The server identifier. 103 | /// The options. 104 | /// if set to true [is offline]. 105 | /// The API client. 106 | /// Task<StreamInfo>. 107 | public async Task GetAudioStreamInfo(string serverId, AudioOptions options, bool isOffline, IApiClient apiClient) 108 | { 109 | var streamBuilder = GetStreamBuilder(); 110 | 111 | var localItem = await _localAssetManager.GetLocalItem(serverId, options.ItemId); 112 | if (localItem != null) 113 | { 114 | var localMediaSource = localItem.Item.MediaSources[0]; 115 | 116 | // Use the local media source, unless a specific server media source was requested 117 | if (string.IsNullOrWhiteSpace(options.MediaSourceId) || 118 | string.Equals(localMediaSource.Id, options.MediaSourceId, 119 | StringComparison.OrdinalIgnoreCase)) 120 | { 121 | // Finally, check to make sure the local file is actually available at this time 122 | var fileExists = await _localAssetManager.FileExists(localMediaSource.Path).ConfigureAwait(false); 123 | 124 | if (fileExists) 125 | { 126 | options.MediaSources = localItem.Item.MediaSources.ToArray(); 127 | 128 | var result = streamBuilder.BuildAudioItem(options); 129 | if (result == null) 130 | { 131 | _logger.Warn("LocalItem returned no compatible streams. Will dummy up a StreamInfo to force it to direct play."); 132 | var mediaSource = localItem.Item.MediaSources.First(); 133 | result = GetForcedDirectPlayStreamInfo(DlnaProfileType.Audio, options, mediaSource); 134 | } 135 | result.PlayMethod = PlayMethod.DirectPlay; 136 | return result; 137 | } 138 | } 139 | } 140 | 141 | PlaybackInfoResponse playbackInfo = null; 142 | string playSessionId = null; 143 | if (!isOffline) 144 | { 145 | playbackInfo = await apiClient.GetPlaybackInfo(new PlaybackInfoRequest 146 | { 147 | Id = options.ItemId, 148 | UserId = apiClient.CurrentUserId, 149 | MaxStreamingBitrate = options.MaxBitrate, 150 | MediaSourceId = options.MediaSourceId 151 | 152 | }).ConfigureAwait(false); 153 | 154 | if (playbackInfo.ErrorCode.HasValue) 155 | { 156 | throw new PlaybackException { ErrorCode = playbackInfo.ErrorCode.Value }; 157 | } 158 | 159 | options.MediaSources = playbackInfo.MediaSources; 160 | playSessionId = playbackInfo.PlaySessionId; 161 | } 162 | 163 | var streamInfo = streamBuilder.BuildAudioItem(options); 164 | EnsureSuccess(streamInfo); 165 | 166 | if (!isOffline) 167 | { 168 | var liveMediaSource = await GetLiveStreamInfo(playSessionId, streamInfo.MediaSource, options, apiClient).ConfigureAwait(false); 169 | 170 | if (liveMediaSource != null) 171 | { 172 | options.MediaSources = new List { liveMediaSource }.ToArray(); 173 | streamInfo = GetStreamBuilder().BuildAudioItem(options); 174 | EnsureSuccess(streamInfo); 175 | } 176 | } 177 | 178 | if (playbackInfo != null) 179 | { 180 | streamInfo.AllMediaSources = playbackInfo.MediaSources.ToList(); 181 | streamInfo.PlaySessionId = playbackInfo.PlaySessionId; 182 | } 183 | 184 | return streamInfo; 185 | } 186 | 187 | /// 188 | /// Sets the live stream. 189 | /// 190 | /// The media source. 191 | /// The options. 192 | /// The API client. 193 | /// Task. 194 | private async Task GetLiveStreamInfo(string playSessionId, MediaSourceInfo mediaSource, AudioOptions options, IApiClient apiClient) 195 | { 196 | if (mediaSource.RequiresOpening) 197 | { 198 | var liveStreamResponse = await apiClient.OpenLiveStream(new LiveStreamRequest(options) 199 | { 200 | OpenToken = mediaSource.OpenToken, 201 | UserId = apiClient.CurrentUserId, 202 | PlaySessionId = playSessionId 203 | 204 | }, CancellationToken.None).ConfigureAwait(false); 205 | 206 | return liveStreamResponse.MediaSource; 207 | } 208 | 209 | return null; 210 | } 211 | 212 | /// 213 | /// Changes the video stream. 214 | /// 215 | /// The current information. 216 | /// The server identifier. 217 | /// The options. 218 | /// The API client. 219 | /// Task<StreamInfo>. 220 | /// 221 | public async Task ChangeVideoStream(StreamInfo currentInfo, string serverId, VideoOptions options, IApiClient apiClient) 222 | { 223 | await StopStranscoding(currentInfo, apiClient).ConfigureAwait(false); 224 | 225 | if (currentInfo.AllMediaSources != null) 226 | { 227 | options.MediaSources = currentInfo.AllMediaSources.ToArray(); 228 | } 229 | 230 | var streamInfo = await GetVideoStreamInfoInternal(serverId, options).ConfigureAwait(false); 231 | streamInfo.PlaySessionId = currentInfo.PlaySessionId; 232 | streamInfo.AllMediaSources = currentInfo.AllMediaSources; 233 | return streamInfo; 234 | } 235 | 236 | /// 237 | /// Gets the video stream information. 238 | /// 239 | /// The server identifier. 240 | /// The options. 241 | /// if set to true [is offline]. 242 | /// The API client. 243 | /// Task<StreamInfo>. 244 | public async Task GetVideoStreamInfo(string serverId, VideoOptions options, bool isOffline, IApiClient apiClient) 245 | { 246 | PlaybackInfoResponse playbackInfo = null; 247 | string playSessionId = null; 248 | 249 | if (!isOffline) 250 | { 251 | playbackInfo = await apiClient.GetPlaybackInfo(new PlaybackInfoRequest 252 | { 253 | Id = options.ItemId, 254 | UserId = apiClient.CurrentUserId, 255 | MaxStreamingBitrate = options.MaxBitrate, 256 | MediaSourceId = options.MediaSourceId, 257 | AudioStreamIndex = options.AudioStreamIndex, 258 | SubtitleStreamIndex = options.SubtitleStreamIndex 259 | 260 | }).ConfigureAwait(false); 261 | 262 | if (playbackInfo.ErrorCode.HasValue) 263 | { 264 | throw new PlaybackException { ErrorCode = playbackInfo.ErrorCode.Value }; 265 | } 266 | 267 | options.MediaSources = playbackInfo.MediaSources; 268 | playSessionId = playbackInfo.PlaySessionId; 269 | } 270 | 271 | var streamInfo = await GetVideoStreamInfoInternal(serverId, options).ConfigureAwait(false); 272 | 273 | if (!isOffline) 274 | { 275 | var liveMediaSource = await GetLiveStreamInfo(playSessionId, streamInfo.MediaSource, options, apiClient).ConfigureAwait(false); 276 | 277 | if (liveMediaSource != null) 278 | { 279 | options.MediaSources = new List { liveMediaSource }.ToArray(); 280 | streamInfo = GetStreamBuilder().BuildVideoItem(options); 281 | EnsureSuccess(streamInfo); 282 | } 283 | } 284 | 285 | if (playbackInfo != null) 286 | { 287 | streamInfo.AllMediaSources = playbackInfo.MediaSources.ToList(); 288 | streamInfo.PlaySessionId = playbackInfo.PlaySessionId; 289 | } 290 | 291 | return streamInfo; 292 | } 293 | 294 | private async Task GetVideoStreamInfoInternal(string serverId, VideoOptions options) 295 | { 296 | var streamBuilder = GetStreamBuilder(); 297 | 298 | var localItem = await _localAssetManager.GetLocalItem(serverId, options.ItemId); 299 | 300 | if (localItem != null) 301 | { 302 | var localMediaSource = localItem.Item.MediaSources[0]; 303 | 304 | // Use the local media source, unless a specific server media source was requested 305 | if (string.IsNullOrWhiteSpace(options.MediaSourceId) || 306 | string.Equals(localMediaSource.Id, options.MediaSourceId, 307 | StringComparison.OrdinalIgnoreCase)) 308 | { 309 | // Finally, check to make sure the local file is actually available at this time 310 | var fileExists = await _localAssetManager.FileExists(localMediaSource.Path).ConfigureAwait(false); 311 | 312 | if (fileExists) 313 | { 314 | options.MediaSources = localItem.Item.MediaSources.ToArray(); 315 | 316 | var result = streamBuilder.BuildVideoItem(options); 317 | if (result == null) 318 | { 319 | _logger.Warn("LocalItem returned no compatible streams. Will dummy up a StreamInfo to force it to direct play."); 320 | var mediaSource = localItem.Item.MediaSources.First(); 321 | result = GetForcedDirectPlayStreamInfo(DlnaProfileType.Video, options, mediaSource); 322 | } 323 | result.PlayMethod = PlayMethod.DirectPlay; 324 | return result; 325 | } 326 | } 327 | } 328 | 329 | var streamInfo = streamBuilder.BuildVideoItem(options); 330 | EnsureSuccess(streamInfo); 331 | return streamInfo; 332 | } 333 | 334 | private StreamInfo GetForcedDirectPlayStreamInfo(DlnaProfileType mediaType, AudioOptions options, MediaSourceInfo mediaSource) 335 | { 336 | return new StreamInfo 337 | { 338 | ItemId = options.ItemId, 339 | MediaType = mediaType, 340 | MediaSource = mediaSource, 341 | RunTimeTicks = mediaSource.RunTimeTicks, 342 | Context = options.Context, 343 | DeviceProfile = options.Profile, 344 | Container = mediaSource.Container, 345 | PlayMethod = PlayMethod.DirectPlay 346 | }; 347 | } 348 | 349 | private void EnsureSuccess(StreamInfo info) 350 | { 351 | if (info == null) 352 | { 353 | throw new PlaybackException 354 | { 355 | ErrorCode = PlaybackErrorCode.NoCompatibleStream 356 | }; 357 | } 358 | } 359 | 360 | /// 361 | /// Reports playback start 362 | /// 363 | /// The information. 364 | /// if set to true [is offline]. 365 | /// The current apiClient. It can be null if offline 366 | /// Task. 367 | public async Task ReportPlaybackStart(PlaybackStartInfo info, bool isOffline, IApiClient apiClient) 368 | { 369 | if (!isOffline) 370 | { 371 | await apiClient.ReportPlaybackStartAsync(info).ConfigureAwait(false); 372 | } 373 | } 374 | 375 | /// 376 | /// Reports playback progress 377 | /// 378 | /// The information. 379 | /// The stream information. 380 | /// if set to true [is offline]. 381 | /// The current apiClient. It can be null if offline 382 | /// Task. 383 | public async Task ReportPlaybackProgress(PlaybackProgressInfo info, StreamInfo streamInfo, bool isOffline, IApiClient apiClient) 384 | { 385 | if (!isOffline) 386 | { 387 | if (streamInfo != null) 388 | { 389 | info.PlaySessionId = streamInfo.PlaySessionId; 390 | 391 | if (streamInfo.MediaSource != null) 392 | { 393 | info.LiveStreamId = streamInfo.MediaSource.LiveStreamId; 394 | } 395 | } 396 | 397 | await apiClient.ReportPlaybackProgressAsync(info).ConfigureAwait(false); 398 | } 399 | } 400 | 401 | /// 402 | /// Reports playback progress 403 | /// 404 | /// The information. 405 | /// The stream information. 406 | /// The server identifier. 407 | /// The user identifier. 408 | /// if set to true [is offline]. 409 | /// The current apiClient. It can be null if offline 410 | /// Task. 411 | public async Task ReportPlaybackStopped(PlaybackStopInfo info, StreamInfo streamInfo, string serverId, string userId, bool isOffline, IApiClient apiClient) 412 | { 413 | if (isOffline) 414 | { 415 | var action = new UserAction 416 | { 417 | Date = DateTime.UtcNow, 418 | ItemId = info.ItemId, 419 | PositionTicks = info.PositionTicks, 420 | ServerId = serverId, 421 | Type = UserActionType.PlayedItem, 422 | UserId = userId 423 | }; 424 | 425 | await _localAssetManager.RecordUserAction(action).ConfigureAwait(false); 426 | return; 427 | } 428 | 429 | if (streamInfo != null) 430 | { 431 | info.PlaySessionId = streamInfo.PlaySessionId; 432 | 433 | if (streamInfo.MediaSource != null) 434 | { 435 | info.LiveStreamId = streamInfo.MediaSource.LiveStreamId; 436 | } 437 | } 438 | 439 | // Put a try/catch here because we need to stop transcoding regardless 440 | try 441 | { 442 | await apiClient.ReportPlaybackStoppedAsync(info).ConfigureAwait(false); 443 | } 444 | catch (Exception ex) 445 | { 446 | _logger.ErrorException("Error in ReportPlaybackStoppedAsync", ex); 447 | } 448 | } 449 | 450 | private async Task StopStranscoding(StreamInfo streamInfo, IApiClient apiClient) 451 | { 452 | if (streamInfo.MediaType != DlnaProfileType.Video) 453 | { 454 | return; 455 | } 456 | 457 | if (streamInfo.PlayMethod != PlayMethod.Transcode) 458 | { 459 | return; 460 | } 461 | 462 | var playSessionId = streamInfo.PlaySessionId; 463 | 464 | try 465 | { 466 | await apiClient.StopTranscodingProcesses(_device.DeviceId, playSessionId).ConfigureAwait(false); 467 | } 468 | catch (Exception ex) 469 | { 470 | _logger.ErrorException("Error in StopStranscoding", ex); 471 | } 472 | } 473 | } 474 | } 475 | --------------------------------------------------------------------------------