diff --git a/.dockerignore b/.dockerignore index 3729ff0..efcde58 100644 --- a/.dockerignore +++ b/.dockerignore @@ -22,4 +22,7 @@ **/secrets.dev.yaml **/values.dev.yaml LICENSE -README.md \ No newline at end of file +README.md + +#ignore nginx so loadbalancer changes don't trigger a rebuild of the rest of the application +/nginx diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b8f4227 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ + +# Format: rule = severity # defintion; justification +dotnet_diagnostic.ASP0014.severity = none # Don't use UseEndpoints in trivial APIs; we have some simplistic program.cs, and still need to manually register UseEndpoints +dotnet_diagnostic.CA2254.severity = none # use logging formatter instead of interpolation; formatted strings in logs give me the creeps - also, too many nights oncall sorting impossible to read logs diff --git a/.gitignore b/.gitignore index 8e5c6e6..6b0b210 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +*/DevConstants.cs + # User-specific files *.rsuser *.suo diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..cabae57 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/Zune.Net.MetaServices/bin/Debug/net7.0/Zune.Net.MetaServices.dll", + "args": [], + "cwd": "${workspaceFolder}/Zune.Net.MetaServices", + "stopAtEntry": false, + // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser + // "serverReadyAction": { + // "action": "openExternally", + // "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + // }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..cb86632 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/Zune.Net.MetaServices/Zune.Net.MetaServices.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/Zune.Net.MetaServices/Zune.Net.MetaServices.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/Zune.Net.MetaServices/Zune.Net.MetaServices.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Atom/Atom.csproj b/Atom/Atom.csproj index 64ca019..f0286c3 100644 --- a/Atom/Atom.csproj +++ b/Atom/Atom.csproj @@ -6,6 +6,10 @@ 0.1.0 + + false + + diff --git a/Zune.DB.Console/Program.cs b/Zune.DB.Console/Program.cs index c8cafbf..aae365a 100644 --- a/Zune.DB.Console/Program.cs +++ b/Zune.DB.Console/Program.cs @@ -15,7 +15,7 @@ class Program static async Task Main(string[] args) { // Set up CLI options - string connectionString = "mongodb://localhost:27017"; + string connectionString = "mongodb://root:rootpassword@192.168.1.2:27017"; string dbName = "Zune"; var options = new OptionSet { @@ -43,25 +43,33 @@ static async Task Main(string[] args) DatabaseName = dbName, }); + await ctx.ClearAlbumLookupAsync(); + return; + await ctx.ClearMembersAsync(); - await ctx.ClearTokensAsync(); - await ctx.ClearImagesAsync(); + // await ctx.ClearTokensAsync(); + // await ctx.ClearImagesAsync(); + + string userName = "EmailAddress@live.com"; + string SID = "S-1-5-21-414912484-GET YOUR OWN PUNK"; + + // var user = await ctx.GetMemberByName(userName); - string userName = "yoshiask@escargot.chat"; var newMember = new Member { Updated = DateTime.UtcNow, Id = Member.GetGuidFromUserName(userName), + SID = SID, // This is my LiveID SID UserName = userName, PlayCount = 206, Xuid = Member.GetXuidFromUserName(userName), - ZuneTag = "YoshiAsk", - DisplayName = "Yoshi Askharoun", + ZuneTag = "xerootg", + DisplayName = "xerootg", Status = "Reviving the Zune social", - Bio = "A computer science student at Texas A&M Univserity that can't help but bring back dead Microsoft products.", - Location = "College Station, Texas", - UserTile = "http://tiles.zunes.me/tiles/avatar/default.jpg", - Background = "http://tiles.zunes.me/tiles/background/USERBACKGROUND-ART-536X196-49.jpg", + Bio = "resident hacker", + Location = "the internet", + UserTile = "http://tiles.zune.net/tiles/avatar/default.jpg", + Background = "http://tiles.zune.net/tiles/background/USERBACKGROUND-ART-536X196-49.jpg", AcceptedTermsOfService = true, AccountSuspended = false, @@ -79,92 +87,58 @@ static async Task Main(string[] args) BillingInstanceId = "6cba2616-c59a-4dd5-bc9e-d41a45215cfa" }; - var tuner = new Tuner - { - Id = "6cba2616-c59a-4dd5-bc9e-d41a45215cfb" - }; - newMember.TunerRegisterInfo = tuner; - - Guid msgId = Guid.Parse("7e366cd9-6d16-4ddf-9dfe-963acdef4450"); - Guid linkId = Guid.Parse("fe9ab096-a072-475b-8e24-0aaacf32852d"); - var message = new Message - { - DetailsLink = string.Empty, - Sender = newMember, - Status = "hi", - Wishlist = false, - MediaId = msgId, - Subject = "Microsoft revives long-dead Zune product line", - Received = DateTime.UtcNow, - Type = "message", - Id = msgId.ToString(), - AltLink = new Link - { - Href = "https://rr.noordstar.me/microsoft-revives-long-dead-zune-product--f9628d12", - Id = linkId.ToString() - }, - TextContent = "Tech giant Microsoft announced early Monday morning that a new Zune music player is in the works", - }; + + // Guid msgId = Guid.Parse("7e366cd9-6d16-4ddf-9dfe-963acdef4450"); + // Guid linkId = Guid.Parse("fe9ab096-a072-475b-8e24-0aaacf32852d"); + // var message = new Message + // { + // DetailsLink = string.Empty, + // Sender = user, + // Status = "hi", + // Wishlist = false, + // MediaId = msgId, + // Subject = "Microsoft revives long-dead Zune product line", + // Received = DateTime.UtcNow, + // Type = "message", + // Id = msgId.ToString(), + // AltLink = new Link + // { + // Href = "https://rr.noordstar.me/microsoft-revives-long-dead-zune-product--f9628d12", + // Id = linkId.ToString() + // }, + // TextContent = "Tech giant Microsoft announced early Monday morning that a new Zune music player is in the works", + // }; //newMember.Messages ??= new System.Collections.Generic.List(1); //newMember.Messages.Add(message); - Guid badgedId = Guid.Parse("fe9ab096-a072-475b-8e24-0aaacf32852f"); - var badge = new Badge - { - Description = "Restore the Zune social", - TypeId = Xml.SocialApi.BadgeType.ActiveForumsBadge_Gold, - Title = "Necromancer", - Image = "https://i.imgur.com/dMwIZs8.png", - MediaId = Guid.NewGuid(), - MediaType = "Application", - //Summary = "Where is this shown? No idea, contact YoshiAsk if you see this in the software" - }; + // Guid badgedId = Guid.Parse("fe9ab096-a072-475b-8e24-0aaacf32852f"); + // var badge = new Badge + // { + // Description = "Restore the Zune social", + // TypeId = Xml.SocialApi.BadgeType.ActiveForumsBadge_Gold, + // Title = "Necromancer", + // Image = "https://i.imgur.com/dMwIZs8.png", + // MediaId = Guid.NewGuid(), + // MediaType = "Application", + // // Summary = "Where is this shown? No idea, contact YoshiAsk if you see this in the software" + // }; await ctx.CreateAsync(newMember); - //ctx.Messages.Add(message); - //ctx.Tuners.Add(tuner); - userName = "wamwoowam@escargot.chat"; - var member2 = new Member - { - Updated = DateTime.UtcNow, - Id = Member.GetGuidFromUserName(userName), - UserName = userName, - PlayCount = 4123, - Xuid = Member.GetXuidFromUserName(userName), - ZuneTag = "WamWooWam", - DisplayName = string.Empty, - Status = "Restoring Windows Phone 7", - Bio = "he/they, pan, nerd with a strange obsession for windows phone", - Location = "Ireland", - UserTile = "http://i.imgur.com/06BuEKG.jpg", - Background = "http://i.imgur.com/KeZIsxF.jpg", + // var tuner = new Tuner + // { + // Id = "6cba2616-c59a-4dd5-bc9e-d41a45215cfb" + // }; + // user.TunerRegisterInfo = tuner; + // user.SID = SID; + // // user.Badges.Add(badge); + // await ctx.UpdateAsync(user); - AcceptedTermsOfService = true, - AccountSuspended = false, - BillingUnavailable = true, - SubscriptionLapsed = true, - TagChangeRequired = false, - UsageCollectionAllowed = false, - ExplicitPrivilege = false, - Lightweight = false, - Locale = "en-GB", - ParentallyControlled = false, - PointsBalance = 100.0, - SongCreditBalance = 0.0, - SongCreditRenewalDate = DateTime.Now.AddDays(1).ToString("O"), - BillingInstanceId = "6cba2616-c59a-4dd5-bc9e-d41a5f215cfa" - }; + // System.Console.WriteLine($"Set SID for {user.UserName} to {user.SID}"); - tuner = new Tuner - { - Id = "6cba2616-c59a-4dd5-bc9e-d441f315cfb" - }; - member2.TunerRegisterInfo = tuner; + //ctx.Messages.Add(message); + // ctx.Tuners.Add(tuner); - await ctx.CreateAsync(member2); - //ctx.Tuners.Add(tuner); - //ctx.SaveChanges(); } } } diff --git a/Zune.DB/Helpers.cs b/Zune.DB/Helpers.cs index 430c369..3fb84d8 100644 --- a/Zune.DB/Helpers.cs +++ b/Zune.DB/Helpers.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Security.Cryptography; using System.Text; @@ -6,25 +7,36 @@ namespace Zune.DB { public static class Helpers { - public static Guid GenerateGuid(string content) + + private static byte[] GetSha256(string content) { - // Using MD5 here is fine, as these GUIDs aren't used for - // anything meant to be secure. We could use SHA256, but - // we'd have to crush it down to 16 bytes for the GUID - // anyway. Might as well use MD5 ¯\_(ツ)_/¯ + using var hasher = SHA256.Create(); + var byteArrayResultOfRawData = Encoding.UTF8.GetBytes(content); - // Compute 128-bit (16-byte) hash - byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(content)); - return new(hash); + return hasher.ComputeHash(byteArrayResultOfRawData); + } + + public static Guid GenerateGuid(string content) + { + var result = GetSha256(content); + var guidBase = new byte[16]; + for(int i = 0;i < 16; i++) + { + guidBase[i] = result[i]; + } + return new(guidBase); } public static string Hash(string str) { - byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes(str)); + var result = GetSha256(str); + + string[] hashStr = new string[result.Length]; - string[] hashStr = new string[hash.Length]; - for (int i = 0; i < hash.Length; i++) - hashStr[i] = hash[i].ToString("X2"); + for (int i = 0; i < result.Length; i++) + { + hashStr[i] = result[i].ToString("X2"); + } return string.Join(string.Empty, hashStr).ToUpperInvariant(); } diff --git a/Zune.DB/Models/Member.cs b/Zune.DB/Models/Member.cs index 8c17626..68c3f4d 100644 --- a/Zune.DB/Models/Member.cs +++ b/Zune.DB/Models/Member.cs @@ -42,6 +42,9 @@ public Guid Id public string UserName { get; set; } + [BsonDefaultValue("NotSet")] + public string SID { get; set; } + public IList Playlists { get; set; } public IList Badges { get; set; } public IList Comments { get; set; } @@ -81,6 +84,7 @@ public Guid Id public string LastLabelTakedownDate { get; set; } public Tuner MediaTypeTunerRegisterInfo { get; set; } + // this is supposed to be a list. public Tuner TunerRegisterInfo { get; set; } public string UserTile { get; set; } diff --git a/Zune.DB/Models/Tuner.cs b/Zune.DB/Models/Tuner.cs index 824a788..0d6f2b4 100644 --- a/Zune.DB/Models/Tuner.cs +++ b/Zune.DB/Models/Tuner.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace Zune.DB.Models { diff --git a/Zune.DB/Models/WMISAlbumIdEntry.cs b/Zune.DB/Models/WMISAlbumIdEntry.cs new file mode 100644 index 0000000..c90b3d9 --- /dev/null +++ b/Zune.DB/Models/WMISAlbumIdEntry.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using MongoDB.Bson.Serialization.Attributes; + +namespace Zune.DB.Models +{ + public class WMISAlbumIdEntry + { + [BsonId] + public string Id; + + public long AlbumId { get; set; } + + public Guid AlbumGuid { get; set; } + + public WMISAlbumIdEntry(Int64 id, Guid guid) + { + Id = $"{guid.ToString()}-{id}"; + AlbumId = id; + AlbumGuid = guid; + } + } +} \ No newline at end of file diff --git a/Zune.DB/Models/WMISAlbumTrackEntry.cs b/Zune.DB/Models/WMISAlbumTrackEntry.cs new file mode 100644 index 0000000..05d5a0c --- /dev/null +++ b/Zune.DB/Models/WMISAlbumTrackEntry.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using MongoDB.Bson.Serialization.Attributes; + +namespace Zune.DB.Models +{ + public class WMISAlbumTrackEntry + { + [BsonId] + public string RecordId { get; set; } + public Guid AlbumMbid { get; set; } // musicbrainz id, so we can fetch the thing later + public Guid TrackMbid { get; set; } + public int TrackId { get; set; } // this is the ID that is used by the WMIS document + public int TrackDuration { get; set; } + + public WMISAlbumTrackEntry(int trackId, int trackDuration, Guid albumMbid, Guid trackMbid) + { + RecordId = (trackId + trackDuration).ToString(); + TrackId = trackId; + TrackDuration = trackDuration; + AlbumMbid = albumMbid; + TrackMbid = trackMbid; + } + } +} \ No newline at end of file diff --git a/Zune.DB/Zune.DB.csproj b/Zune.DB/Zune.DB.csproj index 0d5fec7..dc70449 100644 --- a/Zune.DB/Zune.DB.csproj +++ b/Zune.DB/Zune.DB.csproj @@ -1,7 +1,12 @@  - net6.0 + net7.0 + enable + + + + false diff --git a/Zune.DB/ZuneNetContext.cs b/Zune.DB/ZuneNetContext.cs index 45e302a..aedb8fb 100644 --- a/Zune.DB/ZuneNetContext.cs +++ b/Zune.DB/ZuneNetContext.cs @@ -14,6 +14,8 @@ public class ZuneNetContext private readonly IMongoCollection _memberCollection; private readonly IMongoCollection _authCollection; private readonly IMongoCollection _imageCollection; + private readonly IMongoCollection _albumLookupCollection; + private readonly IMongoCollection _trackLookupCollection; public ZuneNetContext(IOptions dbSettings) : this(dbSettings.Value) { @@ -28,6 +30,8 @@ public ZuneNetContext(ZuneNetContextSettings dbSettings) _memberCollection = mongoDatabase.GetCollection(dbSettings.MemberCollectionName); _authCollection = mongoDatabase.GetCollection(dbSettings.AuthCollectionName); _imageCollection = mongoDatabase.GetCollection(dbSettings.ImageCollectionName); + _albumLookupCollection = mongoDatabase.GetCollection(dbSettings.AlbumLookupCollectionName); + _trackLookupCollection = mongoDatabase.GetCollection(dbSettings.TrackLookupCollectionName); } public async Task> GetAsync(Expression> filter = null) => @@ -49,6 +53,9 @@ public async Task> GetAsync(Expression> filter = public async Task CreateAsync(Member newMember) => await _memberCollection.InsertOneAsync(newMember); + public async Task UpdateAsync(Member updatedMember) => + await _memberCollection.ReplaceOneAsync(x => x.Id == updatedMember.Id, updatedMember); + public async Task UpdateAsync(Guid id, Member updatedMember) => await _memberCollection.ReplaceOneAsync(x => x.Id == id, updatedMember); @@ -57,12 +64,29 @@ public async Task RemoveAsync(Guid id) => public Task ClearMembersAsync() => _memberCollection.DeleteManyAsync(_ => true); + public async Task ClearAlbumLookupAsync() + { + await _albumLookupCollection.DeleteManyAsync(_ => true); + await _trackLookupCollection.DeleteManyAsync(_ => true); + } + public async Task GetCidByToken(string token) { string tokenHash = Helpers.Hash(token); + Console.WriteLine($"hashedToken = {tokenHash}"); return await _authCollection.Find(e => e.TokenHash == tokenHash).FirstOrDefaultAsync(); } + public async Task GetMemberByName(string UserName) + { + return await GetSingleAsync(m => m.UserName == UserName); + } + + public async Task GetMemberBySid(string sid) + { + return await GetSingleAsync(user => user.SID == sid); + } + public async Task GetMemberByToken(string token) { var entry = await GetCidByToken(token); @@ -102,6 +126,97 @@ public async Task AddImageAsync(string url) } public Task ClearImagesAsync() => _imageCollection.DeleteManyAsync(_ => true); + + public async Task GetAlbumIdRecordAsync(long id) + { + try + { + var existing = await _albumLookupCollection.FindAsync(x => x.AlbumId == id); + var record = await existing.SingleAsync(); + return record.AlbumGuid; + } + catch + { + return null; + } + } + + public async Task GetAlbumIdRecordAsync(Guid id) + { + try + { + var existing = await _albumLookupCollection.FindAsync(x => x.AlbumGuid == id); + var record = await existing.SingleAsync(); + return record.AlbumId; + } + catch + { + return null; + } + } + + // It's ugly, but it maps a MBID to an Int64 for WMIS's crazy lookup + public async Task CreateOrGetAlbumIdInt64Async(Guid guid) + { + var existing = await GetAlbumIdRecordAsync(guid); + if (existing.HasValue) + { + return existing.Value; + } + while (true) + { + var id = new Random().NextInt64(99_999_999); // Max of 8 digits for... reasons. + + var found = await GetAlbumIdRecordAsync(id); + if (!found.HasValue) + { + await _albumLookupCollection.InsertOneAsync(new WMISAlbumIdEntry(id, guid)); + return id; + } + } + } + + public async Task GetTrackMbidFromTrackIdAndDurationAsync(int trackNumber, int trackDuration) + { + try + { + var record = await _trackLookupCollection.FindAsync(x => x.TrackId == trackNumber && x.TrackDuration == trackDuration); + var first = await record.FirstOrDefaultAsync(); + return first.TrackMbid; + } + catch { return null; } + } + + public async Task GetAlbumMbidFromTrackIdAndDurationAsync(int trackNumber, int trackDuration) + { + try + { + var record = await _trackLookupCollection.FindAsync(x => x.TrackId == trackNumber && x.TrackDuration == trackDuration); + var first = await record.FirstOrDefaultAsync(); + return first.AlbumMbid; + } + catch { return null; } + } + + public async Task GetAlbumIDFromTrackIdAndDurationAsync(int trackNumber, int trackDuration) + { + try + { + var record = await _trackLookupCollection.FindAsync(x => x.TrackId == trackNumber && x.TrackDuration == trackDuration); + var first = await record.FirstOrDefaultAsync(); + return await CreateOrGetAlbumIdInt64Async(first.AlbumMbid); + } + catch { return null; } + } + + public async Task CreateTrackReverseLookupRecordAsync(Guid albumMbid, Guid trackMbid, int trackNumber, int trackDuration) + { + if (await GetAlbumMbidFromTrackIdAndDurationAsync(trackNumber, trackDuration) == null) + { + await _trackLookupCollection.InsertOneAsync( + new WMISAlbumTrackEntry(trackNumber, trackDuration, albumMbid, trackMbid)); + } + } } } diff --git a/Zune.DB/ZuneNetContextSettings.cs b/Zune.DB/ZuneNetContextSettings.cs index aa8cfb1..fa9b2ad 100644 --- a/Zune.DB/ZuneNetContextSettings.cs +++ b/Zune.DB/ZuneNetContextSettings.cs @@ -11,5 +11,9 @@ public class ZuneNetContextSettings public string AuthCollectionName { get; set; } = "Auth"; public string ImageCollectionName { get; set; } = "Images"; + + public string AlbumLookupCollectionName { get; set; } = "AlbumLookup"; + + public string TrackLookupCollectionName { get; set; } = "TrackLookup"; } } diff --git a/Zune.Net.Catalog.Image/Controllers/ImageController.cs b/Zune.Net.Catalog.Image/Controllers/ImageController.cs index aac66ef..984bec6 100644 --- a/Zune.Net.Catalog.Image/Controllers/ImageController.cs +++ b/Zune.Net.Catalog.Image/Controllers/ImageController.cs @@ -1,96 +1,183 @@ -using Microsoft.AspNetCore.Mvc; +using Flurl.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Processing; using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Globalization; +using System.IO; using System.Linq; +using System.Net.Http; using System.Threading.Tasks; using Zune.DB; using Zune.Net.Helpers; namespace Zune.Net.Catalog.Image.Controllers { + [Route("/test/{culture}/")] [Route("/v3.2/{culture}/")] + [Route("/v3.0/{culture}/")] [Produces(Atom.Constants.ATOM_MIMETYPE)] public class ImageController : Controller { private static readonly ConcurrentDictionary dcArtistCache = new(); - private static readonly int[] caaSupportedSizes = new[] { 250, 500, 1200 }; private readonly ZuneNetContext _database; - public ImageController(ZuneNetContext database) + private readonly ILogger _logger; + public ImageController(ZuneNetContext database, ILogger logger) { _database = database; + _logger = logger; } - [HttpGet, Route("image/{id}")] - public async Task Image(Guid id) + [HttpGet("image/{id}")] + public async Task Image(Guid id, int width, bool resize = false, string contenttype = "image/jpeg") { string? imageUrl = null; (var idA, var idB, var idC) = id.GetGuidParts(); - var imageEntry = await _database.GetImageEntryAsync(id); - if (imageEntry != null) - { - imageUrl = imageEntry.Url; - } - else if (idC == 0) + if (idC == 0) { + // do better. this is... sketch. int dcid = unchecked((int)idA); // Get or update cached artist if (!dcArtistCache.TryGetValue(dcid, out var dc_artist)) { - // Artist not in cache + _logger.LogDebug("artist not in cache, adding"); dc_artist = await Discogs.GetDCArtistByDCID(dcid); dcArtistCache.AddOrUpdate(dcid, _ => dc_artist, (_, _) => dc_artist); } // Get URL for requested image + _logger.LogDebug("getting discogs artist images"); var images = dc_artist.Value("images"); if (images != null && images.Count > idB) { - var image = images[idB]; - imageUrl = image.Value("uri"); + _logger.LogDebug("got discogs artist image, sending"); + var thisImage = images[idB]; + imageUrl = thisImage.Value("uri"); } } else { - // The Cover Art Archive API supports sizes of 250, 500, and 1200 - int requestedWidth = 500; - if (Request.Query.TryGetValue("width", out var widthValues) && widthValues.Count > 0) - int.TryParse(widthValues[0], out requestedWidth); + try + { + var imageEntry = await _database.GetImageEntryAsync(id); + if (imageEntry != null) + { + if (!string.IsNullOrEmpty(imageEntry.Url)) + { + _logger.LogDebug("using database backed image"); + imageUrl = imageEntry.Url; + } + } + } + catch { } + } - int width = caaSupportedSizes.MinBy(x => Math.Abs(x - requestedWidth)); - imageUrl = $"https://coverartarchive.org/release/{id}/front-{width}"; + if (string.IsNullOrEmpty(imageUrl)) + { + return await ArtistImage(id, "failoverFromPrimaryEndpoint", width, resize, contenttype); } - // Request the image from the API and forward it to the Zune software - return imageUrl != null - ? Redirect(imageUrl) - : NotFound(); + return await ReturnResizedImageAsync(imageUrl, resize, width, contenttype); } - [HttpGet, Route("music/artist/{id}/{type}")] - public async Task ArtistImage(string id, string type) + // i.e. http://image.catalog.zune.net/v3.0/en-US/music/track/f32bb0ab-59d6-4620-b239-e86dc68647a4/albumImage?width=240&height=240&resize=true + + [HttpGet("music/{imageKind}/{id}/{type}")] + public async Task ArtistImage(Guid id, string type, int width, bool resize = false, string contenttype = "image/jpeg") { - string? imageUrl = null; + _logger.LogDebug($"Fetching image type: '{type}', starting with DC"); + // known types - deviceBackgroundImage, primaryImage, albumImage. + // primaryImage is mostly what the Zune app asks for + // deviceBackgroundImage is what is displayed on the 'now playing' screen + // albumImage is just the album cover. - if (type == "primaryImage") + string imageUrl; + try { - Guid mbid = Guid.Parse(id); - (var dc_artist, var mb_artist) = await Discogs.GetDCArtistByMBID(mbid); + imageUrl = await GetImageUrlFromDCAsync(id); + } + catch + { + _logger.LogDebug("DC failed for some reason, so we are now going to try Cover Archive"); + imageUrl = GetImageUrlFromCoverArchive(id); + } + + return await ReturnResizedImageAsync(imageUrl, resize, width, contenttype); + } + + private async Task ReturnResizedImageAsync(string imageUrl, bool resize, int? width, string contenttype) + { + try + { + var imgResponse = await imageUrl.GetAsync(); + + if (imgResponse.StatusCode != 200) + { + NotFound(); + } + + var image = await SixLabors.ImageSharp.Image.LoadAsync(await imgResponse.GetStreamAsync()); + if (width.HasValue && resize && image.Size.Width > width.Value) + { + _logger.LogDebug("resizing"); + image.Mutate(x => x.Resize(width.Value, 0)); + } + + using var stream = new MemoryStream(); - imageUrl = dc_artist.Value("images")? - .FirstOrDefault(i => i.Value("type") == "primary")? - .Value("uri"); + if (contenttype.Contains("jpeg")) + { + _logger.LogDebug("sending as jpg"); + image.Save(stream, new JpegEncoder()); + } + else if (contenttype.Contains("bmp")) + { + _logger.LogDebug("bmp"); + image.Save(stream, new BmpEncoder()); + } + else if (contenttype.Contains("png")) + { + _logger.LogDebug("sending as png"); + image.Save(stream, new PngEncoder()); + } + + return File(stream.ToArray(), contenttype); } + catch { return NotFound(); } + } - return imageUrl != null - ? Redirect(imageUrl) - : NotFound(); + private static async Task GetImageUrlFromDCAsync(Guid id) + { + (var dc_artist, var mb_artist) = await Discogs.GetDCArtistByMBID(id); + + return dc_artist.Value("images")? + .FirstOrDefault(i => i.Value("type") == "primary")? + .Value("uri") ?? throw new FileNotFoundException(); + } + + private static string GetImageUrlFromCoverArchive(Guid id) + { + string? albumId = null; + try + { + albumId = MusicBrainz.GetAlbumByRecordingId(id).Id; + } + catch + { + } + if (string.IsNullOrEmpty(albumId)) + { + albumId = id.ToString(); + } + return $"https://coverartarchive.org/release/{albumId}/front"; } } } diff --git a/Zune.Net.Catalog.Image/Dockerfile b/Zune.Net.Catalog.Image/Dockerfile new file mode 100644 index 0000000..f2f6af5 --- /dev/null +++ b/Zune.Net.Catalog.Image/Dockerfile @@ -0,0 +1,16 @@ +# https://hub.docker.com/_/microsoft-dotnet +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +WORKDIR /source + +# cache the build result to speed up subsequent package steps +COPY ./ . +RUN dotnet restore + +RUN dotnet publish -c release -p:PublishDir=./publish --no-restore + +# final stage/image +FROM mcr.microsoft.com/dotnet/aspnet:7.0 +WORKDIR /app +COPY --from=build /source/Zune.Net.Catalog.Image/publish/ ./ +ENV DOTNET_EnableDiagnostics=0 +ENTRYPOINT ["dotnet", "Zune.Net.Catalog.Image.dll"] \ No newline at end of file diff --git a/Zune.Net.Catalog.Image/Program.cs b/Zune.Net.Catalog.Image/Program.cs index f296455..71e3191 100644 --- a/Zune.Net.Catalog.Image/Program.cs +++ b/Zune.Net.Catalog.Image/Program.cs @@ -1,6 +1,8 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using System.Threading.Tasks; namespace Zune.Net.Catalog.Image @@ -11,6 +13,8 @@ public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); + builder.Logging.AddConsole(); + // Add services to the container. builder.Services.AddControllers(); @@ -19,6 +23,8 @@ public static void Main(string[] args) var app = builder.Build(); + // app.UseHttpLogging(); + // Configure the HTTP request pipeline. app.UseRouting(); diff --git a/Zune.Net.Catalog.Image/appsettings.Development.json b/Zune.Net.Catalog.Image/appsettings.Development.json index 0c208ae..8090d9a 100644 --- a/Zune.Net.Catalog.Image/appsettings.Development.json +++ b/Zune.Net.Catalog.Image/appsettings.Development.json @@ -1,8 +1,11 @@ { + "ZuneNetContext": { + "ConnectionString": "mongodb://root:rootpassword@localhost:27017", + "DatabaseName": "Zune" + }, "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Default": "Debug" } } -} +} \ No newline at end of file diff --git a/Zune.Net.Catalog.Image/appsettings.json b/Zune.Net.Catalog.Image/appsettings.json index 429f762..cb9dfc0 100644 --- a/Zune.Net.Catalog.Image/appsettings.json +++ b/Zune.Net.Catalog.Image/appsettings.json @@ -1,6 +1,6 @@ { "ZuneNetContext": { - "ConnectionString": "mongodb://localhost:27017", + "ConnectionString": "mongodb://root:rootpassword@mongodb:27017", "DatabaseName": "Zune" }, "Logging": { diff --git a/Zune.Net.Catalog/Controllers/Music/AlbumController.cs b/Zune.Net.Catalog/Controllers/Music/AlbumController.cs index 42ae944..d958ea3 100644 --- a/Zune.Net.Catalog/Controllers/Music/AlbumController.cs +++ b/Zune.Net.Catalog/Controllers/Music/AlbumController.cs @@ -6,12 +6,13 @@ namespace Zune.Net.Catalog.Controllers.Music { - [Route("/v3.2/{culture}/music/album/")] + [Route("/v3.2/{culture}/music/album/")] // Desktop app + [Route("/v3.0/{culture}/music/album/")] // Zune HD [Produces(Atom.Constants.ATOM_MIMETYPE)] public class AlbumController : Controller { - [HttpGet, Route("")] + [HttpGet("")] public ActionResult> Search() { if (!Request.Query.TryGetValue("q", out var queries) || queries.Count != 1) @@ -19,8 +20,8 @@ public ActionResult> Search() return MusicBrainz.SearchAlbums(queries[0], Request.Path); } - - [HttpGet, Route("{mbid}")] + //"GET /v3.2/en-US/music/album/c7153846-046f-41b7-a9cd-a9d10751ae60 HTTP/1.1" 200 42271 "-" "-" + [HttpGet("{mbid}"), HttpGet("details/{mbid}")] public ActionResult Details(Guid mbid) { return MusicBrainz.GetAlbumByMBID(mbid); diff --git a/Zune.Net.Catalog/Controllers/Music/ArtistController.cs b/Zune.Net.Catalog/Controllers/Music/ArtistController.cs index 9fb85f1..6e956fc 100644 --- a/Zune.Net.Catalog/Controllers/Music/ArtistController.cs +++ b/Zune.Net.Catalog/Controllers/Music/ArtistController.cs @@ -1,13 +1,10 @@ using Atom.Xml; using Flurl.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; using System; -using System.Collections.Generic; using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Text.RegularExpressions; using System.Threading.Tasks; using Zune.DB; using Zune.Net.Helpers; @@ -16,25 +13,30 @@ namespace Zune.Net.Catalog.Controllers.Music { [Route("/v3.2/{culture}/music/artist/")] + [Route("/v3.0/{culture}/music/artist/")] [Produces(Atom.Constants.ATOM_MIMETYPE)] public class ArtistController : Controller { private readonly ZuneNetContext _database; - public ArtistController(ZuneNetContext database) + private readonly ILogger _logger; + + public ArtistController(ILogger logger, ZuneNetContext database) { + _logger = logger; _database = database; } - [HttpGet, Route("")] - public ActionResult> Search() + [HttpGet("")] + public ActionResult> Search(string q) { - if (!Request.Query.TryGetValue("q", out var queries) || queries.Count != 1) + if(string.IsNullOrEmpty(q)) + { return BadRequest(); - - return MusicBrainz.SearchArtists(queries[0], Request.Path); + } + return MusicBrainz.SearchArtists(q, Request.Path); } - [HttpGet, Route("{mbid}")] + [HttpGet("{mbid}")] public async Task> Details(Guid mbid) { (var dc_artist, var mb_artist) = await Discogs.GetDCArtistByMBID(mbid); @@ -62,55 +64,44 @@ public async Task> Details(Guid mbid) return artist; } - [HttpGet, Route("{mbid}/tracks")] - public ActionResult> Tracks(Guid mbid) - { - if (!Request.Query.TryGetValue("chunkSize", out var chunkSizeStrs) || chunkSizeStrs.Count != 1) - return BadRequest(); - - return MusicBrainz.GetArtistTracksByMBID(mbid, Request.Path, int.Parse(chunkSizeStrs[0])); - } - - [HttpGet, Route("{mbid}/albums")] - public ActionResult> Albums(Guid mbid) + [HttpGet("{mbid}/tracks")] + public ActionResult> Tracks(Guid mbid, string orderby = "unset", int chunkSize = 10) { - var feed = MusicBrainz.GetArtistAlbumsByMBID(mbid, Request.Path); - - Comparison sortComparer = (a, b) => a.ReleaseDate.Year.CompareTo(b.ReleaseDate.Year); - if (Request.Query.TryGetValue("orderby", out var orderByValue)) + var tracks = MusicBrainz.GetArtistTracksByMBID(mbid, Request.Path, 100); + var toSort = tracks.Entries; + try { - string orderBy = orderByValue.Single().ToLower(); - switch (orderBy) + switch (orderby.ToLowerInvariant()) { - case "title": - sortComparer = (a, b) => (a.SortTitle ?? a.Title.Value).CompareTo(b.SortTitle ?? b.Title.Value); - break; - - case "mostplayed": - sortComparer = (a, b) => a.Popularity.CompareTo(b.Popularity); + case "playrank": + tracks.Entries.Sort((a, b) => a.Popularity.CompareTo(b)); break; } } - - feed.Entries.Sort(sortComparer); - return feed; + catch { } + tracks.Entries = tracks.Entries.Take(chunkSize).ToList(); + return tracks; } - [HttpGet, Route("{mbid}/primaryImage")] - public async Task PrimaryImage(Guid mbid) + [HttpGet("{mbid}/albums")] + public ActionResult> Albums(Guid mbid, int chunkSize = 10, string orderby = "ReleaseDate") { - (var dc_artist, var mb_artist) = await Discogs.GetDCArtistByMBID(mbid); - if (dc_artist == null) - return StatusCode(404); + var feed = MusicBrainz.GetArtistAlbumsByMBID(mbid, Request.Path); + Comparison sortComparer = orderby.ToLowerInvariant() switch + { + "title" => (a, b) => (a.SortTitle ?? a.Title.Value).CompareTo(b.SortTitle ?? b.Title.Value), - string imgUrl = dc_artist["images"].First(i => i.Value("type") == "primary").Value("uri"); - var imgResponse = await imgUrl.GetAsync(); - if (imgResponse.StatusCode != 200) - return StatusCode(imgResponse.StatusCode); - return File(await imgResponse.GetStreamAsync(), "image/jpeg"); + "mostplayed" or "playrank" => (a, b) => a.Popularity.CompareTo(b.Popularity), + + //default + _ or "releasedate" => (a, b) => a.ReleaseDate.Year.CompareTo(b.ReleaseDate.Year), + }; + feed.Entries.Sort(sortComparer); + feed.Entries = feed.Entries.Take(chunkSize).ToList(); + return feed; } - [HttpGet, Route("{mbid}/biography")] + [HttpGet("{mbid}/biography")] public async Task> Biography(Guid mbid) { (var dc_artist, var mb_artist) = await Discogs.GetDCArtistByMBID(mbid); @@ -127,12 +118,33 @@ public async Task> Biography(Guid mbid) Updated = updated, }; } + + [HttpGet("{mbid}/primaryImage")] + public async Task PrimaryImage(Guid mbid) + { + (var dc_artist, var mb_artist) = await Discogs.GetDCArtistByMBID(mbid); + if (dc_artist == null) + { + _logger.LogDebug($"No artist found for mbid: {mbid}"); + return StatusCode(404); + } + + string imgUrl = dc_artist["images"].First(i => i.Value("type") == "primary").Value("uri"); + var imgResponse = await imgUrl.GetAsync(); + if (imgResponse.StatusCode != 200) + return StatusCode(imgResponse.StatusCode); + return File(await imgResponse.GetStreamAsync(), "image/jpeg"); + } - [HttpGet, Route("{mbid}/images")] - public async Task>> Images(Guid mbid) + [HttpGet("{mbid}/images")] + public async Task>> Images(Guid mbid, int chunkSize = 40) { (var dc_artist, var mb_artist) = await Discogs.GetDCArtistByMBID(mbid); DateTime updated = DateTime.Now; + if(dc_artist == null) + { + return StatusCode(404); + } int dcid = dc_artist.Value("id"); byte[] zero = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 }; @@ -167,16 +179,19 @@ public async Task>> Images(Guid mbid) } } }; - }).ToList(); + }).Take(chunkSize).ToList(); } return feed; } - [HttpGet, Route("{mbid}/similarArtists")] - public async Task>> SimilarArtists(Guid mbid) + [HttpGet("{mbid}/similarArtists")] + [HttpGet("{mbid}/influencers")] + public async Task>> SimilarArtists(Guid mbid, int chunkSize = 10) { - return await LastFM.GetSimilarArtistsByMBID(mbid); + var similar = await LastFM.GetSimilarArtistsByMBID(mbid); + similar.Entries = similar.Entries.Take(chunkSize).ToList(); + return similar; } } } diff --git a/Zune.Net.Catalog/Controllers/Music/ChartController.cs b/Zune.Net.Catalog/Controllers/Music/ChartController.cs index 1d83a88..6db58bc 100644 --- a/Zune.Net.Catalog/Controllers/Music/ChartController.cs +++ b/Zune.Net.Catalog/Controllers/Music/ChartController.cs @@ -9,18 +9,16 @@ namespace Zune.Net.Catalog.Controllers.Music { [Route("/v3.2/{culture}/music/chart/zune/")] + [Route("/v3.0/{culture}/music/chart/zune/")] [Produces(Atom.Constants.ATOM_MIMETYPE)] public class ChartController : Controller { - private const bool useDeezer = true; - - [HttpGet, Route("tracks")] + [HttpGet("tracks")] public async Task>> Tracks() { Feed feed; - if (useDeezer) - { +#if UseDeezer var dz_tracks = await Deezer.GetChartDZTracks(); DateTime updated = DateTime.Now; @@ -44,30 +42,28 @@ public async Task>> Tracks() feed.Entries.Add(track); } - } - else - { - var fm_tracks = await LastFM.GetTopTracks(); - feed = LastFM.CreateFeed("/music/chart/zune/tracks", "Top tracks"); +#else + var fm_tracks = await LastFM.GetTopTracks(); + feed = LastFM.CreateFeed("/music/chart/zune/tracks", "Top tracks"); - foreach (var fm_track in fm_tracks.Take(10)) - { - var mb_recording = LastFM.GetMBRecordingByFMTrack(fm_track); - if (mb_recording == null) - continue; + foreach (var fm_track in fm_tracks.Take(10)) + { + var mb_recording = LastFM.GetMBRecordingByFMTrack(fm_track); + if (mb_recording == null) + continue; - var track = MusicBrainz.MBRecordingToTrack(mb_recording, updated: feed.Updated, includeRights: true); - track.Popularity = fm_track.Rank ?? 0; - track.PlayCount = fm_track.PlayCount ?? 0; + var track = MusicBrainz.MBRecordingToTrack(mb_recording, updated: feed.Updated, includeRights: true); + track.Popularity = fm_track.Rank ?? 0; + track.PlayCount = fm_track.PlayCount ?? 0; - feed.Entries.Add(track); - } + feed.Entries.Add(track); } +#endif return feed; } - [HttpGet, Route("albums")] + [HttpGet("albums")] public async Task>> Albums() { var dz_albums = await Deezer.GetChartDZAlbums(); diff --git a/Zune.Net.Catalog/Controllers/Music/FeaturesController.cs b/Zune.Net.Catalog/Controllers/Music/FeaturesController.cs index d0b49ff..e89c42e 100644 --- a/Zune.Net.Catalog/Controllers/Music/FeaturesController.cs +++ b/Zune.Net.Catalog/Controllers/Music/FeaturesController.cs @@ -7,10 +7,11 @@ namespace Zune.Net.Catalog.Controllers.Music { [Route("/v3.2/{culture}/music/features/")] + [Route("/v3.0/{culture}/music/features/")] [Produces(Atom.Constants.ATOM_MIMETYPE)] public class FeaturesController : Controller { - [HttpGet, Route("")] + [HttpGet("")] public ActionResult> Features() { var updated = DateTime.Now; diff --git a/Zune.Net.Catalog/Controllers/Music/GenreController.cs b/Zune.Net.Catalog/Controllers/Music/GenreController.cs index 72cfcf5..2f6a5e7 100644 --- a/Zune.Net.Catalog/Controllers/Music/GenreController.cs +++ b/Zune.Net.Catalog/Controllers/Music/GenreController.cs @@ -7,17 +7,18 @@ namespace Zune.Net.Catalog.Controllers.Music { [Route("/v3.2/{culture}/music/genre/")] + [Route("/v3.0/{culture}/music/genre/")] [Produces(Atom.Constants.ATOM_MIMETYPE)] public class GenreController : Controller { - [HttpGet, Route("")] + [HttpGet("")] public ActionResult> Genres() { return MusicBrainz.GetGenres(Request.Path); } - [HttpGet, Route("{id}")] + [HttpGet("{id}")] public ActionResult Details(string id) { if (Guid.TryParse(id, out Guid mbid)) @@ -25,7 +26,7 @@ public ActionResult Details(string id) return MusicBrainz.GetGenreByZID(id); } - [HttpGet, Route("{id}/albums")] + [HttpGet("{id}/albums")] public ActionResult> Albums(string id) { if (Guid.TryParse(id, out Guid mbid)) @@ -34,7 +35,7 @@ public ActionResult> Albums(string id) } // Not actually used by Zune, but hey, might as well - [HttpGet, Route("{id}/tracks")] + [HttpGet("{id}/tracks")] public ActionResult> Tracks(string id) { if (Guid.TryParse(id, out Guid mbid)) @@ -42,7 +43,7 @@ public ActionResult> Tracks(string id) return MusicBrainz.GetGenreTracksByZID(id, Request.Path); } - [HttpGet, Route("{id}/artists")] + [HttpGet("{id}/artists")] public ActionResult> Artists(string id) { if (Guid.TryParse(id, out Guid mbid)) diff --git a/Zune.Net.Catalog/Controllers/Music/TrackController.cs b/Zune.Net.Catalog/Controllers/Music/TrackController.cs index b7806f5..c96e369 100644 --- a/Zune.Net.Catalog/Controllers/Music/TrackController.cs +++ b/Zune.Net.Catalog/Controllers/Music/TrackController.cs @@ -10,6 +10,7 @@ namespace Zune.Net.Catalog.Controllers.Music { [Route("/v3.2/{culture}/music/track/")] + [Route("/v3.0/{culture}/music/track/")] [Produces(Atom.Constants.ATOM_MIMETYPE)] public class TrackController : Controller { @@ -20,7 +21,7 @@ public TrackController(IWebHostEnvironment env) _env = env; } - [HttpGet, Route("")] + [HttpGet("")] public ActionResult> Search() { if (!Request.Query.TryGetValue("q", out var queries) || queries.Count != 1) @@ -29,13 +30,13 @@ public ActionResult> Search() return MusicBrainz.SearchTracks(queries[0], Request.Path); } - [HttpGet, Route("{mbid}")] + [HttpGet("{mbid}")] public ActionResult Details(Guid mbid) { return MusicBrainz.GetTrackByMBID(mbid); } - [HttpGet, Route("{mbid}/similarTracks")] + [HttpGet("{mbid}/similarTracks")] public async Task>> SimilarTracks(Guid mbid) { return await LastFM.GetSimilarTracksByMBID(mbid); diff --git a/Zune.Net.Catalog/Controllers/Podcast/ChartController.cs b/Zune.Net.Catalog/Controllers/Podcast/ChartController.cs index 9d2cb68..b3b593e 100644 --- a/Zune.Net.Catalog/Controllers/Podcast/ChartController.cs +++ b/Zune.Net.Catalog/Controllers/Podcast/ChartController.cs @@ -17,7 +17,7 @@ public ChartController(ZuneNetContext database) _database = database; } - [HttpGet, Route("podcasts")] + [HttpGet("podcasts")] public async Task>> Podcasts(string culture) { int limit = 26; diff --git a/Zune.Net.Catalog/Controllers/Podcast/PodcastController.cs b/Zune.Net.Catalog/Controllers/Podcast/PodcastController.cs index c8b546e..4597173 100644 --- a/Zune.Net.Catalog/Controllers/Podcast/PodcastController.cs +++ b/Zune.Net.Catalog/Controllers/Podcast/PodcastController.cs @@ -9,6 +9,7 @@ namespace Zune.Net.Catalog.Controllers.Podcast { + [Route("/v3.0/{culture}/")] [Route("/v3.2/{culture}/")] [Produces(Atom.Constants.ATOM_MIMETYPE)] public class PodcastController : Controller @@ -19,7 +20,7 @@ public PodcastController(ZuneNetContext database) _database = database; } - [HttpGet, Route("podcast")] + [HttpGet("podcast")] public async Task>> Search() { if (!Request.Query.TryGetValue("q", out var queries) || queries.Count != 1) @@ -32,7 +33,7 @@ public async Task>> Search() return feed; } - [HttpGet, Route("podcast/{tdid}")] + [HttpGet("podcast/{tdid}")] public async Task Details(Guid tdid) { var podcast = await Taddy.GetPodcastByTDID(tdid); @@ -40,7 +41,7 @@ public async Task Details(Guid tdid) return podcast; } - [HttpGet, Route("podcastCategories")] + [HttpGet("podcastCategories")] public Feed Categories() { // TODO: Narrow the categories down to a few, not 110 diff --git a/Zune.Net.Catalog/Controllers/StreamController.cs b/Zune.Net.Catalog/Controllers/StreamController.cs index be5feaa..27ecf34 100644 --- a/Zune.Net.Catalog/Controllers/StreamController.cs +++ b/Zune.Net.Catalog/Controllers/StreamController.cs @@ -1,11 +1,9 @@ -using Flurl; -using Flurl.Http; +using Flurl.Http; using MetaBrainz.MusicBrainz; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; using Zune.Net.Helpers; @@ -23,7 +21,7 @@ public StreamController(IWebHostEnvironment env) _env = env; } - [HttpGet, Route("music/{mbid}")] + [HttpGet("music/{mbid}")] public async Task DefaultStreaming(Guid mbid) { var track = MusicBrainz.GetTrackByMBID(mbid); diff --git a/Zune.Net.Catalog/Dockerfile b/Zune.Net.Catalog/Dockerfile new file mode 100644 index 0000000..f71669b --- /dev/null +++ b/Zune.Net.Catalog/Dockerfile @@ -0,0 +1,16 @@ +# https://hub.docker.com/_/microsoft-dotnet +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +WORKDIR /source + +# cache the build result to speed up subsequent package steps +COPY ./ . +RUN dotnet restore + +RUN dotnet publish -c release -p:PublishDir=./publish --no-restore + +# final stage/image +FROM mcr.microsoft.com/dotnet/aspnet:7.0 +WORKDIR /app +COPY --from=build /source/Zune.Net.Catalog/publish/ ./ +ENV DOTNET_EnableDiagnostics=0 +ENTRYPOINT ["dotnet", "Zune.Net.Catalog.dll"] diff --git a/Zune.Net.Catalog/Program.cs b/Zune.Net.Catalog/Program.cs index c88bdd8..b89c328 100644 --- a/Zune.Net.Catalog/Program.cs +++ b/Zune.Net.Catalog/Program.cs @@ -1,11 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; namespace Zune.Net.Catalog { diff --git a/Zune.Net.Catalog/Startup.cs b/Zune.Net.Catalog/Startup.cs index 4cac177..6d2a3f4 100644 --- a/Zune.Net.Catalog/Startup.cs +++ b/Zune.Net.Catalog/Startup.cs @@ -50,7 +50,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseDeveloperExceptionPage(); } - //app.UseHttpsRedirection(); + //app.UseHttpLogging(); app.UseRequestBuffering(); diff --git a/Zune.Net.Catalog/Zune.Net.Catalog.csproj b/Zune.Net.Catalog/Zune.Net.Catalog.csproj index 69d4a97..fb100cf 100644 --- a/Zune.Net.Catalog/Zune.Net.Catalog.csproj +++ b/Zune.Net.Catalog/Zune.Net.Catalog.csproj @@ -3,6 +3,7 @@ net7.0 2a7e8fe8-2bbf-4b58-a184-68ae46722b1c + True diff --git a/Zune.Net.Catalog/appsettings.Development.json b/Zune.Net.Catalog/appsettings.Development.json index 8983e0f..dc23838 100644 --- a/Zune.Net.Catalog/appsettings.Development.json +++ b/Zune.Net.Catalog/appsettings.Development.json @@ -1,9 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "Debug" } } } diff --git a/Zune.Net.Catalog/appsettings.json b/Zune.Net.Catalog/appsettings.json index 485ec63..b7c0152 100644 --- a/Zune.Net.Catalog/appsettings.json +++ b/Zune.Net.Catalog/appsettings.json @@ -1,13 +1,11 @@ { "ZuneNetContext": { - "ConnectionString": "mongodb://localhost:27017", + "ConnectionString": "mongodb://root:rootpassword@mongodb:27017", "DatabaseName": "Zune" }, "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "Information" } }, "AllowedHosts": "*" diff --git a/Zune.Net.Commerce/Config.cs b/Zune.Net.Commerce/Config.cs deleted file mode 100644 index 4176474..0000000 --- a/Zune.Net.Commerce/Config.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Reflection; - -namespace CommerceZuneNet -{ - public class Config - { - public string Host { get; set; } - public string Port { get; set; } = "80"; - public string SslPort { get; set; } = "443"; - - public Config() { } - - public Config(string[] args) - { - Type cfgType = typeof(Config); - foreach (string arg in args) - { - string key; - object value; - - int idx = arg.IndexOf('='); - if (idx >= 0) - { - key = arg[..idx]; - value = arg[(idx + 1)..]; - } - else - { - key = arg; - value = bool.TrueString; - } - - PropertyInfo prop = cfgType.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); - if (prop != null) - { - prop.SetValue(this, value); - } - else - { - Console.WriteLine($"Property \"{key}\" does not exist on {nameof(Config)}."); - } - } - } - } -} diff --git a/Zune.Net.Commerce/Controllers/AccountController.cs b/Zune.Net.Commerce/Controllers/AccountController.cs index 74b6d03..3be4065 100644 --- a/Zune.Net.Commerce/Controllers/AccountController.cs +++ b/Zune.Net.Commerce/Controllers/AccountController.cs @@ -1,5 +1,10 @@ -using Microsoft.AspNetCore.Mvc; +using System; +using System.IO; using System.Threading.Tasks; +using System.Xml.Serialization; +using CommerceZuneNet.Helpers; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using Zune.DB; using Zune.Net.Middleware; using Zune.Xml.Commerce; @@ -12,42 +17,113 @@ namespace CommerceZuneNet.Controllers public class AccountController : ControllerBase { private readonly ZuneNetContext _database; - public AccountController(ZuneNetContext database) + private readonly ILogger _logger; + public AccountController(ZuneNetContext database, ILogger logger) { + _logger = logger; _database = database; } [HttpPost] - public async Task> SignIn(SignInRequest request) + [Produces("application/xml")] + [Consumes("application/x-www-form-urlencoded","application/xml")] + public async Task> SignIn() { - SignInResponse response; if (this.TryGetAuthedMember(out var member)) { - response = member.GetSignInResponse(); + _logger.LogInformation($"Session has been associated with: {member.UserName}"); + return member.GetSignInResponse(); } else { - // TODO: Work out the error response format - response = new SignInResponse + // Get the user by SID and tie the session to that user: + try + { + if(this.TryGetSessionId(out var sessionID)) + { + _logger.LogInformation("We have a session ID, attempting to tie to a SID/User"); + + // parse the SID out of the body the hard way, since the asp.net core xml deserializer chokes on TunerInfo. + var body = await Request.GetRawBodyAsync(); + _logger.LogDebug($"Got a body of: {body.ToString()}"); + using var reader = new StringReader(body); + var serializer = new XmlSerializer(typeof(SignInRequest)); + SignInRequest requestBody = null; + try + { + requestBody = (SignInRequest)serializer.Deserialize(reader); + } + catch (Exception e) + { + _logger.LogError(e,"Failed to deserialize request"); + return Reject(); + } + + if(requestBody != null) + { + // we have a sid, probably + var user_sid = requestBody.TunerInfo.ID; + if(string.IsNullOrEmpty(user_sid)) + { + _logger.LogDebug("failed to get a SID from the request body"); + return Reject(); + } + + _logger.LogDebug($"User SID: {user_sid}"); + + member = await _database.GetMemberBySid(user_sid); + if(member == null) + { + _logger.LogError($"Failed to find a member with SID: {user_sid}"); + return Reject(); + } + _logger.LogInformation("We got a user by SID"); + await _database.AddToken(sessionID, member.UserName); + _logger.LogInformation("Session is associated with SID"); + await _database.UpdateAsync(member); + _logger.LogInformation("Updating the database!"); + member = await _database.GetMemberBySid(user_sid); + + // TODO: We need to be adding the TunerInfo as a tuner to the db when we see a NEW one. + + return member.GetSignInResponse(); + } else + { + _logger.LogInformation("No sid was recovered, rejecting the request"); + return Reject(); + } + } + } + catch (Exception e) + { + _logger.LogError(e, "Failed to get our user"); + } + } + + return Reject(); + } + + private ActionResult Reject() + { + return Unauthorized(new SignInResponse { AccountState = new AccountState { SignInErrorCode = 0x80070057, } - }; - return Unauthorized(response); - } - - return response; + }); } - [HttpPost] - public ActionResult User(GetUserRequest request) + [HttpPost("User")] + [Produces("application/xml")] + [Consumes("application/x-www-form-urlencoded")] + public ActionResult ZuneUser() { return new(new GetUserResponse()); } [HttpPost] + [Produces("application/xml")] public ActionResult Balances() { return new BalancesResponse @@ -55,7 +131,7 @@ public ActionResult Balances() Balances = new() { PointsBalance = 8000, - SongCreditBalance = 0, + SongCreditBalance = 100, SongCreditRenewalDate = null } }; diff --git a/Zune.Net.Commerce/Controllers/BillingController.cs b/Zune.Net.Commerce/Controllers/BillingController.cs index 18ca353..86c3542 100644 --- a/Zune.Net.Commerce/Controllers/BillingController.cs +++ b/Zune.Net.Commerce/Controllers/BillingController.cs @@ -1,8 +1,6 @@ using Atom.Xml; using Microsoft.AspNetCore.Mvc; using System; -using System.Security.Cryptography; -using System.Text; using Zune.DB; using Zune.Xml.Commerce; @@ -20,6 +18,7 @@ public BillingController(ZuneNetContext database) } [HttpPost] + [Produces("application/xml")] public ActionResult PurchaseHistory() { return new Feed @@ -32,7 +31,7 @@ public ActionResult PurchaseHistory() Content = "Purchased something from Chuck Berry", Links = { - new("http://catalog.zunes.me/v3.2/en-US/music/album/06d4ec5e-a1b2-4895-9a09-ca3e8451bcc7") + new("http://catalog.zune.net/v3.2/en-US/music/album/06d4ec5e-a1b2-4895-9a09-ca3e8451bcc7") }, Title = "Oh Baby Doll / Lajaunda (espanol)" } @@ -41,6 +40,7 @@ public ActionResult PurchaseHistory() } [HttpPost] + [Produces("application/xml")] public ActionResult EnumeratePointsBundles() { var id = Guid.NewGuid().ToString(); diff --git a/Zune.Net.Commerce/Controllers/TunerController.cs b/Zune.Net.Commerce/Controllers/TunerController.cs new file mode 100644 index 0000000..0135f24 --- /dev/null +++ b/Zune.Net.Commerce/Controllers/TunerController.cs @@ -0,0 +1,47 @@ + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Zune.DB; +using Zune.Net.Middleware; +using Zune.Xml.Commerce; + +namespace CommerceZuneNet.Controllers +{ + [Route("/{version}/{language}/tuner")] + [Route("/{language}/tuner")] + public class TunerController : ControllerBase + { + private readonly ILogger _logger; + private readonly ZuneNetContext _database; + public TunerController(ILogger logger, ZuneNetContext database) + { + _logger = logger; + _database = database; + } + + [HttpPost("GetRegistrationInfo")] + // [Produces("application/xml")] + [Consumes("application/x-www-form-urlencoded","application/xml")] + [Produces("application/xml")] + public ActionResult GetRegistrationInfo() + { + if (this.TryGetAuthedMember(out var member)) + { + _logger.LogInformation($"Session has been associated with: {member.UserName}"); + //var unused = new GetTunerRegistrationInfoResponse(); + return Ok(@" + + + + + + + "); + } + + _logger.LogError("Failed to get a user for this session."); + return Unauthorized(); + } + + } +} \ No newline at end of file diff --git a/Zune.Net.Commerce/Dockerfile b/Zune.Net.Commerce/Dockerfile new file mode 100644 index 0000000..d1241f0 --- /dev/null +++ b/Zune.Net.Commerce/Dockerfile @@ -0,0 +1,16 @@ +# https://hub.docker.com/_/microsoft-dotnet +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +WORKDIR /source + +# copy csproj and restore as distinct layers +COPY ./ . +RUN dotnet restore + +RUN dotnet publish -c release -p:PublishDir=./publish --no-restore + +# final stage/image +FROM mcr.microsoft.com/dotnet/aspnet:7.0 +WORKDIR /app +COPY --from=build /source/Zune.Net.Commerce/publish/ ./ +ENV DOTNET_EnableDiagnostics=0 +ENTRYPOINT ["dotnet", "Zune.Net.Commerce.dll"] diff --git a/Zune.Net.Commerce/Helpers/RequestHelpers.cs b/Zune.Net.Commerce/Helpers/RequestHelpers.cs new file mode 100644 index 0000000..8d5e920 --- /dev/null +++ b/Zune.Net.Commerce/Helpers/RequestHelpers.cs @@ -0,0 +1,32 @@ +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +// Credit: https://markb.uk/asp-net-core-read-raw-request-body-as-string.html +namespace CommerceZuneNet.Helpers; +public static class RequestBodyHelpers +{ + public static async Task GetRawBodyAsync( + this HttpRequest request, + Encoding encoding = null) +{ + if (!request.Body.CanSeek) + { + // We only do this if the stream isn't *already* seekable, + // as EnableBuffering will create a new stream instance + // each time it's called + request.EnableBuffering(); + } + + request.Body.Position = 0; + + var reader = new StreamReader(request.Body, encoding ?? Encoding.UTF8); + + var body = await reader.ReadToEndAsync().ConfigureAwait(false); + + request.Body.Position = 0; + + return body; +} +} \ No newline at end of file diff --git a/Zune.Net.Commerce/Program.cs b/Zune.Net.Commerce/Program.cs index dbc825d..b042c01 100644 --- a/Zune.Net.Commerce/Program.cs +++ b/Zune.Net.Commerce/Program.cs @@ -1,8 +1,12 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Zune.Net; +using Zune.Net.Middleware; namespace CommerceZuneNet { @@ -10,32 +14,39 @@ public class Program { public static void Main(string[] args) { - CreateHostBuilder(args).Build().Run(); - } + var builder = WebApplication.CreateBuilder(args); + builder.Host.ConfigureLogging(cfg => + { + cfg.AddConsole(); + }); - public static IHostBuilder CreateHostBuilder(string[] args) - { - Config cfg = new(args); + // Add services to the container. + builder.Services.AddControllers().AddXmlSerializerFormatters(); - return Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - if (cfg.Host != null) - webBuilder.UseUrls($"http://{cfg.Host}:{cfg.Port}", $"https://{cfg.Host}:{cfg.SslPort}"); - }) - .ConfigureAppConfiguration((hostingContext, config) => - { - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddEnvironmentVariables(); - }) - .ConfigureServices((ctx, s) => + builder.Host.ConfigureZuneDB(); + + var app = builder.Build(); + + // app.UseHttpLogging(); + + // Configure the HTTP request pipeline. + app.UseRequestBuffering(); + + app.UseRouting(); + + app.UseWlidAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + + endpoints.MapGet("/", ctx => { - s.AddSingleton(sp => cfg); - }) - .ConfigureZuneDB(); + return Task.FromResult(new OkObjectResult("Welcome to the Social")); + }); + }); + + app.Run(); } } } diff --git a/Zune.Net.Commerce/Properties/launchSettings.json b/Zune.Net.Commerce/Properties/launchSettings.json deleted file mode 100644 index afefa89..0000000 --- a/Zune.Net.Commerce/Properties/launchSettings.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:80", - "sslPort": 443 - } - }, - "$schema": "http://json.schemastore.org/launchsettings.json", - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "Zune.Net.Commerce": { - "commandName": "Project", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "dotnetRunMessages": "true", - "applicationUrl": "https://127.0.0.3:443;http://127.0.0.3:80" - } - } -} \ No newline at end of file diff --git a/Zune.Net.Commerce/Startup.cs b/Zune.Net.Commerce/Startup.cs deleted file mode 100644 index ff50fc3..0000000 --- a/Zune.Net.Commerce/Startup.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.OpenApi.Models; -using Zune.Net; -using Zune.Net.Middleware; - -namespace CommerceZuneNet -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddControllersWithViews(o => o.UseZestFormatters()); - services.AddSwaggerGen(c => - { - c.SwaggerDoc("CommerceZuneNet", new OpenApiInfo { Title = "CommerceZuneNet", Version = "v2" }); - }); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, Config cfg) - { - LoggerFactory.Create(loggerFactory => - { - loggerFactory.AddConsole(); - loggerFactory.AddDebug(); - }); - - app.UseStatusCodePages(); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - app.UseSwagger(); - app.UseSwaggerUI(c => { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "Commerce.Zune.Net v2"); - c.RoutePrefix = string.Empty; - //c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First()); - }); - } - - app.UseRequestBuffering(); - - // app.UseHttpsRedirection(); - - app.UseRouting(); - app.UseWlidAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } - } -} diff --git a/Zune.Net.Commerce/appsettings.Development.json b/Zune.Net.Commerce/appsettings.Development.json index 63ed6e1..4bdf8bb 100644 --- a/Zune.Net.Commerce/appsettings.Development.json +++ b/Zune.Net.Commerce/appsettings.Development.json @@ -1,9 +1,10 @@ { + "ZuneNetContext": { + "ConnectionString": "mongodb://root:rootpassword@localhost:27017" + }, "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft": "Information", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "Debug" } } -} +} \ No newline at end of file diff --git a/Zune.Net.Commerce/appsettings.json b/Zune.Net.Commerce/appsettings.json index 0e21171..72801b2 100644 --- a/Zune.Net.Commerce/appsettings.json +++ b/Zune.Net.Commerce/appsettings.json @@ -1,6 +1,6 @@ { "ZuneNetContext": { - "ConnectionString": "mongodb://localhost:27017", + "ConnectionString": "mongodb://root:rootpassword@mongodb:27017", "DatabaseName": "Zune", "MemberCollectionName": "Members" }, @@ -11,7 +11,7 @@ */ "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "Domain": "commerce.zunes.me", + "Domain": "commerce.zune.net", "TenantId": "common", "ClientId": "e7dd59f1-8ea4-49b6-8361-1b10b2551dba", @@ -19,9 +19,7 @@ }, "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "Debug" } }, "AllowedHosts": "*" diff --git a/Zune.Net.Inbox/Controllers/MessagingInboxController.cs b/Zune.Net.Inbox/Controllers/MessagingInboxController.cs index cd526ae..c8065f1 100644 --- a/Zune.Net.Inbox/Controllers/MessagingInboxController.cs +++ b/Zune.Net.Inbox/Controllers/MessagingInboxController.cs @@ -1,24 +1,24 @@ using Atom.Xml; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using System.Xml.Serialization; -using Zune.DB; -using Zune.DB.Models; using Zune.Xml.Inbox; namespace Zune.Net.Inbox.Controllers { + ///en-US/messaging/xerootg/inbox/unreadcount [ApiController] [Route("/{locale}/messaging/{zuneTag}/inbox/{action=List}/{id?}")] + [Route("/{locale}/messaging/{zuneTag}/inbox/")] [Route("/messaging/{zuneTag}/inbox/{action=List}/{id?}")] public class MessagingInboxController : ControllerBase { + private ILogger _logger; + public MessagingInboxController(ILogger logger) + { + _logger = logger; + } + [HttpGet] public Feed List(string locale, string zuneTag) { @@ -65,15 +65,17 @@ public ActionResult Details(string locale, string zuneTag, strin return message; } - [HttpGet] - public IActionResult UnreadCont(string locale, string zuneTag) + [HttpGet("unreadcount")] + public IActionResult UnreadCont(string zuneTag) { - using var ctx = new ZuneNetContext(); - Member member = ctx.Members.First(m => m.ZuneTag == zuneTag); - if (member == null) - return StatusCode(StatusCodes.Status400BadRequest, $"User {zuneTag} does not exist."); + _logger.LogInformation($"{zuneTag} message count requested"); + // using var ctx = new ZuneNetContext(); + // Member member = ctx.Members.First(m => m.ZuneTag == zuneTag); + // if (member == null) + // return StatusCode(StatusCodes.Status400BadRequest, $"User {zuneTag} does not exist."); - return Content(member.Messages.Count(msg => !msg.IsRead).ToString()); + // return Content(member.Messages.Count(msg => !msg.IsRead).ToString()); + return Ok("1"); } } } diff --git a/Zune.Net.Inbox/Dockerfile b/Zune.Net.Inbox/Dockerfile new file mode 100644 index 0000000..5a3d99f --- /dev/null +++ b/Zune.Net.Inbox/Dockerfile @@ -0,0 +1,16 @@ +# https://hub.docker.com/_/microsoft-dotnet +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +WORKDIR /source + +# copy csproj and restore as distinct layers +COPY ./ . +RUN dotnet restore + +RUN dotnet publish -c release -p:PublishDir=./publish --no-restore + +# final stage/image +FROM mcr.microsoft.com/dotnet/aspnet:7.0 +WORKDIR /app +COPY --from=build /source/Zune.Net.Inbox/publish/ ./ +ENV DOTNET_EnableDiagnostics=0 +ENTRYPOINT ["dotnet", "Zune.Net.Inbox.dll"] diff --git a/Zune.Net.Inbox/Program.cs b/Zune.Net.Inbox/Program.cs index 1834ea1..812e49c 100644 --- a/Zune.Net.Inbox/Program.cs +++ b/Zune.Net.Inbox/Program.cs @@ -1,26 +1,18 @@ -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; +using Zune.Net; -namespace Zune.Net.Inbox -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } +var builder = WebApplication.CreateBuilder(args); - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }) - .ConfigureServices((ctx, s) => - { - s.Configure(ctx.Configuration.GetSection("ZuneNetContext")); - s.AddSingleton(); - }); - } -} +// Add services to the container. + +builder.Services.AddControllers().AddXmlSerializerFormatters(); +builder.Host.ConfigureZuneDB(true); + +var app = builder.Build(); + +// app.UseHttpLogging(); + +app.MapControllers(); + +app.Run(); diff --git a/Zune.Net.Inbox/Properties/launchSettings.json b/Zune.Net.Inbox/Properties/launchSettings.json deleted file mode 100644 index ed72345..0000000 --- a/Zune.Net.Inbox/Properties/launchSettings.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:80", - "sslPort": 443 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "Zune.Net.Inbox": { - "commandName": "Project", - "dotnetRunMessages": "true", - "applicationUrl": "https://127.0.0.7:443;http://127.0.0.7:80", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/Zune.Net.Inbox/Startup.cs b/Zune.Net.Inbox/Startup.cs index 552e4d1..ca6e8fa 100644 --- a/Zune.Net.Inbox/Startup.cs +++ b/Zune.Net.Inbox/Startup.cs @@ -1,15 +1,8 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Zune.Net.Middleware; namespace Zune.Net.Inbox @@ -37,7 +30,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseDeveloperExceptionPage(); } - app.UseHttpsRedirection(); + // app.UseHttpLogging(); app.UseRouting(); diff --git a/Zune.Net.Inbox/appsettings.Development.json b/Zune.Net.Inbox/appsettings.Development.json index 8983e0f..dc23838 100644 --- a/Zune.Net.Inbox/appsettings.Development.json +++ b/Zune.Net.Inbox/appsettings.Development.json @@ -1,9 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "Debug" } } } diff --git a/Zune.Net.Inbox/appsettings.json b/Zune.Net.Inbox/appsettings.json index 38175d4..309b7d7 100644 --- a/Zune.Net.Inbox/appsettings.json +++ b/Zune.Net.Inbox/appsettings.json @@ -1,14 +1,12 @@ { "ZuneNetContext": { - "ConnectionString": "mongodb://localhost:27017", + "ConnectionString": "mongodb://root:rootpassword@mongodb:27017", "DatabaseName": "Zune", "MemberCollectionName": "Members" }, "Logging": { "LogLevel": { "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" diff --git a/Zune.Net.Login/Dockerfile b/Zune.Net.Login/Dockerfile new file mode 100644 index 0000000..9289e84 --- /dev/null +++ b/Zune.Net.Login/Dockerfile @@ -0,0 +1,16 @@ +# https://hub.docker.com/_/microsoft-dotnet +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +WORKDIR /source + +# copy csproj and restore as distinct layers +COPY ./ . +RUN dotnet restore + +RUN dotnet publish -c release -p:PublishDir=./publish --no-restore + +# final stage/image +FROM mcr.microsoft.com/dotnet/aspnet:7.0 +WORKDIR /app +COPY --from=build /source/Zune.Net.Login/publish/ ./ +ENV DOTNET_EnableDiagnostics=0 +ENTRYPOINT ["dotnet", "Zune.Net.Login.dll"] diff --git a/Zune.Net.Login/Program.cs b/Zune.Net.Login/Program.cs index 9de59f3..018b04e 100644 --- a/Zune.Net.Login/Program.cs +++ b/Zune.Net.Login/Program.cs @@ -17,6 +17,7 @@ public static void Main(string[] args) builder.Services.AddControllers(); var app = builder.Build(); + // app.UseHttpLogging(); // Configure the HTTP request pipeline. diff --git a/Zune.Net.Login/appsettings.Development.json b/Zune.Net.Login/appsettings.Development.json index 0c208ae..dc23838 100644 --- a/Zune.Net.Login/appsettings.Development.json +++ b/Zune.Net.Login/appsettings.Development.json @@ -1,8 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Default": "Debug" } } } diff --git a/Zune.Net.Login/appsettings.json b/Zune.Net.Login/appsettings.json index 8f33bdc..734fda5 100644 --- a/Zune.Net.Login/appsettings.json +++ b/Zune.Net.Login/appsettings.json @@ -1,12 +1,11 @@ { "ZuneNetContext": { - "ConnectionString": "mongodb://localhost:27017", + "ConnectionString": "mongodb://root:rootpassword@mongodb:27017", "DatabaseName": "Zune", }, "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Default": "Information" } }, "AllowedHosts": "*" diff --git a/Zune.Net.MetaServices/Controllers/Cover.cs b/Zune.Net.MetaServices/Controllers/Cover.cs new file mode 100644 index 0000000..c2c0229 --- /dev/null +++ b/Zune.Net.MetaServices/Controllers/Cover.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Zune.Net.MetaServices.Controllers +{ + [Route("/Cover/")] + public class Cover : Controller + { + private readonly ILogger _logger; + public Cover(ILogger logger) + { + _logger = logger; + } + private static readonly Uri defaultImage = new Uri("https://hellofromseattle.com/wp-content/uploads/sites/6/2020/03/Zune-Basic.png"); + + [Route("{coverId}")] + public async Task GetCover(string coverId) + { + var catalogImageUri = new Uri($"https://coverartarchive.org/release/{coverId}/front"); + if(coverId.Equals("default")) + { + _logger.LogInformation($"default albumart requested"); + catalogImageUri = defaultImage; + } + + using var client = new HttpClient(); + var result = await client.GetAsync(catalogImageUri); + if(!result.IsSuccessStatusCode) + { + _logger.LogInformation($"Falling back to the default image with status {result.StatusCode}, failed to resolve actual artwork"); + result = await client.GetAsync(defaultImage); + } + return new HttpResponseMessageResult(result); + //HttpResponseMessageResult + } + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/Controllers/FAI.cs b/Zune.Net.MetaServices/Controllers/FAI.cs new file mode 100644 index 0000000..8729660 --- /dev/null +++ b/Zune.Net.MetaServices/Controllers/FAI.cs @@ -0,0 +1,65 @@ +using Microsoft.AspNetCore.Mvc; +using Zune.Net.Helpers; +using Zune.Net.MetaServices.DomainModels.Endpoints; +using Zune.Net.MetaServices.DomainModels.MdqCd; + +namespace Zune.Net.MetaServices.Controllers +{ + [Route("/ZuneFAI/")] + public class FAI : Controller + { + private readonly WMIS _wmis; + private readonly ILogger _logger; + public FAI(WMIS wmis, ILogger logger) + { + _wmis = wmis; + _logger = logger; + } + [HttpGet("Search")] + [Produces("application/xml")] + public async Task Search(string SearchString, string resultTypeString, int maxNumberOfResults = 10) => + // var results = await MusicBrainz.MDARSearchAlbums(SearchString); + // //resultTypeString album or artist + // return results[0]; + resultTypeString switch + { + //{WMISFAIAlbumsQuery}, List Album + "album" => Ok(await _wmis.SearchAlbumsAsync(SearchString, maxNumberOfResults)),// mdsr-cd + // UIX Type: AlbumList, of Album + "artist" => Ok("SUCCESS"),// mdsr-cd, but with slightly different elements + // UIX Type: ArtistList, of Artist + "track" => Ok(),// WMISFAITracksQuery->TrackList (see WMISDATA.SCHEMA.XML) + _ => NotFound(), + }; + + // example - http://metaservices.zune.net/ZuneFAI/GetAlbumDetailsFromAlbumId?albumId=8144290952437462496&locale=1033&volume=1 + // Also known as WMISFAIGetAlbumDetailsQuery in the UIX side + // Looking for AlbumDetails - AKA mdar-cd + [HttpPost("GetAlbumDetailsFromAlbumId")] + [HttpGet("GetAlbumDetailsFromAlbumId")] + [Produces("application/xml")] + public async Task MDARGetAlbumDetailsFromAlbumId(Int64 albumId, int locale, string volume, [FromBody] MdqRequestMetadata? request = null) + { + var response = await _wmis.GetMdarCdRequestFromInt64(albumId, volume, request!); + if (request?.MdqCd?.MdqRequestId != null) + { + var mdqRequestId = request?.MdqCd?.MdqRequestId; + if (string.IsNullOrEmpty(mdqRequestId)) + { + response.mdqRequestID = new Guid(); + } + else + { + response.mdqRequestID = new Guid(mdqRequestId); + } + } + return Ok(response); + } + + [HttpPost("SubmitAddFeedback")] + public IActionResult SubmitAddFeedback() + { + return Ok(); + } + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/Controllers/MDRCD.cs b/Zune.Net.MetaServices/Controllers/MDRCD.cs new file mode 100644 index 0000000..26f044b --- /dev/null +++ b/Zune.Net.MetaServices/Controllers/MDRCD.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Mvc; +using Zune.Net.Helpers; +using Zune.Net.MetaServices.DomainModels.MdqCd; + +namespace Zune.Net.MetaServices.Controllers +{ + [Route("MDRCD")] + public class MDRCD : Controller + { + + private readonly WMIS _wmis; + private readonly ILogger _logger; + public MDRCD(WMIS wmis, ILogger logger) + { + _wmis = wmis; + _logger = logger; + } + + [HttpPost("mdrcdposturlbackgroundzune")] + [Produces("application/xml")] + public async Task MdrCdBackground(Guid requestID, [FromBody]MdqRequestMetadata request) + { + if(request.MdqCd.Tracks.Count != 1) + { + return BadRequest($"Got {request.MdqCd.Tracks.Count} tracks, expected 1"); + } + return Ok(await _wmis.GetMdrCdRequestFromTrackIdAsync(request.MdqCd.Tracks[0].trackRequestId, request.MdqCd.Tracks[0].TrackDurationMs, requestID)); + } + + [HttpGet("{type}")] + [HttpPost("{type}")] + public IActionResult Handle(string type) + { + return Ok(type); + } + + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/Controllers/ZuneApi.cs b/Zune.Net.MetaServices/Controllers/ZuneApi.cs deleted file mode 100644 index efd152a..0000000 --- a/Zune.Net.MetaServices/Controllers/ZuneApi.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Atom; -using Microsoft.AspNetCore.Mvc; - -namespace Zune.Net.MetaServices.Controllers -{ - [ApiController] - [Route("/ZuneAPI/")] - public class ZuneApi : ControllerBase - { - private readonly ILogger _logger; - - public ZuneApi(ILogger logger) - { - _logger = logger; - } - - [HttpGet, Route("EndPoints.aspx"), Route("EndPoints")] - public IActionResult EndPoints() - { - return Content(@" - - - -", Constants.XML_MIMETYPE); - } - } -} \ No newline at end of file diff --git a/Zune.Net.MetaServices/Controllers/redir.cs b/Zune.Net.MetaServices/Controllers/redir.cs new file mode 100644 index 0000000..1bd7ed6 --- /dev/null +++ b/Zune.Net.MetaServices/Controllers/redir.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Mvc; +using Zune.Net.MetaServices.DomainModels.Endpoints; + +namespace Zune.Net.MetaServices.Controllers +{ + [ApiController] + [Route("/redir/")] + + public class Redir : ControllerBase{ + + [HttpGet("ZuneFAI/")] + [Produces("application/xml")] + public IActionResult ZuneFai() + { + return Ok(new Metadata()); + } + + //eg: mdrcdposturlbackgroundzune + [HttpGet("get{mdrcd}")] + [Produces("text/plain")] + public string MDRCDdir(string mdrcd) + { + // ZuneNativeLib does this request. It takes this string and seemingly blindly appends &requestId. + // the rest of the string is pretty useless but i don't know enough about it yet. + return $"http://metaservices.zune.net/MDRCD/{mdrcd}{HttpContext.Request.QueryString.ToUriComponent()}"; + } + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/Dockerfile b/Zune.Net.MetaServices/Dockerfile new file mode 100644 index 0000000..79d7246 --- /dev/null +++ b/Zune.Net.MetaServices/Dockerfile @@ -0,0 +1,16 @@ +# https://hub.docker.com/_/microsoft-dotnet +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +WORKDIR /source + +# copy csproj and restore as distinct layers +COPY ./ . +RUN dotnet restore + +RUN dotnet publish -c release -p:PublishDir=./publish --no-restore + +# final stage/image +FROM mcr.microsoft.com/dotnet/aspnet:7.0 +WORKDIR /app +COPY --from=build /source/Zune.Net.MetaServices/publish/ ./ +ENV DOTNET_EnableDiagnostics=0 +ENTRYPOINT ["dotnet", "Zune.Net.MetaServices.dll"] diff --git a/Zune.Net.MetaServices/DomainModels/Endpoints/Endpoint.cs b/Zune.Net.MetaServices/DomainModels/Endpoints/Endpoint.cs new file mode 100644 index 0000000..251df90 --- /dev/null +++ b/Zune.Net.MetaServices/DomainModels/Endpoints/Endpoint.cs @@ -0,0 +1,8 @@ +namespace Zune.Net.MetaServices.DomainModels.Endpoints +{ + public class Endpoint + { + public string Name = string.Empty; + public string Uri = string.Empty; + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/DomainModels/Endpoints/Endpoints.cs b/Zune.Net.MetaServices/DomainModels/Endpoints/Endpoints.cs new file mode 100644 index 0000000..b2e85cf --- /dev/null +++ b/Zune.Net.MetaServices/DomainModels/Endpoints/Endpoints.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace Zune.Net.MetaServices.DomainModels.Endpoints +{ + public class Endpoints + { + [XmlElement("ENDPOINT")] + public List endpoints = new(); + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/DomainModels/Endpoints/Metadata.cs b/Zune.Net.MetaServices/DomainModels/Endpoints/Metadata.cs new file mode 100644 index 0000000..a207fc2 --- /dev/null +++ b/Zune.Net.MetaServices/DomainModels/Endpoints/Metadata.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace Zune.Net.MetaServices.DomainModels.Endpoints +{ + [XmlRoot("METADATA")] + public class Metadata + { + public Metadata() + { + var baseUrl = "http://metaservices.zune.net/ZuneFAI/"; + var endpointList = new List() + { + "Search", "GetAlbumDetailsByToc", "SubmitMatchFeedback", "GetAlbumDetailsFromToc", + "GetAlbumDetails", "GetAlbumLiteByTrackWMname", "GetAlbumLiteByToc", "GetAlbumDetailsFromAlbumId", + "GetAlbumDetailsByAlbumId", "SubmitEditFeedback", "SubmitAddFeedback", "GetResultsForArtist" + }; + + var _endpoints = new List(); + + foreach(var endpoint in endpointList) + { + _endpoints.Add(new Endpoint() + { + Name = endpoint, + Uri = $"{baseUrl}{endpoint}" + }); + } + + endpoints = new Endpoints() + { + endpoints = _endpoints + }; + } + [XmlElement("ENDPOINTS")] + public Endpoints endpoints; + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/DomainModels/MDAR-CD/MdarBackoff.cs b/Zune.Net.MetaServices/DomainModels/MDAR-CD/MdarBackoff.cs new file mode 100644 index 0000000..c130854 --- /dev/null +++ b/Zune.Net.MetaServices/DomainModels/MDAR-CD/MdarBackoff.cs @@ -0,0 +1,11 @@ +using System.Xml.Serialization; + +namespace Zune.Net.MetaServices.DomainModels.MdarCd +{ + public class MdarBackoff + { + + [XmlElement("Time")] + public int Time { get; set; } = 0; + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/DomainModels/MDAR-CD/MdarCd.cs b/Zune.Net.MetaServices/DomainModels/MDAR-CD/MdarCd.cs new file mode 100644 index 0000000..2365d99 --- /dev/null +++ b/Zune.Net.MetaServices/DomainModels/MDAR-CD/MdarCd.cs @@ -0,0 +1,81 @@ +using System.Xml.Serialization; + +namespace Zune.Net.MetaServices.DomainModels.MdarCd +{ + public class MdarCd + { + [XmlElement("A_id")] + public string AId {get; set;} = string.Empty; + + [XmlElement("Title")] + public string Title { get; set; } = string.Empty; + + [XmlElement("AlbumId")] + public long? AlbumId { get; set; } + + [XmlElement("Volume")] + public int? Volume { get; set; } + + [XmlElement("track")] + public List Items { get; set; } = new(); + + [XmlElement("ReturnCode")] + public string ReturnCode = "SUCCESS"; + + [XmlElement("WmCollectionId")] + public Guid? AlbumMBID { get; set; } + + [XmlElement("WmCollectiongroupId")] + public Guid? AlbumGroupMBID { get; set; } + + [XmlElement("AlbumWmid")] + public Guid? AlbumWmid { get; set; } + + [XmlElement("ArtistWmid")] + public Guid? ArtistWmid { get; set; } + + [XmlElement("uniqueFileID")] + public string UniqueFileID { get; set; } = string.Empty; + + [XmlElement("Source")] + public string Source { get; set; } = "AMG"; + + [XmlElement("SmallCoverArtURL")] + public string SmallCoverArtURL { get; set; } = string.Empty; + + [XmlElement("LargeCoverArtURL")] + public string LargeCoverArtURL { get; set; } = string.Empty; + + [XmlElement("ReleaseDate")] + public string DateTime { get; set; } = string.Empty; + + // The default value of " " was found in an example response via google. + [XmlElement("Rating")] + public string Rating { get; set; } = " "; + + [XmlElement("PerformerName")] + public string ArtistName { get; set; } = string.Empty; + + [XmlElement("MoreInfoLink")] + public string MoreInfoID { get; set; } = string.Empty; + + [XmlElement("Label")] + public string LabelName { get; set; } = string.Empty; + + // Till the response correctly identifies _which_ files are matches, this must be false. Otherwise, you end up with doubled matches + [XmlElement("IsExactMatch")] + public bool IsExactMatch { get; set; } = false; + + [XmlElement("Genre")] + public string Genre { get; set; } = string.Empty; + + [XmlElement("DataProviderParams")] + public string DataProviderParams { get; set; } = "Provider=AMG"; + + [XmlElement("DataProviderLogo")] + public string DataProviderLogo { get; set; } = "Provider=AMG"; + + [XmlElement("BuyNowLink")] + public string BuyNowLink { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/DomainModels/MDAR-CD/MdarCdRequestMetadata.cs b/Zune.Net.MetaServices/DomainModels/MDAR-CD/MdarCdRequestMetadata.cs new file mode 100644 index 0000000..16b6411 --- /dev/null +++ b/Zune.Net.MetaServices/DomainModels/MDAR-CD/MdarCdRequestMetadata.cs @@ -0,0 +1,17 @@ +using System.Xml.Serialization; + +namespace Zune.Net.MetaServices.DomainModels.MdarCd +{ + [XmlRoot("METADATA")] + public class MdarCdRequestMetadata + { + [XmlElement("mdqRequestID")] + public Guid? mdqRequestID{get; set;} + + [XmlElement("MDAR-CD")] + public MdarCd MdarCd {get; set;} = new(); + + [XmlElement("BackOff")] + public MdarBackoff Backoff = new(); + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/DomainModels/MDAR-CD/MdarTrack.cs b/Zune.Net.MetaServices/DomainModels/MDAR-CD/MdarTrack.cs new file mode 100644 index 0000000..42c4b3e --- /dev/null +++ b/Zune.Net.MetaServices/DomainModels/MDAR-CD/MdarTrack.cs @@ -0,0 +1,22 @@ +using System.Xml.Serialization; + +namespace Zune.Net.MetaServices.DomainModels.MdarCd +{ + public class MdarTrack + { + [XmlElement("Title")] + public string Title { get; set; } = string.Empty; + + [XmlElement("Performers")] + public string Performers { get; set; } = string.Empty; + + [XmlElement("TrackNum")] + public int TrackNumber { get; set; } = 0; + + [XmlElement("TrackWmid")] + public Guid? TrackWmid {get; set;} + + [XmlElement("TrackRequestID")] + public int TrackRequestID {get; set;} = 0; + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/DomainModels/MDQ-CD/MdqAlbum.cs b/Zune.Net.MetaServices/DomainModels/MDQ-CD/MdqAlbum.cs new file mode 100644 index 0000000..a5664f4 --- /dev/null +++ b/Zune.Net.MetaServices/DomainModels/MDQ-CD/MdqAlbum.cs @@ -0,0 +1,13 @@ +using System.Xml.Serialization; + +namespace Zune.Net.MetaServices.DomainModels.MdqCd +{ + public class MdqAlbum + { + [XmlElement("title")] + public MdqDescriptionElement AlbumTitle = new(); + + [XmlElement("artist")] + public MdqDescriptionElement AlbumArtist = new(); + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/DomainModels/MDQ-CD/MdqCd.cs b/Zune.Net.MetaServices/DomainModels/MDQ-CD/MdqCd.cs new file mode 100644 index 0000000..ce8cd09 --- /dev/null +++ b/Zune.Net.MetaServices/DomainModels/MDQ-CD/MdqCd.cs @@ -0,0 +1,16 @@ +using System.Xml.Serialization; + +namespace Zune.Net.MetaServices.DomainModels.MdqCd +{ + public class MdqCd + { + [XmlElement("mdqRequestID")] + public string MdqRequestId { get; set; } = string.Empty; + + [XmlElement("album")] + public MdqAlbum Album { get; set; } = new(); + + [XmlElement("track")] + public List Tracks { get; set; } = new(); + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/DomainModels/MDQ-CD/MdqDescriptionElement.cs b/Zune.Net.MetaServices/DomainModels/MDQ-CD/MdqDescriptionElement.cs new file mode 100644 index 0000000..9ce64c1 --- /dev/null +++ b/Zune.Net.MetaServices/DomainModels/MDQ-CD/MdqDescriptionElement.cs @@ -0,0 +1,13 @@ +using System.Xml.Serialization; + +namespace Zune.Net.MetaServices.DomainModels.MdqCd +{ + public class MdqDescriptionElement + { + [XmlElement("text")] + public string Text = string.Empty; + + [XmlArrayItem("word")] + public List Words = new(); + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/DomainModels/MDQ-CD/MdqRequestMetadata.cs b/Zune.Net.MetaServices/DomainModels/MDQ-CD/MdqRequestMetadata.cs new file mode 100644 index 0000000..651f717 --- /dev/null +++ b/Zune.Net.MetaServices/DomainModels/MDQ-CD/MdqRequestMetadata.cs @@ -0,0 +1,11 @@ +using System.Xml.Serialization; + +namespace Zune.Net.MetaServices.DomainModels.MdqCd +{ + [XmlRoot("METADATA")] + public partial class MdqRequestMetadata + { + [XmlElement("MDQ-CD")] + public MdqCd MdqCd { get; set; } = new(); + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/DomainModels/MDQ-CD/MdqTrack.cs b/Zune.Net.MetaServices/DomainModels/MDQ-CD/MdqTrack.cs new file mode 100644 index 0000000..8f296d9 --- /dev/null +++ b/Zune.Net.MetaServices/DomainModels/MDQ-CD/MdqTrack.cs @@ -0,0 +1,33 @@ +using System.Xml.Serialization; + +namespace Zune.Net.MetaServices.DomainModels.MdqCd +{ + public class MdqTrack + { + [XmlElement("title")] + public MdqDescriptionElement Title = new(); + + [XmlElement("artist")] + public MdqDescriptionElement Artist = new(); + + [XmlElement("trackNumber")] + public int TrackNumber = 0; + + [XmlElement("trackDuration")] + public int TrackDurationMs = 0; + + [XmlElement("filename")] + public string TrackFilename = string.Empty; + + [XmlElement("bitrate")] + public int Bitrate = 0; + + [XmlElement("drmProtected")] + public int DRMProtected = 0; + + [XmlElement("trackRequestID")] + public int trackRequestId = 0; + [XmlArray("trace")] + public List Trace = new(); + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/DomainModels/MDR-CD/MdrBackoff.cs b/Zune.Net.MetaServices/DomainModels/MDR-CD/MdrBackoff.cs new file mode 100644 index 0000000..0256dfa --- /dev/null +++ b/Zune.Net.MetaServices/DomainModels/MDR-CD/MdrBackoff.cs @@ -0,0 +1,12 @@ +using System.Xml.Serialization; + +namespace Zune.Net.MetaServices.DomainModels.MdrCd +{ + [XmlRoot("Backoff")] + public class Backoff + { + + [XmlElement("Time")] + public int Time { get; set; } + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/DomainModels/MDR-CD/MdrCd.cs b/Zune.Net.MetaServices/DomainModels/MDR-CD/MdrCd.cs new file mode 100644 index 0000000..19d4a27 --- /dev/null +++ b/Zune.Net.MetaServices/DomainModels/MDR-CD/MdrCd.cs @@ -0,0 +1,79 @@ +using System.Xml.Serialization; + +namespace Zune.Net.MetaServices.DomainModels.MdrCd +{ + [XmlRoot("MDR-CD")] + public class MDRCD + { + [XmlElement("mdqRequestID")] + public Guid MdqRequestID { get; set; } = Guid.Empty; + + [XmlElement("uniqueFileID")] + public string UniqueFileID { get; set; } = string.Empty; + + [XmlElement("publisherRating")] + public string PublisherRating { get; set; } = string.Empty; + + // i.e. providerName=AMG&albumID=DC1A65D9-C8A4-4190-87CB-F871B8AC6D37&a_id=R%20%203021180&album=On%20Empty&artistID=1A9ECD8B-230E-49CD-B97E-1C631530581C&p_id=P%20%202986084&artist=Kevin%20Calder + [XmlElement("buyParams")] + public string BuyParams { get; set; } = string.Empty; + + [XmlElement("dataProvider")] + public string DataProvider { get; set; } = "AMG"; + + [XmlElement("dataProviderParams")] + public string DataProviderParams { get; set; } = "Provider=AMG"; + + [XmlElement("dataProviderLogo")] + public string DataProviderLogo { get; set; } = "Provider=AMG"; + + [XmlElement("version")] + public string Version { get; set; } = "5.0"; + + [XmlElement("WMCollectionID")] + public Guid WMCollectionID { get; set; } + + [XmlElement("WMCollectionGroupID")] + public Guid WMCollectionGroupID { get; set; } + + [XmlElement("albumTitle")] + public string AlbumTitle { get; set; } = string.Empty; + + [XmlElement("albumArtist")] + public string AlbumArtist { get; set; } = string.Empty; + + [XmlElement("releaseDate")] + public DateTime ReleaseDate { get; set; } + + [XmlElement("label")] + public string Label { get; set; } = string.Empty; + + [XmlElement("genre")] + public string Genre { get; set; } = string.Empty; + + // i.e. "Pop/Rock" + [XmlElement("providerStyle")] + public string AlbumStyle { get; set; } = string.Empty; + + [XmlElement("needsIDs")] + public int NeedIDs { get; set; } = 0; + + // i.e. 200/drW500/W564/W56460UCJS0.jpg + [XmlElement("largeCoverParams")] + public string LargeCoverAddress { get; set; } = string.Empty; + + // i.e. 075/drW500/W564/W56460UCJS0.jpg + [XmlElement("smallCoverParams")] + public string SmallCoverAddress { get; set; } = string.Empty; + + // i.e. a_id=R%20%203021180 + [XmlElement("moreInfoParams")] + public string MoreInfoId { get; set; } = string.Empty; + + [XmlElement("track")] + public List Track { get; set; } = new(); + + [XmlElement("Volume")] + public int VolumeNumber { get; set; } + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/DomainModels/MDR-CD/MdrRequestMetadata.cs b/Zune.Net.MetaServices/DomainModels/MDR-CD/MdrRequestMetadata.cs new file mode 100644 index 0000000..0ac833b --- /dev/null +++ b/Zune.Net.MetaServices/DomainModels/MDR-CD/MdrRequestMetadata.cs @@ -0,0 +1,20 @@ +using System.Xml.Serialization; + +namespace Zune.Net.MetaServices.DomainModels.MdrCd +{ + [XmlRoot("METADATA")] + public class MdrRequestMetadata + { + + [XmlElement("AlbumId")] + public Guid? AlbumId { get; set; } + [XmlElement("MDR-CD")] + public MDRCD MDRCD { get; set; } = new(); + + [XmlElement("Backoff")] + public Backoff Backoff { get; set; } = new(); + + [XmlElement("mdqRequestID")] + public Guid? MdqRequestID { get; set; } + } +} diff --git a/Zune.Net.MetaServices/DomainModels/MDR-CD/MdrTrack.cs b/Zune.Net.MetaServices/DomainModels/MDR-CD/MdrTrack.cs new file mode 100644 index 0000000..2f6efce --- /dev/null +++ b/Zune.Net.MetaServices/DomainModels/MDR-CD/MdrTrack.cs @@ -0,0 +1,39 @@ +using System.Xml.Serialization; + +namespace Zune.Net.MetaServices.DomainModels.MdrCd +{ + [XmlRoot("track")] + public class Track + { + + [XmlElement("uniqueFileID")] + public string UniqueFileID { get; set; } = string.Empty; + + [XmlElement("WMContentID")] + public string WMContentID { get; set; } = string.Empty; + + [XmlElement("trackRequestID")] + public int TrackRequestID { get; set; } + + [XmlElement("trackTitle")] + public string TrackTitle { get; set; } = string.Empty; + + [XmlElement("trackNumber")] + public string TrackNumber { get; set; } = string.Empty; + + [XmlElement("trackPerformer")] + public string TrackPerformer { get; set; } = string.Empty; + + [XmlElement("trackComposer")] + public string TrackComposer { get; set; } = string.Empty; + + [XmlElement("trackConductor")] + public string TrackConductor { get; set; } = string.Empty; + + [XmlElement("period")] + public string Period { get; set; } = string.Empty; + + [XmlElement("explicitLyrics")] + public int ExplicitLyrics { get; set; } = 0; + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/DomainModels/MDSR-CD/MdsrAlbum.cs b/Zune.Net.MetaServices/DomainModels/MDSR-CD/MdsrAlbum.cs new file mode 100644 index 0000000..6ab1674 --- /dev/null +++ b/Zune.Net.MetaServices/DomainModels/MDSR-CD/MdsrAlbum.cs @@ -0,0 +1,40 @@ +using System.Xml.Serialization; + +namespace Zune.Net.MetaServices.DomainModels.MdsrCd +{ + public class MdsrAlbum + { + [XmlElement("albumFullTitle")] + public string Title { get; set; } = string.Empty; + + [XmlElement("id_album")] + public long AlbumId { get; set; } + + [XmlElement("albumPerformer")] + public string AlbumArtist { get; set; } = string.Empty; + + [XmlElement("albumGenre")] + public string Genre { get; set; } = string.Empty; + + [XmlElement("Volume")] + public int Volume { get; set; } + + [XmlElement("albumReleaseDate")] + public DateTime ReleaseDate { get; set; } + + [XmlElement("numberOfTracks")] + public int NumberOfTracks { get; set; } + + [XmlElement("bestmatch")] + public bool BestMatch { get; set; } + + [XmlElement("IsMultiDisc")] + public bool IsMultiDisc { get; set; } + + [XmlElement("albumCover")] + public string CoverParms { get; set; } = string.Empty; + + [XmlElement("buyNowLink")] + public string BuyNowParms { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/DomainModels/MDSR-CD/MdsrAlbumRequestMetadata.cs b/Zune.Net.MetaServices/DomainModels/MDSR-CD/MdsrAlbumRequestMetadata.cs new file mode 100644 index 0000000..5c9383a --- /dev/null +++ b/Zune.Net.MetaServices/DomainModels/MDSR-CD/MdsrAlbumRequestMetadata.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace Zune.Net.MetaServices.DomainModels.MdsrCd +{ + // irritating name to make it generate correctly + [XmlRoot("METADATA")] + public class MdsrAlbumRequestMetadata + { + [XmlElement("MDSR-CD")] // of type (UIX) Album + public MdsrAlbumSearchResult mDSRcD {get; set;} = new(); + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/DomainModels/MDSR-CD/MdsrAlbumSearchResult.cs b/Zune.Net.MetaServices/DomainModels/MDSR-CD/MdsrAlbumSearchResult.cs new file mode 100644 index 0000000..a30d2af --- /dev/null +++ b/Zune.Net.MetaServices/DomainModels/MDSR-CD/MdsrAlbumSearchResult.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace Zune.Net.MetaServices.DomainModels.MdsrCd +{ + public class MdsrAlbumSearchResult + { + [XmlArray("SearchResult")] + [XmlArrayItem("Result")] + public List Results = new(); + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/Models/WMIS.cs b/Zune.Net.MetaServices/Models/WMIS.cs new file mode 100644 index 0000000..2ef873a --- /dev/null +++ b/Zune.Net.MetaServices/Models/WMIS.cs @@ -0,0 +1,359 @@ +using System.Collections.Concurrent; +using System.Text.RegularExpressions; +using MetaBrainz.MusicBrainz; +using MetaBrainz.MusicBrainz.Interfaces.Entities; +using Zune.DB; +using Zune.Net.MetaServices.DomainModels.MdarCd; +using Zune.Net.MetaServices.DomainModels.MdqCd; +using Zune.Net.MetaServices.DomainModels.MdrCd; +using Zune.Net.MetaServices.DomainModels.MdsrCd; + +namespace Zune.Net.Helpers +{ + public partial class WMIS + { + public readonly Query _query; + private readonly ZuneNetContext _database; + private readonly ILogger _logger; + + public WMIS(Query query, ZuneNetContext database, ILogger logger) + { + _database = database; + _logger = logger; + _query = query; + } + + public async Task SearchAlbumsAsync(string query, int limit) + { + _logger.LogInformation($"Getting MDSR-CD results for AlbumSearch: {query}"); + var results = await _query.FindReleasesAsync(query, simple: true, limit: 10); + var releases = results.Results.Select(x => x.Item).ToList(); + + var resultList = new ConcurrentBag(); + + await Parallel.ForEachAsync(releases, async (release, ct) => + { + try + { + var albumResult = await GetMdsrAlbumByMbid(release.Id, ct, results.Results.Where(x => x.Item.Id == release.Id).First().Score == 100); + if (albumResult != null) + { + resultList.Add(albumResult); + _logger.LogInformation($"Finished building MDSR-CD result for MBID: {release.Id}"); + } + _logger.LogInformation($"No MDSR-CD result for MBID: {release.Id}"); + } + catch (Exception e) + { + _logger.LogError(e, $"Exception occured while processing request for MBID (album) {release.Id}"); + } + }); + + _logger.LogInformation($"Found {resultList.Count} results"); + + // How's that for a stackup? + return new MdsrAlbumRequestMetadata() + { + mDSRcD = new MdsrAlbumSearchResult() + { + Results = resultList.ToList() + } + }; + } + + private async Task GetMdsrAlbumByMbid(Guid guid, CancellationToken ct, bool bestmatch = false) + { + if (ct.IsCancellationRequested) + { + return null; + } + + _logger.LogInformation($"Getting all data for MBID: {guid}"); + var release = await _query.LookupReleaseAsync(guid, inc: Include.Labels | Include.DiscIds | Include.Recordings | Include.Genres | Include.Tags | Include.ArtistCredits | Include.ReleaseGroups); + + if (release.Title == null) + { + return null; + } + + var genre = "Unknown"; + var performerName = "Unknown Artist"; + var releaseDate = DateTime.Now; + var albumArtMbid = "default"; + if (release.Genres?.Count > 0) + { + genre = release.Genres?[0]?.Name; + } + else if (release.Tags?.Any() ?? false) + { + genre = release.Tags.ToList().OrderBy(x => x.VoteCount).ToList()[0].Name; + } + + if (release.ArtistCredit?.Count > 0) + { + performerName = release.ArtistCredit?[0]?.Artist.Name; + } + if (release.Date != null && !release.Date.IsEmpty) + { + + releaseDate = release.Date.NearestDate; + } + if (release.CoverArtArchive?.Front ?? false) + { + _logger.LogInformation($"Release {guid} HAS ARTWORK"); + albumArtMbid = guid.ToString(); + } + + var recordId = await _database.CreateOrGetAlbumIdInt64Async(guid); + + var numberOfTracks = 0; + var isMultiDisc = false; + if (release.Media != null && release.Media.Count > 0) + { + numberOfTracks = release.Media[0].TrackCount; + isMultiDisc = release.Media.Count > 1; + + await Parallel.ForEachAsync(release.Media[0].Tracks, async (track, ct) => + { + // Add the tracks here + }); + } + + return new MdsrAlbum() + { + Title = release.Title, + BestMatch = bestmatch, + AlbumId = recordId, + Volume = 1, + AlbumArtist = performerName, + BuyNowParms = release.Id.ToString(), + ReleaseDate = releaseDate, + Genre = genre, + NumberOfTracks = numberOfTracks, + IsMultiDisc = isMultiDisc, + CoverParms = albumArtMbid + }; + } + + private async Task> GetTracksFromIReleaseAsync(IRelease release, MdqRequestMetadata? requestMetadata) + { + var tracks = new List(); + if (release.Media != null && release.Media.Count > 0) + { + foreach (var track in release.Media[0].Tracks) + { + var trackTitle = track.Title ?? "Unknown Title"; + int trackNumber = 0; + if (track.Number != null && int.TryParse(GetFirstInt().Match(track.Number).Value, out var tryTrackNum)) + { + trackNumber = tryTrackNum; + } + + var trackArtist = track.ArtistCredit?[0]?.Name ?? release.ArtistCredit?[0]?.Name ?? "Unknown Artist"; + var trackMbid = track.Id; + + var trackObj = new MdarTrack() + { + Title = trackTitle, + Performers = trackArtist, + TrackNumber = trackNumber, + TrackWmid = trackMbid + }; + + if (requestMetadata != null) + { + // attempt to bind a trackid to a trackmbid + // Shuld probably also validate track name, string.CompareOrdinal(thisTrackIntId[0].Title.Text, trackTitle) == 0 + var thisTrackIntId = requestMetadata.MdqCd.Tracks.Where(x => x.TrackNumber == trackNumber).ToArray(); + + if (thisTrackIntId != null && thisTrackIntId.Any()) + { + await _database.CreateTrackReverseLookupRecordAsync(release.Id, trackMbid, thisTrackIntId[0].trackRequestId, thisTrackIntId[0].TrackDurationMs); + trackObj.TrackRequestID = thisTrackIntId[0].trackRequestId; + } + } + + tracks.Add(trackObj); + } + } + return tracks; + } + + public async Task GetMdarCdRequestFromInt64(long albumId, string volume, MdqRequestMetadata requestMetadata) + { + var mbid = await _database.GetAlbumIdRecordAsync(albumId); + if (!mbid.HasValue) + { + throw new KeyNotFoundException($"Cannot locate a MBID for {albumId}, please start the FAI request over"); + } + var release = await _query.LookupReleaseAsync(mbid.Value, inc: Include.Labels | Include.DiscIds | Include.Recordings | Include.Genres | Include.ArtistCredits | Include.ReleaseGroups); + + var tracks = await GetTracksFromIReleaseAsync(release, requestMetadata); + + // If we have bound a trackRequestID to every track, this is a match + var isExactMatch = tracks.All(x=>x.TrackRequestID > 0); + + var intVolume = 1; + if (int.TryParse(GetFirstInt().Match(volume).Value, out var tryVolume)) + { + intVolume = tryVolume; + } + + var artistMbid = Guid.Empty; + var artistName = string.Empty; + if (release.ArtistCredit?.Any() ?? false) + { + artistMbid = release.ArtistCredit[0].Artist?.Id ?? Guid.Empty; + artistName = release.ArtistCredit[0].Artist?.Name ?? string.Empty; + } + + var label = string.Empty; + if (release.LabelInfo != null) + { + label = release.LabelInfo[0].Label?.Name ?? release.LabelInfo[0].CatalogNumber; + } + + var genre = string.Empty; + if (release.Genres?.Count > 0) + { + genre = release.Genres?[0]?.Name; + } + else if (release.Tags?.Any() ?? false) + { + genre = release.Tags.ToList().OrderBy(x => x.VoteCount).ToList()[0].Name; + } + + var albumArtMbid = "default"; + if (release.CoverArtArchive?.Front ?? false) + { + _logger.LogInformation($"Release {release.Id} HAS ARTWORK"); + albumArtMbid = release.Id.ToString(); + } + + return new MdarCdRequestMetadata() + { + mdqRequestID = release.Id, + MdarCd = new MdarCd() + { + AId = $"R {albumId}", // will always be 10 characters + Title = release.Title, + AlbumId = albumId, + Volume = intVolume, + Items = tracks, + AlbumGroupMBID = release.Id, + AlbumMBID = release.Id, + AlbumWmid = release.Id, + ArtistWmid = artistMbid, + SmallCoverArtURL = albumArtMbid, + LargeCoverArtURL = albumArtMbid, + ArtistName = artistName, + UniqueFileID = $"AMGa_id=R {albumId}", + MoreInfoID = $"a_id=R {albumId}", + LabelName = label, + Genre = genre, + BuyNowLink = albumId.ToString(), + IsExactMatch = isExactMatch + } + }; + } + + public async Task GetMdrCdRequestFromTrackIdAsync(int TrackRequestID, int trackDuration, Guid requestId) + { + var albumMbid = await _database.GetAlbumMbidFromTrackIdAndDurationAsync(TrackRequestID, trackDuration); + var trackMbid = await _database.GetTrackMbidFromTrackIdAndDurationAsync(TrackRequestID, trackDuration); + var albumId = await _database.GetAlbumIDFromTrackIdAndDurationAsync(TrackRequestID, trackDuration); + + var release = await _query.LookupReleaseAsync(albumMbid.Value, inc: Include.Labels | Include.DiscIds | Include.Recordings | Include.Genres | Include.ArtistCredits | Include.ReleaseGroups | Include.Tags); + var track = release.Media[0].Tracks.Where(x => x.Id == trackMbid).ToList()[0]; + + var performerName = string.Empty; + if (release.ArtistCredit?.Count > 0) + { + performerName = release.ArtistCredit?[0]?.Artist.Name; + } + + var trackPerformer = string.Empty; + if (track.ArtistCredit?.Count > 0) + { + trackPerformer = track.ArtistCredit?[0]?.Artist.Name; + } + + var period = string.Empty; + + if (release.Date != null && !release.Date.IsEmpty) + { + var yearString = release.Date.Year.Value.ToString(); + var decadeString = yearString[..^1]; + period = $"{decadeString}0's"; + } + + var genre = string.Empty; + if (release.Genres?.Count > 0) + { + genre = release.Genres?[0]?.Name; + } + else if (release.Tags?.Any() ?? false) + { + genre = release.Tags.ToList().OrderBy(x => x.VoteCount).ToList()[0].Name; + } + + var label = string.Empty; + if (release.LabelInfo != null) + { + label = release.LabelInfo[0].Label?.Name ?? release.LabelInfo[0].CatalogNumber; + } + + var albumArtMbid = "default"; + if (release.CoverArtArchive?.Front ?? false) + { + _logger.LogInformation($"Release {release.Id} HAS ARTWORK"); + albumArtMbid = release.Id.ToString(); + } + + var padding = string.Empty.PadRight(8 - albumId.ToString().Length);; + + return new MdrRequestMetadata() + { + AlbumId = release.Id, + MdqRequestID = requestId, + Backoff = new Backoff() + { + Time = 0, + }, + MDRCD = new MDRCD() + { + NeedIDs = 1, + MdqRequestID = requestId, + WMCollectionGroupID = release.Id, + WMCollectionID = release.Id, + //A_id = R xxxx + //P_id = P xxxx + Track = new List(){new Track() + { + UniqueFileID = trackMbid.Value.ToString("N"), // AMGp_id=P xxxx;AMGt_id=T xxxxxx + TrackRequestID = TrackRequestID, + WMContentID = trackMbid.Value.ToString(), + TrackTitle = track.Title, + TrackNumber = track.Number, + TrackPerformer = trackPerformer, + Period = period + }}, + Label = label, + UniqueFileID = $"AMGa_ID=R {albumId}", + AlbumTitle = release.Title!, + AlbumArtist = performerName, + ReleaseDate = release.Date.NearestDate, + Genre = genre, + AlbumStyle = genre, + LargeCoverAddress = albumArtMbid, + SmallCoverAddress = albumArtMbid, + MoreInfoId = $"R {padding}albumId", + VolumeNumber = 1 + } + }; + } + + [GeneratedRegex("\\d+")] + private static partial Regex GetFirstInt(); + } +} \ No newline at end of file diff --git a/Zune.Net.MetaServices/Program.cs b/Zune.Net.MetaServices/Program.cs index ac7c10a..8490d5d 100644 --- a/Zune.Net.MetaServices/Program.cs +++ b/Zune.Net.MetaServices/Program.cs @@ -1,17 +1,28 @@ +using MetaBrainz.MusicBrainz; +using Zune.DB; +using Zune.Net; +using Zune.Net.Helpers; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. -builder.Services.AddControllers(); +builder.Services.AddControllers().AddXmlSerializerFormatters(); +builder.Services.AddSwaggerDocument(); +builder.Services.AddResponseCaching(); +builder.Host.ConfigureZuneDB(true); +builder.Services.AddTransient(); +builder.Services.AddSingleton(new Query("Zune", "4.8", "https://github.com/xerootg/ZuneNetApi")); +builder.Services.AddTransient(typeof(WMIS)); var app = builder.Build(); -// Configure the HTTP request pipeline. - -app.UseHttpsRedirection(); - -app.UseAuthorization(); +// app.UseHttpLogging(); app.MapControllers(); +app.UseResponseCaching(); + +app.UseOpenApi(); +app.UseSwaggerUi3(); app.Run(); diff --git a/Zune.Net.MetaServices/Zune.Net.MetaServices.csproj b/Zune.Net.MetaServices/Zune.Net.MetaServices.csproj index 5ba21c1..b3ed1f3 100644 --- a/Zune.Net.MetaServices/Zune.Net.MetaServices.csproj +++ b/Zune.Net.MetaServices/Zune.Net.MetaServices.csproj @@ -8,6 +8,11 @@ + + + + + diff --git a/Zune.Net.MetaServices/appsettings.Development.json b/Zune.Net.MetaServices/appsettings.Development.json index 0c208ae..1e11ded 100644 --- a/Zune.Net.MetaServices/appsettings.Development.json +++ b/Zune.Net.MetaServices/appsettings.Development.json @@ -1,8 +1,11 @@ { + "ZuneNetContext": { + "ConnectionString": "mongodb://root:rootpassword@localhost:27017", + "DatabaseName": "Zune" +}, "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Default": "Debug" } } } diff --git a/Zune.Net.MetaServices/appsettings.json b/Zune.Net.MetaServices/appsettings.json index 10f68b8..3efd2fe 100644 --- a/Zune.Net.MetaServices/appsettings.json +++ b/Zune.Net.MetaServices/appsettings.json @@ -1,8 +1,11 @@ { + "ZuneNetContext": { + "ConnectionString": "mongodb://root:rootpassword@mongodb:27017", + "DatabaseName": "Zune" +}, "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" diff --git a/Zune.Net.Mix/Controllers/AlbumController.cs b/Zune.Net.Mix/Controllers/AlbumController.cs new file mode 100644 index 0000000..704c29e --- /dev/null +++ b/Zune.Net.Mix/Controllers/AlbumController.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Mvc; +using Zune.Net.Helpers; +using Zune.Net.Mix.DomainModel; + +namespace Zune.Net.Mix.Controllers +{ + [Route("/v3.0/model/album/")] + [Route("/v4.0/{culture}/model/album/")] + [Produces("application/xml")] + public class AlbumController : Controller + { + private ILogger _logger; + public AlbumController(ILogger logger) + { + _logger = logger; + } + + [HttpGet("{mbid}")] + public async Task> SimilarAlbum(Guid mbid) + { + // First, get all of the tracks on an album MBID + var trackList = await MusicBrainz.GetTracksByAlbumMbidAsync(mbid); + + // now, get all the genreIDs + var genreIds = await MusicBrainz.GetGenreIdsByAlbumMbidAsync(mbid); + + // If we don't have enough data, 404. The API will eventually rerequest this. + if(genreIds.Count == 0 || trackList.Count == 0) + { + _logger.LogInformation($"No genre data available for albumId{mbid}"); + return NotFound(); + } + + var response = new AlbumModel(); + + foreach(var trackId in trackList) + { + response.Entry.Add(new(trackId, genreIds)); + } + + _logger.LogInformation($"Returning {genreIds.Count} genres and {trackList.Count} tracks for albumId: {mbid}"); + + return Ok(response); + } + } +} diff --git a/Zune.Net.Mix/Controllers/ArtistController.cs b/Zune.Net.Mix/Controllers/ArtistController.cs new file mode 100644 index 0000000..faf743d --- /dev/null +++ b/Zune.Net.Mix/Controllers/ArtistController.cs @@ -0,0 +1,40 @@ +using Atom.Xml; +using Microsoft.AspNetCore.Mvc; +using Zune.Net.Helpers; +using Zune.Xml.Catalog; + +namespace Zune.Net.Mix.Controllers +{ + [Route("/v3.0/")] + [Route("/v4.0/{culture}/")] + public class ArtistController : Controller + { + private ILogger _logger; + public ArtistController(ILogger logger) + { + _logger = logger; + } + + [HttpGet("artist/{mbid}")] + [Produces(Atom.Constants.ATOM_MIMETYPE)] + public async Task>> SimilarArtists(Guid mbid) + { + return await LastFM.GetSimilarArtistsByMBID(mbid); + } + + [HttpGet("model/artist/{mbid}")] + [Produces("application/xml")] + public async Task> SimilarArtistsModel(Guid mbid) + { + var artistGenreIds = await MusicBrainz.GetArtistGenreIdsByArtistIdAsync(mbid); + if(artistGenreIds.Count == 0) + { + _logger.LogInformation($"No genres were found for artistId: {mbid}"); + return NotFound(); + } + + _logger.LogInformation($"Returning {artistGenreIds.Count} genres for artistId: {mbid}"); + return Ok(new VectorEntry(mbid, artistGenreIds)); + } + } +} diff --git a/Zune.Net.Mix/Controllers/TrackController.cs b/Zune.Net.Mix/Controllers/TrackController.cs index 33aace0..324aeb7 100644 --- a/Zune.Net.Mix/Controllers/TrackController.cs +++ b/Zune.Net.Mix/Controllers/TrackController.cs @@ -5,12 +5,13 @@ namespace Zune.Net.Mix.Controllers { + [Route("/v3.0/track/")] [Route("/v4.0/{culture}/track/")] [Produces(Atom.Constants.ATOM_MIMETYPE)] public class TrackController : Controller { - [HttpGet, Route("{mbid}/similarTracks")] + [HttpGet("{mbid}/similarTracks")] public async Task>> SimilarTracks(Guid mbid) { return await LastFM.GetSimilarTracksByMBID(mbid); diff --git a/Zune.Net.Mix/Dockerfile b/Zune.Net.Mix/Dockerfile new file mode 100644 index 0000000..1eae54c --- /dev/null +++ b/Zune.Net.Mix/Dockerfile @@ -0,0 +1,16 @@ +# https://hub.docker.com/_/microsoft-dotnet +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +WORKDIR /source + +# copy csproj and restore as distinct layers +COPY ./ . +RUN dotnet restore + +RUN dotnet publish -c release -p:PublishDir=./publish --no-restore + +# final stage/image +FROM mcr.microsoft.com/dotnet/aspnet:7.0 +WORKDIR /app +COPY --from=build /source/Zune.Net.Mix/publish/ ./ +ENV DOTNET_EnableDiagnostics=0 +ENTRYPOINT ["dotnet", "Zune.Net.Mix.dll"] diff --git a/Zune.Net.Mix/DomainModel/AlbumModel.cs b/Zune.Net.Mix/DomainModel/AlbumModel.cs new file mode 100644 index 0000000..288254b --- /dev/null +++ b/Zune.Net.Mix/DomainModel/AlbumModel.cs @@ -0,0 +1,13 @@ +using System.Xml.Serialization; +namespace Zune.Net.Mix.DomainModel +{ + [XmlRoot(ElementName = "feed")] + public class AlbumModel + { + [XmlElement(ElementName = "updated")] + public DateTime Updated { get; set; } = DateTime.Now; + + [XmlElement(ElementName = "entry")] + public List Entry { get; set; } = new(); + } +} diff --git a/Zune.Net.Mix/DomainModel/VectorEntry.cs b/Zune.Net.Mix/DomainModel/VectorEntry.cs new file mode 100644 index 0000000..1bac34b --- /dev/null +++ b/Zune.Net.Mix/DomainModel/VectorEntry.cs @@ -0,0 +1,35 @@ +using System.Xml.Serialization; + +namespace Zune.Net.Mix +{ + [XmlRoot(ElementName = "entry")] + public class VectorEntry + { + public VectorEntry() { } + public VectorEntry(Guid mbid, IEnumerable genreIds) + { + var genreList = string.Join(",", genreIds); + Vector = $"0,{genreList}"; + ItemMbid = mbid; + } + + [XmlElement(ElementName = "updated")] + public DateTime Updated { get; set; } = DateTime.Now; + + [XmlElement(ElementName = "id")] + public Guid ItemMbid { get; set; } = Guid.Empty; + + [XmlElement(ElementName = "context")] + public int Context { get; set; } = 12; + + [XmlElement(ElementName = "schemeId")] + public int SchemeId { get; set; } = 1; + + // the vector is... interesting. its a combo of generes by int, prepended with "0," for reasons.. Use the constructor to make one. + [XmlElement(ElementName = "vector")] + public string? Vector { get; set; } + + [XmlElement(ElementName = "expirationDate")] + public DateTime ExpirationDate { get; set; } = DateTime.Now + TimeSpan.FromDays(14); + } +} diff --git a/Zune.Net.Mix/Program.cs b/Zune.Net.Mix/Program.cs index 3fced54..b55d748 100644 --- a/Zune.Net.Mix/Program.cs +++ b/Zune.Net.Mix/Program.cs @@ -1,8 +1,10 @@ using Zune.Net; +using Zune.Net.Helpers; var builder = WebApplication.CreateBuilder(args); // Add services to the container. +builder.Services.AddLogging(x=>x.AddConsole()); builder.Services.AddControllers(); @@ -10,9 +12,12 @@ var app = builder.Build(); +// initialize so we have a cache to protect from overcalling +MusicBrainz.Initialize(app.Environment); + // Configure the HTTP request pipeline. -//app.UseHttpsRedirection(); +// app.UseHttpLogging(); app.UseAuthorization(); diff --git a/Zune.Net.Mix/appsettings.Development.json b/Zune.Net.Mix/appsettings.Development.json index 0c208ae..dc23838 100644 --- a/Zune.Net.Mix/appsettings.Development.json +++ b/Zune.Net.Mix/appsettings.Development.json @@ -1,8 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Default": "Debug" } } } diff --git a/Zune.Net.Mix/appsettings.json b/Zune.Net.Mix/appsettings.json index 10f68b8..cdcf7bb 100644 --- a/Zune.Net.Mix/appsettings.json +++ b/Zune.Net.Mix/appsettings.json @@ -2,7 +2,6 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" diff --git a/Zune.Net.Shared/Extensions.cs b/Zune.Net.Shared/Extensions.cs index d780d9f..d3c625f 100644 --- a/Zune.Net.Shared/Extensions.cs +++ b/Zune.Net.Shared/Extensions.cs @@ -30,13 +30,16 @@ public static IApplicationBuilder UseRequestBuffering(this IApplicationBuilder a }); } - public static IHostBuilder ConfigureZuneDB(this IHostBuilder host) + public static IHostBuilder ConfigureZuneDB(this IHostBuilder host, bool skipRegisterContext = false) { return host.ConfigureServices((ctx, s) => { BsonSerializer.RegisterSerializer(new GuidSerializer(BsonType.String)); s.Configure(ctx.Configuration.GetSection("ZuneNetContext")); - s.AddSingleton(); + if(!skipRegisterContext) + { + s.AddSingleton(); + } }); } diff --git a/Zune.Net.Shared/Helpers/HttpResponseMessageResult.cs b/Zune.Net.Shared/Helpers/HttpResponseMessageResult.cs new file mode 100644 index 0000000..c4543ee --- /dev/null +++ b/Zune.Net.Shared/Helpers/HttpResponseMessageResult.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; + +// Credit: https://stackoverflow.com/questions/29975001/how-to-read-httpresponsemessage-content-as-text +public class HttpResponseMessageResult : IActionResult +{ + private readonly HttpResponseMessage _responseMessage; + + public HttpResponseMessageResult(HttpResponseMessage responseMessage) + { + _responseMessage = responseMessage; // could add throw if null + } + + public async Task ExecuteResultAsync(ActionContext context) + { + var response = context.HttpContext.Response; + + + if (_responseMessage == null) + { + var message = "Response message cannot be null"; + + throw new InvalidOperationException(message); + } + + using (_responseMessage) + { + response.StatusCode = (int)_responseMessage.StatusCode; + + var responseFeature = context.HttpContext.Features.Get(); + if (responseFeature != null) + { + responseFeature.ReasonPhrase = _responseMessage.ReasonPhrase; + } + + var responseHeaders = _responseMessage.Headers; + + // Ignore the Transfer-Encoding header if it is just "chunked". + // We let the host decide about whether the response should be chunked or not. + if (responseHeaders.TransferEncodingChunked == true && + responseHeaders.TransferEncoding.Count == 1) + { + responseHeaders.TransferEncoding.Clear(); + } + + foreach (var header in responseHeaders) + { + response.Headers.Append(header.Key, header.Value.ToArray()); + } + + if (_responseMessage.Content != null) + { + var contentHeaders = _responseMessage.Content.Headers; + + // Copy the response content headers only after ensuring they are complete. + // We ask for Content-Length first because HttpContent lazily computes this + // and only afterwards writes the value into the content headers. + var unused = contentHeaders.ContentLength; + + foreach (var header in contentHeaders) + { + response.Headers.Append(header.Key, header.Value.ToArray()); + } + + await _responseMessage.Content.CopyToAsync(response.Body); + } + } + } +} \ No newline at end of file diff --git a/Zune.Net.Shared/Helpers/MusicBrainz.Album.cs b/Zune.Net.Shared/Helpers/MusicBrainz.Album.cs index ad6b6be..bb5c659 100644 --- a/Zune.Net.Shared/Helpers/MusicBrainz.Album.cs +++ b/Zune.Net.Shared/Helpers/MusicBrainz.Album.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Zune.Xml.Catalog; namespace Zune.Net.Helpers @@ -34,6 +35,12 @@ public static Album GetAlbumByMBID(Guid mbid) return MBReleaseToAlbum(mb_rel); } + public static Album GetAlbumByRecordingId(Guid mbid) + { + var mb_rel = _query.LookupRecording(mbid, Include.Releases | Include.ArtistCredits); + + return MBReleaseToAlbum(mb_rel.Releases.First()); + } public static Album MBReleaseToAlbum(IRelease mb_rel, DateTime? updated = null, bool includeRights = true) { @@ -63,13 +70,21 @@ public static Album MBReleaseToAlbum(IRelease mb_rel, DateTime? updated = null, if (mb_rel.Media != null && mb_rel.Media.Count > 0) { - var mb_media = mb_rel.Media[0]; - if (mb_media.Tracks != null && mb_media.Tracks.Count > 0) + album.Tracks = new(); + + for (var mediaId = 0; mediaId < mb_rel.Media.Count; mediaId++) { - album.Tracks = new(); - foreach (var mb_track in mb_media.Tracks) - album.Tracks.Add(MBTrackToTrack(mb_track, trackArtist: artist, updated: updated, includeRights: includeRights)); + var mb_media = mb_rel.Media[mediaId]; + + if (mb_media.Tracks != null && mb_media.Tracks.Count > 0) + { + foreach (var mb_track in mb_media.Tracks) + { + album.Tracks.Add(MBTrackToTrack(mb_track, diskNumber: mediaId + 1, trackArtist: artist, updated: updated, includeRights: includeRights)); + } + } } + } if (includeRights) @@ -87,5 +102,62 @@ public static MiniAlbum MBReleaseToMiniAlbum(IRelease mb_rel) Title = mb_rel.Title }; } + + public static async Task> GetTracksByAlbumMbidAsync(Guid albumId) + { + var trackList = new List(); + try + { + var album = await _query.LookupReleaseAsync(albumId, Include.Recordings); + var medias = album.Media?.ToList() ?? new List(); + medias.ForEach(media => media.Tracks.ToList().ForEach(track => trackList.Add(track.Id))); + } + catch { } + return trackList; + } + + public static async Task> GetGenreIdsByReleaseGroupMbidAsync(Guid releaseGroupId) + { + try + { + var genreIdList = new List(); + var releaseGroup = await _query.LookupReleaseGroupAsync(releaseGroupId, Include.Tags | Include.Genres); + foreach (var genre in releaseGroup.Genres?.ToList() ?? new List()) + { + var genreIndexId = Array.IndexOf(MusicBrainzGenreList.Genres, genre.Name); + if (genreIndexId > -1) + { + genreIdList.Add(genreIndexId); + } + } + + foreach (var tag in releaseGroup.Tags?.ToList() ?? new List()) + { + var genreIndexId = Array.IndexOf(MusicBrainzGenreList.Genres, tag.Name); + if (genreIndexId > -1) + { + genreIdList.Add(genreIndexId); + } + } + + return genreIdList; + } catch + { + return new List(); + } + } + + public static async Task> GetGenreIdsByAlbumMbidAsync(Guid albumId) + { + try + { + var album = await _query.LookupReleaseAsync(albumId, Include.ReleaseGroups); + return await GetGenreIdsByReleaseGroupMbidAsync(album.ReleaseGroup.Id); + } + catch + { + return new List(); + } + } } } diff --git a/Zune.Net.Shared/Helpers/MusicBrainz.Artist.cs b/Zune.Net.Shared/Helpers/MusicBrainz.Artist.cs index fa9a68d..bb53096 100644 --- a/Zune.Net.Shared/Helpers/MusicBrainz.Artist.cs +++ b/Zune.Net.Shared/Helpers/MusicBrainz.Artist.cs @@ -2,7 +2,9 @@ using MetaBrainz.MusicBrainz; using MetaBrainz.MusicBrainz.Interfaces.Entities; using System; +using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Zune.Xml.Catalog; namespace Zune.Net.Helpers @@ -146,5 +148,21 @@ public static MiniArtist MBArtistToMiniArtist(IArtist mb_artist) Title = mb_artist.Name }; } + + public static async Task> GetArtistGenreIdsByArtistIdAsync(Guid artistId) + { + var artistGenreIds = new HashSet(); + + var artistDetails = _query.LookupArtist(artistId, Include.ReleaseGroups); + var releaseGroupIds = artistDetails.ReleaseGroups?.Select(x=> x.Id) ?? new List(); + + foreach(var releaseGroup in releaseGroupIds) + { + var results = await MusicBrainz.GetGenreIdsByReleaseGroupMbidAsync(releaseGroup); + results.ForEach(genreId => artistGenreIds.Add(genreId)); + } + + return artistGenreIds.ToList(); + } } } diff --git a/Zune.Net.Shared/Helpers/MusicBrainz.Track.cs b/Zune.Net.Shared/Helpers/MusicBrainz.Track.cs index cd34ac6..915255b 100644 --- a/Zune.Net.Shared/Helpers/MusicBrainz.Track.cs +++ b/Zune.Net.Shared/Helpers/MusicBrainz.Track.cs @@ -41,7 +41,7 @@ public static Track GetTrackByMBID(Guid mbid) var mb_rec = _query.LookupRecording(mbid, Include.Genres | Include.ArtistCredits | Include.Releases | Include.UrlRelationships | Include.Media); return MBRecordingToTrack(mb_rec, includeRights: true); } - catch (QueryException) + catch { // MusicBrainz Picard likes to put the Track ID instead of the Recording ID var releases = _query.BrowseTrackReleases(mbid, limit: 1, inc: Include.UrlRelationships); @@ -89,12 +89,13 @@ public static Track MBRecordingToTrack(IRecording mb_rec, DateTime? updated = nu return track; } - public static Track MBTrackToTrack(ITrack mb_track, MiniArtist trackArtist, DateTime? updated = null, bool includeRights = true) + public static Track MBTrackToTrack(ITrack mb_track, MiniArtist trackArtist, int diskNumber = 1, DateTime? updated = null, bool includeRights = true) { updated ??= DateTime.Now; Track track = new() { + DiscNumber = diskNumber, Id = mb_track.Id.ToString(), Title = mb_track.Title, PrimaryArtist = trackArtist, diff --git a/Zune.Net.Shared/Helpers/MusicBrainz.cs b/Zune.Net.Shared/Helpers/MusicBrainz.cs index b19fbc1..ab99ffb 100644 --- a/Zune.Net.Shared/Helpers/MusicBrainz.cs +++ b/Zune.Net.Shared/Helpers/MusicBrainz.cs @@ -13,10 +13,16 @@ public static partial class MusicBrainz public static void Initialize(IWebHostEnvironment env) { + // assume worst-case scenario, mix is getting hit at the same time as catalog. The + // frontent cache, nginx, will serve up cached results, but this interleave of 1.5s + // might be enought to avoid a race. + Query.DelayBetweenRequests = 1.5; + _query.ConfigureClientCreation(delegate { + // might be useful to put this as a shared resource between mix and cog var cachePath = System.IO.Path.Combine(env.ContentRootPath, "bin", "cache"); - var cacheTime = TimeSpan.FromMinutes(5); + var cacheTime = TimeSpan.FromHours(1); return new HttpClient(new CachedHttpClientHandler(cachePath, cacheTime)); }); } diff --git a/Zune.Net.Shared/Helpers/MusicBrainzGenreList.cs b/Zune.Net.Shared/Helpers/MusicBrainzGenreList.cs new file mode 100644 index 0000000..190c8b3 --- /dev/null +++ b/Zune.Net.Shared/Helpers/MusicBrainzGenreList.cs @@ -0,0 +1,1794 @@ +namespace Zune.Net.Helpers +{ + public static class MusicBrainzGenreList + { + // fetched with https://musicbrainz.org/ws/2/genre/all?fmt=txt, saved off for index stability + public static string[] Genres = + { + "2 tone", + "2-step", + "aak", + "abhang", + "aboio", + "abstract hip hop", + "acid breaks", + "acid house", + "acid jazz", + "acid rock", + "acid techno", + "acid trance", + "acidcore", + "acousmatic", + "acoustic blues", + "acoustic chicago blues", + "acoustic rock", + "acoustic texas blues", + "adhunik geet", + "afoxê", + "african blues", + "afro house", + "afro rock", + "afro trap", + "afro-cuban jazz", + "afro-funk", + "afro-jazz", + "afro-zouk", + "afrobeat", + "afrobeats", + "afropiano", + "afroswing", + "agbadza", + "agbekor", + "aggrotech", + "ahwash", + "aita", + "akishibu-kei", + "al jeel", + "algerian chaabi", + "algorave", + "alloukou", + "alpenrock", + "alternative country", + "alternative dance", + "alternative folk", + "alternative hip hop", + "alternative metal", + "alternative pop", + "alternative punk", + "alternative r&b", + "alternative rock", + "amapiano", + "ambasse bey", + "ambient", + "ambient americana", + "ambient dub", + "ambient house", + "ambient noise wall", + "ambient pop", + "ambient techno", + "ambient trance", + "ambrosian chant", + "american primitive guitar", + "americana", + "anarcho-punk", + "anatolian rock", + "andalusian classical", + "andean new age", + "anglican chant", + "animal sounds", + "anti-folk", + "aor", + "apala", + "appalachian folk", + "arabesk", + "arabesk rap", + "arena rock", + "arrocha", + "arrocha funk", + "arrocha sertanejo", + "arrochadeira", + "ars antiqua", + "ars nova", + "ars subtilior", + "art pop", + "art punk", + "art rock", + "art song", + "artcore", + "asmr", + "assiko", + "atmospheric black metal", + "atmospheric drum and bass", + "atmospheric sludge metal", + "audio drama", + "avant-folk", + "avant-garde", + "avant-garde jazz", + "avant-garde metal", + "avant-garde pop", + "avant-prog", + "avtorskaya pesnya", + "axé", + "bachata", + "bachatón", + "bagad", + "baião", + "baila", + "baisha xiyue", + "baithak gana", + "bakersfield sound", + "balani show", + "balearic beat", + "balearic trance", + "balinese gamelan", + "balitaw", + "ballad", + "ballad opera", + "ballet", + "ballroom house", + "baltimore club", + "bambuco", + "banda sinaloense", + "bandari", + "bandinha", + "barber beats", + "barbershop", + "bard rock", + "bardcore", + "baroque", + "baroque pop", + "baroque suite", + "bass house", + "bassline", + "batida", + "batidão romântico", + "battle rap", + "batucada", + "batuque", + "baul gaan", + "beat bruxaria", + "beat music", + "beat poetry", + "beatboxing", + "beatdown hardcore", + "bebop", + "bedroom pop", + "beijing opera", + "bélé", + "belgian techno", + "bend-skin", + "beneventan chant", + "benga", + "benna", + "bérite club", + "berlin school", + "bhajan", + "bhangra", + "bhavageethe", + "big band", + "big beat", + "big room house", + "big room trance", + "biguine", + "bikutsi", + "binaural beats", + "biraha", + "birdsong", + "birmingham sound", + "bit music", + "bitpop", + "black 'n' roll", + "black ambient", + "black metal", + "black midi", + "black noise", + "blackened crust", + "blackened death metal", + "blackgaze", + "bleep techno", + "blue-eyed soul", + "bluegrass", + "bluegrass gospel", + "blues", + "blues rock", + "bocet", + "boduberu", + "boedra", + "bogino duu", + "bolero", + "bolero español", + "bolero son", + "bolero-beat", + "bomba", + "bomba del chota", + "bongo flava", + "boogaloo", + "boogie", + "boogie rock", + "boogie-woogie", + "boom bap", + "bossa nova", + "bounce", + "bouncy techno", + "bouyon", + "brass band", + "brazilian bass", + "break-in", + "breakbeat", + "breakbeat hardcore", + "breakbeat kota", + "breakcore", + "breaks", + "breakstep", + "brega", + "brega calypso", + "brega funk", + "brill building", + "brit funk", + "britcore", + "british blues", + "british brass band", + "british folk rock", + "british rhythm & blues", + "britpop", + "bro-country", + "broken beat", + "broken transmission", + "brostep", + "brutal death metal", + "brutal prog", + "bubblegum bass", + "bubblegum dance", + "bubblegum pop", + "bubbling", + "bubbling house", + "budots", + "bulería", + "bullerengue", + "burger-highlife", + "burmese classical", + "burmese mono", + "burmese stereo", + "burning spirits", + "bytebeat", + "byzantine chant", + "c-pop", + "c86", + "ca trù", + "cabaret", + "cabo zouk", + "cadence lypso", + "cadence rampa", + "cải lương", + "cajun", + "cakewalk", + "čalgija", + "calipso venezolano", + "calypso", + "campursari", + "campus folk", + "canción melódica", + "candombe", + "candombe beat", + "cantata", + "cante alentejano", + "canterbury scene", + "canto a lo poeta", + "canto cardenche", + "cantonese opera", + "cantopop", + "cantoria", + "cantu a chiterra", + "cantu a tenore", + "canzone d'autore", + "canzone napoletana", + "canzone neomelodica", + "cape breton fiddling", + "cape jazz", + "carimbó", + "carnatic classical", + "carnavalito", + "carranga", + "celtic", + "celtic electronica", + "celtic metal", + "celtic new age", + "celtic punk", + "celtic rock", + "central asian throat singing", + "chacarera", + "chachachá", + "chalga", + "chamamé", + "chamarrita açoriana", + "chamarrita rioplatense", + "chamber folk", + "chamber pop", + "champeta", + "changa tuki", + "changüí", + "chanson à texte", + "chanson française", + "chanson réaliste", + "charanga", + "chazzanut", + "chèo", + "chicago blues", + "chicago bop", + "chicago drill", + "chicago house", + "chicago soul", + "chicano rap", + "chilena", + "chillout", + "chillstep", + "chillsynth", + "chillwave", + "chimurenga", + "chinese classical", + "chinese opera", + "chinese revolutionary opera", + "chipmunk soul", + "chiptune", + "chopped and screwed", + "choral symphony", + "choro", + "chotis madrileño", + "christian hip hop", + "christian metal", + "christian rock", + "christmas music", + "church music", + "chutney", + "chutney soca", + "cilokaq", + "cinematic classical", + "ciranda", + "circus march", + "city pop", + "classic blues", + "classic country", + "classic jazz", + "classic rock", + "classical", + "classical crossover", + "classical period", + "close harmony", + "cloud rap", + "club", + "cocktail nation", + "coco", + "coladeira", + "coldwave", + "colindă", + "colour bass", + "comedy", + "comedy hip hop", + "comedy rock", + "comfy synth", + "compas", + "complextro", + "concerto", + "concerto for orchestra", + "concerto grosso", + "conducted improvisation", + "conga", + "congolese rumba", + "conscious hip hop", + "contemporary christian", + "contemporary classical", + "contemporary country", + "contemporary folk", + "contemporary gospel", + "contemporary jazz", + "contemporary r&b", + "contra", + "cool jazz", + "coon song", + "copla", + "corrido", + "corrido tumbado", + "country", + "country and irish", + "country blues", + "country boogie", + "country folk", + "country gospel", + "country pop", + "country rap", + "country rock", + "country soul", + "country yodeling", + "countrypolitan", + "coupé-décalé", + "cowpunk", + "crack rock steady", + "crime jazz", + "crossbreed", + "crossover jazz", + "crossover prog", + "crossover thrash", + "cruise", + "crunk", + "crunkcore", + "crust punk", + "csárdás", + "cuarteto", + "cubatón", + "cueca", + "cumbia", + "cumbia argentina", + "cumbia colombiana", + "cumbia mexicana", + "cumbia norteña mexicana", + "cumbia peruana", + "cumbia pop", + "cumbia santafesina", + "cumbia sonidera", + "cumbia turra", + "cumbia villera", + "cumbiatón", + "cuplé", + "currulao", + "cyber metal", + "cybergrind", + "cyberpunk", + "d-beat", + "dabke", + "dance", + "dance-pop", + "dance-punk", + "dance-rock", + "dancefloor drum and bass", + "dancehall", + "dangak", + "dangdut", + "danmono", + "dansband", + "dansktop", + "danzón", + "dark ambient", + "dark cabaret", + "dark disco", + "dark electro", + "dark folk", + "dark jazz", + "dark plugg", + "dark psytrance", + "dark wave", + "darkcore", + "darkstep", + "darksynth", + "data sonification", + "death 'n' roll", + "death industrial", + "death metal", + "death-doom metal", + "deathcore", + "deathgrind", + "deathrock", + "deathstep", + "deconstructed club", + "deep funk", + "deep house", + "deep soul", + "deep tech", + "deep techno", + "delta blues", + "dembow", + "dennery segment", + "denpa", + "depressive black metal", + "descarga", + "desert blues", + "desert rock", + "detroit techno", + "detroit trap", + "dhaanto", + "dhrupad", + "digicore", + "digital cumbia", + "digital fusion", + "digital hardcore", + "dimotiko", + "dirty south", + "disco", + "disco polo", + "dissonant death metal", + "diva house", + "divertissement", + "dixieland", + "djanba", + "djent", + "dobrado", + "doina", + "dongjing", + "donk", + "donosti sound", + "doo-wop", + "doom metal", + "doomcore", + "doskpop", + "downtempo", + "dream pop", + "dream trance", + "dreampunk", + "drift phonk", + "drill", + "drill and bass", + "drone", + "drone metal", + "drum and bass", + "drumfunk", + "drumless hip hop", + "drumline", + "drumstep", + "dub", + "dub poetry", + "dub techno", + "dubstep", + "dubstyle", + "dubwise", + "duma", + "dunedin sound", + "dungeon sound", + "dungeon synth", + "duranguense", + "dutch house", + "eai", + "east coast hip hop", + "easy listening", + "easycore", + "ebm", + "edm", + "electric blues", + "electric texas blues", + "electro", + "electro house", + "electro latino", + "electro swing", + "electro-disco", + "electro-funk", + "electro-industrial", + "electroacoustic", + "electroclash", + "electronic", + "electronic rock", + "electronica", + "electronicore", + "electropop", + "electropunk", + "electrotango", + "eleki", + "embolada", + "emo", + "emo pop", + "emo rap", + "emocore", + "emoviolence", + "enka", + "éntekhno", + "epic collage", + "epic doom metal", + "estrada", + "ethereal wave", + "ethio-jazz", + "étude", + "euphoric hardstyle", + "euro house", + "euro-disco", + "euro-trance", + "eurobeat", + "eurodance", + "europop", + "euskal kantagintza berria", + "exotica", + "experimental", + "experimental big band", + "experimental electronic", + "experimental hip hop", + "experimental rock", + "expressionism", + "extratone", + "fado", + "fado de coimbra", + "fairy tale", + "fakaseasea", + "falak", + "famo", + "fandango", + "fandango caiçara", + "fantasia", + "fantezi", + "festejo", + "festival progressive house", + "festival trap", + "fidget house", + "field recording", + "fife and drum", + "fife and drum blues", + "filin", + "filk", + "filmi", + "finnish tango", + "flamenco", + "flamenco jazz", + "flamenco pop", + "flashcore", + "flex dance music", + "florida breaks", + "fm synthesis", + "folk", + "folk metal", + "folk pop", + "folk punk", + "folk rock", + "folkhop", + "folktronica", + "fon leb", + "footwork", + "footwork jungle", + "forest psytrance", + "forró", + "forró eletrônico", + "forró universitário", + "freak folk", + "freakbeat", + "free folk", + "free improvisation", + "free jazz", + "free tekno", + "freeform hardcore", + "freestyle", + "french electro", + "french house", + "frenchcore", + "frevo", + "fuji", + "full-on", + "funaná", + "funeral doom metal", + "funeral march", + "fungi", + "funk", + "funk carioca", + "funk mandelão", + "funk melody", + "funk metal", + "funk ostentação", + "funk proibidão", + "funk rock", + "funknejo", + "funkot", + "funktronica", + "funky house", + "future bass", + "future bounce", + "future core", + "future funk", + "future garage", + "future house", + "future rave", + "future riddim", + "futurepop", + "futurism", + "g-funk", + "g-house", + "gabber", + "gaelic psalm singing", + "gagaku", + "gagok", + "gaita zuliana", + "gambang kromong", + "gamelan", + "gamelan angklung", + "gamelan beleganjur", + "gamelan degung", + "gamelan gender wayang", + "gamelan gong gede", + "gamelan gong kebyar", + "gamelan jegog", + "gamelan joged bumbung", + "gamelan salendro", + "gamelan sekaten", + "gamelan selunding", + "gamelan semar pegulingan", + "gamelan siteran", + "gangsta rap", + "garage house", + "garage punk", + "garage rock", + "garage rock revival", + "garba", + "geek rock", + "genge", + "għana", + "ghazal", + "ghetto house", + "ghettotech", + "ginan", + "glam", + "glam metal", + "glam punk", + "glam rock", + "glitch", + "glitch hop", + "glitch hop edm", + "glitch pop", + "gnawa", + "go-go", + "goa trance", + "gondang", + "goombay", + "goregrind", + "gorenoise", + "gospel", + "gospel house", + "gospel reggae", + "gothic", + "gothic country", + "gothic metal", + "gothic rock", + "gqom", + "grand opera", + "grebo", + "gregorian chant", + "grime", + "grindcore", + "griot", + "groove metal", + "group sounds", + "grunge", + "guaguancó", + "guajira", + "guaracha", + "guaracha edm", + "guarania", + "guided meditation", + "guitarrada", + "gumbe", + "guoyue", + "gwo ka", + "gypsy jazz", + "gypsy punk", + "habanera", + "haitian vodou drumming", + "halftime", + "hambo", + "hamburger schule", + "hands up", + "happy hardcore", + "harawi", + "hard bop", + "hard drum", + "hard house", + "hard nrg", + "hard rock", + "hard techno", + "hard trance", + "hard trap", + "hardbag", + "hardbass", + "hardcore breaks", + "hardcore hip hop", + "hardcore punk", + "hardcore techno", + "hardgroove techno", + "hardstep", + "hardstyle", + "hardvapour", + "hardwave", + "harsh noise", + "harsh noise wall", + "hát tuồng", + "hauntology", + "heartland rock", + "heaven trap", + "heavy metal", + "heavy psych", + "heikyoku", + "hexd", + "hi-nrg", + "hi-tech", + "highlife", + "hill country blues", + "himene tarava", + "hindustani classical", + "hip hop", + "hip hop soul", + "hip house", + "hiplife", + "holy minimalism", + "honky tonk", + "honkyoku", + "hopepunk", + "horror punk", + "horror synth", + "horrorcore", + "house", + "huapango", + "huayno", + "humppa", + "hyangak", + "hybrid trap", + "hyper techno", + "hyperpop", + "hyphy", + "hypnagogic pop", + "idm", + "idol kayō", + "illbient", + "impressionism", + "indeterminacy", + "indian classical", + "indian pop", + "indie folk", + "indie pop", + "indie rock", + "indie surf", + "indietronica", + "indo jazz", + "indorock", + "industrial", + "industrial hardcore", + "industrial hip hop", + "industrial metal", + "industrial musical", + "industrial rock", + "industrial techno", + "instrumental", + "instrumental hip hop", + "instrumental jazz", + "instrumental rock", + "integral serialism", + "interview", + "iraqi maqam", + "irish folk", + "isicathamiya", + "islamic modal music", + "italo dance", + "italo house", + "italo-disco", + "izlan", + "izvorna bosanska muzika", + "j-core", + "j-pop", + "j-rock", + "jácara", + "jaipongan", + "jam band", + "jamaican ska", + "james bay fiddling", + "jamgrass", + "jangle pop", + "japanese classical", + "javanese gamelan", + "jazz", + "jazz blues", + "jazz fusion", + "jazz poetry", + "jazz pop", + "jazz rap", + "jazz rock", + "jazz-funk", + "jazzstep", + "jeongak", + "jerk rap", + "jersey club", + "jersey drill", + "jersey sound", + "jesus music", + "jiangnan sizhu", + "jit", + "jiuta", + "joik", + "jongo", + "joropo", + "jōruri", + "jota", + "jovem guarda", + "jug band", + "jùjú", + "juke", + "jump blues", + "jump up", + "jumpstyle", + "jungle", + "jungle dutch", + "jungle terror", + "junkanoo", + "k-pop", + "kabarett", + "kacapi suling", + "kadongo kamu", + "kafi", + "kagura", + "kalindula", + "kalon'ny fahiny", + "kaneka", + "kankyō ongaku", + "kantan chamorrita", + "kanto", + "kapuka", + "kaseko", + "kasékò", + "kawaii future bass", + "kawaii metal", + "kayōkyoku", + "kecak", + "keroncong", + "ketuk tilu", + "khrueang sai", + "khyal", + "kidandali", + "kidumbak", + "kilapanga", + "kirtan", + "kizomba", + "klapa", + "kleinkunst", + "klezmer", + "kliningan", + "könsrock", + "koplo", + "korean ballad", + "korean classical", + "korean revolutionary opera", + "kouta", + "krakowiak", + "krautrock", + "kuda lumping", + "kuduro", + "kujawiak", + "kulintang", + "kumi-daiko", + "kumiuta", + "kundiman", + "kwaito", + "kwassa kwassa", + "kwela", + "kyivan chant", + "laiko", + "lambada", + "landó", + "langgam jawa", + "latin", + "latin ballad", + "latin disco", + "latin funk", + "latin house", + "latin jazz", + "latin pop", + "latin rock", + "latin soul", + "lavani", + "lecture", + "leftfield", + "lento violento", + "levenslied", + "lied", + "liedermacher", + "liquid funk", + "liscio", + "livetronica", + "lo-fi", + "lo-fi hip hop", + "lo-fi house", + "lolicore", + "louisiana blues", + "lounge", + "lovers rock", + "lowercase", + "luk krung", + "luk thung", + "lundu", + "lute song", + "mad", + "madchester", + "maddahi", + "madrigal", + "maftirim", + "mahori", + "mahraganat", + "mainstream rock", + "makina", + "makossa", + "malagueña venezolana", + "malay gamelan", + "malhun", + "mallsoft", + "maloya", + "maloya élektrik", + "mambo", + "mambo chileno", + "mambo urbano", + "mandopop", + "manele", + "mangue beat", + "manila sound", + "manyao", + "marabi", + "maracatu", + "march", + "marching band", + "marchinha", + "mariachi", + "marinera", + "marrabenta", + "martial industrial", + "mashcore", + "maskanda", + "mass", + "math pop", + "math rock", + "mathcore", + "maxixe", + "mazurka", + "mbalax", + "mbaqanga", + "mbube", + "mchiriku", + "medieval", + "medieval lyric poetry", + "medieval metal", + "medieval rock", + "meiji shinkyoku", + "melbourne bounce", + "melodic black metal", + "melodic death metal", + "melodic dubstep", + "melodic hardcore", + "melodic house", + "melodic metalcore", + "melodic techno", + "melodic trance", + "mélodie", + "memphis rap", + "mento", + "menzuma", + "merecumbé", + "merengue", + "merengue típico", + "merenhouse", + "merequetengue", + "méringue", + "merseybeat", + "metal", + "metalcore", + "meyxana", + "miami bass", + "microhouse", + "microsound", + "microtonal classical", + "midtempo bass", + "midwest emo", + "miejski folk", + "milonga", + "min'yō", + "minatory", + "mincecore", + "minimal drum and bass", + "minimal synth", + "minimal techno", + "minimal wave", + "minimalism", + "minneapolis sound", + "minstrelsy", + "mobb music", + "mod", + "mod revival", + "moda de viola", + "modal jazz", + "modern blues", + "modern classical", + "modern creative", + "modern hardtek", + "modern laiko", + "modinha", + "monodrama", + "mood kayō", + "moogsploitation", + "moombahcore", + "moombahton", + "mor lam", + "mor lam sing", + "morna", + "moroccan chaabi", + "motet", + "motown", + "moutya", + "movimiento alterado", + "mozarabic chant", + "mpb", + "mugham", + "muiñeira", + "mulatós", + "muliza", + "murga", + "murga uruguaya", + "musette", + "music hall", + "música cebolla", + "música criolla", + "música de intervenção", + "musical", + "musique concrète", + "musique concrète instrumentale", + "muziki wa dansi", + "nagauta", + "narcocorrido", + "narodnozabavna glasba", + "nasheed", + "nashville sound", + "native american new age", + "nature sounds", + "natya sangeet", + "nederbeat", + "nederpop", + "neo kyma", + "neo soul", + "neo-acoustic", + "neo-medieval folk", + "neo-progressive rock", + "neo-psychedelia", + "neo-rockabilly", + "néo-trad", + "neo-traditional country", + "neoclassical dark wave", + "neoclassical metal", + "neoclassical new age", + "neoclassicism", + "neocrust", + "neofolk", + "neofolklore", + "neon pop punk", + "neoperreo", + "nerdcore", + "nerdcore techno", + "neue deutsche härte", + "neue deutsche welle", + "neurofunk", + "neurohop", + "new age", + "new beat", + "new complexity", + "new jack swing", + "new orleans blues", + "new orleans r&b", + "new rave", + "new romantic", + "new wave", + "ngoma", + "nhạc đỏ", + "nhạc vàng", + "night full-on", + "nightcore", + "nigun", + "nintendocore", + "nitzhonot", + "njuup", + "no melody trap", + "no wave", + "nocturne", + "noh", + "noise", + "noise pop", + "noise rock", + "noisecore", + "non-music", + "nortec", + "norteño", + "northern soul", + "nouveau zydeco", + "nova cançó", + "novelty piano", + "novo dub", + "nu disco", + "nu jazz", + "nu metal", + "nu skool breaks", + "nu style gabber", + "nueva canción", + "nueva canción chilena", + "nueva canción española", + "nueva cumbia chilena", + "nueva trova", + "nuevo cancionero", + "nuevo flamenco", + "nuevo tango", + "nwobhm", + "nyū myūjikku", + "oberek", + "occult rock", + "odissi classical", + "oi", + "old roman chant", + "old school death metal", + "old school hip hop", + "old-time", + "omutibo", + "onda nueva", + "ondō", + "onkyo", + "opera", + "opera buffa", + "opéra comique", + "opera semiseria", + "opera seria", + "opera-ballet", + "operatic pop", + "operetta", + "opm", + "oratorio", + "orchestral", + "orchestral jazz", + "orchestral song", + "organic house", + "oriental ballad", + "orkes gambus", + "orthodox pop", + "outlaw country", + "outsider house", + "overture", + "özgün müzik", + "p-funk", + "pachanga", + "pacific reggae", + "pagan black metal", + "pagan folk", + "pagodão", + "pagode", + "pagode romântico", + "paisley underground", + "palingsound", + "palm-wine", + "pansori", + "parang", + "partido alto", + "pasillo", + "pasodoble", + "payada", + "peak time techno", + "pep band", + "persian classical", + "persian pop", + "philly club", + "philly soul", + "phleng phuea chiwit", + "phonk", + "piano blues", + "piano rock", + "picopop", + "piedmont blues", + "pilón", + "pimba", + "pinpeat", + "pìobaireachd", + "pipe band music", + "piphat", + "piseiro", + "piyyut", + "pizzica", + "plainchant", + "plena", + "plugg", + "pluggnb", + "plunderphonics", + "poetry", + "polca criolla", + "political hip hop", + "polka", + "polka paraguaya", + "polonaise", + "pon-chak disco", + "pop", + "pop ghazal", + "pop kreatif", + "pop metal", + "pop minang", + "pop punk", + "pop raï", + "pop rap", + "pop rock", + "pop soul", + "pop yeh-yeh", + "porn groove", + "pornogrind", + "porro", + "post-bop", + "post-britpop", + "post-classical", + "post-grunge", + "post-hardcore", + "post-industrial", + "post-metal", + "post-minimalism", + "post-punk", + "post-punk revival", + "post-rock", + "powada", + "power electronics", + "power metal", + "power noise", + "power pop", + "power soca", + "powerstomp", + "powerviolence", + "praise & worship", + "prank calls", + "prelude", + "production music", + "progressive", + "progressive bluegrass", + "progressive breaks", + "progressive country", + "progressive electronic", + "progressive folk", + "progressive house", + "progressive metal", + "progressive pop", + "progressive psytrance", + "progressive rock", + "progressive trance", + "proto-punk", + "psybient", + "psychedelic", + "psychedelic folk", + "psychedelic pop", + "psychedelic rock", + "psychedelic soul", + "psychobilly", + "psycore", + "psystyle", + "psytrance", + "pub rock", + "punk", + "punk blues", + "punk rap", + "punk rock", + "punta", + "punto", + "purple sound", + "q-pop", + "qaraami", + "qasidah modern", + "qawwali", + "quan họ", + "queercore", + "quiet storm", + "quyi", + "r&b", + "rabiz", + "raga rock", + "ragga", + "ragga hip-hop", + "ragga jungle", + "raggacore", + "raggatek", + "ragtime", + "raï", + "ranchera", + "rap metal", + "rap rock", + "rapcore", + "rapso", + "rara", + "rasin", + "rasqueado cuiabano", + "rasteirinha", + "rautalanka", + "rave", + "raw punk", + "rawphoric", + "rawstyle", + "rebetiko", + "red dirt", + "red song", + "reductionism", + "reggae", + "reggae-pop", + "reggaeton", + "regional mexicano", + "renaissance", + "repente", + "requiem", + "revue", + "rhumba", + "riddim dubstep", + "rigsar", + "riot grrrl", + "ripsaw", + "ritual ambient", + "rizitika", + "rkt", + "rock", + "rock and roll", + "rock andaluz", + "rock andino", + "rock musical", + "rock opera", + "rock urbano", + "rockabilly", + "rocksteady", + "rōkyoku", + "rom kbach", + "romanian popcorn", + "romantic classical", + "romantische oper", + "roots reggae", + "roots rock", + "rumba", + "rumba catalana", + "rumba cubana", + "rumba flamenca", + "runo song", + "russian chanson", + "russian romance", + "ryūkōka", + "sacred harp", + "sacred steel", + "saeta", + "salegy", + "salsa", + "salsa choke", + "salsa dura", + "salsa romántica", + "saluang klasik", + "samba", + "samba de breque", + "samba de gafieira", + "samba de roda", + "samba de terreiro", + "samba soul", + "samba-canção", + "samba-choro", + "samba-enredo", + "samba-exaltação", + "samba-jazz", + "samba-joia", + "samba-reggae", + "samba-rock", + "sambalanço", + "sambass", + "sampledelia", + "sanjo", + "santé engagé", + "sarala gee", + "sardana", + "sasscore", + "sawt", + "saya afroboliviana", + "schlager", + "schottische", + "schranz", + "screamo", + "scrumpy and western", + "sea shanty", + "sean-nós", + "seapunk", + "séga", + "seggae", + "seguidilla", + "semba", + "semi-trot", + "serenade", + "serialism", + "sertanejo", + "sertanejo raiz", + "sertanejo romântico", + "sertanejo universitário", + "seto leelo", + "sevdalinka", + "sevillanas", + "shaabi", + "shabad kirtan", + "shan'ge", + "shangaan electro", + "shanto", + "shashmaqam", + "shatta", + "shibuya-kei", + "shidaiqu", + "shima-uta", + "shinkyoku", + "shoegaze", + "shōmyō", + "sierreño", + "sigidrigi", + "sigilkore", + "sinawi", + "sinfonia concertante", + "singeli", + "singer-songwriter", + "singspiel", + "ska", + "ska punk", + "skacore", + "skate punk", + "sketch comedy", + "skiffle", + "skiladiko", + "skinhead reggae", + "skullstep", + "skweee", + "slack-key guitar", + "slacker rock", + "slam death metal", + "slam poetry", + "slap house", + "slow waltz", + "slowcore", + "sludge metal", + "slushwave", + "smooth jazz", + "smooth soul", + "snap", + "soca", + "soft rock", + "sōkyoku", + "son calentano", + "son cubano", + "son huasteco", + "son istmeño", + "son jarocho", + "son montuno", + "sonata", + "songo", + "sonorism", + "sophisti-pop", + "soukous", + "soul", + "soul blues", + "soul jazz", + "sound art", + "sound collage", + "sound effects", + "sound poetry", + "southeast asian classical", + "southern gospel", + "southern hip hop", + "southern metal", + "southern rock", + "southern soul", + "sovietwave", + "space age pop", + "space ambient", + "space disco", + "space rock", + "space rock revival", + "spacesynth", + "spectralism", + "speech", + "speed garage", + "speed house", + "speed metal", + "speedcore", + "spirituals", + "splittercore", + "spoken word", + "spouge", + "standup comedy", + "steampunk", + "stenchcore", + "stochastic music", + "stoner metal", + "stoner rock", + "stornello", + "street punk", + "stride", + "sufi rock", + "sufiana kalam", + "sundanese pop", + "sungura", + "sunshine pop", + "suomisaundi", + "surf", + "surf punk", + "surf rock", + "sutartinės", + "swamp blues", + "swamp pop", + "swamp rock", + "swancore", + "swing", + "swing revival", + "symphonic black metal", + "symphonic metal", + "symphonic poem", + "symphonic prog", + "symphonic rock", + "symphony", + "synth funk", + "synth-pop", + "synthwave", + "taarab", + "tajaraste", + "takamba", + "talempong", + "talking blues", + "tallava", + "tamborera", + "tamborito", + "tammurriata", + "tango", + "tanjidor", + "tape music", + "taquirari", + "tarantella", + "tarraxinha", + "tassu", + "tchinkoumé", + "tearout", + "tech house", + "tech trance", + "technical death metal", + "technical thrash metal", + "techno", + "techno bass", + "techno kayō", + "technobanda", + "techstep", + "tecnobrega", + "tecnofunk", + "tecnorumba", + "teen pop", + "tejano", + "tembang cianjuran", + "terrorcore", + "tex-mex", + "texas blues", + "texas country", + "thai classical", + "third stream", + "third wave ska", + "thrash metal", + "thrashcore", + "thumri", + "tiento", + "timba", + "timbila", + "tin pan alley", + "tizita", + "toccata", + "tonada potosina", + "tonadilla", + "tondero", + "totalism", + "township bubblegum", + "township jive", + "tradi-modern", + "traditional black gospel", + "traditional country", + "traditional doom metal", + "traditional pop", + "tragédie en musique", + "trallalero", + "trampská hudba", + "trance", + "trance metal", + "trancestep", + "trap", + "trap edm", + "trap metal", + "trap shaabi", + "tread", + "tribal ambient", + "tribal guarachero", + "tribal house", + "trip hop", + "tropical house", + "tropical rock", + "tropicália", + "tropicanibalismo", + "tropipop", + "trot", + "trova", + "trova yucateca", + "truck driving country", + "tsapiky", + "tsonga disco", + "tsugaru-jamisen", + "tumba", + "tumba francesa", + "tumbélé", + "turbo-folk", + "turkish classical", + "turntablism", + "twee pop", + "twerk", + "uk drill", + "uk funky", + "uk garage", + "uk hardcore", + "uk street soul", + "uk82", + "underground hip hop", + "unyago", + "upopo", + "uptempo hardcore", + "urban cowboy", + "urtiin duu", + "urumi melam", + "utopian virtual", + "uzun hava", + "v-pop", + "vallenato", + "vals criollo", + "valsa brasileira", + "vanera", + "vaportrap", + "vaporwave", + "vaudeville", + "vaudeville blues", + "verbunkos", + "verismo", + "vietnamese bolero", + "vietnamese classical", + "viking metal", + "viking rock", + "vinahouse", + "visa", + "visual kei", + "vocal house", + "vocal jazz", + "vocal surf", + "vocal trance", + "volkstümliche musik", + "vude", + "waka", + "waltz", + "war metal", + "wassoulou", + "waulking song", + "wave", + "weightless", + "west coast breaks", + "west coast hip hop", + "west coast swing", + "western", + "western classical", + "western swing", + "whale song", + "whistling", + "white voice", + "winter synth", + "witch house", + "wong shadow", + "wonky", + "wonky techno", + "world fusion", + "xẩm", + "xote", + "xuc", + "yacht rock", + "yakousei", + "yaraví", + "yayue", + "yé-yé", + "yodeling", + "ytpmv", + "yu-mex", + "yue opera", + "yukar", + "zamacueca", + "zamba", + "zamrock", + "zarzuela", + "zeitoper", + "zenonesque", + "zeuhl", + "zeybek", + "zhongguo feng", + "ziglibithy", + "zinli", + "znamenny chant", + "zoblazo", + "zohioliin duu", + "zolo", + "zouglou", + "zouk", + "zouk love", + "zydeco", + }; + } +} \ No newline at end of file diff --git a/Zune.Net.Shared/Middleware/WlidMiddleware.cs b/Zune.Net.Shared/Middleware/WlidMiddleware.cs index d89faa5..0a77da4 100644 --- a/Zune.Net.Shared/Middleware/WlidMiddleware.cs +++ b/Zune.Net.Shared/Middleware/WlidMiddleware.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using System; using System.Threading.Tasks; using Zune.DB; using Zune.DB.Models; @@ -10,6 +11,7 @@ namespace Zune.Net.Middleware public class WlidMiddleware { internal const string AUTHED_MEMBER_KEY = "Member"; + internal const string WLID_SESSION_ID = "WLID-SESSIONID"; private readonly RequestDelegate _next; public WlidMiddleware(RequestDelegate next) @@ -20,23 +22,36 @@ public WlidMiddleware(RequestDelegate next) public async Task InvokeAsync(HttpContext context, ZuneNetContext database) { Member authedMember = null; + var authHeader = context.Request.Headers.Authorization; if (authHeader.Count > 0) { - string token = authHeader[0]; - int idxToken = token.IndexOf(' '); - if (idxToken >= 0) - token = token[(idxToken + 1)..]; + var token = authHeader[0]; - if (!string.IsNullOrWhiteSpace(token) && database != null) + // This is how the Escargot project grabs the session, presumably it's reliable enough? + // WLID1.0 t=[0-20] - session ID + if(!string.IsNullOrWhiteSpace(token) && token.StartsWith("WLID1.0 t=") && database != null) { + var idxToken = token.IndexOf(' '); + if (idxToken >= 0) + { + var start = idxToken + 3; + var stop = idxToken + 23; + token = token[start..stop]; + context.Items.Add(WLID_SESSION_ID, token); + } + authedMember = await database.GetMemberByToken(token); } - } - + } + if (authedMember != null) + { context.Items.Add(AUTHED_MEMBER_KEY, authedMember); + // holy cow I wish I had an ILogger here. + Console.WriteLine($"Authed Member: {authedMember.ZuneTag}"); + } // Call the next delegate/middleware in the pipeline. await _next(context); @@ -56,5 +71,12 @@ public static bool TryGetAuthedMember(this ControllerBase controller, out Member member = obj as Member; return isSuccess; } + + public static bool TryGetSessionId(this ControllerBase controller, out string wlid_session_id) + { + bool isSuccess = controller.HttpContext.Items.TryGetValue(WlidMiddleware.WLID_SESSION_ID, out var obj); + wlid_session_id = obj as string; + return isSuccess; + } } } diff --git a/Zune.Net.Shared/Zune.Net.Shared.csproj b/Zune.Net.Shared/Zune.Net.Shared.csproj index 74ee99f..e3f6185 100644 --- a/Zune.Net.Shared/Zune.Net.Shared.csproj +++ b/Zune.Net.Shared/Zune.Net.Shared.csproj @@ -21,6 +21,7 @@ + @@ -28,4 +29,4 @@ - + \ No newline at end of file diff --git a/Zune.Net.SocialApi/Controllers/MembersController.cs b/Zune.Net.SocialApi/Controllers/MembersController.cs index eb651ae..bb947ac 100644 --- a/Zune.Net.SocialApi/Controllers/MembersController.cs +++ b/Zune.Net.SocialApi/Controllers/MembersController.cs @@ -28,17 +28,12 @@ public async Task> Info(string zuneTag) { var member = await _database.GetByIdOrZuneTag(zuneTag); - Member response; if (member != null) { - response = member.GetXmlMember(); - } - else - { - return NotFound(); + return member.GetXmlMember(); } - return response; + return NotFound(); } [Route("{zuneTag}/friends")] @@ -100,13 +95,14 @@ public async Task>> Badges(string zuneTag) var feed = new Feed { - Id = Guid.Empty.ToString(), + Id = new Guid().ToString(), Links = { new Link(requestUrl) }, Title = member.ZuneTag + "'s Badges", - Entries = - { - badge1 - } + Updated = DateTime.Now - TimeSpan.FromDays(1), + // Entries = + // { + // badge1 + // } }; return feed; diff --git a/Zune.Net.SocialApi/Controllers/WebStatsController.cs b/Zune.Net.SocialApi/Controllers/WebStatsController.cs new file mode 100644 index 0000000..2dacfb7 --- /dev/null +++ b/Zune.Net.SocialApi/Controllers/WebStatsController.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using System.IO; + +namespace Zune.Net.SocialApi.Controllers +{ + [ApiController] + public class WebStatsController : ControllerBase + { + private readonly IWebHostEnvironment _env; + + public WebStatsController(IWebHostEnvironment env) + { + _env = env; + } + + // sometimes things crash when m.webtrends.net doesnt return a valid gif, for example m.webtrends.net/something/something.gif&abunch=of&args=that... + + [Route("/{gibberish}/{file}.gif")] + public ActionResult LiveTOU() + { + string path = Path.Combine(_env.ContentRootPath, "Resources", "1x1px.gif"); + var image = System.IO.File.ReadAllBytes(path); + + return File(image, "image/gif"); + } + } +} diff --git a/Zune.Net.SocialApi/Dockerfile b/Zune.Net.SocialApi/Dockerfile new file mode 100644 index 0000000..d562e39 --- /dev/null +++ b/Zune.Net.SocialApi/Dockerfile @@ -0,0 +1,16 @@ +# https://hub.docker.com/_/microsoft-dotnet +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +WORKDIR /source + +# copy csproj and restore as distinct layers +COPY ./ . +RUN dotnet restore + +RUN dotnet publish -c release -p:PublishDir=./publish --no-restore + +# final stage/image +FROM mcr.microsoft.com/dotnet/aspnet:7.0 +WORKDIR /app +COPY --from=build /source/Zune.Net.SocialApi/publish/ ./ +ENV DOTNET_EnableDiagnostics=0 +ENTRYPOINT ["dotnet", "Zune.Net.SocialApi.dll"] diff --git a/Zune.Net.SocialApi/Properties/launchSettings.json b/Zune.Net.SocialApi/Properties/launchSettings.json deleted file mode 100644 index d445250..0000000 --- a/Zune.Net.SocialApi/Properties/launchSettings.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://127.0.0.5:80", - "sslPort": 443 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "Zune.Net.SocialApi": { - "commandName": "Project", - "dotnetRunMessages": true, - "applicationUrl": "https://127.0.0.5:443;http://127.0.0.5:80;https://127.0.0.13:443;http://127.0.0.13:80", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/Zune.Net.SocialApi/Resources/1x1px.gif b/Zune.Net.SocialApi/Resources/1x1px.gif new file mode 100644 index 0000000..3c82891 Binary files /dev/null and b/Zune.Net.SocialApi/Resources/1x1px.gif differ diff --git a/Zune.Net.SocialApi/Startup.cs b/Zune.Net.SocialApi/Startup.cs index 316d791..f13bef9 100644 --- a/Zune.Net.SocialApi/Startup.cs +++ b/Zune.Net.SocialApi/Startup.cs @@ -39,7 +39,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseDeveloperExceptionPage(); } - //app.UseHttpsRedirection(); + // app.UseHttpLogging(); app.UseRequestBuffering(); diff --git a/Zune.Net.SocialApi/Zune.Net.SocialApi.csproj b/Zune.Net.SocialApi/Zune.Net.SocialApi.csproj index f7ae83d..87c7c7a 100644 --- a/Zune.Net.SocialApi/Zune.Net.SocialApi.csproj +++ b/Zune.Net.SocialApi/Zune.Net.SocialApi.csproj @@ -4,6 +4,13 @@ net7.0 + + + PreserveNewest + Always + + + @@ -11,4 +18,4 @@ - + \ No newline at end of file diff --git a/Zune.Net.SocialApi/appsettings.Development.json b/Zune.Net.SocialApi/appsettings.Development.json index 8983e0f..dc23838 100644 --- a/Zune.Net.SocialApi/appsettings.Development.json +++ b/Zune.Net.SocialApi/appsettings.Development.json @@ -1,9 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "Debug" } } } diff --git a/Zune.Net.SocialApi/appsettings.json b/Zune.Net.SocialApi/appsettings.json index 38175d4..309b7d7 100644 --- a/Zune.Net.SocialApi/appsettings.json +++ b/Zune.Net.SocialApi/appsettings.json @@ -1,14 +1,12 @@ { "ZuneNetContext": { - "ConnectionString": "mongodb://localhost:27017", + "ConnectionString": "mongodb://root:rootpassword@mongodb:27017", "DatabaseName": "Zune", "MemberCollectionName": "Members" }, "Logging": { "LogLevel": { "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" diff --git a/Zune.Net.Tiles/Dockerfile b/Zune.Net.Tiles/Dockerfile new file mode 100644 index 0000000..58e8eef --- /dev/null +++ b/Zune.Net.Tiles/Dockerfile @@ -0,0 +1,16 @@ +# https://hub.docker.com/_/microsoft-dotnet +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +WORKDIR /source + +# copy csproj and restore as distinct layers +COPY ./ . +RUN dotnet restore + +RUN dotnet publish -c release -p:PublishDir=./publish --no-restore + +# final stage/image +FROM mcr.microsoft.com/dotnet/aspnet:7.0 +WORKDIR /app +COPY --from=build /source/Zune.Net.Tiles/publish/ ./ +ENV DOTNET_EnableDiagnostics=0 +ENTRYPOINT ["dotnet", "Zune.Net.Tiles.dll"] diff --git a/Zune.Net.Tiles/Program.cs b/Zune.Net.Tiles/Program.cs index ac7c10a..4456171 100644 --- a/Zune.Net.Tiles/Program.cs +++ b/Zune.Net.Tiles/Program.cs @@ -6,9 +6,9 @@ var app = builder.Build(); -// Configure the HTTP request pipeline. +// app.UseHttpLogging(); -app.UseHttpsRedirection(); +// Configure the HTTP request pipeline. app.UseAuthorization(); diff --git a/Zune.Net.Tiles/Zune.Net.Tiles.csproj b/Zune.Net.Tiles/Zune.Net.Tiles.csproj index b2da86a..22170ca 100644 --- a/Zune.Net.Tiles/Zune.Net.Tiles.csproj +++ b/Zune.Net.Tiles/Zune.Net.Tiles.csproj @@ -6,8 +6,11 @@ enable - - - + + + PreserveNewest + Always + + diff --git a/Zune.Net.Tiles/appsettings.Development.json b/Zune.Net.Tiles/appsettings.Development.json index 0c208ae..dc23838 100644 --- a/Zune.Net.Tiles/appsettings.Development.json +++ b/Zune.Net.Tiles/appsettings.Development.json @@ -1,8 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Default": "Debug" } } } diff --git a/Zune.Net.Tiles/appsettings.json b/Zune.Net.Tiles/appsettings.json index 10f68b8..cdcf7bb 100644 --- a/Zune.Net.Tiles/appsettings.json +++ b/Zune.Net.Tiles/appsettings.json @@ -2,7 +2,6 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" diff --git a/Zune.Net.Tuners/Dockerfile b/Zune.Net.Tuners/Dockerfile new file mode 100644 index 0000000..65baf2a --- /dev/null +++ b/Zune.Net.Tuners/Dockerfile @@ -0,0 +1,16 @@ +# https://hub.docker.com/_/microsoft-dotnet +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +WORKDIR /source + +# copy csproj and restore as distinct layers +COPY ./ . +RUN dotnet restore + +RUN dotnet publish -c release -p:PublishDir=./publish --no-restore + +# final stage/image +FROM mcr.microsoft.com/dotnet/aspnet:7.0 +WORKDIR /app +COPY --from=build /source/Zune.Net.Tuners/publish/ ./ +ENV DOTNET_EnableDiagnostics=0 +ENTRYPOINT ["dotnet", "Zune.Net.Tuners.dll"] diff --git a/Zune.Net.Tuners/Program.cs b/Zune.Net.Tuners/Program.cs index 54c86dc..260906f 100644 --- a/Zune.Net.Tuners/Program.cs +++ b/Zune.Net.Tuners/Program.cs @@ -4,13 +4,12 @@ var app = builder.Build(); -// Configure the HTTP request pipeline. - -app.UseHttpsRedirection(); +// app.UseHttpLogging(); app.MapGet("/{locale}/ZunePCClient/{version}/{file}.xml", (string locale, string version, string file) => { string filePath = Path.Combine(app.Environment.ContentRootPath, "Resources", file + ".xml"); + Console.WriteLine($"Serving up {filePath}"); string zuneConfig = File.ReadAllText(filePath); return zuneConfig; diff --git a/Zune.Net.Tuners/Resources/configuration.xml b/Zune.Net.Tuners/Resources/configuration.xml index 53a2a8e..d8ee0e2 100644 --- a/Zune.Net.Tuners/Resources/configuration.xml +++ b/Zune.Net.Tuners/Resources/configuration.xml @@ -2384,7 +2384,7 @@ mix - 0 + 100 diff --git a/Zune.Net.Tuners/Zune.Net.Tuners.csproj b/Zune.Net.Tuners/Zune.Net.Tuners.csproj index 9b8a345..e1c242d 100644 --- a/Zune.Net.Tuners/Zune.Net.Tuners.csproj +++ b/Zune.Net.Tuners/Zune.Net.Tuners.csproj @@ -6,8 +6,10 @@ enable - - + + + PreserveNewest + Always + - diff --git a/Zune.Net.Tuners/appsettings.Development.json b/Zune.Net.Tuners/appsettings.Development.json index 0c208ae..dc23838 100644 --- a/Zune.Net.Tuners/appsettings.Development.json +++ b/Zune.Net.Tuners/appsettings.Development.json @@ -1,8 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Default": "Debug" } } } diff --git a/Zune.Net.Tuners/appsettings.json b/Zune.Net.Tuners/appsettings.json index 10f68b8..6a845cf 100644 --- a/Zune.Net.Tuners/appsettings.json +++ b/Zune.Net.Tuners/appsettings.json @@ -1,8 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Default": "Information" } }, "AllowedHosts": "*" diff --git a/Zune.Xml/Catalog/Track.cs b/Zune.Xml/Catalog/Track.cs index cc9a298..9a74a9a 100644 --- a/Zune.Xml/Catalog/Track.cs +++ b/Zune.Xml/Catalog/Track.cs @@ -39,16 +39,16 @@ public class Track : Media public int PointsPrice { get; set; } [XmlElement("CanPlay")] - public bool CanPlay { get; set; } + public bool CanPlay { get; set; } = true; [XmlElement("CanDownload")] - public bool CanDownload { get; set; } + public bool CanDownload { get; set; } = true; [XmlElement("CanPurchase")] - public bool CanPurchase { get; set; } + public bool CanPurchase { get; set; } = true; [XmlElement("CanPurchaseMP3")] - public bool CanPurchaseMP3 { get; set; } + public bool CanPurchaseMP3 { get; set; } = true; [XmlElement("CanPurchaseAlbumOnly")] public bool CanPurchaseAlbumOnly { get; set; } diff --git a/Zune.Xml/Commerce/GetTunerRegistrationInfoResponse.cs b/Zune.Xml/Commerce/GetTunerRegistrationInfoResponse.cs new file mode 100644 index 0000000..8bac0d2 --- /dev/null +++ b/Zune.Xml/Commerce/GetTunerRegistrationInfoResponse.cs @@ -0,0 +1,49 @@ +using Atom; +using System; +using System.Collections.Generic; +using System.Xml.Serialization; + +// this class is not production-ready. The signin workflow needs to actually build this correctly +namespace Zune.Xml.Commerce +{ + public enum MediaTypeEnum + { + Subscription, + AppStore, + } + + public class TunerInfoDef + { + [XmlElement("ID")] + public string TunerId { get; set; } + + [XmlElement("Name")] + public string Name { get; set; } + + [XmlElement("RegistrationDate")] + public DateTime RegistrationDate { get; set; } + [XmlElement("Type")] + public string Type { get; set; } + + [XmlElement("Version")] + public string Version { get; set; } + + } + public class MediaTypeTunerPair + { + [XmlArray("RegisteredType")] + public MediaTypeEnum RegisteredType { get; set; } + [XmlArray("TunerList")] + public List TunerList { get; set; } + [XmlArray("NextTunerTypeDeregistrationDate")] + public List> NextTunerTypeDeregistrationDate { get; set; } + [XmlArray("TunerTypeMaxRegistered")] + public List> TunerTypeMaxRegistered { get; set; } + } + [XmlRoot(nameof(GetTunerRegistrationInfoResponse), Namespace = Constants.ZUNE_COMMERCE_NAMESPACE)] + public class GetTunerRegistrationInfoResponse + { + [XmlArray("RegisteredTuners/MediaTypeTunerPair")] + public List MediaTypeTunerPair { get; set; } + } +} \ No newline at end of file diff --git a/Zune.Xml/Commerce/SignInRequest.cs b/Zune.Xml/Commerce/SignInRequest.cs index 72bbd99..7be9c97 100644 --- a/Zune.Xml/Commerce/SignInRequest.cs +++ b/Zune.Xml/Commerce/SignInRequest.cs @@ -1,14 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Xml.Serialization; +using System.Xml.Serialization; using Atom; namespace Zune.Xml.Commerce { - [XmlRoot(ElementName = nameof(SignInRequest), Namespace = Constants.ZUNE_COMMERCE_NAMESPACE)] + [XmlRoot(nameof(SignInRequest), Namespace = Constants.ZUNE_COMMERCE_NAMESPACE)] public class SignInRequest { + public SignInRequest(){} + public TunerInfo TunerInfo { get; set; } } } diff --git a/Zune.Xml/Zune.Xml.csproj b/Zune.Xml/Zune.Xml.csproj index 74307e8..e9c86a2 100644 --- a/Zune.Xml/Zune.Xml.csproj +++ b/Zune.Xml/Zune.Xml.csproj @@ -6,6 +6,10 @@ 0.1.0 + + false + + $(DefineConstants);NETSTANDARD1_1_OR_GREATER;NETSTANDARD2_0_OR_GREATER;NETSTANDARD2_1_OR_GREATER diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..fce6a0b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,147 @@ +version: '3.4' + +networks: + backend: + name: backend + zune.net: + name: zune.net + driver: bridge + +services: + mongodb: + restart: always + image: mongo:latest + environment: + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: rootpassword + ports: + - 27017:27017 + volumes: + - mongodb_data_container:/data/db + networks: + - backend + - zune.net + + nginx: + restart: unless-stopped + build: + context: nginx + ports: + - 80:80 + - 443:443 + networks: + - backend + - zune.net + + catalog: + restart: unless-stopped + build: + context: ./ + dockerfile: Zune.Net.Catalog/Dockerfile + depends_on: + - mongodb + - nginx + networks: + - backend + + catalog.image: + restart: unless-stopped + build: + context: ./ + dockerfile: Zune.Net.Catalog.Image/Dockerfile + depends_on: + - mongodb + - nginx + networks: + - backend + + commerce: + restart: unless-stopped + build: + context: ./ + dockerfile: Zune.Net.Commerce/Dockerfile + depends_on: + - mongodb + - nginx + networks: + - backend + + inbox: + restart: unless-stopped + build: + context: ./ + dockerfile: Zune.Net.Inbox/Dockerfile + depends_on: + - mongodb + - nginx + networks: + - backend + + login: + restart: unless-stopped + build: + context: ./ + dockerfile: Zune.Net.Login/Dockerfile + depends_on: + - mongodb + - nginx + networks: + - backend + + metaservices: + restart: unless-stopped + build: + context: ./ + dockerfile: Zune.Net.MetaServices/Dockerfile + depends_on: + - mongodb + - nginx + networks: + - backend + + mix: + restart: unless-stopped + build: + context: ./ + dockerfile: Zune.Net.Mix/Dockerfile + depends_on: + - mongodb + - nginx + networks: + - backend + + social: + restart: unless-stopped + build: + context: ./ + dockerfile: Zune.Net.SocialApi/Dockerfile + depends_on: + - mongodb + - nginx + networks: + - backend + + tiles: + restart: unless-stopped + build: + context: ./ + dockerfile: Zune.Net.Tiles/Dockerfile + depends_on: + - mongodb + - nginx + networks: + - backend + + tuners: + restart: unless-stopped + build: + context: ./ + dockerfile: Zune.Net.Tuners/Dockerfile + depends_on: + - mongodb + - nginx + networks: + - backend + +volumes: + mongodb_data_container: \ No newline at end of file diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 0000000..f61d9d2 --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,3 @@ +FROM nginx:alpine +COPY ssl/ /etc/ssl/ +COPY nginx.conf /etc/nginx/nginx.conf \ No newline at end of file diff --git a/nginx/generate-certs.sh b/nginx/generate-certs.sh new file mode 100644 index 0000000..635e489 --- /dev/null +++ b/nginx/generate-certs.sh @@ -0,0 +1,2 @@ +#! /bin/bash +openssl req -x509 -nodes -days 365 -subj "/C=CA/ST=QC/O=Microsoft, Inc./CN=*.zune.net" -newkey rsa:2048 -keyout ./ssl/private/nginx-selfsigned.key -out ./ssl/certs/nginx-selfsigned.crt; \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..8b152f8 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,211 @@ +worker_processes 1; + +events { worker_connections 1024; } + +http { + + sendfile on; + + proxy_cache_path /tmp/global levels=1:2 keys_zone=global_cache:1000m; + proxy_cache_path /tmp/mix levels=1:2 keys_zone=mix_cache:1000m; + proxy_cache_path /tmp/img levels=1:2 keys_zone=image_cache:1000m; + + server{ + listen 80; + server_name toc.music.metaservices.microsoft.com metaservices.zune.net fai.music.metaservices.microsoft.com redir.metaservices.microsoft.com images.metaservices.microsoft.com; + location / { + proxy_pass http://metaservices; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + expires -1; + } + } + + server { + listen 80; + server_name catalog.zune.net; + location / { + proxy_pass http://catalog; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + + proxy_buffering on; + + proxy_ignore_headers Expires Cache-Control X-Accel-Expires; + proxy_ignore_headers Set-Cookie; + + proxy_cache global_cache; + proxy_cache_valid 24h; + } + } + + server { + listen 80; + server_name image.catalog.zune.net; + location / { + proxy_pass http://catalog.image; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + + proxy_buffering on; + + proxy_ignore_headers Expires Cache-Control X-Accel-Expires; + proxy_ignore_headers Set-Cookie; + + proxy_cache image_cache; + proxy_cache_valid 24h; + } + } + + server { + listen 80; + listen 443 ssl; + server_name commerce.zune.net; + + ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt; + ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key; + + location / { + proxy_pass http://commerce; + proxy_redirect off; + + proxy_buffering on; + + proxy_ignore_headers Expires Cache-Control X-Accel-Expires; + proxy_ignore_headers Set-Cookie; + + proxy_cache global_cache; + proxy_cache_valid 24h; + } + } + + server { + listen 80; + listen 443 ssl; + server_name inbox.zune.net; + + ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt; + ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key; + location / { + proxy_pass http://inbox; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + expires -1; + } + } + + server { + listen 80; + server_name mix.zune.net; + location / { + proxy_pass http://mix; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + + proxy_buffering on; + + proxy_ignore_headers Expires Cache-Control X-Accel-Expires; + proxy_ignore_headers Set-Cookie; + + proxy_cache mix_cache; + proxy_cache_valid 1d; + } + } + + server { + listen 80; + listen 443 ssl; + server_name tuners.zune.net tuners.zunes.me; + + ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt; + ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key; + location / { + proxy_pass http://tuners; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + expires -1; + } + } + + server { + listen 80; + server_name tiles.zune.net; + location / { + proxy_pass http://tiles; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + expires -1; + } + } + + server { + listen 80; + listen 443 ssl; + server_name social.zune.net; + + ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt; + ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key; + location / { + proxy_pass http://social; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + expires -1; + } + } + + server { + listen 80; + listen 443 ssl; + server_name socialapi.zune.net; + + ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt; + ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key; + location / { + proxy_pass http://social; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + expires -1; + } + } + + server { + listen 80; + server_name login.zune.net; + location / { + proxy_pass http://login; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + expires -1; + } + } +} diff --git a/runDev.sh b/runDev.sh new file mode 100755 index 0000000..84ca93c --- /dev/null +++ b/runDev.sh @@ -0,0 +1,4 @@ +#!/bin/bash +docker compose stop +docker compose build +docker compose up \ No newline at end of file diff --git a/runProd.sh b/runProd.sh new file mode 100755 index 0000000..5b982c5 --- /dev/null +++ b/runProd.sh @@ -0,0 +1,5 @@ +#!/bin/bash +docker compose stop +docker compose build +docker compose up -d --remove-orphans +docker compose logs --follow --tail 0