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
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