diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index 0dfd812c3..e79163d80 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -106,6 +106,7 @@ + diff --git a/MediaBrowser.Api/Social/SharingService.cs b/MediaBrowser.Api/Social/SharingService.cs new file mode 100644 index 000000000..93540f8ca --- /dev/null +++ b/MediaBrowser.Api/Social/SharingService.cs @@ -0,0 +1,138 @@ +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Social; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Social; +using ServiceStack; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace MediaBrowser.Api.Social +{ + [Route("/Social/Shares/{Id}", "GET", Summary = "Gets a share")] + [Authenticated] + public class GetSocialShareInfo : IReturn + { + [ApiMember(Name = "Id", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + } + + [Route("/Social/Shares/Public/{Id}", "GET", Summary = "Gets a share")] + public class GetPublicSocialShareInfo : IReturn + { + [ApiMember(Name = "Id", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + } + + [Route("/Social/Shares/Public/{Id}/Image", "GET", Summary = "Gets a share")] + public class GetShareImage + { + [ApiMember(Name = "Id", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + } + + [Route("/Social/Shares", "POST", Summary = "Creates a share")] + [Authenticated] + public class CreateShare : IReturn + { + [ApiMember(Name = "ItemId", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string ItemId { get; set; } + + [ApiMember(Name = "UserId", Description = "The user id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string UserId { get; set; } + } + + [Route("/Social/Shares/{Id}", "DELETE", Summary = "Deletes a share")] + [Authenticated] + public class DeleteShare : IReturnVoid + { + [ApiMember(Name = "Id", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] + public string Id { get; set; } + } + + public class SharingService : BaseApiService + { + private readonly ISharingManager _sharingManager; + private readonly ILibraryManager _libraryManager; + private readonly IDlnaManager _dlnaManager; + + public SharingService(ISharingManager sharingManager, IDlnaManager dlnaManager, ILibraryManager libraryManager) + { + _sharingManager = sharingManager; + _dlnaManager = dlnaManager; + _libraryManager = libraryManager; + } + + public object Get(GetSocialShareInfo request) + { + var info = _sharingManager.GetShareInfo(request.Id); + + return ToOptimizedResult(info); + } + + public object Get(GetPublicSocialShareInfo request) + { + var info = _sharingManager.GetShareInfo(request.Id); + + if (info.ExpirationDate >= DateTime.UtcNow) + { + throw new ResourceNotFoundException(); + } + + return ToOptimizedResult(info); + } + + public async Task Post(CreateShare request) + { + var info = await _sharingManager.CreateShare(request.ItemId, request.UserId).ConfigureAwait(false); + + return ToOptimizedResult(info); + } + + public void Delete(DeleteShare request) + { + var task = _sharingManager.DeleteShare(request.Id); + Task.WaitAll(task); + } + + public object Get(GetShareImage request) + { + var share = _sharingManager.GetShareInfo(request.Id); + + if (share == null) + { + throw new ResourceNotFoundException(); + } + if (share.ExpirationDate >= DateTime.UtcNow) + { + throw new ResourceNotFoundException(); + } + + var item = _libraryManager.GetItemById(share.ItemId); + + var image = item.GetImageInfo(ImageType.Primary, 0); + + if (image != null) + { + return ToStaticFileResult(image.Path); + } + + // Grab a dlna icon if nothing else is available + using (var response = _dlnaManager.GetIcon("logo240.jpg")) + { + using (var ms = new MemoryStream()) + { + response.Stream.CopyTo(ms); + + ms.Position = 0; + var bytes = ms.ToArray(); + return ResultFactory.GetResult(bytes, "image/" + response.Format.ToString().ToLower()); + } + } + + } + } +} diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 62578e675..fcb938acc 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -329,6 +329,7 @@ + diff --git a/MediaBrowser.Controller/Social/ISharingManager.cs b/MediaBrowser.Controller/Social/ISharingManager.cs new file mode 100644 index 000000000..ded37771a --- /dev/null +++ b/MediaBrowser.Controller/Social/ISharingManager.cs @@ -0,0 +1,28 @@ +using MediaBrowser.Model.Social; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Social +{ + public interface ISharingManager + { + /// + /// Creates the share. + /// + /// The item identifier. + /// The user identifier. + /// Task<SocialShareInfo>. + Task CreateShare(string itemId, string userId); + /// + /// Gets the share information. + /// + /// The identifier. + /// SocialShareInfo. + SocialShareInfo GetShareInfo(string id); + /// + /// Deletes the share. + /// + /// The identifier. + /// Task. + Task DeleteShare(string id); + } +} diff --git a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj index 3238e79b7..a1b7ce396 100644 --- a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj +++ b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj @@ -1076,6 +1076,9 @@ Session\UserDataChangeInfo.cs + + Social\SocialShareInfo.cs + Sync\CompleteSyncJobInfo.cs diff --git a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj index be7277607..d4a373733 100644 --- a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj +++ b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj @@ -1032,6 +1032,9 @@ Session\UserDataChangeInfo.cs + + Social\SocialShareInfo.cs + Sync\CompleteSyncJobInfo.cs diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index e7490b3fa..19403a55e 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -25,7 +25,7 @@ namespace MediaBrowser.Model.Configuration /// /// The public HTTPS port. public int PublicHttpsPort { get; set; } - + /// /// Gets or sets the HTTP server port number. /// @@ -49,7 +49,7 @@ namespace MediaBrowser.Model.Configuration /// /// true if [enable user specific user views]; otherwise, false. public bool EnableUserSpecificUserViews { get; set; } - + /// /// Gets or sets the value pointing to the file system where the ssl certiifcate is located.. /// @@ -103,7 +103,7 @@ namespace MediaBrowser.Model.Configuration /// /// true if [enable library metadata sub folder]; otherwise, false. public bool EnableLibraryMetadataSubFolder { get; set; } - + /// /// Gets or sets the preferred metadata language. /// @@ -211,6 +211,8 @@ namespace MediaBrowser.Model.Configuration public AutoOnOff EnableLibraryMonitor { get; set; } + public int SharingExpirationDays { get; set; } + /// /// Initializes a new instance of the class. /// @@ -231,6 +233,7 @@ namespace MediaBrowser.Model.Configuration EnableUPnP = true; + SharingExpirationDays = 30; MinResumePct = 5; MaxResumePct = 90; diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 3daacdd73..b36fa2362 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -379,6 +379,7 @@ + diff --git a/MediaBrowser.Model/Social/SocialShareInfo.cs b/MediaBrowser.Model/Social/SocialShareInfo.cs new file mode 100644 index 000000000..1b1c225c4 --- /dev/null +++ b/MediaBrowser.Model/Social/SocialShareInfo.cs @@ -0,0 +1,16 @@ +using System; + +namespace MediaBrowser.Model.Social +{ + public class SocialShareInfo + { + public string Id { get; set; } + public string Url { get; set; } + public string ItemId { get; set; } + public string UserId { get; set; } + public DateTime ExpirationDate { get; set; } + public string Name { get; set; } + public string ImageUrl { get; set; } + public string Overview { get; set; } + } +} diff --git a/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json b/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json index 8bc244891..254e3c2f3 100644 --- a/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json +++ b/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json @@ -38,7 +38,7 @@ "HeaderSupportTheTeam": "Support the Emby Team", "TextEnjoyBonusFeatures": "Enjoy Bonus Features", "TitleLiveTV": "Live TV", - "ButtonCancelSyncJob": "Cancel sync job", + "ButtonCancelSyncJob": "Cancel sync job", "TitleSync": "Sync", "HeaderSelectDate": "Select Date", "ButtonDonate": "Donate", @@ -811,5 +811,8 @@ "ErrorMessagePasswordNotMatchConfirm": "The password and password confirmation must match.", "ErrorMessageUsernameInUse": "The username is already in use. Please choose a new name and try again.", "ErrorMessageEmailInUse": "The email address is already in use. Please enter a new email address and try again, or use the forgot password feature.", - "MessageThankYouForConnectSignUp": "Thank you for signing up for Emby Connect. An email will be sent to your address with instructions on how to confirm your new account. Please confirm the account and then return here to sign in." + "MessageThankYouForConnectSignUp": "Thank you for signing up for Emby Connect. An email will be sent to your address with instructions on how to confirm your new account. Please confirm the account and then return here to sign in.", + "HeaderShare": "Share", + "ButtonShareHelp": "Only a web page containing media information will be shared. Media files are never shared publicly." + } diff --git a/MediaBrowser.Server.Implementations/Localization/Server/server.json b/MediaBrowser.Server.Implementations/Localization/Server/server.json index d8628575a..bec5755b9 100644 --- a/MediaBrowser.Server.Implementations/Localization/Server/server.json +++ b/MediaBrowser.Server.Implementations/Localization/Server/server.json @@ -313,6 +313,9 @@ "OptionAllowRemoteControlOthers": "Allow remote control of other users", "OptionAllowRemoteSharedDevices": "Allow remote control of shared devices", "OptionAllowRemoteSharedDevicesHelp": "Dlna devices are considered shared until a user begins controlling it.", + "OptionAllowLinkSharing": "Allow social media sharing", + "OptionAllowLinkSharingHelp": "Only web pages containing media information are shared. Media files are never shared publicly. Shares are time-limited and will expire based on your server sharing settings.", + "HeaderSharing": "Sharing", "HeaderRemoteControl": "Remote Control", "OptionMissingTmdbId": "Missing Tmdb Id", "OptionIsHD": "HD", diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index b461fb78b..33b2493f5 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -227,6 +227,8 @@ + + diff --git a/MediaBrowser.Server.Implementations/Social/SharingManager.cs b/MediaBrowser.Server.Implementations/Social/SharingManager.cs new file mode 100644 index 000000000..1c3f35389 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Social/SharingManager.cs @@ -0,0 +1,88 @@ +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Social; +using MediaBrowser.Model.Social; +using System; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.Social +{ + public class SharingManager : ISharingManager + { + private readonly SharingRepository _repository; + private readonly IServerConfigurationManager _config; + private readonly ILibraryManager _libraryManager; + private readonly IServerApplicationHost _appHost; + + public SharingManager(SharingRepository repository, IServerConfigurationManager config, ILibraryManager libraryManager, IServerApplicationHost appHost) + { + _repository = repository; + _config = config; + _libraryManager = libraryManager; + _appHost = appHost; + } + + public async Task CreateShare(string itemId, string userId) + { + if (string.IsNullOrWhiteSpace(itemId)) + { + throw new ArgumentNullException("itemId"); + } + if (string.IsNullOrWhiteSpace(userId)) + { + throw new ArgumentNullException("userId"); + } + + var item = _libraryManager.GetItemById(itemId); + + if (item == null) + { + throw new ResourceNotFoundException(); + } + + var externalUrl = _appHost.GetSystemInfo().WanAddress; + + if (string.IsNullOrWhiteSpace(externalUrl)) + { + throw new InvalidOperationException("No external server address is currently available."); + } + + var info = new SocialShareInfo + { + Id = Guid.NewGuid().ToString("N"), + ExpirationDate = DateTime.UtcNow.AddDays(_config.Configuration.SharingExpirationDays), + ItemId = itemId, + UserId = userId, + Overview = item.Overview, + Name = GetTitle(item) + }; + + info.ImageUrl = externalUrl + "/Social/Shares/Public/" + info.Id + "/Image"; + info.ImageUrl = externalUrl + "/web/shared.html?id=" + info.Id; + + await _repository.CreateShare(info).ConfigureAwait(false); + + return GetShareInfo(info.Id); + } + + private string GetTitle(BaseItem item) + { + return item.Name; + } + + public SocialShareInfo GetShareInfo(string id) + { + var info = _repository.GetShareInfo(id); + + return info; + } + + public Task DeleteShare(string id) + { + return _repository.DeleteShare(id); + } + } +} diff --git a/MediaBrowser.Server.Implementations/Social/SharingRepository.cs b/MediaBrowser.Server.Implementations/Social/SharingRepository.cs new file mode 100644 index 000000000..d6d7f021a --- /dev/null +++ b/MediaBrowser.Server.Implementations/Social/SharingRepository.cs @@ -0,0 +1,184 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Social; +using MediaBrowser.Server.Implementations.Persistence; +using System; +using System.Data; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.Social +{ + public class SharingRepository : BaseSqliteRepository + { + private IDbConnection _connection; + private IDbCommand _saveShareCommand; + private readonly IApplicationPaths _appPaths; + + public SharingRepository(ILogManager logManager, IApplicationPaths appPaths) + : base(logManager) + { + _appPaths = appPaths; + } + + /// + /// Opens the connection to the database + /// + /// Task. + public async Task Initialize() + { + var dbFile = Path.Combine(_appPaths.DataPath, "shares.db"); + + _connection = await SqliteExtensions.ConnectToDb(dbFile, Logger).ConfigureAwait(false); + + string[] queries = { + + "create table if not exists Shares (Id GUID, ItemId TEXT, UserId TEXT, ExpirationDate DateTime, PRIMARY KEY (Id))", + "create index if not exists idx_Shares on Shares(Id)", + + //pragmas + "pragma temp_store = memory", + + "pragma shrink_memory" + }; + + _connection.RunQueries(queries, Logger); + + PrepareStatements(); + } + + /// + /// Prepares the statements. + /// + private void PrepareStatements() + { + _saveShareCommand = _connection.CreateCommand(); + _saveShareCommand.CommandText = "replace into Shares (Id, ItemId, UserId, ExpirationDate) values (@Id, @ItemId, @UserId, @ExpirationDate)"; + + _saveShareCommand.Parameters.Add(_saveShareCommand, "@Id"); + _saveShareCommand.Parameters.Add(_saveShareCommand, "@ItemId"); + _saveShareCommand.Parameters.Add(_saveShareCommand, "@UserId"); + _saveShareCommand.Parameters.Add(_saveShareCommand, "@ExpirationDate"); + } + + public async Task CreateShare(SocialShareInfo info) + { + if (info == null) + { + throw new ArgumentNullException("info"); + } + if (string.IsNullOrWhiteSpace(info.Id)) + { + throw new ArgumentNullException("info.Id"); + } + + var cancellationToken = CancellationToken.None; + + cancellationToken.ThrowIfCancellationRequested(); + + await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + IDbTransaction transaction = null; + + try + { + transaction = _connection.BeginTransaction(); + + _saveShareCommand.GetParameter(0).Value = new Guid(info.Id); + _saveShareCommand.GetParameter(1).Value = info.ItemId; + _saveShareCommand.GetParameter(2).Value = info.UserId; + _saveShareCommand.GetParameter(3).Value = info.ExpirationDate; + + _saveShareCommand.Transaction = transaction; + + _saveShareCommand.ExecuteNonQuery(); + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save share:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + + WriteLock.Release(); + } + } + + public SocialShareInfo GetShareInfo(string id) + { + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentNullException("id"); + } + + var cmd = _connection.CreateCommand(); + cmd.CommandText = "select Id, ItemId, UserId, ExpirationDate from Shares where id = @id"; + + cmd.Parameters.Add(cmd, "@id", DbType.Guid).Value = new Guid(id); + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) + { + if (reader.Read()) + { + return GetSocialShareInfo(reader); + } + } + + return null; + } + + private SocialShareInfo GetSocialShareInfo(IDataReader reader) + { + var info = new SocialShareInfo(); + + info.Id = reader.GetGuid(0).ToString("N"); + info.ItemId = reader.GetString(1); + info.UserId = reader.GetString(2); + info.ExpirationDate = reader.GetDateTime(3).ToUniversalTime(); + + return info; + } + + public async Task DeleteShare(string id) + { + + } + + protected override void CloseConnection() + { + if (_connection != null) + { + if (_connection.IsOpen()) + { + _connection.Close(); + } + + _connection.Dispose(); + _connection = null; + } + } + } +} diff --git a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs index bed3aac63..fab6682d7 100644 --- a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs +++ b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs @@ -38,6 +38,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.Social; using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Controller.Sync; @@ -84,6 +85,7 @@ using MediaBrowser.Server.Implementations.Playlists; using MediaBrowser.Server.Implementations.Security; using MediaBrowser.Server.Implementations.ServerManager; using MediaBrowser.Server.Implementations.Session; +using MediaBrowser.Server.Implementations.Social; using MediaBrowser.Server.Implementations.Sync; using MediaBrowser.Server.Implementations.Themes; using MediaBrowser.Server.Implementations.TV; @@ -522,6 +524,10 @@ namespace MediaBrowser.Server.Startup.Common MediaEncoder, ChapterManager); RegisterSingleInstance(EncodingManager); + var sharingRepo = new SharingRepository(LogManager, ApplicationPaths); + await sharingRepo.Initialize().ConfigureAwait(false); + RegisterSingleInstance(new SharingManager(sharingRepo, ServerConfigurationManager, LibraryManager, this)); + RegisterSingleInstance(new SsdpHandler(LogManager.GetLogger("SsdpHandler"), ServerConfigurationManager, this)); var activityLogRepo = await GetActivityLogRepository().ConfigureAwait(false); diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index 08c366c7a..5b1a88703 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -193,6 +193,15 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -274,6 +283,17 @@ PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + PreserveNewest + PreserveNewest @@ -2416,6 +2436,17 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + +