diff --git a/Selector.CLI/Command/SpotHistoryCommand.cs b/Selector.CLI/Command/SpotHistoryCommand.cs index e9d5ea7..4b073f3 100644 --- a/Selector.CLI/Command/SpotHistoryCommand.cs +++ b/Selector.CLI/Command/SpotHistoryCommand.cs @@ -52,7 +52,6 @@ namespace Selector.CLI var directoryContents = Directory.EnumerateFiles(path); var endSongs = directoryContents.Where(f => f.Contains("endsong_")).ToArray(); - foreach(var file in endSongs) { streams.Add(File.OpenRead(file)); diff --git a/Selector.CLI/Extensions/ServiceExtensions.cs b/Selector.CLI/Extensions/ServiceExtensions.cs index 1ac1ea8..0f155c3 100644 --- a/Selector.CLI/Extensions/ServiceExtensions.cs +++ b/Selector.CLI/Extensions/ServiceExtensions.cs @@ -118,8 +118,12 @@ namespace Selector.CLI.Extensions options.UseNpgsql(config.DatabaseOptions.ConnectionString) ); - services.AddTransient(); - services.AddTransient(); + services.AddTransient() + .AddTransient(); + + //services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddHostedService(); diff --git a/Selector.CLI/ScrobbleSaver.cs b/Selector.CLI/ScrobbleSaver.cs index 2c0c652..3cfbe27 100644 --- a/Selector.CLI/ScrobbleSaver.cs +++ b/Selector.CLI/ScrobbleSaver.cs @@ -112,7 +112,7 @@ namespace Selector logger.LogDebug("Identifying difference sets"); var time = Stopwatch.StartNew(); - (var toAdd, var toRemove) = ScrobbleMatcher.IdentifyDiffs(currentScrobbles, nativeScrobbles); + (var toAdd, var toRemove) = ListenMatcher.IdentifyDiffs(currentScrobbles, nativeScrobbles); time.Stop(); logger.LogTrace("Finished diffing: {:n}ms", time.ElapsedMilliseconds); diff --git a/Selector.Data/HistoryPersister.cs b/Selector.Data/HistoryPersister.cs index c252140..f9e3cc5 100644 --- a/Selector.Data/HistoryPersister.cs +++ b/Selector.Data/HistoryPersister.cs @@ -50,7 +50,7 @@ public class HistoryPersister var parsed = await JsonSerializer.DeserializeAsync(singleInput, Json.EndSongArray); songs = songs.Concat(parsed); - Logger?.LogDebug("Parsed {} items for {}", parsed.Length, Config.Username); + Logger?.LogDebug("Parsed {.2f} items for {}", parsed.Length, Config.Username); } await Process(songs); @@ -67,7 +67,13 @@ public class HistoryPersister var counter = 0; - foreach(var item in input) + var filtered = input.Where(x => x.ms_played > 20000) + .DistinctBy(x => (x.offline_timestamp, x.ts, x.spotify_track_uri)) + .ToArray(); + + Logger.LogInformation("{.2f} items after filtering", filtered.Length); + + foreach (var item in filtered) { if(!string.IsNullOrWhiteSpace(item.master_metadata_track_name)) { diff --git a/Selector.Model/ApplicationDbContext.cs b/Selector.Model/ApplicationDbContext.cs index c656451..492a17d 100644 --- a/Selector.Model/ApplicationDbContext.cs +++ b/Selector.Model/ApplicationDbContext.cs @@ -87,7 +87,12 @@ namespace Selector.Model .Property(s => s.LastfmArtistName) .UseCollation("case_insensitive"); - modelBuilder.Entity().HasKey(s => s.Timestamp); + modelBuilder.Entity().HasKey(s => s.SpotifyUri); + modelBuilder.Entity() + .Property(s => s.LastfmArtistName) + .UseCollation("case_insensitive"); + + modelBuilder.Entity().HasKey(s => s.Id); modelBuilder.Entity() .Property(s => s.TrackName) .UseCollation("case_insensitive"); diff --git a/Selector.Model/Listen/IListenRepository.cs b/Selector.Model/Listen/IListenRepository.cs new file mode 100644 index 0000000..22861ed --- /dev/null +++ b/Selector.Model/Listen/IListenRepository.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace Selector.Model +{ + public interface IListenRepository + { + IEnumerable GetAll(string include = null, string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null); + int Count(string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null); + } +} + diff --git a/Selector.Model/Listen/ISpotifyListenRepository.cs b/Selector.Model/Listen/ISpotifyListenRepository.cs index 7e5e5c5..bf7902a 100644 --- a/Selector.Model/Listen/ISpotifyListenRepository.cs +++ b/Selector.Model/Listen/ISpotifyListenRepository.cs @@ -4,18 +4,18 @@ using System.Threading.Tasks; namespace Selector.Model { - public interface ISpotifyListenRepository + public interface ISpotifyListenRepository: IListenRepository { void Add(SpotifyListen item); void AddRange(IEnumerable item); - IEnumerable GetAll(string include = null, string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null); + //IEnumerable GetAll(string include = null, string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null); SpotifyListen Find(DateTime key, string include = null); void Remove(DateTime key); public void Remove(SpotifyListen scrobble); public void RemoveRange(IEnumerable scrobbles); void Update(SpotifyListen item); Task Save(); - int Count(string include = null, string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null); + //int Count(string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null); } } diff --git a/Selector.Model/Listen/IUserListen.cs b/Selector.Model/Listen/IUserListen.cs new file mode 100644 index 0000000..1369048 --- /dev/null +++ b/Selector.Model/Listen/IUserListen.cs @@ -0,0 +1,10 @@ +using System; + +namespace Selector.Model; + +public interface IUserListen: IListen +{ + string UserId { get; set; } + ApplicationUser User { get; set; } +} + diff --git a/Selector.Model/Listen/MetaListenRepository.cs b/Selector.Model/Listen/MetaListenRepository.cs new file mode 100644 index 0000000..fa997c7 --- /dev/null +++ b/Selector.Model/Listen/MetaListenRepository.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Selector.Model; + +public enum PreferenceMode +{ + Greedy, LastFm, Spotify +} + +public class MetaListenRepository: IListenRepository +{ + private readonly IScrobbleRepository scrobbleRepository; + private readonly ISpotifyListenRepository spotifyRepository; + + public MetaListenRepository(IScrobbleRepository scrobbleRepository, ISpotifyListenRepository listenRepository) + { + this.scrobbleRepository = scrobbleRepository; + spotifyRepository = listenRepository; + } + + public int Count( + string userId = null, + string username = null, + string trackName = null, + string albumName = null, + string artistName = null, + DateTime? from = null, + DateTime? to = null) + { + throw new NotImplementedException(); + } + + public IEnumerable GetAll( + string includes = null, + string userId = null, + string username = null, + string trackName = null, + string albumName = null, + string artistName = null, + DateTime? from = null, + DateTime? to = null) + { + var scrobbles = scrobbleRepository.GetAll( + include: includes, + userId: userId, + username: username, + trackName: trackName, + albumName: albumName, + artistName: artistName, + from: from, + to: to) + .OrderBy(x => x.Timestamp) + .ToArray(); + + var spotListens = spotifyRepository.GetAll( + include: includes, + userId: userId, + username: username, + trackName: trackName, + albumName: albumName, + artistName: artistName, + from: from, + to: to) + .OrderBy(x => x.Timestamp) + .ToArray(); + + var scrobbleIter = scrobbles.GetEnumerator(); + var spotIter = spotListens.GetEnumerator(); + + return scrobbles; + } +} + diff --git a/Selector.Model/Listen/SpotifyListen.cs b/Selector.Model/Listen/SpotifyListen.cs index d3f8570..8491ff3 100644 --- a/Selector.Model/Listen/SpotifyListen.cs +++ b/Selector.Model/Listen/SpotifyListen.cs @@ -2,8 +2,10 @@ namespace Selector.Model; -public class SpotifyListen: Listen +public class SpotifyListen: Listen, IUserListen { + public int Id { get; set; } + public int? PlayedDuration { get; set; } public string TrackUri { get; set; } diff --git a/Selector.Model/Listen/SpotifyListenRepository.cs b/Selector.Model/Listen/SpotifyListenRepository.cs index eefabfd..ce728b4 100644 --- a/Selector.Model/Listen/SpotifyListenRepository.cs +++ b/Selector.Model/Listen/SpotifyListenRepository.cs @@ -95,7 +95,7 @@ namespace Selector.Model return listens; } - public IEnumerable GetAll(string include = null, string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null) + public IEnumerable GetAll(string include = null, string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null) => GetAllQueryable(include: include, userId: userId, username: username, trackName: trackName, albumName: albumName, artistName: artistName, from: from, to: to).AsEnumerable(); public void Remove(DateTime key) @@ -123,7 +123,7 @@ namespace Selector.Model return db.SaveChangesAsync(); } - public int Count(string include = null, string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null) - => GetAllQueryable(include: include, userId: userId, username: username, trackName: trackName, albumName: albumName, artistName: artistName, from: from, to: to).Count(); + public int Count(string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null) + => GetAllQueryable(userId: userId, username: username, trackName: trackName, albumName: albumName, artistName: artistName, from: from, to: to).Count(); } } diff --git a/Selector.Model/Migrations/20221007214100_SpotifyHistory.Designer.cs b/Selector.Model/Migrations/20221007214100_SpotifyHistory.Designer.cs new file mode 100644 index 0000000..95dfb0f --- /dev/null +++ b/Selector.Model/Migrations/20221007214100_SpotifyHistory.Designer.cs @@ -0,0 +1,491 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Selector.Model; + +#nullable disable + +namespace Selector.Model.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20221007214100_SpotifyHistory")] + partial class SpotifyHistory + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:case_insensitive", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "6.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + + b.HasData( + new + { + Id = "00c64c0a-3387-4933-9575-83443fa9092b", + ConcurrencyStamp = "4b4a37c7-cc65-485a-ac0e-d88ef6dede78", + Name = "Admin", + NormalizedName = "ADMIN" + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Selector.Model.AlbumLastfmSpotifyMapping", b => + { + b.Property("SpotifyUri") + .HasColumnType("text"); + + b.Property("LastfmAlbumName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property("LastfmArtistName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.HasKey("SpotifyUri"); + + b.ToTable("AlbumMapping"); + }); + + modelBuilder.Entity("Selector.Model.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastFmUsername") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SaveScrobbles") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("SpotifyAccessToken") + .HasColumnType("text"); + + b.Property("SpotifyIsLinked") + .HasColumnType("boolean"); + + b.Property("SpotifyLastRefresh") + .HasColumnType("timestamp with time zone"); + + b.Property("SpotifyRefreshToken") + .HasColumnType("text"); + + b.Property("SpotifyTokenExpiry") + .HasColumnType("integer"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Selector.Model.ArtistLastfmSpotifyMapping", b => + { + b.Property("SpotifyUri") + .HasColumnType("text"); + + b.Property("LastfmArtistName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.HasKey("SpotifyUri"); + + b.ToTable("ArtistMapping"); + }); + + modelBuilder.Entity("Selector.Model.SpotifyListen", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlbumName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property("ArtistName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property("PlayedDuration") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("TrackName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property("TrackUri") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("SpotifyListen"); + }); + + modelBuilder.Entity("Selector.Model.TrackLastfmSpotifyMapping", b => + { + b.Property("SpotifyUri") + .HasColumnType("text"); + + b.Property("LastfmArtistName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property("LastfmTrackName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.HasKey("SpotifyUri"); + + b.ToTable("TrackMapping"); + }); + + modelBuilder.Entity("Selector.Model.UserScrobble", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlbumArtistName") + .HasColumnType("text"); + + b.Property("AlbumName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property("ArtistName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("TrackName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Scrobble"); + }); + + modelBuilder.Entity("Selector.Model.Watcher", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Watcher"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Selector.Model.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Selector.Model.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Selector.Model.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Selector.Model.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Selector.Model.SpotifyListen", b => + { + b.HasOne("Selector.Model.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Selector.Model.UserScrobble", b => + { + b.HasOne("Selector.Model.ApplicationUser", "User") + .WithMany("Scrobbles") + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Selector.Model.Watcher", b => + { + b.HasOne("Selector.Model.ApplicationUser", "User") + .WithMany("Watchers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Selector.Model.ApplicationUser", b => + { + b.Navigation("Scrobbles"); + + b.Navigation("Watchers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Selector.Model/Migrations/20221007214100_SpotifyHistory.cs b/Selector.Model/Migrations/20221007214100_SpotifyHistory.cs new file mode 100644 index 0000000..a127b88 --- /dev/null +++ b/Selector.Model/Migrations/20221007214100_SpotifyHistory.cs @@ -0,0 +1,63 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Selector.Model.Migrations +{ + public partial class SpotifyHistory : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "SpotifyListen", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + PlayedDuration = table.Column(type: "integer", nullable: true), + TrackUri = table.Column(type: "text", nullable: true), + UserId = table.Column(type: "text", nullable: true), + TrackName = table.Column(type: "text", nullable: true, collation: "case_insensitive"), + AlbumName = table.Column(type: "text", nullable: true, collation: "case_insensitive"), + ArtistName = table.Column(type: "text", nullable: true, collation: "case_insensitive"), + Timestamp = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SpotifyListen", x => x.Id); + table.ForeignKey( + name: "FK_SpotifyListen_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id"); + }); + + migrationBuilder.UpdateData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: "00c64c0a-3387-4933-9575-83443fa9092b", + column: "ConcurrencyStamp", + value: "4b4a37c7-cc65-485a-ac0e-d88ef6dede78"); + + migrationBuilder.CreateIndex( + name: "IX_SpotifyListen_UserId", + table: "SpotifyListen", + column: "UserId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SpotifyListen"); + + migrationBuilder.UpdateData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: "00c64c0a-3387-4933-9575-83443fa9092b", + column: "ConcurrencyStamp", + value: "ec454f56-2b26-4bd8-be8e-a7fd34981ac2"); + } + } +} diff --git a/Selector.Model/Migrations/ApplicationDbContextModelSnapshot.cs b/Selector.Model/Migrations/ApplicationDbContextModelSnapshot.cs index d79f2ca..d296389 100644 --- a/Selector.Model/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Selector.Model/Migrations/ApplicationDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ namespace Selector.Model.Migrations #pragma warning disable 612, 618 modelBuilder .HasAnnotation("Npgsql:CollationDefinition:case_insensitive", "en-u-ks-primary,en-u-ks-primary,icu,False") - .HasAnnotation("ProductVersion", "6.0.2") + .HasAnnotation("ProductVersion", "6.0.9") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -52,7 +52,7 @@ namespace Selector.Model.Migrations new { Id = "00c64c0a-3387-4933-9575-83443fa9092b", - ConcurrencyStamp = "ec454f56-2b26-4bd8-be8e-a7fd34981ac2", + ConcurrencyStamp = "4b4a37c7-cc65-485a-ac0e-d88ef6dede78", Name = "Admin", NormalizedName = "ADMIN" }); @@ -282,6 +282,45 @@ namespace Selector.Model.Migrations b.ToTable("ArtistMapping"); }); + modelBuilder.Entity("Selector.Model.SpotifyListen", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlbumName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property("ArtistName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property("PlayedDuration") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("TrackName") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property("TrackUri") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("SpotifyListen"); + }); + modelBuilder.Entity("Selector.Model.TrackLastfmSpotifyMapping", b => { b.Property("SpotifyUri") @@ -409,6 +448,15 @@ namespace Selector.Model.Migrations .IsRequired(); }); + modelBuilder.Entity("Selector.Model.SpotifyListen", b => + { + b.HasOne("Selector.Model.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Selector.Model.UserScrobble", b => { b.HasOne("Selector.Model.ApplicationUser", "User") diff --git a/Selector.Model/Scrobble/DBPlayCountPuller.cs b/Selector.Model/Scrobble/DBPlayCountPuller.cs index 802326c..2a3f6e2 100644 --- a/Selector.Model/Scrobble/DBPlayCountPuller.cs +++ b/Selector.Model/Scrobble/DBPlayCountPuller.cs @@ -9,12 +9,12 @@ namespace Selector.Cache { public class DBPlayCountPuller { - protected readonly IScrobbleRepository ScrobbleRepository; + protected readonly IListenRepository ScrobbleRepository; private readonly IOptions nowOptions; public DBPlayCountPuller( IOptions options, - IScrobbleRepository scrobbleRepository + IListenRepository scrobbleRepository ) { ScrobbleRepository = scrobbleRepository; diff --git a/Selector.Model/Scrobble/IScrobbleRepository.cs b/Selector.Model/Scrobble/IScrobbleRepository.cs index a59994d..030034f 100644 --- a/Selector.Model/Scrobble/IScrobbleRepository.cs +++ b/Selector.Model/Scrobble/IScrobbleRepository.cs @@ -6,17 +6,17 @@ using System.Threading.Tasks; namespace Selector.Model { - public interface IScrobbleRepository + public interface IScrobbleRepository: IListenRepository { void Add(UserScrobble item); void AddRange(IEnumerable item); - IEnumerable GetAll(string include = null, string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null); + //IEnumerable GetAll(string include = null, string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null); UserScrobble Find(int key, string include = null); void Remove(int key); public void Remove(UserScrobble scrobble); public void RemoveRange(IEnumerable scrobbles); void Update(UserScrobble item); Task Save(); - int Count(string include = null, string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null); + //int Count(string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null); } } diff --git a/Selector.Model/Scrobble/ScrobbleRepository.cs b/Selector.Model/Scrobble/ScrobbleRepository.cs index 77dcb9f..90b5386 100644 --- a/Selector.Model/Scrobble/ScrobbleRepository.cs +++ b/Selector.Model/Scrobble/ScrobbleRepository.cs @@ -95,7 +95,7 @@ namespace Selector.Model return scrobbles; } - public IEnumerable GetAll(string include = null, string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null) + public IEnumerable GetAll(string include = null, string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null) => GetAllQueryable(include: include, userId: userId, username: username, trackName: trackName, albumName: albumName, artistName: artistName, from: from, to: to).AsEnumerable(); public void Remove(int key) @@ -123,7 +123,7 @@ namespace Selector.Model return db.SaveChangesAsync(); } - public int Count(string include = null, string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null) - => GetAllQueryable(include: include, userId: userId, username: username, trackName: trackName, albumName: albumName, artistName: artistName, from: from, to: to).Count(); + public int Count(string userId = null, string username = null, string trackName = null, string albumName = null, string artistName = null, DateTime? from = null, DateTime? to = null) + => GetAllQueryable(userId: userId, username: username, trackName: trackName, albumName: albumName, artistName: artistName, from: from, to: to).Count(); } } diff --git a/Selector.Model/Scrobble/UserScrobble.cs b/Selector.Model/Scrobble/UserScrobble.cs index 5a379d8..fce0be8 100644 --- a/Selector.Model/Scrobble/UserScrobble.cs +++ b/Selector.Model/Scrobble/UserScrobble.cs @@ -2,7 +2,7 @@ namespace Selector.Model { - public class UserScrobble: Scrobble + public class UserScrobble: Scrobble, IUserListen { public int Id { get; set; } public string UserId { get; set; } diff --git a/Selector.Web/Startup.cs b/Selector.Web/Startup.cs index ea9f355..dd82986 100644 --- a/Selector.Web/Startup.cs +++ b/Selector.Web/Startup.cs @@ -64,7 +64,11 @@ namespace Selector.Web options.UseNpgsql(Configuration.GetConnectionString("Default")) ); services.AddDBPlayCountPuller(); - services.AddTransient(); + services.AddTransient() + .AddTransient(); + + //services.AddTransient(); + services.AddTransient(); services.AddIdentity() .AddEntityFrameworkStores() diff --git a/Selector/Scrobble/PlayDensity.cs b/Selector/Scrobble/PlayDensity.cs index 1622e11..445af28 100644 --- a/Selector/Scrobble/PlayDensity.cs +++ b/Selector/Scrobble/PlayDensity.cs @@ -6,9 +6,9 @@ namespace Selector { public static class PlayDensity { - public static decimal Density(this IEnumerable scrobbles, TimeSpan window) => scrobbles.Density(DateTime.UtcNow - window, DateTime.UtcNow); + public static decimal Density(this IEnumerable scrobbles, TimeSpan window) => scrobbles.Density(DateTime.UtcNow - window, DateTime.UtcNow); - public static decimal Density(this IEnumerable scrobbles, DateTime from, DateTime to) + public static decimal Density(this IEnumerable scrobbles, DateTime from, DateTime to) { var filteredScrobbles = scrobbles.Where(s => s.Timestamp > from && s.Timestamp < to); @@ -17,7 +17,7 @@ namespace Selector return filteredScrobbles.Count() / dayDelta; } - public static decimal Density(this IEnumerable scrobbles) + public static decimal Density(this IEnumerable scrobbles) { var minDate = scrobbles.Select(s => s.Timestamp).Min(); var maxDate = scrobbles.Select(s => s.Timestamp).Max(); diff --git a/Selector/Scrobble/ScrobbleMatcher.cs b/Selector/Scrobble/ScrobbleMatcher.cs index 6daa5c9..c17fcfa 100644 --- a/Selector/Scrobble/ScrobbleMatcher.cs +++ b/Selector/Scrobble/ScrobbleMatcher.cs @@ -6,22 +6,22 @@ using System.Linq; namespace Selector { - public static class ScrobbleMatcher + public static class ListenMatcher { - public static bool MatchTime(Scrobble nativeScrobble, LastTrack serviceScrobble) + public static bool MatchTime(IListen nativeScrobble, LastTrack serviceScrobble) => serviceScrobble.TimePlayed.Equals(nativeScrobble); - public static bool MatchTime(Scrobble nativeScrobble, Scrobble serviceScrobble) + public static bool MatchTime(IListen nativeScrobble, IListen serviceScrobble) => serviceScrobble.Timestamp.Equals(nativeScrobble.Timestamp); - public static (IEnumerable, IEnumerable) IdentifyDiffs(IEnumerable existing, IEnumerable toApply, bool matchContents = true) + public static (IEnumerable, IEnumerable) IdentifyDiffs(IEnumerable existing, IEnumerable toApply, bool matchContents = true) { existing = existing.OrderBy(s => s.Timestamp); toApply = toApply.OrderBy(s => s.Timestamp); var toApplyIter = toApply.GetEnumerator(); - var toAdd = new List(); - var toRemove = new List(); + var toAdd = new List(); + var toRemove = new List(); var toApplyOverrun = false; @@ -79,7 +79,7 @@ namespace Selector return (toAdd, toRemove); } - public static void MatchData(Scrobble currentExisting, Scrobble toApply) + public static void MatchData(IListen currentExisting, IListen toApply) { if (!currentExisting.TrackName.Equals(toApply.TrackName, StringComparison.InvariantCultureIgnoreCase)) { @@ -97,19 +97,19 @@ namespace Selector } } - public static (IEnumerable, IEnumerable) IdentifyDiffsContains(IEnumerable existing, IEnumerable toApply) + public static (IEnumerable, IEnumerable) IdentifyDiffsContains(IEnumerable existing, IEnumerable toApply) { - var toAdd = toApply.Where(s => !existing.Contains(s, new ScrobbleComp())); - var toRemove = existing.Where(s => !toApply.Contains(s, new ScrobbleComp())); + var toAdd = toApply.Where(s => !existing.Contains(s, new ListenComp())); + var toRemove = existing.Where(s => !toApply.Contains(s, new ListenComp())); return (toAdd, toRemove); } - public class ScrobbleComp : IEqualityComparer + public class ListenComp : IEqualityComparer { - public bool Equals(Scrobble x, Scrobble y) => x.Timestamp == y.Timestamp; + public bool Equals(IListen x, IListen y) => x.Timestamp == y.Timestamp; - public int GetHashCode([DisallowNull] Scrobble obj) => obj.Timestamp.GetHashCode(); + public int GetHashCode([DisallowNull] IListen obj) => obj.Timestamp.GetHashCode(); } } }