mirror of
https://github.com/sphildreth/roadie
synced 2024-11-22 12:13:10 +00:00
Work on Subsonic layer using JamStash for testing.
This commit is contained in:
parent
e150e51891
commit
e893532680
5 changed files with 465 additions and 8 deletions
|
@ -1,18 +1,20 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Roadie.Api.Services;
|
||||
using Roadie.Library.Caching;
|
||||
using Roadie.Library.Identity;
|
||||
using Roadie.Library.Models.ThirdPartyApi.Subsonic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Roadie.Api.Controllers
|
||||
{
|
||||
[Produces("application/json")]
|
||||
[Route("subsonic")]
|
||||
//[Produces("application/json")]
|
||||
[Route("subsonic/rest")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
//[Authorize]
|
||||
public class SubsonicController : EntityControllerBase
|
||||
{
|
||||
private ISubsonicService SubsonicService { get; }
|
||||
|
@ -23,5 +25,61 @@ namespace Roadie.Api.Controllers
|
|||
this._logger = logger.CreateLogger("RoadieApi.Controllers.SubsonicController");
|
||||
this.SubsonicService = subsonicService;
|
||||
}
|
||||
|
||||
[HttpGet("getIndexes.view")]
|
||||
[ProducesResponseType(200)]
|
||||
public async Task<IActionResult> GetIndexes([FromQuery]Request request, string musicFolderId = null, long? ifModifiedSince = null)
|
||||
{
|
||||
var result = await this.SubsonicService.GetIndexes(request, musicFolderId, ifModifiedSince);
|
||||
return this.BuildResponse(request, result.Data, "indexes");
|
||||
}
|
||||
|
||||
[HttpGet("getMusicFolders.view")]
|
||||
[ProducesResponseType(200)]
|
||||
public async Task<IActionResult> GetMusicFolders([FromQuery]Request request)
|
||||
{
|
||||
var result = await this.SubsonicService.GetMusicFolders(request);
|
||||
return this.BuildResponse(request, result.Data, "musicFolders");
|
||||
}
|
||||
|
||||
[HttpGet("getPlaylists.view")]
|
||||
[ProducesResponseType(200)]
|
||||
public async Task<IActionResult> GetPlaylists([FromQuery]Request request, string username)
|
||||
{
|
||||
var result = await this.SubsonicService.GetPlaylists(request, null, username);
|
||||
return this.BuildResponse(request, result.Data, "playlists");
|
||||
}
|
||||
|
||||
[HttpGet("ping.view")]
|
||||
[HttpPost("ping.view")]
|
||||
[ProducesResponseType(200)]
|
||||
public IActionResult Ping([FromQuery]Request request)
|
||||
{
|
||||
var result = this.SubsonicService.Ping(request);
|
||||
return this.BuildResponse(request, result.Data);
|
||||
}
|
||||
|
||||
#region Response Builder Methods
|
||||
|
||||
private string BuildJsonResult(Response response, string responseType)
|
||||
{
|
||||
if (responseType == null)
|
||||
{
|
||||
return "{ \"subsonic-response\": { \"status\":\"" + response.status.ToString() + "\", \"version\": \"" + response.version + "\" }}";
|
||||
}
|
||||
return "{ \"subsonic-response\": { \"status\":\"" + response.status.ToString() + "\", \"version\": \"" + response.version + "\", \"" + responseType + "\":" + JsonConvert.SerializeObject(response.Item) + "}}";
|
||||
}
|
||||
|
||||
private IActionResult BuildResponse(Request request, Response response = null, string reponseType = null)
|
||||
{
|
||||
if (request.IsJSONRequest)
|
||||
{
|
||||
this.Response.ContentType = "application/json";
|
||||
return Content(this.BuildJsonResult(response, reponseType));
|
||||
}
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
#endregion Response Builder Methods
|
||||
}
|
||||
}
|
|
@ -1,6 +1,21 @@
|
|||
namespace Roadie.Api.Services
|
||||
using Roadie.Library;
|
||||
using Roadie.Library.Models.ThirdPartyApi.Subsonic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Roadie.Api.Services
|
||||
{
|
||||
public interface ISubsonicService
|
||||
{
|
||||
Task<OperationResult<Response>> GetGenres(Request request);
|
||||
|
||||
Task<OperationResult<Response>> GetIndexes(Request request, string musicFolderId = null, long? ifModifiedSince = null);
|
||||
|
||||
Task<OperationResult<Response>> GetMusicFolders(Request request);
|
||||
|
||||
Task<OperationResult<Response>> GetPlaylists(Request request, Roadie.Library.Models.Users.User roadieUser, string filterToUserName);
|
||||
|
||||
Task<OperationResult<Response>> GetPodcasts(Request request);
|
||||
|
||||
OperationResult<Response> Ping(Request request);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,25 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Mapster;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Roadie.Library;
|
||||
using Roadie.Library.Caching;
|
||||
using Roadie.Library.Configuration;
|
||||
using Roadie.Library.Encoding;
|
||||
using Roadie.Library.Enums;
|
||||
using Roadie.Library.Extensions;
|
||||
using Roadie.Library.Models;
|
||||
using Roadie.Library.Models.Pagination;
|
||||
using Roadie.Library.Models.Releases;
|
||||
using Roadie.Library.Models.Statistics;
|
||||
using subsonic = Roadie.Library.Models.ThirdPartyApi.Subsonic;
|
||||
using Roadie.Library.Models.Users;
|
||||
using Roadie.Library.Utility;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Linq.Dynamic.Core;
|
||||
using System.Threading.Tasks;
|
||||
using data = Roadie.Library.Data;
|
||||
|
||||
namespace Roadie.Api.Services
|
||||
|
@ -15,6 +32,10 @@ namespace Roadie.Api.Services
|
|||
/// </summary>
|
||||
public class SubsonicService : ServiceBase, ISubsonicService
|
||||
{
|
||||
public const string ArtistIdIdentifier = "A:";
|
||||
public const string CollectionIdentifier = "C:";
|
||||
public const string SubsonicVersion = "1.16.1";
|
||||
|
||||
public SubsonicService(IRoadieSettings configuration,
|
||||
IHttpEncoder httpEncoder,
|
||||
IHttpContext httpContext,
|
||||
|
@ -26,5 +47,199 @@ namespace Roadie.Api.Services
|
|||
: base(configuration, httpEncoder, context, cacheManager, logger, httpContext)
|
||||
{
|
||||
}
|
||||
|
||||
public OperationResult<subsonic.Response> Ping(subsonic.Request request)
|
||||
{
|
||||
return new OperationResult<subsonic.Response>
|
||||
{
|
||||
IsSuccess = true,
|
||||
Data = new subsonic.Response
|
||||
{
|
||||
version = SubsonicService.SubsonicVersion,
|
||||
status = subsonic.ResponseStatus.ok
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all configured top-level music folders. Takes no extra parameters.
|
||||
/// </summary>
|
||||
public async Task<OperationResult<subsonic.Response>> GetMusicFolders(subsonic.Request request)
|
||||
{
|
||||
return new OperationResult<subsonic.Response>
|
||||
{
|
||||
IsSuccess = true,
|
||||
Data = new subsonic.Response
|
||||
{
|
||||
version = SubsonicService.SubsonicVersion,
|
||||
status = subsonic.ResponseStatus.ok,
|
||||
ItemElementName = subsonic.ItemChoiceType.musicFolders,
|
||||
Item = new subsonic.MusicFolders
|
||||
{
|
||||
musicFolder = this.MusicFolders().ToArray()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public List<subsonic.MusicFolder> MusicFolders()
|
||||
{
|
||||
return new List<subsonic.MusicFolder>
|
||||
{
|
||||
new subsonic.MusicFolder { id = 1, name = "Collections"},
|
||||
new subsonic.MusicFolder { id = 2, name = "Music"}
|
||||
};
|
||||
}
|
||||
|
||||
public subsonic.MusicFolder CollectionMusicFolder()
|
||||
{
|
||||
return this.MusicFolders().First(x => x.id == 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an indexed structure of all artists.
|
||||
/// </summary>
|
||||
/// <param name="request">Query from application.</param>
|
||||
/// <param name="musicFolderId">If specified, only return artists in the music folder with the given ID.</param>
|
||||
/// <param name="ifModifiedSince">If specified, only return a result if the artist collection has changed since the given time (in milliseconds since 1 Jan 1970).</param>
|
||||
public async Task<OperationResult<subsonic.Response>> GetIndexes(subsonic.Request request, string musicFolderId = null, long? ifModifiedSince = null)
|
||||
{
|
||||
|
||||
var modifiedSinceFilter = ifModifiedSince.HasValue ? (DateTime?)ifModifiedSince.Value.FromUnixTime() : null;
|
||||
subsonic.MusicFolder musicFolderFilter = string.IsNullOrEmpty(musicFolderId) ? null : this.MusicFolders().FirstOrDefault(x => x.id == SafeParser.ToNumber<int>(musicFolderId));
|
||||
var indexes = new List<subsonic.Index>();
|
||||
|
||||
if (musicFolderFilter == this.CollectionMusicFolder())
|
||||
{
|
||||
// Collections for Music Folders by Alpha First
|
||||
foreach (var collectionFirstLetter in (from c in this.DbContext.Collections
|
||||
let first = c.Name.Substring(0, 1)
|
||||
orderby first
|
||||
select first).Distinct().ToArray())
|
||||
{
|
||||
indexes.Add(new subsonic.Index
|
||||
{
|
||||
name = collectionFirstLetter,
|
||||
artist = (from c in this.DbContext.Collections
|
||||
where c.Name.Substring(0, 1) == collectionFirstLetter
|
||||
where modifiedSinceFilter == null || c.LastUpdated >= modifiedSinceFilter
|
||||
orderby c.SortName, c.Name
|
||||
select new subsonic.Artist
|
||||
{
|
||||
id = SubsonicService.CollectionIdentifier + c.RoadieId.ToString(),
|
||||
name = c.Name,
|
||||
artistImageUrl = this.MakeCollectionThumbnailImage(c.RoadieId).Url,
|
||||
averageRating = 0,
|
||||
userRating = 0
|
||||
}).ToArray()
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Indexes for Artists alphabetically
|
||||
foreach (var artistFirstLetter in (from c in this.DbContext.Artists
|
||||
let first = c.Name.Substring(0, 1)
|
||||
orderby first
|
||||
select first).Distinct().ToArray())
|
||||
{
|
||||
indexes.Add(new subsonic.Index
|
||||
{
|
||||
name = artistFirstLetter,
|
||||
artist = (from c in this.DbContext.Artists
|
||||
where c.Name.Substring(0, 1) == artistFirstLetter
|
||||
where modifiedSinceFilter == null || c.LastUpdated >= modifiedSinceFilter
|
||||
orderby c.SortName, c.Name
|
||||
select new subsonic.Artist
|
||||
{
|
||||
id = SubsonicService.ArtistIdIdentifier + c.RoadieId.ToString(),
|
||||
name = c.Name,
|
||||
artistImageUrl = this.MakeArtistThumbnailImage(c.RoadieId).Url,
|
||||
averageRating = 0,
|
||||
userRating = 0
|
||||
}).ToArray()
|
||||
});
|
||||
}
|
||||
}
|
||||
return new OperationResult<subsonic.Response>
|
||||
{
|
||||
IsSuccess = true,
|
||||
Data = new subsonic.Response
|
||||
{
|
||||
version = SubsonicService.SubsonicVersion,
|
||||
status = subsonic.ResponseStatus.ok,
|
||||
ItemElementName = subsonic.ItemChoiceType.indexes,
|
||||
Item = new subsonic.Indexes
|
||||
{
|
||||
index = indexes.ToArray()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<OperationResult<subsonic.Response>> GetPlaylists(subsonic.Request request, User roadieUser, string filterToUserName)
|
||||
{
|
||||
var playlists = (from playlist in this.DbContext.Playlists
|
||||
join u in this.DbContext.Users on playlist.UserId equals u.Id
|
||||
let trackCount = (from pl in this.DbContext.PlaylistTracks
|
||||
where pl.PlayListId == playlist.Id
|
||||
select pl.Id).Count()
|
||||
let playListDuration = (from pl in this.DbContext.PlaylistTracks
|
||||
join t in this.DbContext.Tracks on pl.TrackId equals t.Id
|
||||
where pl.PlayListId == playlist.Id
|
||||
select t.Duration).Sum()
|
||||
where (playlist.IsPublic) || (roadieUser != null && playlist.UserId == roadieUser.Id)
|
||||
select new subsonic.Playlist
|
||||
{
|
||||
id = playlist.RoadieId.ToString(),
|
||||
name = playlist.Name,
|
||||
comment = playlist.Description,
|
||||
owner = u.Username,
|
||||
songCount = trackCount,
|
||||
duration = playListDuration ?? 0,
|
||||
created = playlist.CreatedDate,
|
||||
changed = playlist.LastUpdated ?? playlist.CreatedDate,
|
||||
coverArt = this.MakePlaylistThumbnailImage(playlist.RoadieId).Url,
|
||||
@public = playlist.IsPublic,
|
||||
publicSpecified = playlist.IsPublic
|
||||
}
|
||||
);
|
||||
|
||||
return new OperationResult<subsonic.Response>
|
||||
{
|
||||
IsSuccess = true,
|
||||
Data = new subsonic.Response
|
||||
{
|
||||
version = SubsonicService.SubsonicVersion,
|
||||
status = subsonic.ResponseStatus.ok,
|
||||
ItemElementName = subsonic.ItemChoiceType.playlists,
|
||||
Item = new subsonic.Playlists
|
||||
{
|
||||
playlist = playlists.ToArray()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<OperationResult<subsonic.Response>> GetGenres(subsonic.Request request)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task<OperationResult<subsonic.Response>> GetPodcasts(subsonic.Request request)
|
||||
{
|
||||
return new OperationResult<subsonic.Response>
|
||||
{
|
||||
IsSuccess = true,
|
||||
Data = new subsonic.Response
|
||||
{
|
||||
version = SubsonicService.SubsonicVersion,
|
||||
status = subsonic.ResponseStatus.ok,
|
||||
ItemElementName = subsonic.ItemChoiceType.podcasts,
|
||||
Item = new object[0]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -181,12 +182,16 @@ namespace Roadie.Api
|
|||
|
||||
services.AddSignalR();
|
||||
|
||||
services.AddMvc()
|
||||
services.AddMvc(options =>
|
||||
{
|
||||
options.RespectBrowserAcceptHeader = true; // false by default
|
||||
})
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
|
||||
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
|
||||
})
|
||||
.AddXmlSerializerFormatters()
|
||||
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
|
||||
|
||||
services.AddHttpContextAccessor();
|
||||
|
|
164
RoadieLibrary/Models/ThirdPartyApi/Subsonic/Request.cs
Normal file
164
RoadieLibrary/Models/ThirdPartyApi/Subsonic/Request.cs
Normal file
|
@ -0,0 +1,164 @@
|
|||
using Roadie.Library.Extensions;
|
||||
using System;
|
||||
|
||||
namespace Roadie.Library.Models.ThirdPartyApi.Subsonic
|
||||
{
|
||||
[Serializable]
|
||||
public class Request
|
||||
{
|
||||
/// <summary>
|
||||
/// A unique string identifying the client application.
|
||||
/// </summary>
|
||||
public string c { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <seealso cref="f"/>
|
||||
/// </summary>
|
||||
public string callback { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Request data to be returned in this format. Supported values are "xml", "json" (since 1.4.0) and "jsonp" (since 1.6.0). If using jsonp, specify name of javascript callback function using a callback parameter.
|
||||
/// </summary>
|
||||
public string f { get; set; }
|
||||
|
||||
public bool IsCallbackSet
|
||||
{
|
||||
get
|
||||
{
|
||||
return !string.IsNullOrEmpty(this.callback);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public bool IsJSONRequest
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(this.f))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return this.f.ToLower().StartsWith("j");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The password, either in clear text or hex-encoded with a "enc:" prefix. Since 1.13.0 this should only be used for testing purposes.
|
||||
/// </summary>
|
||||
public string p { get; set; }
|
||||
|
||||
public string Password
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(this.p))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (this.p.StartsWith("enc:"))
|
||||
{
|
||||
return this.p.ToLower().Replace("enc:", "").FromHexString();
|
||||
}
|
||||
return this.p;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A random string ("salt") used as input for computing the password hash. See below for details.
|
||||
/// </summary>
|
||||
public string s { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The authentication token computed as md5(password + salt). See below for details
|
||||
/// </summary>
|
||||
public string t { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The username
|
||||
/// </summary>
|
||||
public string u { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The protocol version implemented by the client, i.e., the version of the subsonic-rest-api.xsd schema used (see below).
|
||||
/// </summary>
|
||||
public string v { get; set; }
|
||||
|
||||
//public user CheckPasswordGetUser(ICacheManager<object> cacheManager, RoadieDbContext context)
|
||||
//{
|
||||
// user user = null;
|
||||
// if (string.IsNullOrEmpty(this.UsernameValue))
|
||||
// {
|
||||
// return null;
|
||||
// }
|
||||
// try
|
||||
// {
|
||||
// var cacheKey = string.Format("urn:user:byusername:{0}", this.UsernameValue.ToLower());
|
||||
// var resultInCache = cacheManager.Get<user>(cacheKey);
|
||||
// if (resultInCache == null)
|
||||
// {
|
||||
// user = context.users.FirstOrDefault(x => x.username.Equals(this.UsernameValue, StringComparison.OrdinalIgnoreCase));
|
||||
// var claims = new List<string>
|
||||
// {
|
||||
// new Claim(Library.Authentication.ClaimTypes.UserId, user.id.ToString()).ToString()
|
||||
// };
|
||||
// var sql = @"select ur.name FROM `userrole` ur LEFT JOIN usersInRoles uir on ur.id = uir.userRoleId where uir.userId = " + user.id + ";";
|
||||
// var userRoles = context.Database.SqlQuery<string>(sql).ToList();
|
||||
// if (userRoles != null && userRoles.Any())
|
||||
// {
|
||||
// foreach (var userRole in userRoles)
|
||||
// {
|
||||
// claims.Add(new Claim(Library.Authentication.ClaimTypes.UserRole, userRole).ToString());
|
||||
// }
|
||||
// }
|
||||
// user.ClaimsValue = claims;
|
||||
// cacheManager.Add(cacheKey, user);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// user = resultInCache;
|
||||
// }
|
||||
// if (user == null)
|
||||
// {
|
||||
// return null;
|
||||
// }
|
||||
// var password = this.Password;
|
||||
// var wasAuthenticatedAgainstPassword = false;
|
||||
// if (!string.IsNullOrEmpty(this.s))
|
||||
// {
|
||||
// var token = ModuleBase.MD5Hash((user.apiToken ?? user.email) + this.s);
|
||||
// if (!token.Equals(this.t, StringComparison.OrdinalIgnoreCase))
|
||||
// {
|
||||
// user = null;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// wasAuthenticatedAgainstPassword = true;
|
||||
// }
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// if (user != null && !BCrypt.Net.BCrypt.Verify(password, user.password))
|
||||
// {
|
||||
// user = null;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// wasAuthenticatedAgainstPassword = true;
|
||||
// }
|
||||
// }
|
||||
// if (wasAuthenticatedAgainstPassword)
|
||||
// {
|
||||
// // Since API dont update LastLogin which likely invalidates any browser logins
|
||||
// user.lastApiAccess = DateTime.UtcNow;
|
||||
// context.SaveChanges();
|
||||
// }
|
||||
// return user;
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Trace.WriteLine("Error CheckPassword [" + ex.Serialize() + "]");
|
||||
// }
|
||||
// return null;
|
||||
//}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue